Repository navigation
feat: add ACP v2 support again - #620
Merged
Merged
Conversation
2 tasks done
EugeneTheDev
added a commit
that referenced
this pull request
Oct 13, 2026
## Summary The E2E run on #620 ([run](https://github.com/agentclientprotocol/codex-acp/actions/runs/38395294465)) failed on two model-dependent tests. #620 didn't cause it: the run before it shows the same behavior and passed only by luck. This PR changes test code only. - **Default e2e model `gpt-5.2` → `gpt-6-luna[low]`.** `gpt-5.2` isn't in the Codex model list anymore, so Codex used fallback metadata. In 0.160.1 that has `apply_patch_tool_type: None`, so the model ran `apply_patch` through the shell. A malformed patch came in as an `execute` approval, not `edit`, and broke `applies an approved workspace file edit`. - **Shell approval prompts ask for elevated permissions on the first try.** In read-only mode the model sometimes ran the command in the sandbox, hit `Read-only file system`, and ended the turn without asking for approval (`cancels every command when cancel is selected` got `end_turn`). This applies only to the tests that expect an approval prompt. - **Cancellation test follows how Codex handles interrupts.** `gpt-6-luna` runs commands through unified exec, and `turn/interrupt` doesn't kill those processes: they keep running as background terminals. The test now: - waits until the client has seen the command's tool call; - cancels and asserts the turn is `cancelled` and the process is still running; - stops the process with `_session/async_task/stop` and asserts it exits. The ACP SDK client rejects the `async_task_spawned` update because its schema doesn't include it, so the test uses the tool call id as the task id. For a root command they're the same. - Fixture helpers: the fixture records tool calls, and `createAuthenticatedFixture` can declare AIR capabilities. ## Test plan - [x] `npx tsc --noEmit` - [x] E2E workflow on this branch: [run 38406199843](https://github.com/agentclientprotocol/codex-acp/actions/runs/38406199843). 33 passed, 2 skipped. One retry, in the exec-policy amendment test: the model mistyped the long temp path, so the saved approval rule didn't match.
31 of 57 tasks
Rizzen
pushed a commit
that referenced
this pull request
Oct 26, 2026
🤖 I have created a release *beep* *boop* --- ## [2.3.0](v2.2.2...v2.3.0) (2026-10-26) ### Features * add ACP v2 support again ([#620](#620)) ([9e1690f](9e1690f)) * mark session list rows of worktrees Codex made (agentWorktree) ([#631](#631)) ([aa02d43](aa02d43)) * narrow models to what the providers/set gateway lists ([#627](#627)) ([3f555f9](3f555f9)) * report the model of each native subagent session ([#678](#678)) ([51005a6](51005a6)) * return initial ACP v2 session commands ([#652](#652)) ([5e9b7c5](5e9b7c5)) * support AIR sessionIndex extension ([#590](#590)) v2 ([#622](#622)) ([2fd758c](2fd758c)) * support append-mode ACP system prompts ([#653](#653)) ([5993a12](5993a12)) ### Bug Fixes * disable Codex's Visualize skill, whose output ACP clients show as raw text ([#667](#667)) ([0ba94f5](0ba94f5)) * display the selected wire provider as JetBrains Air Gateway ([#589](#589)) ([cc54abf](cc54abf)) * explain Codex config load errors caused by a deleted working directory ([#626](#626)) ([a636804](a636804)) * explain invalid_encrypted_content errors after a provider change ([#668](#668)) ([a77829f](a77829f)) * hold a fork back from the session list until its session/fork response is out ([#642](#642)) ([59974c7](59974c7)) * include subagent edits in the agent file-change report ([#632](#632)) ([7d07123](7d07123)) * leave codex exec runs out of the session list without sessionIndex ([#639](#639)) ([df75d2b](df75d2b)) * migrate to ACP SDK 2.0 ([#649](#649)) ([c8fd166](c8fd166)) * publish the commands when the skills change ([#643](#643)) ([a62b568](a62b568)) * report structured ACP v2 failures ([#651](#651)) ([5377bfe](5377bfe)) * show a Codex subagent's follow-up turn in a new subagent session ([#634](#634)) ([7c761c7](7c761c7)) * synchronize ACP v2 foreground state ([#650](#650)) ([986955a](986955a)) * update codex to 0.164.0 ([#666](#666)) ([e227c0e](e227c0e)) * wait for the compact turn to finish ([#654](#654)) ([ced4c34](ced4c34)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
mrmans0n
added a commit
to mrmans0n/codex-acp
that referenced
this pull request
Oct 26, 2026
Upstream v2.3.0 (agentclientprotocol#620) installs a baseline session subscription (startCodexTurnTracker) when a session is opened, resumed or moved by a provider restart, so a Codex-started turn before the first prompt is tracked and rendered. It denies every approval and elicitation, and the lazy resume after a crash restart does not install it again. Upstream also dropped the adapter-started goal continuation, so startGoalContinuationIfCurrent is not restored. Keep every original +/- line: goal control still subscribes a never-prompted session on the installed client, with forwarded, loss-aware approvals and elicitations. Because that subscription replaces the baseline tracker, its handler now also runs the tracker's duties (foreground work, steer landing, skills change, turn completion). A new test fails without them. Adapted from a52405d (original 9d2c300).
mrmans0n
added a commit
to mrmans0n/codex-acp
that referenced
this pull request
Oct 26, 2026
Upstream v2.3.0 moved the ACP request routing from index.ts into AcpAgentRouter.ts (agentclientprotocol#620), wraps app-server request errors with explainCodexError (agentclientprotocol#626), adds insertion-aware prompts for steering (agentclientprotocol#620), a provider request ordering for providers/set (agentclientprotocol#627), and registers a baseline turn tracker before a provider restart resumes a session. None of this absorbs the downstream hook review (no upstream PR). Keep the downstream behaviour: the hook execution gate still fences thread/start, thread/resume, thread/fork, turn/start and review/start inside the new error wrapper; session/new, load, resume, fork and prompt still run under the review (prompt now forwards upstream's insertion); providers/set still invalidates the review after the routing changes; a provider restart still leaves a reviewed session for its reviewed lazy resume, before any baseline tracker is registered for it. The codexHooks initialize capability moves into initializeExtensionsMeta and stays ACP v1 only (the review is configured only by the v1 initialize). _codex/hooks/trust must keep accepting reviewToken, and its route now lives in AcpAgentRouter.ts, which is not a ledger file of this patch: this commit binds only after that path is added to the patch files (level 3). A new router test fails without it. Adapted from 4d43405.
mrmans0n
added a commit
to mrmans0n/codex-acp
that referenced
this pull request
Oct 26, 2026
Upstream v2.3.0 (agentclientprotocol#620) removed adapter-started goal continuation turns (Codex continues an active goal itself) and gave external prompts a turn-start reservation: startNewTurnFromExternalPrompt now hands its reservation to prompt() together with the insertion. No upstream PR absorbs this patch. Keep upstream's reservation call and move the downstream continueOperation into promptAfterReservation, so a turn started inside a reviewed operation still continues that review and any other prompt starts one as before. Goal set/resume control, the active thread/goal/set gate, provider inputs and the authentication invalidation apply unchanged. The fallback goal tests now expect no adapter turn/start, and the dispatch-history test moves from the removed goal fallback turn to a prompt after a crash, whose lazy thread/resume and turn/start still dispatch twice. Mutating the dispatch mark, the goal gate or the goal control review fails the tests. Adapted from 960f779.
This was referenced Oct 26, 2026
SeanLF
added a commit
to SeanLF/claude-code-subagent-harnesses
that referenced
this pull request
Oct 28, 2026
…e closest spelling first
codex-acp 2.3.0 adds ACP v2 beside v1 ("v1 stays fully supported; this is not
a cutover", agentclientprotocol/codex-acp#620); the relay still asks for v1.
It carries codex 0.164.0 and fixes cancel/close racing a turn's start. Checked
live: a read-only run answers and signs, a refused model gets suggestions, and
three parallel runs finish. The fixtures stay the 2.2.2 capture: v1's frames
change only for the bug fixes #620 lists.
gpt-6-lunar shares gpt and 6 with every gpt-6 model, so the suggestions came
in list order (astra, sol, luna); ties now go to the longest common start.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0131zPk82o4yhGkjvhSWGU4o
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
initializeand keeps it:acpV2.agentProtocolRouter().withV1(v1Agent).withV2(v2Agent).connect(stream)(src/index.ts).session updates fork by version in one place:
toV2SessionUpdate(src/AcpV2SessionUpdate.ts), called fromACPSessionConnection.Implemented (v2)
The v2 method set is implemented as specified:
initialize,session/new|list|close|delete|resume|fork,session/prompt,session/cancel,$/cancel_request,session/set_config_option,auth/login|logout,session/request_permission,elicitation/create|complete,state_update, plus the extensions below. Only the partsthat are project-specific or not obvious from the spec are described here.
Prompts and turns
session/promptis answered with{messageId}once the user message is actually inserted into a Codex turn, notwhen the request arrives.
ends. They are not steered into the running turn; steering stays a separate
_session/steeringrequest.session/cancelinterrupts the running turn and rejects every queued, not-yet-inserted prompt with -32800. Thoseprompts get no
state_update.session/neworsession/resume). These getrunning/idlelike any other turn, with no matchingsession/prompt. Clients should not assume everyrunning/idlepair belongs to one of their prompts.requires_action/runningbracket a permission or elicitation request only while a turn is running. Requests sentwhile the session is idle (e.g. MCP OAuth re-auth when a session opens) go out without the bracket.
user_message_chunk(withmessageId) per prompt block, the same shape resumereplay produces. An empty prompt sends no user message update.
/compactand/goalprompts render live only; Codex persists no user message for them, so replay doesn't showthem.
Tool calls and terminals
terminal_output_chunks without Codex's shell-wrapper framing. Completion doesn't resendthe full output: the streamed chunks are the only copy.
changes[]; adds and deletes get a synthesized git patch.Resume replay
session/resumewithreplayFrom: {type: "start"}replays the full history (messages, tool calls, plans, subagentand async-task updates) with stable ids. Each replayed message starts with a
content: []primer before its firstchunk.
recovers. Its message chunks are used to position those tool calls and are never sent.
Extensions
providers/list|set|disablereuse the v1 logic.providers/set|disablerestarts the Codex app-server and resumesevery session the client has loaded; a turn that was running at restart ends with
idle/cancelled._session/goal(set/pause/resume/clear) and_session/async_task/stop.subagent_spawned/subagent_state_updatebecome one_subagent_update(sent when AIR declaresnativeSubagentSessions);async_task_spawned/async_task_state_updatebecome
_async_task_spawned/_async_task_state_update(sent when AIR declaresasyncTasks). Payloads areunchanged.
Compatibility details
rejected.
session/listandauth/logoutaccept a request withparamsomitted. The ACP SDK (1.5.0) rejects that eventhough every field is optional, so a stream transform fills in
params: {}for these two methods only.The full client contract, including what AIR relies on, is in
readme-dev.md("ACP v2 support", "AIR v2 clientcontract").
v1 compatibility
v1 wire behavior is unchanged apart from the fixes below. Verified by:
behavior.
CONFORMANT, 50 pass / 1 fail / 5 skip, identical to the baseline before this work. The singlefailure is the pre-existing advisory
ACP-SCHEMA-002.v1Agentinsrc/index.ts) is built exactly as before, and the SDK'sprotocol router binds each connection to one chain for its lifetime.
Fixes
Pre-existing bugs, fixed on both v1 and v2 unless stated otherwise.
A goal turn that Codex starts right after a session is loaded loses its opening content
Codex auto-continues an active goal a few milliseconds after
thread/resume. The turn tracker was registered onlyafter
model/listandaccount/read, and the app-server client drops notifications that have no handler yet. Thetracker is now registered before
thread/resume/thread/loadand held until the session is ready. Paths that didn'thit the race send exactly the same frames as before.
Turns Codex starts on its own are invisible
A turn no
session/promptowns (e.g. a goal auto-continuation) was tracked but never rendered. It now renders throughthe same session-scoped path a prompt uses. A prompt-owned turn can't be rendered twice: a session has only one current
dispatcher.
Closing or cancelling a session can leave Codex running a turn
When
turn/interruptraced the turn's registration, the retry only covered cancel, not close, and it computed the turnid once instead of on each attempt. Close now gets the same retry, and the id is recomputed on every attempt.
/reviewcompletion goes to the wrong turn, and a review that fails to start hangs the promptreview/startresponds with the parent turn id, whileturn/startedcarries the reviewer's child turn id, andturn/interruptneeds the child id. The two ids are now tracked separately, so completion and errors are attributed tothe review's own turn, and a review that fails before starting ends the prompt. A successful review reports the same as
before.
Cancel leaves a pending permission or elicitation request open, and a cancelled review reports
end_turnsession/cancelonly aborted work that hadn't started a turn yet. Each prompt now has an interaction signal that canceland close abort. That withdraws any permission or elicitation request still waiting on the client (
$/cancel_request),so a late answer can't be applied. A cancelled plan-implementation review now reports
stopReason: cancelled.Replay resurrects tool calls from a rolled-back history
The response-item history fallback for old rollouts ignored
thread_rolled_back. It now bails out when the rolloutshows a rollback. Only that legacy replay path is affected.
codex-acp starts its own goal continuations on top of Codex's
codex-acp had a fallback that continued an active goal after a turn ended. Codex 0.156.1 does this itself, so the
fallback could start an unchecked turn (even for a goal whose budget was exhausted) or steer into a turn Codex had just
started. The fallback is removed; goal continuation is left to Codex.
An unknown permission outcome is treated as a selection
Permission responses were read as "selected" whenever the outcome wasn't
cancelled, but the outcome is an open union(
ACP-ENUM-203). The readers now checkoutcome === "selected". Every valid outcome behaves as before.Provider restart re-resumes forks the client hasn't loaded yet
After a fork, the session waits for the client's
session/load, but a provider restart resumed it anyway. Restart nowskips such sessions; their
session/loadresumes them as usual.Provider restart leaves the cut-off turn open forever
providers/set|disablekills the old app-server while a turn may still be running, so that turn'sturn/completedanditem/completednever arrive. Before, the resumed session also lost its turn tracker, so turns after the restartweren't rendered. Now, per session, the restart:
failedand closes its terminal;idle/cancelled;The order matters. Codex can start a goal turn about 3 ms after
thread/resume, and closing out after the resume wouldcancel that new turn instead of the old one. The list of open tool calls is cleared when a turn completes, so a later
restart can't fail a tool call from an earlier turn.
On v1 the only new frames are the
failedtool call updates and the terminal exit.session/newhas no fallback for a missingmcpServersThe other session-creation paths default it to
[];session/newnow does too.Known issues (not fixed)
A session that was never prompted is lost on provider restart
Codex writes a thread's rollout only after its first turn. If
providers/set|disableruns before a session's firstprompt,
thread/resumefails with "no rollout found", and so does thethread/readfallback. Theproviders/*requestfails with -32603 "Failed to resume 1 session(s)", and the next
session/promptfails with "thread not found". Thisalready happens on
main. Deferred: it needs a separate design for restarting threads that were never persisted. AFIXMEmarks the resume loop, andreadme-dev.md"Known gaps" describes it.A disabled
openaiprovider is still reported as currentAfter
providers/disable("openai"),providers/liststill reports native OpenAI routing ascurrent; the providersRFD says it should be
null. Left as-is deliberately and documented inreadme-dev.md"Known gaps".Fork points can't target a user message sent with a client id
AIR's fork points match Codex item ids, but on v2 a user message's id is the client's
clientIdwhen the clientsupplied one. A fork at such a message wouldn't resolve. This is an open question for AIR, documented in
readme-dev.md"Known gaps".stopReasonis only everend_turnorcancelledcodex-acp never reports
refusal,max_tokensormax_turn_requests. This predates the PR; v2 only needed themapping moved, not made more precise.
The title generator keeps using the old client after a provider restart
Title generation after a restart goes to the app-server that was killed. Out of scope for this PR.
Races with a pending provider update
A session opened while
providers/setis in flight may run on the client that is about to be killed._session/goaland_session/steeringlikely don't wait for a pending provider update either (not verified). Out ofscope.
Idle-time permission and elicitation requests can't be cancelled
session/cancelwithdraws requests through the current prompt's interaction signal. A request sent while no prompt isrunning reuses the finished prompt's signal, so cancel doesn't reach it. Low impact: these are rare (e.g. MCP OAuth
re-auth).
Extra
runningafter a late permission answer on v2If a v2 client cancels during
requires_actionand then answers the permission request anyway, it gets a redundantrunningbeforeidle. The final state is correct.Closing a session during a turn Codex started gets no interrupt retry
The registration-race retry covers prompt-owned turns only. Closing during a self-started turn that hasn't registered
yet can leave it running.
Testing
New test areas:
initialize-v2,session-lifecycle-v2(including fork and resume/replay),session-update-v2,cancel-v2,elicitation-v2,providers-v2,session-goal-v2,async-task-stop-v2,steering-v2,prompt-v2,agent-initiated-turns-v1/v2,resume-goal-turn-timing,provider-restart-turn-tracking,v2-resume-replay(including subagents),
subagent-and-async-task-updates-v2,session-config-options-v2.TCK, full run for both protocol versions against the same build:
ACP-SCHEMA-002, pre-existing advisory)Live checks against real Codex 0.156.1 (isolated
CODEX_HOME, raw JSON-RPC driver): a goal turn right aftersession/new, cancel during a real command approval, resume with an active goal in a fresh process, and a providerrestart with an active goal. The restart check found the last provider-restart fix above and the unfixed
never-prompted-session issue.
Reviewer notes
Suggested reading order:
src/index.ts: where the v1/v2 protocol router is wired up.src/AcpAgentRouter.ts: the v2 agent chain (method registrations).src/ACPSessionConnection.ts+src/AcpV2SessionUpdate.ts: thetoV2SessionUpdatefork point. This pair explainshow the rest of the code avoids version-specific logic.
src/CodexAcpServer.ts: shared business logic (turn tracking, session lifecycle, provider restart). The largest andmost-changed file.
src/CodexSessionToolCalls.ts: small new tracker of open tool calls, used by provider restart.Worth extra attention:
enqueueProviderUpdateinCodexAcpServer.ts). The per-session order (drain, closeout, then register the tracker and resume) is load-bearing and easy to break.
acquireTurnStartReservation,promptAfterReservation,promptV2).This is the one new concurrency mechanism; the rest mostly reuses existing v1 logic.