diff --git a/server/src/services/native-runtime/native-session-executor.test.ts b/server/src/services/native-runtime/native-session-executor.test.ts index 928353e57be..b67247d57a4 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -2650,6 +2650,30 @@ describe("remote provider pack manifest", () => { leaseOwner: "controller", controllerGeneration: 2, providerAttempt: 1, restartKind: "graceful", recoveryRequestId: null, remote: { providerLeaseId: "sandbox", remoteCwd: "/workspace" } }, }; + // Staging failure (OpenCode and ACPX): a same-run + // retry under a persisted runner artifact found the runner settled and + // exited. Nothing is adopted, so the transport relaunches it from its + // durable state and needs the admitted provider pack's launch inputs. + const settledExecute = vi.fn(async (request: { args: string[] }) => { + if (request.args[2] === "paperclip-runner-recovery-check") return { exitCode: 45, timedOut: false, stderr: "", stdout: "" }; + if (request.args[1]?.includes("base64")) return { exitCode: 0, timedOut: false, stderr: "", stdout: Buffer.from(JSON.stringify({ + ...identity, lifecycle: "suspended", outbox: [], pendingTerminalDelivery: null, pendingProviderCleanup: null, warmTransition: null, + })).toString("base64") }; + return recoveryExecute(request); + }); + const settledTarget = { ...(recoveryInput.runnerExecutionTarget as unknown as Record), + runner: { execute: settledExecute, syncIn: vi.fn(async () => undefined), syncOut: vi.fn(async () => undefined) } }; + await createRunnerdBackend({ ...recoveryInput, runnerExecutionTarget: settledTarget as never, + controllerPersistence: { bootId: "replacement-controller", runnerArtifact: artifact } }); + state.createBackend.mock.calls.at(-1)![1].codexTransportFactory!(); + const relaunched = state.createTransport.mock.calls.at(-1)![0] as RunnerTransportOptions; + expect(relaunched.adoptExistingRunner).toBeUndefined(); + expect(relaunched).toMatchObject({ providerPackAuthorityDigest: admitted.digest, runnerArtifact: artifact, + providerNodeCommandSha256: admitted.payload.artifacts.nodeCommand.sha256, + ...(opencode + ? { opencodeCommand: `/opt/paperclip-runner/provider-pack/${admitted.payload.artifacts.opencodeExecutable.path}`, + opencodeCommandSha256: admitted.payload.artifacts.opencodeExecutable.sha256 } + : { acpxSidecarSha256: (admitted.payload.artifacts as Record).acpxSidecar.sha256 }) }); const recoveredBackend = await createRunnerdBackend(recoveryInput); state.createBackend.mock.calls.at(-1)![1].codexTransportFactory!(); const recovered = state.createTransport.mock.calls.at(-1)![0]; diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index 37a7a8631d2..fb5c533b658 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -15435,6 +15435,10 @@ async function createRunnerdBackendWithinSessionClaim( let expectedProviderPackManifest: RemoteProviderPackManifest | null = providerPackAuthority?.manifest ?? providerArtifacts?.manifest as RemoteProviderPackManifest ?? null; let providerPackVerified = false; + // Set when a persisted-runner reattachment finds that runner settled and + // exited. The transport then relaunches it from durable state, which needs + // the admitted provider pack's launch inputs that adoption does not. + let relaunchesSettledRemoteRunner = false; const explicitRemoteCodex = input.runnerRemoteCodexPath?.trim() || null; const remoteCodexNpmSpec = input.runnerRemoteCodexNpmSpec?.trim() || null; if (explicitRemoteCodex && remoteCodexNpmSpec) { @@ -15595,7 +15599,7 @@ async function createRunnerdBackendWithinSessionClaim( return; selectedRemoteMode = requiredMode; if (input.restartRecovery?.kind === "reattach_remote_runner" || input.remoteWarmRecovery) { - if (input.controllerPersistence?.runnerArtifact) { + if (input.controllerPersistence?.runnerArtifact && !relaunchesSettledRemoteRunner) { remotePrepared = true; return; } @@ -16690,6 +16694,13 @@ async function createRunnerdBackendWithinSessionClaim( signalVerifiedRecoveryProcess(localRecoveryProcess, signal), } : undefined; + if (adoptingPersistedRemoteRunner && !adoptedProcess && requiresRemoteProviderPack) { + // Bind the relaunch to the pack this session was admitted with (the saved + // authority, verified as an exact installed identity), never a newer one. + relaunchesSettledRemoteRunner = true; + providerPackAuthority = selectProviderPackAuthority(true); + expectedProviderPackManifest = providerPackAuthority?.manifest ?? expectedProviderPackManifest; + } if (adoptedProcess && remoteTarget) { const markSpawned = resolveRemoteRunnerProcessSpawned as (() => void) | null; markSpawned?.();