diff --git a/packages/paperclip-runner/src/native-session-runtime.test.ts b/packages/paperclip-runner/src/native-session-runtime.test.ts index 8bc9af6512f..588b64556df 100644 --- a/packages/paperclip-runner/src/native-session-runtime.test.ts +++ b/packages/paperclip-runner/src/native-session-runtime.test.ts @@ -228,6 +228,51 @@ function highestContiguous(events: PrpEvent[]): number { } describe("executeNativeSession recovery", () => { + it.each([false, true].flatMap(recovered => [ + { recovered, cwd: "/home/agent" }, + { recovered, cwd: "/projects/repository/worktrees/task" }, + { recovered, cwd: undefined }, + ]))("uses the admitted session cwd in the task envelope (recovered=$recovered, cwd=$cwd)", async ({ recovered, cwd }) => { + const executionInput = preparedInput(); + const originalWorkspace = structuredClone(executionInput.workspace); + const capabilities = { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true }; + const snapshot: PersistedNativeSession = { + backendKind: "mock", sessionId: "driver-recovery", identity, providerSessionId: "provider-recovery", + workingDirectory: cwd, cursor: null, activeTurnId: null, semanticResult: null, terminal: null, + terminalTurns: [], pendingRuntimeRequests: [], lineage: [], + }; + const startTurn = vi.fn(async () => ({ turnId: "turn-recovery" })); + const session: NativeSession = { + identity: () => identity, + async capabilities() { return capabilities; }, + async *events() { + yield runnerEvent(1, "run.result.proposed", result); + yield runnerEvent(2, "turn.completed"); + }, + startTurn, + async result() { return { result, terminal, turnId: "turn-recovery" }; }, + async snapshot() { return structuredClone(snapshot); }, + async close() {}, + }; + const backend: NativeSessionBackend = { + async descriptor() { return { kind: "mock", name: "workspace-fixture", version: "1", capabilities, + runtimeContextCapabilities: { instructions: "native", skills: "native", mcp: "native" } }; }, + async openSession() { return session; }, + async recoverSession() { return { recovered: true, session }; }, + }; + const port: ControlPlanePort = { + async openRun() {}, async checkpointSession() {}, + async loadSessionCheckpoint() { return recovered ? structuredClone(snapshot) : null; }, + async appendEvent(event) { return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, + async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; }, + async completeRun() {}, + }; + 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 }); + expect(executionInput.workspace).toEqual(originalWorkspace); + }); + it.each([ "completed", "pending", "duplicate", "conflicting", "other turn", "newer turn", "wrong session", "invalid", diff --git a/packages/paperclip-runner/src/native-session-runtime.ts b/packages/paperclip-runner/src/native-session-runtime.ts index e7875ec63a9..c9ef9feed4d 100644 --- a/packages/paperclip-runner/src/native-session-runtime.ts +++ b/packages/paperclip-runner/src/native-session-runtime.ts @@ -2601,6 +2601,9 @@ export async function executeNativeSession( if (!(dispositionOnlyRecovery && !effectFreeInitialAcpxTurn) && "requestedSkills" in modelEnvelope && options.backend.preparedTaskConstraints) { modelEnvelope.constraints = [...options.backend.preparedTaskConstraints]; } + if ("workspace" in modelEnvelope && modelEnvelope.workspace && recoveredSnapshot.workingDirectory) { + modelEnvelope.workspace.cwd = recoveredSnapshot.workingDirectory; + } await session.startTurn({ message: { role: "user", text: JSON.stringify(modelEnvelope) }, ...(recovered && modelEnvelope.schema === "paperclip.native-continuation.v1"