From 8c46620678ef19f74f28d04ab04eaa67bc7552f9 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 10 Jul 2026 13:33:50 +0000 Subject: [PATCH] Fall back to Codex CLI when ACP rejects session configuration A codex_local agent configured with a manual model id (e.g. gpt-5.5-pro) failed every ACP-lane run with acpx_session_config_failed: codex-acp validates session/set_config_option values against its internal model list and rejects ids it does not advertise (ACP -32602), even though the Codex CLI lane accepts manual model ids via --model. The existing ACP->CLI fallback only covered thrown startup errors, not structured failed results from the configure_session phase. Auto-selected ACP now falls back to the CLI lane when the ACP server rejects session configuration, preserving the configured model/effort/fast-mode via startup args. Explicit engine=acp remains strict and fails loudly. Co-Authored-By: Claude Fable 5 --- .../src/acpx-engine/constants.ts | 5 ++ .../adapter-utils/src/acpx-engine/execute.ts | 3 +- packages/adapters/codex-local/src/index.ts | 2 +- .../src/server/execute.acp-fallback.test.ts | 71 +++++++++++++++++++ .../codex-local/src/server/execute.ts | 19 ++++- 5 files changed, 97 insertions(+), 3 deletions(-) diff --git a/packages/adapter-utils/src/acpx-engine/constants.ts b/packages/adapter-utils/src/acpx-engine/constants.ts index 86368f5f97b..dc5e65aea3c 100644 --- a/packages/adapter-utils/src/acpx-engine/constants.ts +++ b/packages/adapter-utils/src/acpx-engine/constants.ts @@ -5,6 +5,11 @@ export const DEFAULT_ACP_ENGINE_NON_INTERACTIVE_PERMISSIONS = "deny"; export const DEFAULT_ACP_ENGINE_TIMEOUT_SEC = 0; export const DEFAULT_ACP_ENGINE_WARM_HANDLE_IDLE_MS = 0; +// The ACP server rejected a session/set_config_option value (for example a +// manual model id its validation list does not include). Adapters with a +// non-ACP lane can treat this as recoverable and retry there. +export const ACPX_SESSION_CONFIG_FAILED_ERROR_CODE = "acpx_session_config_failed"; + export const ACPX_ADAPTER_AGENT_IDS = { claude_local: "claude", codex_local: "codex", diff --git a/packages/adapter-utils/src/acpx-engine/execute.ts b/packages/adapter-utils/src/acpx-engine/execute.ts index 6d4a849d9fd..1e97358f19a 100644 --- a/packages/adapter-utils/src/acpx-engine/execute.ts +++ b/packages/adapter-utils/src/acpx-engine/execute.ts @@ -54,6 +54,7 @@ import { type AcpRuntimeTurnResult, } from "acpx/runtime"; import { + ACPX_SESSION_CONFIG_FAILED_ERROR_CODE, DEFAULT_ACP_ENGINE_AGENT, DEFAULT_ACP_ENGINE_MODE, DEFAULT_ACP_ENGINE_NON_INTERACTIVE_PERMISSIONS, @@ -1442,7 +1443,7 @@ function classifyError( if (acpCode === "ACP_BACKEND_MISSING") return "acpx_backend_missing"; if (acpCode === "ACP_BACKEND_UNAVAILABLE") return "acpx_backend_unavailable"; if (phase === "ensure_session") return "acpx_session_init_failed"; - if (phase === "configure_session") return "acpx_session_config_failed"; + if (phase === "configure_session") return ACPX_SESSION_CONFIG_FAILED_ERROR_CODE; if (phase === "turn") return "acpx_turn_failed"; return null; })(); diff --git a/packages/adapters/codex-local/src/index.ts b/packages/adapters/codex-local/src/index.ts index 6af08521c29..96a0cff1126 100644 --- a/packages/adapters/codex-local/src/index.ts +++ b/packages/adapters/codex-local/src/index.ts @@ -104,5 +104,5 @@ Notes: - Some model/tool combinations reject certain effort levels (for example minimal with web search enabled). - Fast mode is supported on GPT-5.5, GPT-5.4 and manual model IDs. When enabled for those models, Paperclip applies \`service_tier="fast"\` and \`features.fast_mode=true\`. - When Paperclip realizes a workspace/runtime for a run, it injects PAPERCLIP_WORKSPACE_* and PAPERCLIP_RUNTIME_* env vars for agent-side tooling. -- Codex ACP is the preferred auto lane when Node >=22.13.0 and the Codex ACP server are available. It reuses shared ACP prompt/runtime guidance, selected skill materialization into CODEX_HOME/skills, model/reasoning/fast-mode session config, and existing quota-window reporting. Auto selection falls back to CLI when ACP prerequisites are unavailable; explicit engine="acp" fails loudly. +- Codex ACP is the preferred auto lane when Node >=22.13.0 and the Codex ACP server are available. It reuses shared ACP prompt/runtime guidance, selected skill materialization into CODEX_HOME/skills, model/reasoning/fast-mode session config, and existing quota-window reporting. Auto selection falls back to CLI when ACP prerequisites are unavailable or the ACP server rejects the session configuration (for example a manual model id its set_config_option validation does not list); explicit engine="acp" fails loudly. `; diff --git a/packages/adapters/codex-local/src/server/execute.acp-fallback.test.ts b/packages/adapters/codex-local/src/server/execute.acp-fallback.test.ts index 85203b0b511..0c4284ac084 100644 --- a/packages/adapters/codex-local/src/server/execute.acp-fallback.test.ts +++ b/packages/adapters/codex-local/src/server/execute.acp-fallback.test.ts @@ -158,4 +158,75 @@ describe("codex_local ACP startup fallback", () => { expect(runAdapterExecutionTargetProcess).not.toHaveBeenCalled(); }); + + it("falls back to Codex CLI when auto-selected ACP rejects session configuration", async () => { + executeCodexAcp.mockResolvedValueOnce({ + exitCode: 1, + signal: null, + timedOut: false, + errorMessage: + 'Agent rejected session/set_config_option for "model"="gpt-5.5-pro": Invalid params (ACP -32602).', + errorCode: "acpx_session_config_failed", + provider: "acpx", + model: "gpt-5.5-pro", + resultJson: { phase: "configure_session" }, + summary: "config rejected", + } as never); + const ctx = buildContext({ model: "gpt-5.5-pro" }); + + const result = await execute(ctx as never); + + expect(result.exitCode).toBe(0); + expect(result.summary).toBe("hello"); + expect(executeCodexAcp).toHaveBeenCalledTimes(1); + expect(runAdapterExecutionTargetProcess).toHaveBeenCalledTimes(1); + expect(ctx.onLog).toHaveBeenCalledWith( + "stderr", + expect.stringContaining("Codex ACP rejected session configuration"), + ); + expect(ctx.onLog).toHaveBeenCalledWith( + "stderr", + expect.stringContaining("gpt-5.5-pro"), + ); + }); + + it("returns the session-config failure unchanged when engine=acp is explicit", async () => { + const failed = { + exitCode: 1, + signal: null, + timedOut: false, + errorMessage: "Agent rejected session/set_config_option", + errorCode: "acpx_session_config_failed", + provider: "acpx", + resultJson: { phase: "configure_session" }, + summary: "config rejected", + }; + executeCodexAcp.mockResolvedValueOnce(failed as never); + const ctx = buildContext({ engine: "acp" }); + + const result = await execute(ctx as never); + + expect(result).toBe(failed); + expect(runAdapterExecutionTargetProcess).not.toHaveBeenCalled(); + }); + + it("does not fall back for other failed ACP results", async () => { + const failed = { + exitCode: 1, + signal: null, + timedOut: false, + errorMessage: "turn failed", + errorCode: "acpx_turn_failed", + provider: "acpx", + resultJson: { phase: "turn" }, + summary: "turn failed", + }; + executeCodexAcp.mockResolvedValueOnce(failed as never); + const ctx = buildContext(); + + const result = await execute(ctx as never); + + expect(result).toBe(failed); + expect(runAdapterExecutionTargetProcess).not.toHaveBeenCalled(); + }); }); diff --git a/packages/adapters/codex-local/src/server/execute.ts b/packages/adapters/codex-local/src/server/execute.ts index 976b63cf988..7cabaabd1fd 100644 --- a/packages/adapters/codex-local/src/server/execute.ts +++ b/packages/adapters/codex-local/src/server/execute.ts @@ -64,6 +64,7 @@ import { formatOutputInactivityMonitorErrorMessage, resolveCodexInactivityTimeout, } from "./output-inactivity-monitor.js"; +import { ACPX_SESSION_CONFIG_FAILED_ERROR_CODE } from "@paperclipai/adapter-utils/acpx-engine/constants"; import { createCodexAcpExecutor, formatCodexAcpFallbackMessage, @@ -330,8 +331,9 @@ export async function ensureCodexSkillsInjected( export async function execute(ctx: AdapterExecutionContext): Promise { const engineSelection = await resolveCodexExecutionEngineForRun(ctx); if (engineSelection.engine === "acp") { + let acpResult: AdapterExecutionResult | null = null; try { - return await executeCodexAcp(ctx); + acpResult = await executeCodexAcp(ctx); } catch (err) { if (engineSelection.explicit) throw err; const reason = err instanceof Error ? err.message : String(err); @@ -340,6 +342,21 @@ export async function execute(ctx: AdapterExecutionContext): Promise