fix(runner): read saved API responses in bounded text pages

Keep large response artifacts inspectable through the existing authenticated API route. Preserve company and run authority, UTF-8 boundaries, and transfer limits. Reproduce the artifact loop through real HTTP and add an explicit full-stack fixture.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-27 16:03:15 -05:00
1 parent 6131a5858f
commit c2744f77b4
16 files changed
+249 -12

No files matched your search

+12
View File
@@ -1042,3 +1042,15 @@ keys with low request limits. It can only lower the configured campaign limit.
Keep subscription qualification separate from API-key results.
The explicit-only eight-cell [continuation accounting baseline](CONTINUATION-ACCOUNTING.md) tests productive work, bounded repair, restart and late gates with real providers.
### Bounded API response reading
`api-response-reading` is an explicit-only, two-cell native Codex suite (local
and Daytona). Each cell creates synthetic diagnostic evidence over 24 KiB via
the public API, outside the agent assignment. The browser starts one ordinary
task which must read the saved response in bounded text pages and persist the
exact hidden evidence code. The oracle also requires completed API tool events;
missing events or a narrative completion cannot pass. Existing run, copyback,
screenshot, billing and environment cleanup checks apply. Use
`--id api-response-reading.runner-codex.daytona.saved-text-pages` with an
immutable Daytona image; no private hooks or fixture database writes are used.
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { apiResponseReadingTask, responseEvidenceCode, responseEvidenceDescription, successfulApiReadCount } from "./api-response-reading.js";
describe("large API response evidence fixture", () => {
it("keeps evidence beyond both preview and inline limits and out of the assignment", () => {
const source = responseEvidenceDescription("fixture");
const code = responseEvidenceCode("fixture");
expect(Buffer.byteLength(source)).toBeGreaterThan(24 * 1024);
expect(source.indexOf(code)).toBeGreaterThan(24 * 1024);
expect(apiResponseReadingTask.buildPrompt("fixture")).not.toContain(code);
expect(apiResponseReadingTask.buildPrompt("fixture")).toContain("responseText.nextOffsetBytes");
});
it("counts only real completed API tool events, never narrative claims or missing evidence", () => {
const event = { eventType: "tool.execution.completed", payload: { prpEvent: { payload: { name: "call_api", status: "completed" } } } };
expect(successfulApiReadCount([event])).toBe(1);
expect(successfulApiReadCount([])).toBe(0);
expect(successfulApiReadCount([{ ...event, eventType: "item.delta" }, { ...event, payload: null }, { ...event, payload: { prpEvent: { payload: { name: "call_api", status: "failed" } } } }])).toBe(0);
});
});
+43
View File
@@ -0,0 +1,43 @@
import { createHash } from "node:crypto";
import type { RunnerTaskFixture } from "./types.js";
export function responseEvidenceCode(nonce: string) {
return createHash("sha256").update(`bounded-response-evidence:${nonce}`).digest("hex");
}
export function responseEvidenceDescription(nonce: string) {
return `${"Synthetic diagnostic padding.\n".repeat(1400)}\nEvidence code: ${responseEvidenceCode(nonce)}\n`;
}
export function successfulApiReadCount(events: readonly { eventType?: string; payload?: unknown }[]) {
return events.filter(event => {
const outer = event.payload as { prpEvent?: { payload?: { name?: string; status?: string } } } | null;
const payload = outer?.prpEvent?.payload;
return event.eventType === "tool.execution.completed" && payload?.name === "call_api" && payload.status === "completed";
}).length;
}
export const apiResponseReadingTask: RunnerTaskFixture = {
id: "saved-text-pages", label: "Read saved large API evidence", groups: [],
workMode: "standard", flow: "single_turn", expectedRunCount: 1,
attemptTimeoutMs: { local: 10 * 60_000, daytona: 10 * 60_000 },
expectedTerminalState: { issue: "done", run: "succeeded" },
buildTitle: nonce => `Read large API evidence ${nonce}`,
buildVisibleMarker: nonce => `API_RESPONSE_READ_${nonce}`,
buildPrompt: nonce => [
"Inspect diagnostic evidence task {{API_RESPONSE_SOURCE_ID}} using the Paperclip API tools.",
"Discover GET /api/issues/{id}, call it for that task, and retain the returned response artifact.",
"Read the saved artifact through GET /api/assets/{assetId}/content with responseText: {offsetBytes:0,limitBytes:8192}.",
"Continue using responseText.nextOffsetBytes until null. Extract the Evidence code at the end of its description.",
"The large response must be read with bounded responseText pages. Do not use other tools or API routes to obtain the evidence; if bounded reading fails, report the failure instead of substituting a different reader.",
"Write only that code followed by a newline to api-response-proof.txt in the current workspace. Do not edit the evidence task or create child tasks.",
`Verify the proof file and finish this task with paperclip_finish, reportedWorkDisposition done, and summary API_RESPONSE_READ_${nonce}.`,
].join("\n"),
buildMatchers: (nonce, execution) => [
{ kind: "file_exact", path: "api-response-proof.txt", expected: `${responseEvidenceCode(nonce)}\n` },
{ kind: "message_contains", expected: `API_RESPONSE_READ_${nonce}` },
{ kind: "issue_status", expected: "done" },
{ kind: "run_status", expected: "succeeded" },
{ kind: "runtime_mode", expected: execution.profile.expectedRuntimeMode },
{ kind: "environment", expected: execution.environment.id },
],
};
+3 -3
View File
@@ -71,10 +71,10 @@ describe("runner E2E catalog", () => {
expect(localIntegrityTasks).toHaveLength(2);
expect(openRouterBreadthTasks).toHaveLength(3);
expect(runnerSuites.map((suite) => suite.expectedMatrixSize)).toEqual([
8, 46, 23, 47, 52, 28, 18, 6, 6, 42, 14, 10, 2,
2, 8, 46, 23, 47, 52, 28, 18, 6, 6, 42, 14, 10, 2,
]);
expect(validateRunnerCatalog()).toHaveLength(302);
expect(new Set(runnerMatrix.map((entry) => entry.id)).size).toBe(302);
expect(validateRunnerCatalog()).toHaveLength(304);
expect(new Set(runnerMatrix.map((entry) => entry.id)).size).toBe(304);
expect(
runnerMatrix.filter((entry) => entry.suite.id === "core-compatibility"),
).toHaveLength(42);
+9
View File
@@ -1,3 +1,4 @@
import { apiResponseReadingTask } from "./api-response-reading.js";
import { accountingTasks } from "./accounting-cases.js";
import { continuationTasks } from "./continuation-cases.js";
import { lifecycleLiveTasks, lifecycleLiveDefinitionDigest } from "./lifecycle-live-cases.js";
@@ -911,6 +912,14 @@ const everydayProfiles = [
].map(productionStoryProfile);
export const runnerSuites: readonly RunnerSuiteFixture[] = [
{
id: "api-response-reading", label: "Bounded API response reading", manualOnly: true,
description: "Retrieve evidence beyond a saved API preview through authorized bounded text windows.",
groups: [], environments: runnerEnvironments,
profiles: runnerProfiles.filter(profile => profile.id === "runner-codex"),
tasks: [apiResponseReadingTask], expectedMatrixSize: 2,
definitionMetadata: { version: 1, grading: "hidden-evidence-exact-copy-and-api-tool-events", scheduling: "explicit-only" },
},
{
id: "continuation-accounting", label: "Continuation accounting baseline", manualOnly: true,
description: "Structured productive steps, bounded repair, restart and late gates; comments cannot buy more attempts.",
+14
View File
@@ -1,3 +1,4 @@
import { responseEvidenceDescription, successfulApiReadCount } from "./api-response-reading.js";
import { observeBrowserBootstrap } from "./browser-bootstrap-diagnostics.js";
import { runAccountingFlow } from "./accounting-flow.js";
import type { Issue } from "../../packages/shared/src/types/issue.js";
@@ -794,6 +795,14 @@ for (const execution of executions) {
daytonaImage: process.env.PAPERCLIP_E2E_DAYTONA_IMAGE,
});
if (execution.suite.id === "api-response-reading") {
const source = await api.post<{ id: string }>(`/api/companies/${fixtures.company.id}/issues`, {
title: `Synthetic diagnostic evidence ${nonce}`,
description: responseEvidenceDescription(nonce), status: "backlog",
});
prompt = prompt.replaceAll("{{API_RESPONSE_SOURCE_ID}}", source.id);
}
if (execution.suite.id === "lifecycle-baseline" && lifecycleLiveCase(execution.task.id)?.family === "blocker") {
const prerequisite = await api.post<{ id: string }>(`/api/companies/${fixtures.company.id}/issues`, {
title: `Supply dataset ${nonce}`,
@@ -1885,6 +1894,11 @@ for (const execution of executions) {
}
}
if (execution.suite.id === "api-response-reading") {
const completedReads = successfulApiReadCount(runEventsByRun.flatMap(captured => captured.events));
if (completedReads < 3) invariantFailures.push(`Expected source read and multiple bounded artifact reads; observed ${completedReads} completed API calls`);
}
const context = record(run.contextSnapshot);
const environmentContext = record(context.paperclipEnvironment);
const workspaceContext = record(context.paperclipWorkspace);