diff --git a/doc/execution-semantics.md b/doc/execution-semantics.md index 7ee6c0e10d1..0cea0e95403 100644 --- a/doc/execution-semantics.md +++ b/doc/execution-semantics.md @@ -2643,7 +2643,10 @@ session's own protocol failure ends a pending turn start, the run reports The continuation tells the agent to finish in that turn, not to end it while waiting for a background command or a notification, and to report one -`paperclip_finish` or `paperclip_block`. The continuation only borrows the +`paperclip_finish` or `paperclip_block`. It also says that processes started in +earlier turns may no longer run after the restart: the agent checks the +workspace for their outputs and re-runs a command when its expected output is +missing, instead of blocking on the lost process. The continuation only borrows the one-shot disposition marker so a crash can adopt its accepted turn. If the continuation completes without a semantic result, the driver returns the marker and the same attempt sends one disposition request. That request does not use diff --git a/packages/paperclip-runner/src/native-session-runtime.test.ts b/packages/paperclip-runner/src/native-session-runtime.test.ts index 2f86ee43083..3d0a7a180d3 100644 --- a/packages/paperclip-runner/src/native-session-runtime.test.ts +++ b/packages/paperclip-runner/src/native-session-runtime.test.ts @@ -212,6 +212,20 @@ function canonicalTestJson(value: unknown): string { return JSON.stringify(value) ?? "undefined"; } +// A restart can kill processes that earlier turns started. The continuation +// must point the agent at workspace outputs instead of the lost process. +const RESTART_LOST_PROCESS_GUIDANCE = "may no longer be running"; +function expectRestartContinuationGuidance(text: string) { + const events = String(JSON.parse(text).events); + expect(events).toContain("This is a resumed session after a Paperclip control-plane restart."); + expect(events).toContain("Shell commands and background processes started in earlier turns " + RESTART_LOST_PROCESS_GUIDANCE); + expect(events).toContain("check the workspace for the outputs or artifacts"); + expect(events).toContain("If the expected output is missing, re-run the command."); + expect(events).toContain("Do not block only because such a process or its tool session cannot be found or inspected."); + expect(events).not.toContain("blindly rerun"); + expect(events).toContain("Reconcile any unfinished tool or command"); +} + function runnerEvent( sourceSeq: number, eventType: PrpEvent["eventType"], @@ -334,6 +348,8 @@ describe("executeNativeSession recovery", () => { await executeNativeSession({ input: executionInput, backend, controlPlane: port, runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery" }); expect(JSON.parse(startTurn.mock.calls[0]![0].message.text).workspace).toEqual({ cwd: cwd ?? originalWorkspace.cwd }); + // Fresh and ordinary resumed turns do not carry restart-continuation guidance. + expect(startTurn.mock.calls[0]![0].message.text).not.toContain(RESTART_LOST_PROCESS_GUIDANCE); expect(executionInput.workspace).toEqual(originalWorkspace); }); @@ -563,7 +579,7 @@ describe("executeNativeSession recovery", () => { expect(startTurn).toHaveBeenCalledOnce(); const submitted = startTurn.mock.calls[0]![0] as Parameters[0]; expect(submitted.continuation).toBe(true); - expect(submitted.message.text).toContain("Reconcile any unfinished tool or command"); + expectRestartContinuationGuidance(submitted.message.text); expect(submitted.message.text).not.toContain(input.task.prompt); expect(events.filter(event => event.eventType === "turn.failed")).toHaveLength(1); expect(events.filter(event => event.eventType === "run.terminal")).toHaveLength(1); @@ -631,7 +647,7 @@ describe("executeNativeSession recovery", () => { expect(startTurn).toHaveBeenCalledOnce(); const submitted = startTurn.mock.calls[0]![0] as Parameters[0]; expect(submitted.continuation).toBe(true); - expect(submitted.message.text).toContain("Reconcile any unfinished tool or command"); + expectRestartContinuationGuidance(submitted.message.text); expect(nativeRestartInterruptedTurnId(interrupted)).toBe("turn-recovery"); }); diff --git a/packages/paperclip-runner/src/native-session-runtime.ts b/packages/paperclip-runner/src/native-session-runtime.ts index a0897f40317..570ada37be7 100644 --- a/packages/paperclip-runner/src/native-session-runtime.ts +++ b/packages/paperclip-runner/src/native-session-runtime.ts @@ -2649,10 +2649,11 @@ export async function executeNativeSession( message: { role: "user" as const, text: JSON.stringify({ schema: "paperclip.native-continuation.v1", events: [ - "Paperclip restarted and restored this same conversation. The previous turn was interrupted.", + "This is a resumed session after a Paperclip control-plane restart. Paperclip restored this same conversation and workspace; the restart interrupted the previous turn.", "Continue the current user request from the recorded progress and preserved workspace.", - "Reconcile any unfinished tool or command before proceeding: inspect its current state and recorded outcomes.", - "Do not repeat completed work or blindly rerun an action whose outcome is unknown. If an external action cannot be reconciled, report the specific blocker.", + "Shell commands and background processes started in earlier turns may no longer be running, and their tool sessions may be gone. Do not block only because such a process or its tool session cannot be found or inspected.", + "Reconcile any unfinished tool or command before proceeding: check the workspace for the outputs or artifacts it should have produced. If the expected output is present, use it. If the expected output is missing, re-run the command.", + "Do not repeat completed work. Before you repeat an action with external effects, such as a push, a message or a deployment, check whether it already took effect. If you cannot determine that, report the specific blocker.", "Finish in this turn. Nothing resumes this conversation after the turn ends, so do not end it to wait for a background command or a notification; wait for or check that work first.", "Then invoke exactly one paperclip_finish or paperclip_block with the accurate disposition.", ].join("\n"),