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 96d47b42c97..32025817354 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -3064,6 +3064,35 @@ describe("remote provider pack manifest", () => { ? { 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 }) }); + if (opencode) { + // Staging failure after an upgrade: the run first launched OpenCode + // from its admitted artifact root. The relaunched Runner replays that + // launch, so it must get the same root, not the provider pack root. + const launchRoot = `/home/runtime-account/.paperclip-runtime/paperclip-runner/provider-artifacts/${admitted.digest.slice(7)}`; + const artifactReceipt = { schema: "paperclip.native-provider-artifacts.v1" as const, artifactId: randomUUID(), + closureDigest: "sha256:" + "9".repeat(64), providerPackDigest: admitted.digest, manifest: admitted, launchRoot, accountUid: 1000 }; + const storageMode = vi.spyOn(harnessStorage, "durableNativeHarnessStorage").mockReturnValue({} as never); + const backupOwner = vi.spyOn(harnessStorage, "nativeHarnessBackupOwner").mockResolvedValue({} as never); + const restore = vi.spyOn(providerArtifactStorage, "restoreNativeProviderArtifacts").mockResolvedValue(undefined); + onTestFinished(() => { storageMode.mockRestore(); backupOwner.mockRestore(); restore.mockRestore(); }); + const artifactExecute = vi.fn(async (request: { args: string[] }) => request.args?.[1]?.startsWith("const os=require") + ? { exitCode: 0, timedOut: false, stderr: "", stdout: JSON.stringify({ home: "/home/runtime-account", uid: 1000 }) } + : settledExecute(request)); + await createRunnerdBackend({ ...recoveryInput, + runnerExecutionTarget: { ...settledTarget, runner: { execute: artifactExecute, + syncIn: vi.fn(async () => undefined), syncOut: vi.fn(async () => undefined) } } as never, + controllerPersistence: { bootId: "replacement-controller", runnerArtifact: artifact }, + providerPackAdmissionSource: { runId: remoteExecution.binding.runId, receipt: null, state: null, artifacts: artifactReceipt } }); + state.createBackend.mock.calls.at(-1)![1].codexTransportFactory!(); + expect(restore).toHaveBeenCalledWith(expect.objectContaining({ receipt: artifactReceipt })); + expect(state.createTransport.mock.calls.at(-1)![0]).toMatchObject({ providerPackAuthorityDigest: admitted.digest, + providerNodeCommand: `${launchRoot}/${admitted.payload.artifacts.nodeCommand.path}`, + opencodeProxyPath: `${launchRoot}/${admitted.payload.artifacts.opencodeProxy.path}`, + opencodeCommand: `${launchRoot}/${admitted.payload.artifacts.opencodeExecutable.path}` }); + storageMode.mockRestore(); + backupOwner.mockRestore(); + restore.mockRestore(); + } 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 3368538bca0..28028a99062 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -16622,9 +16622,17 @@ async function createRunnerdBackendWithinSessionClaim( const prepareRemoteRunner = async (mode: "dial_wss" | "listen_ws") => { await prepareRemoteRunnerAssets(mode); + // A settled Runner relaunched from its durable state replays the run's + // first launch, whose OpenCode command lives under the admitted artifact + // root. Rebuild that same root; the pack root would fail the Runner's + // qualified-profile check. + const relaunchAdmittedArtifacts = relaunchesSettledRemoteRunner && providerArtifacts !== undefined; if (!artifactStorage || openCodeLaunchArtifacts || !remoteCommandRunner || !remoteTarget - || !expectedProviderPackManifest || liveProviderRecovery || legacyProviderContinuity) return; + || !expectedProviderPackManifest || (liveProviderRecovery && !relaunchAdmittedArtifacts) || legacyProviderContinuity) return; if (!remoteCommandRunner.syncIn || !remoteCommandRunner.syncOut) throw new Error("native_provider_artifacts_bulk_transport_required"); + if (relaunchAdmittedArtifacts && liveProviderRecovery) await restoreNativeProviderArtifacts({ db: input.db, storage: artifactStorage, + companyId: input.execution.binding.companyId, receipt: providerArtifacts!, targetRoot: restoredArtifactDirectory, + signal: providerArtifactsSignal() }); const bootstrap = activeRemoteProviderPackRoot ? { root: activeRemoteProviderPackRoot, manifest: expectedProviderPackManifest } : remoteNativeArtifacts?.providerPackInventory().find(pack => pack.immutable);