diff --git a/src/lib/onboard/sandbox-gpu-create-flow.test.ts b/src/lib/onboard/sandbox-gpu-create-flow.test.ts index 217880717a1..690005f3810 100644 --- a/src/lib/onboard/sandbox-gpu-create-flow.test.ts +++ b/src/lib/onboard/sandbox-gpu-create-flow.test.ts @@ -507,8 +507,10 @@ describe("runSandboxGpuCreateFlow proof authorization", () => { }); describe("runSandboxGpuCreateFlow native failure and readiness", () => { - it("threads restart-safe startup resource limits on the no-GPU Docker route", async () => { + it("defers restart-safe no-GPU recreation until the create process exits (#8720)", async () => { const input = createInput(); + const patch = createPatch(); + mocks.createDockerGpuSandboxCreatePatch.mockReturnValueOnce(patch); input.sandboxGpuConfig = { ...input.sandboxGpuConfig, mode: "0", @@ -534,6 +536,17 @@ describe("runSandboxGpuCreateFlow native failure and readiness", () => { requiredUlimits: input.requiredUlimits, }), ); + expect(mocks.streamSandboxCreate).toHaveBeenCalledWith( + "openshell", + ["sandbox", "create"], + input.sandboxEnv, + expect.objectContaining({ waitForReadyTermination: true }), + ); + const onPoll = mocks.streamSandboxCreate.mock.calls[0]?.[3]?.onPoll; + expect(onPoll).toBeTypeOf("function"); + onPoll(); + expect(patch.maybeApplyDuringCreate).not.toHaveBeenCalled(); + expect(patch.ensureApplied).toHaveBeenCalledOnce(); }); it("does not replace a native GPU container solely to persist its startup command", async () => { diff --git a/src/lib/onboard/sandbox-gpu-create-run-attempt.ts b/src/lib/onboard/sandbox-gpu-create-run-attempt.ts index 8cecbc64b2a..5b91caa7368 100644 --- a/src/lib/onboard/sandbox-gpu-create-run-attempt.ts +++ b/src/lib/onboard/sandbox-gpu-create-run-attempt.ts @@ -128,6 +128,10 @@ export function createSandboxGpuCreateAttemptRunner( }, }) : null; + const persistRestartSafeStartup = + input.persistStartupCommand === true && (route !== "native" || hasRequiredUlimits); + const deferRestartSafeCutover = + !managedLifecycle && !compatibility && persistRestartSafeStartup; const runtimePatch = managedLifecycle?.patch ?? createDockerGpuSandboxCreatePatch({ @@ -135,8 +139,7 @@ export function createSandboxGpuCreateAttemptRunner( // The startup clone preserves native CDI devices, so DCode can apply its // exact required limits without replacing the native GPU envelope. // Other native routes are not swapped solely to persist a command. - persistStartupCommand: - input.persistStartupCommand === true && (route !== "native" || hasRequiredUlimits), + persistStartupCommand: persistRestartSafeStartup, externalRecreation: false, sandboxName: input.sandboxName, gpuDevice: input.sandboxGpuConfig.sandboxGpuDevice, @@ -161,13 +164,16 @@ export function createSandboxGpuCreateAttemptRunner( const list = deps.runCaptureOpenshell(["sandbox", "list"], { ignoreError: true }); return isSandboxReady(list, input.sandboxName); }, - onPoll: () => runtimePatch.maybeApplyDuringCreate(), + onPoll: () => { + if (!deferRestartSafeCutover) runtimePatch.maybeApplyDuringCreate(); + }, readyCheckOutputPatterns: getReadyCheckOutputPatternsForAgent( input.terminalAgent, input.sandboxEnv, ), failureCheck: runtimePatch.createFailureMessage, traceEvent: addTraceEvent, + waitForReadyTermination: deferRestartSafeCutover, initialPhase: compatibility && (input.prebuild.imageRef || state.compatibilityArgv) ? "create"