From 06ab95bb3c36494202ddd43e2e0aee786d817649 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 11 Sep 2026 14:25:28 -0500 Subject: [PATCH] fix: verify lazy native provider launch during restored model selection --- .../runner-core/src/acpx_sidecar_transport.rs | 3 ++ .../acpx/codex-runtime-adapter.test.ts | 30 +++++++++++++++++++ .../src/drivers/acpx/codex-runtime-adapter.ts | 19 ++++++++---- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs index 5a063ab684..5060e5bc90 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs @@ -642,6 +642,9 @@ fn response_error_classification(error: &ResponseError) -> &'static str { _ => {} } match error.message.as_str() { + "ACPX provider spawned after ownership admission was sealed" => { + "provider_spawn_after_ownership_seal" + } "ACPX recovery identity conflicts with the immutable session configuration" => { "recovery_configuration_mismatch" } diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts index fa7fdab8f2..4b0584264e 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts @@ -1311,6 +1311,36 @@ describe("Codex ACPX runtime adapter", () => { }); }); + it("verifies a lazy recovered provider spawned by model selection before returning", async () => { + const runtime = fakeRuntime(); + const command = fakeCommand(); + vi.mocked(command.spawn).mockReturnValue(fakeChild()); + let runtimeOptions: AcpRuntimeOptions | undefined; + let acknowledgeOwnership!: () => void; + const ownership = new Promise((resolve) => { acknowledgeOwnership = resolve; }); + vi.mocked(runtime.setConfigOption!).mockImplementation(async () => { + await Promise.resolve(); + runtimeOptions?.spawnAgent?.({ command: "ignored", args: ["--stdio"], options: {} }); + }); + const port = await openCodexAcpxRuntime(openOptions(command), { + createRegistry: () => registry(), createStore: () => store(), + awaitProviderOwnership: () => ownership, + awaitProviderExit: providerOwnershipEstablished, + createRuntime: (options) => { runtimeOptions = options; return runtime; }, + }); + let admitted = false; + const selection = port.setModel!("gpt-5.6-sol").then(() => { admitted = true; }); + void selection.catch(() => undefined); + await vi.waitFor(() => expect(command.spawn).toHaveBeenCalledOnce()); + expect(admitted).toBe(false); + acknowledgeOwnership(); + await selection; + expect(admitted).toBe(true); + expect(() => runtimeOptions?.spawnAgent?.({ command: "ignored", args: [], options: {} })) + .toThrow("provider spawned after ownership admission was sealed"); + await port.close({ reason: "test complete" }); + }); + it("admits a verified provider that starts with the first recovered turn", async () => { const runtime = fakeRuntime(); const child = fakeChild(); diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts index 88927177cb..a499b407ec 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts @@ -1156,11 +1156,20 @@ function runtimePort( ...(runtime.setConfigOption ? { async setModel(model: string) { - await runtime.setConfigOption?.({ - handle, - key: "model", - value: model, - }); + // A restored handle can be lazy: selecting the pinned model may + // launch its first provider before any prompt. Admit that spawn + // only for this control call, and verify ownership before return. + const finishOwnershipAdmission = + children.beginLifetimeOwnershipAdmission(); + try { + await runtime.setConfigOption?.({ + handle, + key: "model", + value: model, + }); + } finally { + await finishOwnershipAdmission(); + } }, } : {}),