diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/bin/fake-codex-app-server.rs b/packages/paperclip-runner/runner/crates/runner-core/src/bin/fake-codex-app-server.rs index 3490a4c1b08..18059de632f 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/bin/fake-codex-app-server.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/bin/fake-codex-app-server.rs @@ -1619,56 +1619,71 @@ fn run() -> Result<(), Box> { }))?; } if emit_post_completion_passive_statuses { - if let Some(gate) = post_completion_notification_gate.as_ref() { - let deadline = std::time::Instant::now() + Duration::from_secs(5); - while !gate.is_file() { - if std::time::Instant::now() >= deadline { - return Err( - "post-completion notification gate timed out".into() - ); + let gate = post_completion_notification_gate.clone(); + let thread_id = state.thread_id.clone(); + let tail_turn_id = provider_turn_id.clone(); + // Goal reconciliation reads must remain responsive while + // the test holds back the post-terminal passive notices. + thread::spawn(move || { + let result = (|| -> io::Result<()> { + if let Some(gate) = gate.as_ref() { + let deadline = + std::time::Instant::now() + Duration::from_secs(5); + while !gate.is_file() { + if std::time::Instant::now() >= deadline { + return Err(io::Error::new( + io::ErrorKind::TimedOut, + "post-completion notification gate timed out", + )); + } + thread::sleep(Duration::from_millis(1)); + } } - thread::sleep(Duration::from_millis(1)); + for notification in [ + json!({ + "method": "deprecationNotice", + "params": {"summary": "A provider setting is deprecated", "details": null} + }), + json!({ + "method": "remoteControl/status/changed", + "params": {"status": "disabled", "environmentId": null} + }), + json!({ + "method": "mcpServer/startupStatus/updated", + "params": {"name": "codex_apps", "status": "ready", "error": null} + }), + json!({ + "method": "account/rateLimits/updated", + "params": {"rateLimits": {}} + }), + json!({ + "method": "rawResponseItem/completed", + "params": {"threadId": thread_id, "turnId": tail_turn_id, "item": {"id": "raw-tail", "type": "reasoning"}} + }), + json!({ + "method": "rawResponse/completed", + "params": {"threadId": thread_id, "turnId": tail_turn_id, "response": {"id": "response-tail"}} + }), + json!({ + "method": "thread/goal/updated", + "params": {"threadId": thread_id, "goal": "finish the turn"} + }), + json!({ + "method": "thread/goal/cleared", + "params": {"threadId": thread_id} + }), + ] { + send(notification)?; + } + if let Some(gate) = gate.as_ref() { + fs::write(gate.with_extension("emitted"), b"emitted")?; + } + Ok(()) + })(); + if let Err(error) = result { + eprintln!("post-completion passive tail failed: {error}"); } - } - for notification in [ - json!({ - "method": "deprecationNotice", - "params": {"summary": "A provider setting is deprecated", "details": null} - }), - json!({ - "method": "remoteControl/status/changed", - "params": {"status": "disabled", "environmentId": null} - }), - json!({ - "method": "mcpServer/startupStatus/updated", - "params": {"name": "codex_apps", "status": "ready", "error": null} - }), - json!({ - "method": "account/rateLimits/updated", - "params": {"rateLimits": {}} - }), - json!({ - "method": "rawResponseItem/completed", - "params": {"threadId": state.thread_id, "turnId": provider_turn_id, "item": {"id": "raw-tail", "type": "reasoning"}} - }), - json!({ - "method": "rawResponse/completed", - "params": {"threadId": state.thread_id, "turnId": provider_turn_id, "response": {"id": "response-tail"}} - }), - json!({ - "method": "thread/goal/updated", - "params": {"threadId": state.thread_id, "goal": "finish the turn"} - }), - json!({ - "method": "thread/goal/cleared", - "params": {"threadId": state.thread_id} - }), - ] { - send(notification)?; - } - if let Some(gate) = post_completion_notification_gate.as_ref() { - fs::write(gate.with_extension("emitted"), b"emitted")?; - } + }); } if emit_post_completion_foreign_turn { let gate = post_completion_notification_gate.clone(); 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 45ad16975f5..9ebcda1bd6c 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 @@ -101,7 +101,10 @@ describe("Codex ACPX runtime adapter", () => { }); }); - it.each([["claude" as const, "claude-sonnet-5", "claude-sonnet-5"]])( + it.each([ + ["claude" as const, "claude-sonnet-5", "claude-sonnet-5"], + ["claude" as const, "custom-claude-model", "custom-claude-model"], + ])( "opens the qualified %s session through the verified lease", async (agent, model, providerModel) => { const runtime = fakeRuntime(); @@ -109,7 +112,7 @@ describe("Codex ACPX runtime adapter", () => { const options = openOptions(command); let runtimeOptions: AcpRuntimeOptions | undefined; options.profile = resolveQualifiedAcpxProfile(agent, model); - options.launchEnvironment = { PATH: "/verified/bin" }; + options.launchEnvironment = { PATH: "/verified/bin", ANTHROPIC_CUSTOM_MODEL_OPTION: "stale-model" }; await openCodexAcpxRuntime(options, { createRegistry: ({ overrides }) => { @@ -128,6 +131,7 @@ describe("Codex ACPX runtime adapter", () => { expect(runtimeOptions?.spawnEnvironment?.()).toEqual({ PATH: "/verified/bin", PAPERCLIP_ACPX_ISOLATED_CONTEXT: "1", + ANTHROPIC_CUSTOM_MODEL_OPTION: providerModel, }); expect(runtime.ensureSession).toHaveBeenCalledWith( expect.objectContaining({ 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 b92419eec63..a5c1d2d456f 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts @@ -317,7 +317,14 @@ export async function openQualifiedAcpxRuntime( spawnEnvironment: () => ({ ...definedEnvironment(options.launchEnvironment), ...(options.profile.agent === "claude" - ? { PAPERCLIP_ACPX_ISOLATED_CONTEXT: "1" } + ? { + PAPERCLIP_ACPX_ISOLATED_CONTEXT: "1", + // Claude ACP otherwise maps concrete IDs back to rolling picker + // aliases (e.g. claude-sonnet-5 -> sonnet). Advertise the exact + // host-requested ID so model verification stays exact on resume + // and before the first billable prompt. + ANTHROPIC_CUSTOM_MODEL_OPTION: options.profile.reportedModelId, + } : {}), }), spawnCwd: options.cwd,