Merge current journey candidate into the qualification checkpoint

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-21 09:20:10 -05:00
commit 09a45d29bb
146 files changed
+6892 -1264

No files matched your search

+65
View File
@@ -98,6 +98,71 @@ pnpm test:e2e:runner -- --id legacy-conversion.runner-codex.local.convert-and-de
pnpm test:e2e:runner -- --id automatic-repository-configured-models.runner-codex.local.automatic-repository-resume --max-automatic-retries 0
```
## Necessary free-text question
The explicit-only `necessary-question` suite has four development cells: the
recorded Codex and Claude models, on local and Daytona environments. It requests
one essential booking reference that only the test user knows. The browser must
show the actual free-text prompt and its explanation, then submit the real answer.
The oracle checks the unchanged question, its server-owned provider or semantic
receipt, the exact stored user answer, and one bound continuation. The saved
confirmation must contain that answer exactly. The final reply's link must open
that same task document and display its confirmation.
A title, success claim, replacement card, duplicate wake, or uncommitted result
cannot establish a pass.
This extends the existing continuation flow and keeps the older question suites
unchanged. It uses production hire instructions, one attempt, one or two provider
admissions, and 1,000-cent company and agent budgets. API-key authentication and
per-turn lifecycle remain explicit development differences. It does not qualify
Slack, staging subscription credentials, warm reuse, or the complete checkpoint.
```sh
pnpm test:e2e:runner -- --list --suite necessary-question
pnpm test:e2e:runner -- --id necessary-question.runner-codex.local.necessary-free-text --max-automatic-retries 0
```
## Latency observation bounds
The explicit `codie-latency-diagnostic` arms its observation recorder before
dispatching task creation and measures elapsed time with `performance.now()`.
The first browser read starts before dispatch; subsequent reads run every 100 ms
without overlapping. The observer stops and drains before fixture teardown,
with one one-second deadline for its pending and final reads. On expiry, it
records `observationTimeouts` and a failed observation for each waiting probe,
starts no more reads, and ignores late completions. Fixture cleanup owns the
browser operation that can still be running after that deadline.
It retains `latencyProbes` in the existing result JSON and dashboard, separately
for the first visible substantive response and final task/run settlement, with
an inclusive 30-second target. Platform-generated receipts do not count as the
substantive response. Browser polls bound persistent visible output; this does
not measure Slack delivery or qualify transient output between observations.
These timing verdicts are diagnostic dimensions and do not replace the existing
workflow matchers or rewrite the original attempt grade.
`latency-timing.ts` is shared by response and acknowledgement timing evidence.
The browser adapter samples both from one snapshot when an acknowledgement
target is explicitly configured; the Daytona diagnostic does not enable that
feature or infer a missing Slack acknowledgement from its browser observations.
An absent observation and a present observation give `(lastAbsent, firstPresent]`;
an asynchronous absent read uses its start time, while a present read uses its
completion time. Each bound is widened by the declared clock resolution.
Targets specify `<` or `<=`; open and closed endpoints determine equality.
Pass requires the entire interval to satisfy the target, fail requires the
entire interval to violate it, and all other cases are inconclusive. Missing
observations remain null; unknown resolution and invalid/empty intervals cannot
produce a conclusive verdict. Failed reads are not absent observations.
The historical acknowledgement `(1901, 2427] ms` is inconclusive against
`<= 2000 ms`; the reply `(52965, 53334] ms` fails `<= 30000 ms`. These are
deterministic calibration examples, not fresh live measurements. Provider
message creation/edit timestamps remain separate metadata; an edit can expose
new response text on an older message. Neither those timestamps nor final run
settlement substitutes for first visible response. The legacy warm-turn
`responseLatencyMs` field continues to mean final settlement and is labelled
accordingly. Report regeneration recomputes interval verdicts, retains changed
`recordedVerdict` values and all bounds, and leaves the original files intact.
## Live provider connection journeys
See [PROVIDER-CONNECTIONS.md](PROVIDER-CONNECTIONS.md) for the explicit-only
+3 -3
View File
@@ -211,10 +211,10 @@ describe("runner E2E catalog", () => {
expect(localIntegrityTasks).toHaveLength(2);
expect(openRouterBreadthTasks).toHaveLength(3);
expect(runnerSuites.map((suite) => suite.expectedMatrixSize)).toEqual([
1, 3, 4, 4, 4, 4, 4, 8, 10, 6, 63, 12, 6, 4, 2, 4, 2, 30, 3, 16, 16, 2, 6, 8, 46, 2, 23, 15, 52, 6, 6, 20, 26, 52, 28, 18, 2, 6, 6, 13, 10, 48, 16, 10, 2, 1, 1, 3, 116, 2,
1, 3, 4, 4, 4, 4, 4, 4, 8, 10, 6, 63, 12, 6, 4, 2, 4, 2, 30, 3, 16, 16, 2, 6, 8, 46, 2, 23, 15, 52, 6, 6, 20, 26, 52, 28, 18, 2, 6, 6, 13, 10, 48, 16, 10, 2, 1, 1, 3, 116, 2,
]);
expect(validateRunnerCatalog()).toHaveLength(752);
expect(new Set(runnerMatrix.map((entry) => entry.id)).size).toBe(752);
expect(validateRunnerCatalog()).toHaveLength(756);
expect(new Set(runnerMatrix.map((entry) => entry.id)).size).toBe(756);
expect(
runnerMatrix.filter((entry) => entry.suite.id === "core-compatibility"),
).toHaveLength(48);
+13 -1
View File
@@ -1,3 +1,4 @@
import { necessaryQuestionTask, necessaryQuestionDigest } from "./necessary-question.js";
import { caseHandoffTask, caseHandoffDefinitionDigest } from "./case-handoffs-cases.js";
import { caseReceiptTask, caseReceiptDefinitionDigest, CASE_RECEIPT_BUDGET_CENTS, CASE_RECEIPT_MAX_TURNS, CASE_RECEIPT_RUN_TIMEOUT_MS } from "./case-receipt-cases.js";
import { TASK_WORKSPACES_SUITE, taskWorkspaceTask, taskWorkspaceProfile, taskWorkspaceDefinitionDigest } from "./task-workspaces-cases.js";
@@ -1173,7 +1174,8 @@ export const runnerSuites: readonly RunnerSuiteFixture[] = [
tasks: runnerTasks.filter(task => task.id === "message-marker")
.map(task => ({ ...task, automaticRetryPolicy: "single_attempt" as const })),
expectedMatrixSize: 1,
definitionMetadata: { diagnostic: true, modelSource: "observed-staging-run", providerAuth: "api_key", stagingAuth: "subscription", companyData: "fresh-fixture", slackDelivery: false, maximumAutomaticRetries: 0 },
definitionMetadata: { diagnostic: true, modelSource: "observed-staging-run", providerAuth: "api_key", stagingAuth: "subscription", companyData: "fresh-fixture", slackDelivery: false, maximumAutomaticRetries: 0,
timing: { version: 1, clock: "monotonic", responseTargetMs: 30_000, event: "first_visible_response", settlementSeparate: true } },
},
{
id: TASK_WORKSPACES_SUITE, label: "Task workspaces without projects", manualOnly: true,
@@ -1185,6 +1187,16 @@ export const runnerSuites: readonly RunnerSuiteFixture[] = [
definitionMetadata: { version: 1, definitionDigest: taskWorkspaceDefinitionDigest(), scheduling: "explicit-only", providerTurns: 3,
attempts: 1, budgetMonthlyCents: 1000, project: "none", cwd: "unconfigured-task-default", oracle: "public-file-download-and-host-nested-git", remoteLifecycle: "per_turn" },
},
{
id: "necessary-question", label: "Necessary free-text question and verified confirmation", manualOnly: true,
description: "One required private input, a visible question and context, and one exactly bound answer continuation.",
groups: [], profiles: repositoryModelProfiles.map(({ native }) => productionDefaultHireProfile(native)),
environments: runnerEnvironments, tasks: [necessaryQuestionTask], expectedMatrixSize: 4,
definitionMetadata: { version: 1, definitionDigest: necessaryQuestionDigest(), scheduling: "explicit-only", attempts: 1,
providerTurns: "1-2; provider pause or one semantic successor", budgetMonthlyCents: 1000, instructions: "production-default",
surface: "paperclip", target: "isolated-development-instance", authentication: "api-key", stagingAuthentication: "subscription",
remoteLifecycle: "per_turn", stagingQualification: false },
},
...([false, true] as const).map(conversion => ({
id: conversion ? "legacy-conversion" : "configured-repository",
label: conversion ? "Legacy conversion with verified delivery" : "Configured repository with verified delivery",
+8 -2
View File
@@ -10,9 +10,13 @@ export const CONTINUATION_CASES = [
"question-tool-documentation",
"provider-question-bridge",
] as const;
export type ContinuationCase = (typeof CONTINUATION_CASES)[number] | "question-answer-resume";
export type ContinuationCase = (typeof CONTINUATION_CASES)[number] | "question-answer-resume" | "necessary-free-text";
export function necessaryQuestionContract() {
return { prompt: "What booking reference should appear on the confirmation?",
context: "Only you have the booking reference. I need it to write the confirmation correctly." };
}
export function continuationScenario(id: string, nonce: string) {
if (id !== "question-answer-resume" && !CONTINUATION_CASES.includes(id as (typeof CONTINUATION_CASES)[number]))
if (id !== "question-answer-resume" && id !== "necessary-free-text" && !CONTINUATION_CASES.includes(id as (typeof CONTINUATION_CASES)[number]))
throw new Error(`Unknown continuation case: ${id}`);
const marker = `AMBER${nonce.replace(/[^a-z0-9]/gi, "")}`;
const old = `COBALT${nonce.replace(/[^a-z0-9]/gi, "")}`;
@@ -22,7 +26,9 @@ export function continuationScenario(id: string, nonce: string) {
const document =
"Save the final note as a task document. No other deliverables or child tasks are needed.";
const questionPrompt = "Help me write a one-sentence welcome note for a club meetup. First let me choose Morning or Afternoon using clickable choices. After I choose, ask me for a reference to include using an open text field. Ask only one question at a time and wait for my answers. Then save the note as a task document, including the selected time and my reference exactly as supplied, and finish. Do not create any other tasks or deliverables.";
const necessary = necessaryQuestionContract();
const prompts: Record<ContinuationCase, string> = {
"necessary-free-text": `Prepare a booking confirmation. The booking reference is required, is known only to me, and has not been supplied. Ask one open text question with exactly this prompt: ${necessary.prompt} Explain this context in the question: ${necessary.context} Wait for my answer. Then save one task document with exactly the text Booking confirmed: followed by a space and my complete answer, unchanged. Deliver its accessible link here and finish. Do not create other tasks or deliverables.`,
"question-answer-resume": questionPrompt,
"provider-question-bridge": `Use your built-in AskUserQuestion tool (not Paperclip's request_human_input) to ask which reference to include, with two choices: ${marker} and ${old}. Wait for my real answer, then save a one-sentence welcome note including only my selected reference as a task document and finish. No other tasks or deliverables are needed.`,
"question-tool-documentation": questionPrompt,
+35 -2
View File
@@ -1,3 +1,5 @@
import { gradeNecessaryQuestion } from "./necessary-question.js";
import { necessaryQuestionContract } from "./continuation-cases.js";
import { validResumeQuestionForm } from "./question-resume-scoring.js";
import { gradeLifecycleBaseline, type LifecycleCheckpoint } from "./lifecycle-baseline.js";
import { lifecycleLiveCase, lifecycleLiveContinuation, gradeLifecycleNarrative } from "./lifecycle-live-cases.js";
@@ -107,7 +109,7 @@ export async function runContinuationFlow(input: {
return ready;
},
reject: (state) =>
state.runs.length > (scenario.id === "question-answer-resume" ? 3 : 12)
state.runs.length > (scenario.id === "necessary-free-text" ? 2 : scenario.id === "question-answer-resume" ? 3 : 12)
? "Bounded continuation run count exceeded"
: state.runs.some((r) =>
["failed", "timed_out", "cancelled"].includes(r.status),
@@ -171,6 +173,34 @@ export async function runContinuationFlow(input: {
});
await input.evidence("api-state.json", checkpoints.at(-1));
await open();
if (scenario.id === "necessary-free-text") {
if (phase === "initial") {
const contract = necessaryQuestionContract();
await expect(page.getByText(contract.prompt, { exact: true }).last()).toBeVisible({ timeout: 30_000 });
await expect(page.getByText(contract.context, { exact: true }).last()).toBeVisible({ timeout: 30_000 });
const question = interactions.find(interaction => interaction.kind === "ask_user_questions" && interaction.status === "pending");
checkpoints.at(-1)!.questionPresentation = { interactionId: question?.id ?? "", promptVisible: true, contextVisible: true };
} else if (phase === "final") {
expect(documents).toHaveLength(1);
const document = documents[0]!;
expect(document.latestRevisionId).toBeTruthy();
const href = `/${encodeURIComponent(fixtures.company.issuePrefix!)}/issues/${encodeURIComponent(issue!.identifier ?? issue!.id)}#document-${encodeURIComponent(document.key)}`;
const url = new URL(href, page.url()).href;
const link = page.getByTestId("task-chat-agent-bubble").locator(`a[href=${JSON.stringify(href)}]`).last();
await expect(link).toBeVisible({ timeout: 30_000 });
await link.click();
await expect(page).toHaveURL(url);
const target = page.locator([
`[id=${JSON.stringify(`document-${document.key}`)}]:visible`,
`[id=${JSON.stringify(`side-panel-content-document:${document.key}`)}]:visible`,
].join(", "));
await expect(target).toHaveCount(1);
await expect(target).toBeVisible();
const visibleBody = `Booking confirmed: ${scenario.answer}`;
await expect(target).toContainText(visibleBody);
checkpoints.at(-1)!.outputDocument = { issueId: issue!.id, key: document.key, revisionId: document.latestRevisionId, href, visibleBody };
}
}
await captureLoadedContinuation(page, String(issue!.title), () => input.capture(
phase,
`Continuation: ${phase}`,
@@ -186,7 +216,9 @@ export async function runContinuationFlow(input: {
);
expect(questions, "one real question must be shown").toHaveLength(1);
const set = chatQuestionPresentation(questions[0].payload);
if (scenario.id === "question-answer-resume") {
if (scenario.id === "necessary-free-text") {
expect(validResumeQuestionForm(checkpoints[0]!, questions[0], false), "a bound text-only question").toBe(true);
} else if (scenario.id === "question-answer-resume") {
expect(validResumeQuestionForm(checkpoints.at(-1)!, questions[0], !!choice), "a verified native question path and the requested form").toBe(true);
} else if (scenario.id === "provider-question-bridge") {
expect(isSingleClaudeQuestion(set.questions), "one choice question with only the optional provider Other field").toBe(true);
@@ -294,6 +326,7 @@ export async function runContinuationFlow(input: {
runtimeMode: execution.profile.expectedRuntimeMode,
});
checks.push(...gradeLifecycleBaseline(checkpoints));
if (scenario.id === "necessary-free-text") checks.push(...gradeNecessaryQuestion(checkpoints, scenario.answer));
if (lifecycleProbe) checks.push(gradeLifecycleNarrative({
narrative: lifecycleProbe.narrative,
agentId: fixtures.agent.id,
+3 -1
View File
@@ -14,6 +14,8 @@ export interface ContinuationCheckpoint {
documents: Array<{ key: string; body: string; latestRevisionId?: string }>;
attachments: unknown[];
comments: unknown[];
questionPresentation?: { interactionId: string; promptVisible: boolean; contextVisible: boolean };
outputDocument?: { issueId: string; key: string; revisionId: string; href: string; visibleBody: string };
interactions: unknown[];
runs: Array<{ id: string; status: string; runtimeMode?: string }>;
}
@@ -50,7 +52,7 @@ export function gradeContinuation(input: {
const before = input.checkpoints.filter((c) => c.phase !== "final");
check(
"recorded-continuation",
before.length > 0 && !!final && final.runs.length >= (["provider-question-bridge", "question-answer-resume"].includes(input.id) ? 1 : 2),
before.length > 0 && !!final && final.runs.length >= (["provider-question-bridge", "question-answer-resume", "necessary-free-text"].includes(input.id) ? 1 : 2),
"Initial and final turns must both be recorded.",
);
for (const c of before) {
+8 -1
View File
@@ -2,6 +2,7 @@ import { html, durationLabel, tokenLabel, llmCostLabel, usdLabel } from "./repor
import { renderReportShell, reportNavigationStyles, reportShellScript } from "./report-navigation.js";
import { reportBrowserScript } from "./report-browser.js";
import { isIncompleteFirstTaskResult, renderCaseOutcome } from "./case-outcome.js";
import { latencyIntervalLabel } from "./latency-timing.js";
import { campaignSuiteSummaryStyles, renderCampaignSuiteSummary, renderSuiteCaseIndex } from "./campaign-summary.js";
import { journeyCapabilities, journeyFeatureIds } from "./journey-features.js";
import { journeyFeatureStyles, renderJourneyFeatureCoverage, renderJourneyFeatureOptions } from "./feature-coverage.js";
@@ -217,6 +218,11 @@ function renderCase(
</tr>`,
)
.join("");
const latencyRows = (entry?.result.latencyProbes ?? []).map(probe => `<tr>
<td>${html(probe.event)}</td><td>${html(probe.verdict)}</td>
<td>${html(latencyIntervalLabel(probe))}</td><td>${html(`${probe.target.operator} ${probe.target.ms} ms`)}</td>
<td>${html(probe.resolutionMs === null ? "unknown" : `${probe.resolutionMs} ms`)}</td>
</tr>`).join("");
const startupReuseRows = (entry?.result.startupReuse ?? (entry?.result.runIds ?? []).map(runId => ({
runId, ...summarizeStartupReuse([]),
}))).map(reuse => `<tr>
@@ -310,7 +316,8 @@ function renderCase(
: ""
}
<p class="detail">${html(detail)}</p>
${turnTimingRows ? `<div class="matcher-wrap"><table class="matchers"><thead><tr><th>Turn</th><th>Run</th><th>Lease</th><th>Scheduler</th><th>Run duration</th><th>Response</th></tr></thead><tbody>${turnTimingRows}</tbody></table></div>` : ""}
${turnTimingRows ? `<div class="matcher-wrap"><table class="matchers"><thead><tr><th>Turn</th><th>Run</th><th>Lease</th><th>Scheduler</th><th>Run duration</th><th>Final settlement (legacy response field)</th></tr></thead><tbody>${turnTimingRows}</tbody></table></div>` : ""}
${latencyRows ? `<div class="matcher-wrap"><table class="matchers"><thead><tr><th>Event</th><th>Timing verdict</th><th>Possible latency</th><th>Target</th><th>Resolution</th></tr></thead><tbody>${latencyRows}</tbody></table></div>` : ""}
${startupReuseRows ? `<div class="matcher-wrap"><table class="matchers"><caption>Startup reuse evidence</caption><thead><tr><th>Run</th><th>Warmth</th><th>Environment/lease</th><th>Workspace</th><th>Runner</th><th>Provider process</th><th>Provider session</th><th>Provider launches</th></tr></thead><tbody>${startupReuseRows}</tbody></table></div>` : ""}
${matcherRows ? `<div class="matcher-wrap"><table class="matchers"><thead><tr><th>Result</th><th>Matcher</th><th>Expectation</th><th>Detail</th></tr></thead><tbody>${matcherRows}</tbody></table></div>` : `<p class="detail">No matcher result was recorded.</p>`}
${entry ? `<details class="usage"><summary>Usage and billing metadata</summary><pre>${html(JSON.stringify({ billing, rawUsage: entry.result.usage ?? null }, null, 2))}</pre></details>` : ""}
+2 -2
View File
@@ -51,8 +51,8 @@ describe("extended ACP harness qualification", () => {
const server = buildPaperclipServerEnvironment(env);
for (const name of Object.keys(ambient).filter(name => name !== "PAPERCLIP_RUNNER_ACPX_QUALIFICATION")) expect(server[name]).toBeUndefined();
expect(buildRunnerE2EProcessEnvironment(ambient, []).PAPERCLIP_RUNNER_ACPX_QUALIFICATION).toBeUndefined();
const pending = selected.find(value => value.profile.qualificationCandidate === "copilot")!;
expect(() => buildRunnerE2EProcessEnvironment({}, [{ ...pending, suite: { ...pending.suite, manualOnly: false } }])).toThrow("explicit");
const copilot = selected.find(value => value.profile.qualificationCandidate === "copilot")!;
expect(() => buildRunnerE2EProcessEnvironment({}, [{ ...copilot, suite: { ...copilot.suite, manualOnly: false } }])).toThrow("explicit");
});
it("adds six explicit local/Daytona warm cells without admitting them implicitly", () => {
const warm = runnerMatrix.filter(cell => cell.suite.id === "rich-acp-warm-continuity");
+6
View File
@@ -1,4 +1,5 @@
import { isIncompleteFirstTaskResult } from "./case-outcome.js";
import { classifyLatency } from "./latency-timing.js";
import { summarizeStartupReuse } from "../../packages/adapter-utils/src/startup-reuse.js";
import { validateRetainedRunnerResult } from "./result-validation.js";
import {
@@ -29,6 +30,11 @@ export function canonicalExecutionId(id: string) {
export function upgradeRunnerResult(result: RunnerE2EResult): RunnerE2EResult {
validateRetainedRunnerResult(result);
if (result.latencyProbes) result = { ...result, latencyProbes: result.latencyProbes.map(probe => {
const corrected = classifyLatency(probe, probe.target);
return probe.verdict === corrected.verdict ? corrected
: { ...corrected, recordedVerdict: probe.recordedVerdict ?? probe.verdict };
}) };
result = { ...result, startupReuse: result.startupReuse ?? (result.runIds ?? []).map(runId => ({
runId, ...summarizeStartupReuse([]),
})) };
+1 -1
View File
@@ -60,7 +60,7 @@ it("admits only the 20 agreed Copilot cells through normal installed resolution
expect(() => assertInstalledCliSelection(env, [{ ...cell, id: `${cell.id}-unreviewed` }])).toThrow("explicit supported");
}
});
it("current qualified providers drop ambient admission, including explicit Copilot cells", () => {
it("qualified providers drop ambient admission without granting custom model overrides", () => {
for (const cell of cells) expect(buildRunnerE2EProcessEnvironment({ PAPERCLIP_RUNNER_ACPX_QUALIFICATION: "ambient" }, [cell]).PAPERCLIP_RUNNER_ACPX_QUALIFICATION).toBeUndefined();
for (const agent of ["copilot"]) {
const cell = runnerMatrix.find(e => e.profile.qualificationCandidate === agent)!;
+122
View File
@@ -0,0 +1,122 @@
import { describe, expect, it } from "vitest";
import { armLatencyProbe, classifyLatency, observationTiming } from "./latency-timing.js";
const target = { operator: "<=" as const, ms: 2_000 };
const observed = (lastAbsentMs: number | null, firstPresentMs: number | null, resolutionMs: number | null = 0) =>
observationTiming({ event: "acknowledgement", lastAbsentMs, firstPresentMs, resolutionMs });
describe("interval latency classification", () => {
it("retains the historical acknowledgement bounds as inconclusive", () => {
expect(classifyLatency(observed(1_901, 2_427), target)).toMatchObject({
verdict: "inconclusive", lastAbsentMs: 1_901, firstPresentMs: 2_427,
interval: { lower: { ms: 1_901, inclusive: false }, upper: { ms: 2_427, inclusive: true } },
});
expect(classifyLatency({ ...observed(52_965, 53_334), event: "first_visible_response" }, { operator: "<=", ms: 30_000 }).verdict).toBe("fail");
});
it.each(["<", "<="] as const)("classifies every endpoint equality for %s", operator => {
// The same number is not sufficient: endpoint inclusion determines equality.
for (const lowerInclusive of [false, true]) for (const upperInclusive of [false, true]) {
const verdict = (lower: number, upper: number) => classifyLatency({
...observed(null, null), interval: {
lower: { ms: lower, inclusive: lowerInclusive }, upper: { ms: upper, inclusive: upperInclusive },
},
}, { operator, ms: 2_000 }).verdict;
expect(verdict(1_000, 2_000)).toBe(operator === "<=" || !upperInclusive ? "pass" : "inconclusive");
expect(verdict(2_000, 3_000)).toBe(operator === "<" || !lowerInclusive ? "fail" : "inconclusive");
expect(verdict(1_999, 2_001)).toBe("inconclusive");
expect(verdict(2_000, 2_000)).toBe(lowerInclusive && upperInclusive ? (operator === "<=" ? "pass" : "fail") : "inconclusive");
}
});
it.each([
[null, null, "inconclusive"], [null, 1_999, "pass"], [null, 2_001, "inconclusive"],
[1_999, null, "inconclusive"], [2_000, null, "fail"], [2_001, null, "fail"],
] as const)("handles missing observations (%s, %s)", (lower, upper, verdict) => {
const result = classifyLatency(observed(lower, upper), target);
expect(result.verdict).toBe(verdict);
expect(result.lastAbsentMs).toBe(lower);
expect(result.firstPresentMs).toBe(upper);
});
it.each([
[-1, 1_000], [NaN, 1_000], [1_000, Infinity], [2_001, 2_000], [2_000, 2_000],
])("does not turn invalid observations %s, %s into exact latency", (lower, upper) => {
expect(classifyLatency(observed(lower!, upper!), target)).toMatchObject({ verdict: "inconclusive", reason: "invalid_evidence" });
});
it("keeps unknown resolution and widens rounded measurements before grading", () => {
expect(classifyLatency(observed(1_900, 2_000, null), target).verdict).toBe("inconclusive");
expect(classifyLatency(observed(1_900, 2_000, 1), target)).toMatchObject({
verdict: "inconclusive", resolutionMs: 1, firstPresentMs: 2_000,
interval: { lower: { ms: 1_899 }, upper: { ms: 2_001 } },
});
expect(classifyLatency(observed(0, 1, 1), { operator: "<=", ms: 0 }).verdict).toBe("inconclusive");
for (const resolution of [-1, NaN, Infinity]) expect(classifyLatency(observed(1, 2, resolution), target).reason).toBe("invalid_evidence");
for (const ms of [-1, NaN, Infinity]) expect(classifyLatency(observed(1, 2), { ...target, ms }).reason).toBe("invalid_evidence");
});
it("matches an independent finite-domain oracle on strict and inclusive targets", () => {
for (const operator of ["<", "<="] as const) for (const lowerInclusive of [false, true]) for (const upperInclusive of [false, true]) {
for (let lower = 0; lower <= 4; lower++) for (let upper = lower; upper <= 4; upper++) {
const values = Array.from({ length: 9 }, (_, n) => n / 2)
.filter(n => (n > lower || (lowerInclusive && n === lower)) && (n < upper || (upperInclusive && n === upper)));
const passes = values.map(n => operator === "<" ? n < 2 : n <= 2);
const expected = !values.length ? "inconclusive" : passes.every(Boolean) ? "pass" : passes.every(p => !p) ? "fail" : "inconclusive";
const interval = { lower: { ms: lower, inclusive: lowerInclusive }, upper: { ms: upper, inclusive: upperInclusive } };
expect(classifyLatency({ ...observed(null, null), interval }, { operator, ms: 2 }).verdict).toBe(expected);
}
}
});
});
describe("monotonic observation probes", () => {
it("measures an edited message's first matching visibility independently of creation and settlement", async () => {
let clock = 10_000;
const now = () => clock;
const reply = armLatencyProbe({ event: "first_visible_response", target, resolutionMs: 0, now });
const settled = armLatencyProbe({ event: "final_settlement", target, resolutionMs: 0, now });
// The observer is armed before dispatch; the response can arrive before send returns.
const send = async () => {
clock += 1_901;
await reply.sample(async () => ({ present: false }));
clock = 12_427;
await reply.sample(async () => ({ present: true, messageTimestamps: { createdAt: "2026-01-01T00:00:00Z", editedAt: "2026-01-01T00:00:10Z" } }));
};
await send();
clock = 63_334;
await reply.sample(async () => ({ present: false }));
await settled.sample(async () => ({ present: true }));
expect(reply.result()).toMatchObject({ event: "first_visible_response", verdict: "inconclusive", lastAbsentMs: 1_901, firstPresentMs: 2_427,
messageTimestamps: { createdAt: "2026-01-01T00:00:00Z", editedAt: "2026-01-01T00:00:10Z" } });
expect(settled.result()).toMatchObject({ event: "final_settlement", lastAbsentMs: null, firstPresentMs: 53_334 });
});
it("uses poll start for absence and poll completion for presence", async () => {
let clock = 0;
const probe = armLatencyProbe({ event: "acknowledgement", target, resolutionMs: 0, now: () => clock });
clock = 1_901;
await probe.sample(async () => { clock = 2_100; return { present: false }; });
clock = 2_200;
await probe.sample(async () => { clock = 2_427; return { present: true }; });
expect(probe.result()).toMatchObject({ verdict: "inconclusive", lastAbsentMs: 1_901, firstPresentMs: 2_427 });
});
it("does not count failed reads as absence or use wall-clock message metadata", async () => {
let clock = 0;
const probe = armLatencyProbe({ event: "acknowledgement", target, resolutionMs: 0, now: () => clock });
clock = 3_000;
await expect(probe.sample(async () => { throw new Error("unavailable"); })).rejects.toThrow("unavailable");
expect(probe.result()).toMatchObject({ verdict: "inconclusive", lastAbsentMs: null, firstPresentMs: null });
await probe.sample(async () => ({ present: true, messageTimestamps: { createdAt: "invalid", editedAt: "2099-01-01" } }));
expect(probe.result()).toMatchObject({ verdict: "inconclusive", firstPresentMs: 3_000 });
});
it("rejects a backwards clock instead of clamping latency to zero", async () => {
let clock = 100;
const probe = armLatencyProbe({ event: "acknowledgement", target, resolutionMs: 0, now: () => clock });
clock = 99;
await probe.sample(async () => ({ present: true }));
expect(probe.result()).toMatchObject({ verdict: "inconclusive", reason: "invalid_evidence" });
});
});
+130
View File
@@ -0,0 +1,130 @@
/** Probe timing is an interval, never the timestamp of the poll that found it. */
export type LatencyEvent = "acknowledgement" | "first_visible_response" | "message_creation" | "final_settlement";
export type LatencyVerdict = "pass" | "fail" | "inconclusive";
export interface TimingBound { ms: number; inclusive: boolean }
export interface LatencyTarget { operator: "<" | "<="; ms: number }
export interface LatencyEvidence {
event: LatencyEvent;
interval: { lower: TimingBound | null; upper: TimingBound | null };
lastAbsentMs: number | null;
firstPresentMs: number | null;
/** Conservative uncertainty on each elapsed observation; null means unknown. */
resolutionMs: number | null;
invalid?: boolean;
observationFailures?: number;
/** Reads abandoned when the browser observer's shutdown deadline expires. */
observationTimeouts?: number;
/** Provider wall-clock metadata is retained separately and never measures visibility. */
messageTimestamps?: { createdAt?: string; editedAt?: string };
}
export interface LatencyResult extends LatencyEvidence {
target: LatencyTarget;
verdict: LatencyVerdict;
/** Original retained verdict, if report regeneration corrects the timing grade. */
recordedVerdict?: LatencyVerdict;
reason: "within_target" | "outside_target" | "threshold_overlap" | "missing_observations" | "invalid_evidence" | "unknown_resolution";
}
const nonnegative = (value: unknown): value is number =>
typeof value === "number" && Number.isFinite(value) && value >= 0;
export function classifyLatency(evidence: LatencyEvidence, target: LatencyTarget): LatencyResult {
const { lower, upper } = evidence.interval ?? {};
const result = (verdict: LatencyVerdict, reason: LatencyResult["reason"]): LatencyResult =>
({ ...evidence, target, verdict, reason });
const validBound = (bound: TimingBound | null) => bound === null
|| (bound && nonnegative(bound.ms) && typeof bound.inclusive === "boolean");
if (evidence.invalid || !nonnegative(target.ms) || !["<", "<="].includes(target.operator)
|| !validBound(lower) || !validBound(upper)
|| (evidence.lastAbsentMs !== null && !nonnegative(evidence.lastAbsentMs))
|| (evidence.firstPresentMs !== null && !nonnegative(evidence.firstPresentMs))
|| (evidence.lastAbsentMs !== null && evidence.firstPresentMs !== null && evidence.lastAbsentMs > evidence.firstPresentMs)
|| (lower && upper && (lower.ms > upper.ms || (lower.ms === upper.ms && !(lower.inclusive && upper.inclusive))))
|| (evidence.resolutionMs !== null && !nonnegative(evidence.resolutionMs))) {
return result("inconclusive", "invalid_evidence");
}
if (evidence.resolutionMs === null) return result("inconclusive", "unknown_resolution");
if (!lower && !upper) return result("inconclusive", "missing_observations");
if (upper && (upper.ms < target.ms || (upper.ms === target.ms && (target.operator === "<=" || !upper.inclusive)))) {
return result("pass", "within_target");
}
if (lower && (lower.ms > target.ms || (lower.ms === target.ms && (target.operator === "<" || !lower.inclusive)))) {
return result("fail", "outside_target");
}
return result("inconclusive", "threshold_overlap");
}
export function observationTiming(input: Omit<LatencyEvidence, "interval">): LatencyEvidence {
const uncertainty = input.resolutionMs ?? 0;
return {
...input,
interval: {
lower: input.lastAbsentMs === null ? null : { ms: Math.max(0, input.lastAbsentMs - uncertainty), inclusive: input.lastAbsentMs - uncertainty < 0 },
upper: input.firstPresentMs === null ? null : { ms: input.firstPresentMs + uncertainty, inclusive: true },
},
};
}
/** Arm immediately before dispatch. Inject a monotonic clock for deterministic tests.
* The predicate must describe a persistent event (or a latched event observer).
* A missing/failed read is not an absent observation. */
export function armLatencyProbe(input: {
event: LatencyEvent;
target: LatencyTarget;
resolutionMs: number;
now?: () => number;
}) {
const now = input.now ?? (() => performance.now());
const start = now();
let lastClock = start;
let invalid = !Number.isFinite(start);
let lastAbsentMs: number | null = null;
let firstPresentMs: number | null = null;
let observationFailures = 0;
let messageTimestamps: LatencyEvidence["messageTimestamps"];
const elapsed = () => {
const current = now();
if (!Number.isFinite(current) || current < lastClock) invalid = true;
lastClock = current;
return current - start;
};
type Observation = { present: boolean; messageTimestamps?: LatencyEvidence["messageTimestamps"] };
const beginObservation = () => {
const before = elapsed();
let completed = false;
return (observation: Observation) => {
if (completed) throw new Error("Observation already completed");
completed = true;
const after = elapsed();
if (typeof observation.present !== "boolean" || after < before) invalid = true;
// Concurrent or late reads cannot replace the first positive observation.
if (firstPresentMs !== null) return;
if (observation.present) {
firstPresentMs = after;
messageTimestamps = observation.messageTimestamps;
} else {
lastAbsentMs = Math.max(lastAbsentMs ?? 0, before);
}
};
};
return {
beginObservation,
async sample(load: () => Promise<Observation>) {
if (firstPresentMs !== null) return;
const complete = beginObservation();
try { complete(await load()); }
catch (error) { observationFailures++; throw error; }
},
result(): LatencyResult {
return classifyLatency(observationTiming({ event: input.event, lastAbsentMs, firstPresentMs,
resolutionMs: input.resolutionMs, ...(invalid ? { invalid: true } : {}),
...(observationFailures ? { observationFailures } : {}),
...(messageTimestamps ? { messageTimestamps } : {}) }), input.target);
},
};
}
export function latencyIntervalLabel(evidence: LatencyEvidence): string {
const { lower, upper } = evidence.interval;
return `${lower?.inclusive ? "[" : "("}${lower?.ms ?? "unknown"}, ${upper?.ms ?? "unknown"}${upper?.inclusive ? "]" : ")"} ms`;
}
@@ -0,0 +1,85 @@
import { describe, expect, it } from "vitest";
import { runnerMatrix } from "./catalog.js";
import { continuationScenario, necessaryQuestionContract } from "./continuation-cases.js";
import { gradeNecessaryQuestion } from "./necessary-question.js";
import { gradeContinuation, type ContinuationCheckpoint } from "./continuation-scoring.js";
import { gradeLifecycleBaseline } from "./lifecycle-baseline.js";
type Row = Record<string, any>;
function recording(provider = false): any[] {
const contract = necessaryQuestionContract(), scenario = continuationScenario("necessary-free-text", "nonce");
const card: Row = { id: "card", kind: "ask_user_questions", status: "pending", companyId: "company", issueId: "task", createdByAgentId: "agent",
sourceRunId: "first", continuationPolicy: provider ? "none" : "wake_assignee",
idempotencyKey: provider ? "paperclip-runner-question:first:request" : "question",
payload: { ...(provider ? { runtimeRequestId: "request" } : {}), questionSet: { schema: "paperclip.question_set.v1", description: contract.context,
questions: [{ id: "reference", answerMode: "text", prompt: contract.prompt, required: true }] } } };
const run: Row = { id: "first", companyId: "company", issueId: "task", agentId: "agent", status: provider ? "running" : "succeeded", runtimeMode: "native",
nativePhase: provider ? "running" : "committed", resultJson: { finalizationPhase: provider ? null : "committed", semanticToolReceipts: provider ? {} : {
question: { operationId: "request_human_input", result: { disposition: "applied", interaction: structuredClone(card) } },
} } };
const initial = { phase: "initial", issue: { id: "task", companyId: "company", assigneeAgentId: "agent", status: provider ? "in_progress" : "in_review" },
lifecycle: { executionRunId: provider ? "first" : null, scheduledRetry: null, activeRecoveryAction: null, monitorNextCheckAt: null },
runs: [run], interactions: [card], documents: [], children: [], attachments: [], comments: [],
questionPresentation: { interactionId: "card", promptVisible: true, contextVisible: true } };
const final = structuredClone(initial) as Row;
final.phase = "final"; final.issue.status = "done"; final.lifecycle.executionRunId = null;
final.interactions[0] = { ...final.interactions[0], status: "answered", resolvedByUserId: "local-board",
result: { answers: [{ questionId: "reference", optionIds: [], otherText: scenario.answer }] } };
if (provider) { final.runs[0].status = "succeeded"; final.runs[0].nativePhase = "committed"; final.runs[0].resultJson.finalizationPhase = "committed"; }
else final.runs.push({ ...structuredClone(run), id: "second", contextSnapshot: { issueId: "task", sourceRunId: "first",
interactionId: "card", interactionKind: "ask_user_questions", interactionStatus: "answered" } });
final.outputDocument = { issueId: "task", key: "confirmation", revisionId: "revision", href: "/QA/issues/task#document-confirmation", visibleBody: `Booking confirmed: ${scenario.answer}` };
final.documents = [{ key: "confirmation", body: `Booking confirmed: ${scenario.answer}`, latestRevisionId: "revision" }];
return [structuredClone(initial), final];
}
const failures = (points: ContinuationCheckpoint[]) => gradeNecessaryQuestion(points, continuationScenario("necessary-free-text", "nonce").answer).filter(check => !check.passed);
describe("necessary free-text question journey", () => {
it.each([false, true])("accepts one exactly bound answer on the provider path=%s", provider => {
const points = recording(provider);
expect(failures(points)).toEqual([]);
expect(gradeContinuation({ ...continuationScenario("necessary-free-text", "nonce"), runtimeMode: "native", checkpoints: points }).filter(check => !check.passed)).toEqual([]);
expect(gradeLifecycleBaseline(points).filter(check => !check.passed)).toEqual([]);
});
it.each([
["title without question", (points: Row[]) => { points[0].interactions[0].payload.questionSet.questions[0].prompt = "Booking reference"; }],
["missing explanation", (points: Row[]) => { delete points[0].interactions[0].payload.questionSet.description; }],
["choice instead of text", (points: Row[]) => { points[0].interactions[0].payload.questionSet.questions[0].answerMode = "single_select"; }],
["optional question", (points: Row[]) => { points[0].interactions[0].payload.questionSet.questions[0].required = false; }],
["hidden context", (points: Row[]) => { points[0].questionPresentation.contextVisible = false; }],
["unrelated rendered card", (points: Row[]) => { points[0].questionPresentation.interactionId = "foreign"; }],
["missing semantic receipt", (points: Row[]) => { points[0].runs[0].resultJson.semanticToolReceipts = {}; }],
["foreign question", (points: Row[]) => { points[0].interactions[0].companyId = "foreign"; }],
["foreign resolved company", (points: Row[]) => { points[1].interactions[0].companyId = "foreign"; }],
["changed creator", (points: Row[]) => { points[1].interactions[0].createdByAgentId = "foreign"; }],
["changed idempotency key", (points: Row[]) => { points[1].interactions[0].idempotencyKey = "foreign"; }],
["changed continuation policy", (points: Row[]) => { points[1].interactions[0].continuationPolicy = "none"; }],
["fabricated agent answer", (points: Row[]) => { points[1].interactions[0].resolvedByAgentId = "agent"; }],
["wrong answer", (points: Row[]) => { points[1].interactions[0].result.answers[0].otherText = "invented"; }],
["duplicate answer", (points: Row[]) => { points[1].interactions[0].result.answers.push(points[1].interactions[0].result.answers[0]); }],
["changed question", (points: Row[]) => { points[1].interactions[0].payload.questionSet.questions[0].prompt = "Different question"; }],
["duplicate wake", (points: Row[]) => { points[1].runs.push({ ...points[1].runs[1], id: "third" }); }],
["wrong wake binding", (points: Row[]) => { points[1].runs[1].contextSnapshot.interactionId = "foreign"; }],
["uncommitted result", (points: Row[]) => { points[1].runs[1].resultJson.finalizationPhase = "pending"; }],
["wrong saved result", (points: Row[]) => { points[1].documents[0].body = "Claimed complete"; }],
["inaccessible result", (points: Row[]) => { delete points[1].outputDocument; }],
["foreign linked task", (points: Row[]) => { points[1].outputDocument.issueId = "foreign"; }],
["different linked document", (points: Row[]) => { points[1].outputDocument.key = "other"; }],
["stale linked revision", (points: Row[]) => { points[1].outputDocument.revisionId = "older"; }],
["wrong displayed content", (points: Row[]) => { points[1].outputDocument.visibleBody = "Claimed complete"; }],
["duplicate question", (points: Row[]) => { points[1].interactions.push(points[1].interactions[0]); }],
] as const)("rejects %s", (_name, change) => { const points = recording(); change(points); expect(failures(points).length).toBeGreaterThan(0); });
it("does not put the required answer in the original request", () => {
const scenario = continuationScenario("necessary-free-text", "nonce");
expect(scenario.prompt).not.toContain(scenario.marker);
expect(scenario.prompt).toContain("has not been supplied");
});
it("registers four explicit development cells without claiming staging or Slack proof", () => {
const cells = runnerMatrix.filter(cell => cell.suite.id === "necessary-question");
expect(cells).toHaveLength(4);
expect(new Set(cells.map(cell => cell.profile.model))).toEqual(new Set(["gpt-6.1-sol", "claude-opus-5-5"]));
expect(cells.every(cell => cell.suite.manualOnly && cell.suite.groups.length === 0 && cell.suite.definitionMetadata?.stagingQualification === false && cell.task.automaticRetryPolicy === "single_attempt")).toBe(true);
expect(runnerMatrix.filter(cell => cell.suite.id === "question-resume")).toHaveLength(2);
expect(runnerMatrix.filter(cell => cell.suite.id === "continuation")).toHaveLength(23);
});
});
+54
View File
@@ -0,0 +1,54 @@
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { continuationScenario, continuationTasks, necessaryQuestionContract } from "./continuation-cases.js";
import { questionResumedOnce, sameQuestion, validResumeQuestionForm } from "./question-resume-scoring.js";
import type { ContinuationCheckpoint } from "./continuation-scoring.js";
import type { RunnerTaskFixture } from "./types.js";
type Row = Record<string, any>;
export const necessaryQuestionTask: RunnerTaskFixture = {
...continuationTasks.find(task => task.id === "answer-updates-scope")!,
id: "necessary-free-text", label: "Necessary free-text question and one continuation",
expectedRunCount: 2, minimumExpectedRunCount: 1, attemptTimeoutMs: { local: 10 * 60_000, daytona: 15 * 60_000 }, turnTimeoutMs: 4 * 60_000,
buildTitle: nonce => `Booking confirmation ${nonce}`,
buildPrompt: nonce => continuationScenario("necessary-free-text", nonce).prompt,
buildVisibleMarker: nonce => continuationScenario("necessary-free-text", nonce).marker,
};
export function gradeNecessaryQuestion(checkpoints: ContinuationCheckpoint[], answer: string) {
const initial = checkpoints.find(point => point.phase === "initial"), final = checkpoints.find(point => point.phase === "final");
const cards = (point?: ContinuationCheckpoint) => (point?.interactions ?? []).filter((value: any) => value.kind === "ask_user_questions") as Row[];
const pending = cards(initial), finished = cards(final), question = pending[0], resolved = finished[0];
const field = question?.payload?.questionSet?.questions?.[0], contract = necessaryQuestionContract();
const check = (id: string, passed: boolean, detail: string) => ({ id, passed: Boolean(passed), detail });
const response = resolved?.result?.answers, document = final?.documents[0], navigation = final?.outputDocument;
return [
check("necessary-question.form", checkpoints.length === 2 && !!initial && pending.length === 1 && question?.status === "pending" && initial.documents.length === 0 && initial.attachments.length === 0 &&
validResumeQuestionForm(initial, question, false) && field?.required === true && field.prompt === contract.prompt &&
[question.payload.questionSet.description, field.helpText].includes(contract.context),
"One bound text-only question must show the required prompt and its explanation before delivery."),
check("necessary-question.visible", initial?.questionPresentation?.interactionId === question?.id &&
initial?.questionPresentation?.promptVisible === true && initial?.questionPresentation?.contextVisible === true,
"The browser must observe the actual question and context, not only its title."),
check("necessary-question.answer", finished.length === 1 && !!question && sameQuestion(question, resolved) && resolved?.status === "answered" &&
typeof resolved.resolvedByUserId === "string" && !!resolved.resolvedByUserId && !resolved.resolvedByAgentId &&
Array.isArray(response) && response.length === 1 && response[0]?.questionId === field?.id && response[0]?.otherText === answer &&
!response[0]?.optionIds?.length,
"One real user answer must remain attached to the unchanged question."),
check("necessary-question.one-continuation", questionResumedOnce(initial, final, question) && !!final && final.runs.length <= 2 &&
final.runs.every((run: Row) => run.status === "succeeded" && run.runtimeMode === "native" && run.nativePhase === "committed" && run.resultJson?.finalizationPhase === "committed"),
"The answer must resume its bound provider run or admit exactly one bound semantic successor, without duplicate work."),
check("necessary-question.result", final?.issue.status === "done" && final.documents.length === 1 &&
!!document?.latestRevisionId && document?.body.trim() === `Booking confirmed: ${answer}` &&
navigation?.issueId === final.issue.id && navigation?.key === document.key && navigation?.revisionId === document.latestRevisionId &&
navigation?.visibleBody === document.body.trim(),
"The final reply must open this task's saved confirmation and show its exact answer."),
];
}
export function necessaryQuestionDigest() {
const hash = createHash("sha256");
for (const file of ["necessary-question.ts", "continuation-cases.ts", "continuation-flow.ts", "continuation-scoring.ts", "question-resume-scoring.ts", "stock-harness.ts"])
hash.update(file).update(readFileSync(new URL(file, import.meta.url)));
return hash.digest("hex");
}
@@ -180,7 +180,7 @@ describe("Pi controls catalog admission", () => {
expect(cells).toHaveLength(4); expect(cells.every(c => c.profile.qualificationCandidate === "pi" && c.task.expectedRunCount === 1)).toBe(true);
expect(cells.map(c => `${c.environment.id}/${c.task.id}`).sort()).toEqual(["daytona/pending-permission-stop", "daytona/same-turn-steering", "local/pending-permission-stop", "local/same-turn-steering"]);
expect(selectRunnerExecutions(parseRunnerSelectors(["--all"])).some(c => c.suite.id === "pi-controls")).toBe(false);
expect(validateRunnerCatalog()).toHaveLength(752);
expect(validateRunnerCatalog()).toHaveLength(756);
for (const cell of cells) expect(buildRunnerE2EProcessEnvironment({}, [cell]).PAPERCLIP_RUNNER_ACPX_QUALIFICATION).toBeUndefined();
expect(() => assertRemoteNativeEvidencePrerequisites(cells, {})).toThrow();
});
+3 -3
View File
@@ -46,14 +46,14 @@ export function validResumeQuestionForm(checkpoint: ContinuationCheckpoint, i: R
(fields.length === 1 || (path === "provider" && isSingleClaudeQuestion(fields)));
}
function sameQuestion(a: Row, b?: Row) {
export function sameQuestion(a: Row, b?: Row) {
return !!b && ["id", "kind", "sourceRunId", "issueId", "companyId", "createdByAgentId", "idempotencyKey", "continuationPolicy"]
.every(k => a[k] === b[k]) && isDeepStrictEqual(a.payload, b.payload);
}
function answeredByUser(i?: Row) {
return i?.status === "answered" && nonempty(i.resolvedByUserId) && !i.resolvedByAgentId;
}
function resumedRun(before: ContinuationCheckpoint | undefined, after: ContinuationCheckpoint | undefined, i?: Row) {
export function questionResumedOnce(before: ContinuationCheckpoint | undefined, after: ContinuationCheckpoint | undefined, i?: Row) {
if (!before || !after || !i) return false;
const path = questionPath(before, i);
const prior = new Set(before.runs.map(r => r.id));
@@ -112,7 +112,7 @@ export function gradeQuestionResume(checkpoints: ContinuationCheckpoint[], marke
detail: "The real board answer must remain bound to the unchanged question, with no fabricated or substituted response.",
}, {
id: `question-resume.${answer}.path`,
passed: resumedRun(before, after, i),
passed: questionResumedOnce(before, after, i),
detail: `Observed path: ${questionPath(before, i) ?? "unknown"}; provider answers resume the same run; semantic answers require one exactly bound response wake.`,
});
}
+36
View File
@@ -6,6 +6,7 @@ import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { STARTUP_REUSE_LAYERS, STARTUP_REUSE_SCHEMA, summarizeStartupReuse } from "../../packages/adapter-utils/src/startup-reuse.js";
import type { RunnerE2EResult } from "./types.js";
import { classifyLatency, observationTiming } from "./latency-timing.js";
const execFileAsync = promisify(execFile);
const repositoryRoot = path.resolve(import.meta.dirname, "../..");
@@ -19,6 +20,41 @@ afterEach(async () => {
});
describe("runner E2E report aggregation", () => {
it("corrects overconfident timing verdicts without changing retained bounds or original files", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "runner-latency-report-"));
cleanupDirectories.push(root);
const executionId = "core-compatibility.runner-codex.local.message-marker";
const directory = path.join(root, "attempt-1");
await mkdir(directory);
const acknowledgement = classifyLatency(observationTiming({ event: "acknowledgement", lastAbsentMs: 1_901, firstPresentMs: 2_427, resolutionMs: 0 }), { operator: "<=", ms: 2_000 });
const response = classifyLatency(observationTiming({ event: "first_visible_response", lastAbsentMs: 52_965, firstPresentMs: 53_334, resolutionMs: 0 }), { operator: "<=", ms: 30_000 });
const result: RunnerE2EResult = {
schema: "paperclip.runner-e2e.result/v2", executionId, suiteId: "core-compatibility", attempt: 1, status: "passed",
profileId: "runner-codex", environmentId: "local", caseId: "message-marker", provider: "codex", model: "fixture-model", runtimeMode: "native",
startedAt: "2026-10-18T00:00:00Z", finishedAt: "2026-10-18T00:01:00Z", durationMs: 60_000, cleanup: "passed",
// An old report called the crossing interval a failure (or a pass).
latencyProbes: [{ ...acknowledgement, verdict: "fail", reason: "outside_target", observationTimeouts: 1 }, { ...acknowledgement, verdict: "pass", reason: "within_target" }, response],
};
const raw = JSON.stringify(result);
await writeFile(path.join(directory, "result.json"), raw);
await writeFile(path.join(directory, "final-state.png"), "fixture-png");
await writeFile(path.join(directory, "evidence-manifest.json"), JSON.stringify({ files: ["final-state.png"], leaks: [], missing: [] }));
const output = path.join(root, "merged");
await execFileAsync(process.execPath, [path.join(repositoryRoot, "cli/node_modules/tsx/dist/cli.mjs"), path.join(repositoryRoot, "tests/runner-e2e/report.ts")], {
cwd: repositoryRoot, env: { ...process.env, PAPERCLIP_RUNNER_E2E_REPORT_ROOT: root, PAPERCLIP_RUNNER_E2E_REPORT_OUT: output, PAPERCLIP_RUNNER_E2E_EXPECTED_IDS: JSON.stringify([executionId]) },
});
const normalized = JSON.parse(await readFile(path.join(output, "normalized-results.json"), "utf8"));
expect(normalized.results[0].latencyProbes).toEqual([
{ ...acknowledgement, recordedVerdict: "fail", observationTimeouts: 1 }, { ...acknowledgement, recordedVerdict: "pass" }, response,
]);
expect(normalized.passed).toBe(1);
expect(await readFile(path.join(directory, "result.json"), "utf8")).toBe(raw);
const dashboard = await readFile(path.join(output, "index.html"), "utf8");
expect(dashboard).toContain("inconclusive");
expect(dashboard).toContain("(1901, 2427] ms");
expect(dashboard).toContain("(52965, 53334] ms");
expect(dashboard).toContain("first_visible_response");
});
it("retains full reuse and a workspace-only invalidation in the generated report", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "runner-reuse-report-"));
cleanupDirectories.push(root);
@@ -58,7 +58,19 @@ export async function runRepositoryVerificationFlow(input: {
}
async function save(phase: string) {
const checks = gradeRepositoryProof(observation);
if (issue) input.observe(issue, runs, checks);
if (issue) {
input.observe(issue, runs, checks);
await input.evidence("api-state.json", {
capturePhase: phase,
issue: { id: issue.id, companyId: issue.companyId, identifier: issue.identifier, status: issue.status,
assigneeAgentId: issue.assigneeAgentId, executionWorkspaceId: issue.executionWorkspaceId, statusVersion: issue.statusVersion },
runs: runs.map(run => ({ id: run.id, companyId: run.companyId, agentId: run.agentId, issueId: run.issueId,
nativeIssueId: run.nativeIssueId, status: run.status, runtimeMode: run.runtimeMode, nativePhase: run.nativePhase,
startedAt: run.startedAt, finishedAt: run.finishedAt, errorCode: run.errorCode,
finalizationPhase: run.resultJson?.finalizationPhase })),
checks,
});
}
await input.evidence("repository-verification.json", { schema: "paperclip.e2e.repository-verification.v1", phase, observation, checks,
runs: runs.map(run => ({ id: run.id, status: run.status, nativePhase: run.nativePhase, source: run.contextSnapshot?.source,
startedAt: run.startedAt, finishedAt: run.finishedAt })) });
+119
View File
@@ -0,0 +1,119 @@
import type { Page } from "@playwright/test";
import { afterEach, expect, it, vi } from "vitest";
import { armResponseTiming } from "./response-timing.js";
afterEach(() => { vi.unstubAllGlobals(); vi.useRealTimers(); });
it("starts observation before dispatch and drains without overlapping reads on teardown", async () => {
vi.useFakeTimers();
const absent = { response: false, acknowledgement: false };
let complete: (value: typeof absent) => void = () => {};
const read = vi.fn().mockImplementationOnce(() => new Promise<typeof absent>(resolve => { complete = resolve; }))
.mockResolvedValue(absent);
const locator = { getByTestId: () => locator, evaluateAll: read };
const timing = armResponseTiming({ page: locator as unknown as Page });
expect(read).toHaveBeenCalledTimes(1); // No dispatch or awaited acknowledgement yet.
await vi.advanceTimersByTimeAsync(1_000);
expect(read).toHaveBeenCalledTimes(1);
let stopped = false;
const stopping = timing.stop().then(() => { stopped = true; });
await vi.advanceTimersByTimeAsync(500);
expect(stopped).toBe(false);
expect(read).toHaveBeenCalledTimes(1);
complete(absent);
await stopping;
expect(stopped).toBe(true);
// One final read after draining the in-flight request, then no more ticks.
expect(read).toHaveBeenCalledTimes(2);
await vi.advanceTimersByTimeAsync(1_000);
expect(read).toHaveBeenCalledTimes(2);
});
it.each(["pending", "final"])("bounds shutdown when the %s read never returns and ignores late completions", async phase => {
vi.useFakeTimers();
const absent = { response: false, acknowledgement: false };
let complete!: (value: typeof absent) => void;
const stuck = () => new Promise<typeof absent>(resolve => { complete = resolve; });
const read = phase === "pending" ? vi.fn(stuck) : vi.fn().mockResolvedValueOnce(absent).mockImplementation(stuck);
const locator = { getByTestId: () => locator, evaluateAll: read };
const timing = armResponseTiming({ page: locator as unknown as Page, acknowledgementTarget: { operator: "<=", ms: 2_000 } });
await vi.advanceTimersByTimeAsync(0);
const stopping = timing.stop();
await vi.advanceTimersByTimeAsync(1_000);
await stopping;
const evidence = timing.results();
for (const probe of evidence.filter(probe => probe.event !== "final_settlement")) {
expect(probe).toMatchObject({ verdict: "inconclusive", firstPresentMs: null, observationFailures: 1, observationTimeouts: 1 });
expect(probe.lastAbsentMs).toBe(phase === "pending" ? null : 0);
}
expect(read).toHaveBeenCalledTimes(phase === "pending" ? 1 : 2);
complete({ response: true, acknowledgement: true });
await vi.advanceTimersByTimeAsync(1_000);
await timing.stop();
await timing.observeResponse(locator as unknown as Page);
expect(timing.results()).toEqual(evidence);
expect(read).toHaveBeenCalledTimes(phase === "pending" ? 1 : 2);
expect(vi.getTimerCount()).toBe(0);
});
it("observes edited visible text separately from a platform acknowledgement and final settlement", async () => {
let clock = 0;
let receipt = true;
let text = "Platform receipt";
let visible = true;
vi.stubGlobal("getComputedStyle", () => ({ display: "block", visibility: visible ? "visible" : "hidden" }));
const element = {
closest: () => ({ querySelector: () => receipt ? {} : null }),
get textContent() { return text; },
getClientRects: () => visible ? [{}] : [],
};
const locator = {
getByTestId: () => locator,
evaluateAll: async (read: (elements: Element[]) => unknown) => read([element as unknown as Element]),
};
const page = locator as unknown as Page;
const timing = armResponseTiming({ now: () => clock });
clock = 1_900;
const firstSettlement = timing.beginSettlement();
await timing.observeResponse(page);
firstSettlement({ present: false });
// The same message now has substantive content; creation time does not
// change. Hidden content is still not a visible response.
receipt = false;
text = "The answer";
visible = false;
clock = 29_900;
await timing.observeResponse(page);
visible = true;
clock = 30_100;
await timing.observeResponse(page);
clock = 52_965;
const lastAbsent = timing.beginSettlement();
clock = 53_000;
lastAbsent({ present: false });
const present = timing.beginSettlement();
clock = 53_334;
present({ present: true });
expect(timing.results()).toMatchObject([
{ event: "first_visible_response", verdict: "inconclusive", lastAbsentMs: 29_900, firstPresentMs: 30_100, resolutionMs: 1 },
{ event: "final_settlement", verdict: "fail", lastAbsentMs: 52_965, firstPresentMs: 53_334 },
]);
});
it("uses the same observation bounds for an explicitly expected acknowledgement", async () => {
let clock = 0;
let acknowledgement = false;
const read = vi.fn(async () => ({ response: false, acknowledgement }));
const locator = { getByTestId: () => locator, evaluateAll: read };
const timing = armResponseTiming({ now: () => clock, acknowledgementTarget: { operator: "<=", ms: 2_000 } });
clock = 1_901;
await timing.observeResponse(locator as unknown as Page);
clock = 2_427;
acknowledgement = true;
await timing.observeResponse(locator as unknown as Page);
expect(read).toHaveBeenCalledTimes(2); // One snapshot per poll shared by both events.
expect(timing.results().find(probe => probe.event === "acknowledgement")).toMatchObject({
verdict: "inconclusive", lastAbsentMs: 1_901, firstPresentMs: 2_427,
interval: { lower: { ms: 1_900, inclusive: false }, upper: { ms: 2_428, inclusive: true } },
});
});
+102
View File
@@ -0,0 +1,102 @@
import type { Page } from "@playwright/test";
import { armLatencyProbe, type LatencyTarget } from "./latency-timing.js";
/** Browser observations, independent of provider creation/edit timestamps and run settlement.
* Used only by the explicit latency diagnostic; these are not Slack delivery probes. */
export function armResponseTiming(input: { now?: () => number; page?: Page; acknowledgementTarget?: LatencyTarget } = {}) {
const common = { now: input.now, resolutionMs: 1 };
const response = armLatencyProbe({ ...common, event: "first_visible_response", target: { operator: "<=", ms: 30_000 } });
const settlement = armLatencyProbe({ ...common, event: "final_settlement", target: { operator: "<=", ms: 30_000 } });
// Only measure acknowledgement when the caller expects that feature. The
// Daytona response diagnostic does not enable receipts or measure Slack.
const acknowledgement = input.acknowledgementTarget
? armLatencyProbe({ ...common, event: "acknowledgement", target: input.acknowledgementTarget }) : undefined;
const shutdown = new AbortController();
const timeouts = new Map<ReturnType<typeof armLatencyProbe>, number>();
let stopped = false;
const observeResponse = async (page: Page) => {
if (stopped || shutdown.signal.aborted) return;
const observing = [response, ...(acknowledgement ? [acknowledgement] : [])]
.filter(probe => probe.result().firstPresentMs === null);
if (!observing.length) return;
const read = () => page.getByTestId("task-chat-thread")
.getByTestId("task-chat-agent-bubble").evaluateAll(elements => {
const visible = { response: false, acknowledgement: false };
for (const element of elements) {
// Receipt details live beside the bubble inside its thread anchor.
// A platform receipt is not the agent's first substantive response.
const anchor = element.closest("[data-thread-anchor]");
const receipt = anchor?.querySelector('[aria-label="Message details"]');
const style = getComputedStyle(element);
if (element.textContent?.trim() && element.getClientRects().length > 0
&& style.visibility !== "hidden" && style.display !== "none") {
if (receipt) visible.acknowledgement = true;
else visible.response = true;
}
}
return visible;
});
let resolve!: (value: Awaited<ReturnType<typeof read>>) => void;
let reject!: (error: unknown) => void;
const snapshot = new Promise<Awaited<ReturnType<typeof read>>>((yes, no) => { resolve = yes; reject = no; });
// Both lower observations must precede the shared read, including the
// second probe. Starting that probe after read() could overstate absence.
const samples = Promise.all([
response.sample(async () => ({ present: (await snapshot).response })),
acknowledgement?.sample(async () => ({ present: (await snapshot).acknowledgement })),
]);
const abandon = () => {
for (const probe of observing) timeouts.set(probe, (timeouts.get(probe) ?? 0) + 1);
// Reject the shared snapshot so a late browser completion cannot alter
// evidence after cleanup resumes. The browser operation itself may still
// be running; do not start another read after this deadline.
reject(new Error("Latency observer shutdown deadline exceeded"));
};
shutdown.signal.addEventListener("abort", abandon, { once: true });
try {
try { read().then(resolve, reject); } catch (error) { reject(error); }
await samples;
} finally {
shutdown.signal.removeEventListener("abort", abandon);
}
};
let pending: Promise<void> | undefined;
let timer: ReturnType<typeof setInterval> | undefined;
let stopping: Promise<void> | undefined;
if (input.page) {
const page = input.page;
const tick = () => {
if (pending) return;
// A failed browser read is counted by the probe, never recorded as absence.
pending = observeResponse(page).catch(() => {}).finally(() => {
pending = undefined;
if (response.result().firstPresentMs !== null
&& (!acknowledgement || acknowledgement.result().firstPresentMs !== null)) clearInterval(timer);
});
};
tick(); // The first read starts before dispatch, not after its acknowledgement.
timer = setInterval(tick, 100);
}
return {
beginSettlement: () => settlement.beginObservation(),
observeResponse,
stop() {
return stopping ??= (async () => {
clearInterval(timer);
// One deadline covers both draining and the final observation. Abort
// only our snapshot wait, leaving page teardown to the fixture owner.
const deadline = setTimeout(() => shutdown.abort(), 1_000);
try {
await pending;
if (input.page) await observeResponse(input.page).catch(() => {});
} finally {
stopped = true;
clearTimeout(deadline);
}
})();
},
results: () => [response, settlement, ...(acknowledgement ? [acknowledgement] : [])].map(probe => ({
...probe.result(), ...(timeouts.has(probe) ? { observationTimeouts: timeouts.get(probe)! } : {}),
})),
};
}
+21
View File
@@ -66,6 +66,26 @@ const date: Rule = (value, at) => {
string(value, at);
if (!Number.isFinite(Date.parse(value as string))) invalid(at);
};
const finite: Rule = (value, at) => {
if (typeof value !== "number" || !Number.isFinite(value)) invalid(at);
};
// Finite invalid bounds remain inspectable evidence with an inconclusive verdict.
const timingBound = nullable(shape({ ms: finite, inclusive: boolean }));
const latencyProbe: Rule = (value, at) => {
shape({
event: oneOf("acknowledgement", "first_visible_response", "message_creation", "final_settlement"),
interval: shape({ lower: timingBound, upper: timingBound }),
lastAbsentMs: nullable(finite), firstPresentMs: nullable(finite), resolutionMs: nullable(finite),
invalid: optional(boolean),
observationFailures: optional(integer),
observationTimeouts: optional(integer),
messageTimestamps: optional(shape({ createdAt: optional(string), editedAt: optional(string) })),
target: shape({ operator: oneOf("<", "<="), ms: finite }),
verdict: oneOf("pass", "fail", "inconclusive"),
recordedVerdict: optional(oneOf("pass", "fail", "inconclusive")),
reason: oneOf("within_target", "outside_target", "threshold_overlap", "missing_observations", "invalid_evidence", "unknown_resolution"),
})(value, at);
};
const url: Rule = (value, at) => {
string(value, at);
try {
@@ -211,6 +231,7 @@ const fields = {
revisionAtStart: matching(/^[a-f0-9]{40}$/), revisionAtEnd: matching(/^[a-f0-9]{40}$/),
target: oneOf("isolated-candidate", "isolated-staging") }),
})),
latencyProbes: optional(array(latencyProbe)),
startupReuse: optional(array(shape({
runId: string,
warmth: oneOf("cold", "warm", "partial_reuse", "unknown"),
+10
View File
@@ -42,6 +42,7 @@ import { runRepositorySkillsFlow } from "./repository-skills.js";
import { captureStockHarness, gradeStockHarness, gradeStockHire } from "./stock-harness.js";
import { verifyStockHarnessPreflight, STOCK_PREFLIGHT_ENV } from "./stock-harness-admission.js";
import { createTaskThroughUi, submitTaskReply } from "./user-actions.js";
import { armResponseTiming } from "./response-timing.js";
import { projectFirstTaskResult, runFirstTaskFlow, setupFirstTaskFixtures } from "./first-task-flow.js";
import { runChatFlow } from "./chat-flow.js";
@@ -649,6 +650,7 @@ for (const execution of executions) {
};
let firstTaskEvidence: RunnerE2EResult["firstTask"];
let turnTimings: NonNullable<RunnerE2EResult["turnTimings"]> | undefined;
let responseTiming: ReturnType<typeof armResponseTiming> | undefined;
let startupReuse: RunnerE2EResult["startupReuse"];
const turnSubmissionTimesMs: number[] = [];
const screenshots: NonNullable<RunnerE2EResult["screenshots"]> = [];
@@ -1258,6 +1260,8 @@ for (const execution of executions) {
workMode: execution.task.workMode,
projectName: fixtures.project?.name,
requireExplicitTitle: execution.suite.id === "task-titles" && Boolean(title),
beforeDispatch: execution.suite.id === "codie-latency-diagnostic"
? () => { responseTiming = armResponseTiming({ page }); } : undefined,
});
turnSubmissionTimesMs.push(createdTask.submittedAtMs);
@@ -1310,6 +1314,7 @@ for (const execution of executions) {
);
const loadTaskState = async () => {
const observeSettlement = responseTiming?.beginSettlement();
const [currentIssue, runs, comments, interactions] = await Promise.all([
api.get<IssueRecord>(`/api/issues/${issue!.id}`),
api.get<RunRecord[]>(
@@ -1322,6 +1327,9 @@ for (const execution of executions) {
),
api.get<InteractionRecord[]>(`/api/issues/${issue!.id}/interactions`),
]);
observeSettlement?.({ present: currentIssue.status === execution.task.expectedTerminalState.issue
&& matchingRuns(runs, currentIssue).length >= minimumRunCount(execution.task)
&& matchingRuns(runs, currentIssue).every(run => TERMINAL_RUN_STATUSES.has(run.status)) });
return {
currentIssue,
taskRuns: matchingRuns(runs, currentIssue),
@@ -3038,6 +3046,7 @@ for (const execution of executions) {
}
}
} finally {
await responseTiming?.stop();
await reviewProvider?.close();
try {
await writeSanitizedJson(
@@ -3202,6 +3211,7 @@ for (const execution of executions) {
issueIdentifier: issue?.identifier ?? null,
runIds: selectedRuns.map((run) => run.id),
...(turnTimings ? { turnTimings } : {}),
...(responseTiming ? { latencyProbes: responseTiming.results() } : {}),
...(startupReuse ? { startupReuse } : {}),
startedAt,
finishedAt: new Date(finishedAtMs).toISOString(),
+1
View File
@@ -320,6 +320,7 @@ export interface RunnerE2EResult {
issueId?: string;
issueIdentifier?: string | null;
runIds?: string[];
latencyProbes?: import("./latency-timing.js").LatencyResult[];
startupReuse?: Array<{ runId: string } & import("../../packages/adapter-utils/src/startup-reuse.js").StartupReuseSummary>;
turnTimings?: Array<{
turn: number;
+3 -1
View File
@@ -28,6 +28,7 @@ export async function createTaskThroughUi(input: {
projectName?: string;
requireExplicitTitle?: boolean;
attachments?: readonly string[];
beforeDispatch?: () => void;
}) {
// Search's public creation action exposes the explicit title field. Strict
// native-operation fixtures use it so automatic task naming is not an extra
@@ -92,7 +93,6 @@ export async function createTaskThroughUi(input: {
await projectSearch.fill(input.projectName);
await input.page.getByText(input.projectName, { exact: true }).last().click();
}
const submittedAtMs = Date.now();
if (input.attachments?.length) {
const chooser = input.page.waitForEvent("filechooser");
const upload = dialog.getByRole("button", { name: "Upload", exact: true });
@@ -106,6 +106,8 @@ export async function createTaskThroughUi(input: {
}
// The prompt-only composer generates a provisional title which the provider
// can rename immediately. Bind the fixture to the actual creation response.
input.beforeDispatch?.();
const submittedAtMs = Date.now();
const [response] = await Promise.all([
input.page.waitForResponse(response => response.request().method() === "POST"
&& /^\/api\/companies\/[^/]+\/issues$/.test(new URL(response.url()).pathname)),