mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-03 17:51:58 +01:00
## Thinking Path > - Paperclip manages AI agents and their saved work. > - Product E2E tests must check saved personal files across separate turns. > - The server records file collection in the durable `instruction_save` run-log event. > - The warm test read optional terminal result metadata instead and rejected a recorded successful save. > - This pull request reads the complete event stream for the current run and uses its latest save receipt. > - The prompt now specifies the exact nine bytes required by the temporary-file transfer count. > - Human review actions select the actual interaction card, which can share its interaction ID with a navigation button. > - A later real three-turn run exposed a missing reply after workspace finalization moved to its durable recovery owner. > - Reply recovery handled completed tasks but omitted ordinary tasks that awaited review. > - This pull request also publishes their recorded final reply once while preserving the pending review. ## Linked Issues or Issue Description **What happened?** The [retained live continuity attempt](https://github.com/paperclipai/paperclip/actions/runs/41477302876/job/125420001074) saved the correct memory file and unchanged 8 MiB binary. Its durable run log contained a successful `instruction_save` receipt. The oracle read `resultJson.instructionSave`, which was absent, and failed before turn two. The temporary file also had a trailing LF. Its prompt allowed that result, while the byte-count check required nine bytes. **Expected behavior** The oracle must use the documented authoritative save event. A missing receipt, a later failed save, incorrect transfer counts, or another run's events must fail. The prompt must specify the bytes that its independent check requires. **Steps to reproduce** 1. Run `daytona-warm-continuity.runner-codex.daytona.warm-three-turn` with the configured protected credentials and immutable image. 2. Complete a native turn whose terminal result has no `instructionSave` field. 3. Compare the oracle output with the same run's successful `instruction_save` event. A subsequent [three-turn attempt](https://github.com/paperclipai/paperclip/actions/runs/41495337334/job/125451304553) passed every personal-file assertion and reused the same warm process. Its first final reply was recorded as a completed agent-message event but was absent from comments. The log shows `native_workspace_finalization_busy`; the durable recovery path excluded `needs_review` replies. The final message matcher correctly failed. This failure remains recorded. The observed application revision was `aab87ce70744660a4a21c77278b0ede267c04720`. Related work: #14695 and #18494. The retained failed result keeps its original grade and definition. ## What Changed - Read all run-event pages through the existing bounded collector. - Require events to belong to the requested run and select the latest `instruction_save` event. - Retain strict byte counts and reject missing, failed, pending or superseded receipts. - Specify nine ASCII bytes and no trailing newline for the temporary file. - Scope human review actions to the interaction card and its exact interaction ID. - Advance the warm suite definition to version 5 and document its evidence source. - Recover recorded final replies for ordinary native tasks with an accepted `needs_review` result and terminal state. - Preserve pending reviews, existing reply selection, company/turn/digest checks and external governed-response authorization. - Update the task feature and agent access maps. ## Verification - Real PostgreSQL regression: deferred review reply discovery failed before the production repair (expected one candidate; found zero). The final-message event and pending review were present. - Recovery cases cover one publication, no repeated provider run, unchanged review, and rejection of absent/wrong-turn final messages, bad digest and suppressed comments. - Red: seven new boundary regressions failed against the old receipt lookup. - Green: 169 focused oracle, event pagination and catalog checks passed. Product E2E typecheck passed after rebuilding local package declarations. - Retained-evidence replay confirms that the new lookup finds the real successful save event. The original attempt remains failed because its temporary file included an LF. - Recursive typecheck passed before the final recovery change; server typecheck passed afterward. The recovery and control-plane suites passed all 112 tests. Full build and map checks passed. - The broad local test run reached one unrelated 30-second timeout in `attention-retention-work.test.ts` (maintained-review/confirmation batching). The same unchanged case then passed on one targeted retry (39.9 seconds including module load). The original failure remains retained. The broad run also reported one unchanged workspace-conflict case, which passed on its targeted retry (14.85 seconds including module load). Both original failures remain retained. The redundant broad local run was stopped after current-head CI completed all required gates: 55 successful checks and 3 selection skips. No all-green local broad-run claim is made. - The separate artifact-transfer cell already passed on the recorded application revision: independent source and delivered bytes matched at 407,485 bytes, with a completed task, succeeded native run and cleared execution locks. This does not establish a three-turn continuity pass. - [Real native three-turn acceptance](https://github.com/paperclipai/paperclip/actions/runs/41499102994/job/125465795210) **passed** at `3b207a42943e9eeb371d4f2d5623de72093eb6d5`, suite definition 5. Turn one exercised `committed_task_response_recovered`. All three completed final replies were persisted once and in order. All three strict personal-file receipts passed. The unchanged 8 MiB binary survived; later transfers copied only 36 and 54 bytes. The lease was created, resumed, and resumed with the same runner PID/start time, runner instance, native session and provider session. Final task was `done`, all three runs succeeded, and checkout/execution locks were null. - The aggregate startup summary has no separate provider-process PID proof. The raw run records independently prove the stable runner process and provider session stated above. This single native cell does not qualify the legacy cell or a full provider matrix. - The passing campaign reused the signed immutable runner image. It required no disk expansion. Earlier campaign image builds used the recorded temporary 120 GiB mitigation; default 60 GiB build capacity remains unqualified. All scoped completed probes were removed and independently returned 404; org CPU/disk usage returned to zero. This does not resolve unknown historical resources under a different credential identity. - Current-head Greptile is 5/5; review-thread pagination is complete with zero threads. All 55 current-head checks are successful; 3 selection checks are skipped. The paid campaign, result enforcement and report publication all completed successfully. ## Risks - Publishing a reply must not approve the work. Recovery only copies the accepted turn’s completed final message. It preserves the pending review and does not start an executor. - Conversation and external governed-response paths retain their existing separate authorization. The latest save event can record failure after an earlier success. The oracle rejects that case. Event pagination, missing receipts and cross-run evidence also fail closed. The exact temporary-file byte count remains required. The review selector still requires the intended interaction ID and enabled action on its actual card. The production change is limited to ordinary internal-task reply recovery. It still requires an accepted result, a matching successful terminal disposition, the completed final message, and existing publication authority. ## Model Used OpenAI Codex, based on GPT-6, with shell and code tools. The exact deployment variant and context-window size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes: #` / `Refs: #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
39 lines
3.0 KiB
TypeScript
39 lines
3.0 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import type { RunnerApi } from "./api.js";
|
|
import { collectRunEvents } from "./run-observations.js";
|
|
|
|
const record = (v: unknown): Record<string, unknown> => v && typeof v === "object" && !Array.isArray(v) ? v as Record<string, unknown> : {};
|
|
export function gradeWarmManagedFiles(input: {
|
|
turn: number; nonce: string; content: unknown; binary: Buffer; deletedStatus: number; save: unknown; native: boolean;
|
|
}) {
|
|
const expected = `${Array.from({ length: input.turn }, (_, i) => `T${i + 1}-${input.nonce}`).join("\n")}\n`;
|
|
const expectedBinary = Buffer.alloc(8 * 1024 * 1024, 93);
|
|
const save = record(input.save), stats = record(save.checkpointStats);
|
|
const checks = [
|
|
{ id: "managed-memory-exact", passed: input.content === expected },
|
|
{ id: "managed-binary-exact", passed: input.binary.equals(expectedBinary) },
|
|
{ id: "managed-deletion", passed: input.deletedStatus === (input.turn === 1 ? 200 : 404) },
|
|
{ id: "managed-save-receipt", passed: save.state === "saved" && save.contract === "agent_files" && !save.errorCode },
|
|
...(input.native ? [{ id: "incremental-payload", passed: typeof stats.copiedFiles === "number" &&
|
|
(input.turn === 1 ? stats.copiedFiles === 3 && stats.copiedBytes === expectedBinary.length + Buffer.byteLength(expected) + 9
|
|
: stats.copiedFiles === 1 && stats.copiedBytes === Buffer.byteLength(expected) && stats.hashedBytes === Buffer.byteLength(expected)) }] : []),
|
|
];
|
|
return { passed: checks.every(c => c.passed), checks, expected, binaryBytes: input.binary.length,
|
|
binarySha256: createHash("sha256").update(input.binary).digest("hex"), save };
|
|
}
|
|
export async function warmManagedFileEvidence(api: RunnerApi, agentId: string, runId: string, turn: number, nonce: string, native: boolean) {
|
|
const base = `/api/agents/${agentId}/instructions-bundle/file?path=`;
|
|
const note = await api.get<{ content: string }>(`${base}notes%2Fwarm-memory.txt`);
|
|
const binary = await api.request.get(`${base}notes%2Funchanged.bin&download=true`);
|
|
if (!binary.ok()) throw new Error(`Managed binary download failed: ${binary.status()}`);
|
|
const deleted = await api.request.get(`${base}notes%2Fdelete-me.txt&download=true`);
|
|
const events = await collectRunEvents<{ seq: number; runId: string; eventType: string; payload?: unknown }>((afterSeq, limit) =>
|
|
api.get(`/api/heartbeat-runs/${encodeURIComponent(runId)}/events?afterSeq=${afterSeq}&limit=${limit}`));
|
|
if (events.some(event => event.runId !== runId)) throw new Error("Managed file evidence returned events for a different run");
|
|
// The durable run-log event is authoritative. Terminal result metadata may
|
|
// omit this receipt, and an older success must not hide a later failed save.
|
|
const receipt = events.filter(event => event.eventType === "instruction_save").at(-1);
|
|
return { ...gradeWarmManagedFiles({ turn, nonce, content: note.content, binary: await binary.body(), deletedStatus: deleted.status(),
|
|
save: receipt?.payload, native }), saveEventSeq: receipt?.seq ?? null };
|
|
}
|