From 50d5ecea39a0febfe66cac1b62bfba93f670d964 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 27 Aug 2026 11:13:35 -0500 Subject: [PATCH] fix(runner): restore externally owned session state --- .../src/live/runnerd-codex-transport.test.ts | 23 +++++++++++++++++-- .../src/live/runnerd-codex-transport.ts | 3 ++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts index f7ac3bace5d..9a5a93d3542 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from "node:fs/promises"; import { createHash } from "node:crypto"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; @@ -993,8 +993,27 @@ it("cold-restores a suspended provider session under a new run binding", async ( await first.transport.close(); } + // A remote process owner keeps runner-state outside the controller's local + // session root. Resume must defer to its explicit state reader instead of + // rejecting recovery before the remote checkpoint can be made available. + const externallyOwnedRunnerStateDirectory = join( + stateDirectory, + "externally-owned-runner", + ); + await rename( + join(stateDirectory, "runner"), + externallyOwnedRunnerStateDirectory, + ); const restored = createCapabilityRunnerdCodexTransport({ ...options, + runnerStateDirectory: externallyOwnedRunnerStateDirectory, + readRunnerState: async () => + JSON.parse( + await readFile( + join(externallyOwnedRunnerStateDirectory, "runner-state.json"), + "utf8", + ), + ) as Record, resumeDynamicTools: dynamicTools, prpIdentity: { ...baseIdentity, @@ -1014,7 +1033,7 @@ it("cold-restores a suspended provider session under a new run binding", async ( expect((await stat(join(stateDirectory, "codex-home", "skills", "assigned", "SKILL.md"))).mode & 0o222).toBe(0); const durable = JSON.parse( await readFile( - join(stateDirectory, "runner", "runner-state.json"), + join(externallyOwnedRunnerStateDirectory, "runner-state.json"), "utf8", ), ) as { providerToolBridge?: { authorized?: Record } }; diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts index c423871a9ba..c7f244270a7 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts @@ -1666,7 +1666,8 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { const desiredIdentity = this.options.prpIdentity; if ( desiredIdentity === undefined || - !existsSync(resolve(this.#root, "runner", "runner-state.json")) + (this.options.readRunnerState === undefined && + !existsSync(resolve(this.#root, "runner", "runner-state.json"))) ) { throw new Error("PRP provider resume state is unavailable"); }