codex
ACP bridge for the Codex CLI. Spawn launches codex app-server and translates
its JSON-RPC notifications into ACP session updates.
Protocol reference
Codex App Server
The app-server surface is what this package speaks: initialize, thread/start,
turn/start, and the item/* and turn/* notification streams handled in
events.go.
ACP versions
This adapter implements ACP v1 using acp-go-sdk v0.13.5. A client requesting
v2 receives protocolVersion: 1 and can continue with v1 or disconnect, as required
by version negotiation.
Codex's App Server API also has types named v2; that is a separate protocol and
does not indicate ACP v2 support.
The ACP v2 draft was
reviewed on 2026-09-07. It needs more than a version-number change:
| Surface |
Implemented v1 |
Draft v2 |
| Prompt completion |
Pending request returns stopReason |
Immediate acknowledgement; state_update reports completion |
| Initialization |
agentInfo, agentCapabilities |
info, capabilities with a different structure |
| Tools and permissions |
tool_call, tool_call_update, permission toolCall |
Tool upserts, permission title and subject |
| History |
session/load replays history |
session/resume with replayFrom |
Keep one implementation while the Go SDK targets v1. Native v2 support should
migrate the connection surface together with these lifecycle changes when SDK
support is available. The wire tests exercise both a v1 initialization and a v2
initialization falling back to v1, including both cancellation methods.
Connection reliability
Each turn has an ordered, bounded event queue. The app-server reader only routes
events, so a client update or permission dialog cannot block RPC replies. Events
and approval requests with a turn ID must match the active turn, including events
that arrive before the turn/start reply. Replaced sessions cannot remove the
replacement's handlers during cancellation cleanup.
Command output is buffered and sent once when the command completes. Each command
retains at most the last 1 MiB, with a visible truncation marker. The same limit
applies to command output replayed from history. This follows the scratch Codex
adapter's fixes for quadratic output growth (4e5cffb, 8aef91b).
RPC acknowledgements have a 30-second deadline; interrupts have a 2-second
deadline. A timed-out start or interrupt retires the backend connection because
the adapter cannot establish which work is still running. Stalled client writes fail after
10 seconds and close the stdio output. These limits apply to transport operations;
model turns and user approval waits have no fixed overall deadline. Backend EOF,
framing errors, malformed completion payloads, exhausted retries, and failed turns become errors. Retrying error
events keep the turn open, and already-received completion events survive EOF.
Process exit is also monitored independently of stdout, so a tool that inherits
the pipe cannot keep a dead app-server session pending.
Codex answers no active turn to interrupt until a just-started turn becomes
interruptible, so a cancellation retries turn/interrupt with a short bounded
backoff instead of letting that turn run on.
Cancellation releases active and cancelled queued prompts. Late permission
responses cannot approve cancelled work, and steering leaves an open permission
request intact. These checks apply the lifecycle lessons from the reference
Codex cancellation fix (f67ca5f) and Claude's exact result attribution and
JetBrains approval/steering fixes (a04d354, 8710ce1). The Codex app-server's
turn/steer API already queues input without Claude's interrupting now delivery.
The next prompt waits for cancellation cleanup, even when the previous start
reply arrived after cancellation. Resolving a backend approval cancels only that
request's dialog, using both its request ID and thread ID.
URL elicitation IDs are generated per interaction. Accepting a URL request means
consent to open it; serverRequest/resolved does not prove that the external
workflow finished. The adapter therefore omits the optional elicitation/complete
notification instead of completing unrelated URL interactions. This follows the
elicitation lifecycle.
The pinned Go SDK still lacks top-level elicitation scope fields; session scope
is currently carried in _meta.sessionId. Standard permission requests carry
their normal top-level sessionId.
reliability_test.go, rpc_test.go, turn_stream_test.go,
approval_lifecycle_test.go, and session_lifecycle_test.go cover these cases with local transports and a helper
process. protocol_review_test.go and cancellation_order_test.go verify protocol
envelopes and cancellation ordering. These tests do not make model requests or
replace an IntelliJ integration test.
History and reference implementations
session/load reads all history before reporting success. Paginated Codex stores
use thread/turns/list with full items, preserve chronological replay order, and
reject repeated cursors. Legacy stores retain thread/read with includeTurns.
Read failures are reported instead of silently returning an empty conversation.
Resume and fork requests omit unnecessary history hydration. These changes follow
the current adapter's history pagination fix.
history_review_test.go covers both store formats and failure cases.
Codex writes a thread's rollout on its first user message. Resuming or loading a
session that was never prompted falls back to thread/read and replays no
history; deleting it, an unknown ID, or an ID Codex cannot parse succeeds.
The 2026-10-13 review compared the App Server adapter at a636804 (v2.2.2,
pinned to Codex 0.160.1; Wingman launches 0.162.1) and classified all 18 commits
since ca1d971. Prompt usage now covers every model request of the prompt,
including later backend turns such as plan implementation (95bc33f): it is
the thread total at the end minus the total at the start, or the sum of the
distinct per-request counts when the start is unknown or the total moves back.
A new thread starts at zero; the RPC reader retains restored thread totals and
interrupt-cleanup updates even without an active prompt consumer. Compaction
also counts as model work, including before ordinary output on a resumed thread.
Synthetic context estimates and context-full markers are excluded from prompt
spend; they still update the baseline used to identify the next model request.
Cache writes are separated from fresh input
and reported as cachedWriteTokens. usage_test.go covers these cases, and
TestLiveCodexPromptUsageCoversEveryRequest checks a tool-using prompt against
the installed CLI. Whitespace-only session titles and the deleted-workspace
error hint (90fea43, a636804) remain low-priority differences.
The 2026-10-05 review compared scratch Codex at 7c2ce90716, the retired scratch
Codex adapter at 296069e, Claude ACP at 0724b17, and the current
App Server adapter at ca1d971.
The scratch Codex adapter has moved development to that App Server adapter.
Wingman already consumes consolidated aggregatedOutput and accepts developer
input messages for base instructions. The review added readable service errors,
an actionable thread_active_writer error on resume/load, and imported attachment
replay. Desktop attachment envelopes become resource links with the original
request text; duplicate native references are omitted, while unfamiliar envelopes
remain verbatim. File mentions and local/remote audio also remain visible.
upstream_review_test.go and attachments_test.go cover these fixes.
Upstream session notices and compaction_update need session update variants
and client capabilities that acp-go-sdk v0.13.5 does not model, so Codex
advisories and compactions keep their text and tool-call presentation. Terminal
output metadata is not implemented here.
Extensions
- Tool calls name Codex's model-facing tool in
_meta.codex.toolName
(exec_command, write_stdin, view_image, request_permissions, or a
namespaced dynamic tool). ACP v1 has no tool name field.
_meta.mcpStartupAwaitTimeoutMs on session/new, session/resume, and
session/fork waits up to that many milliseconds for the requested MCP
servers in that session to report ready, failed, or cancelled.
- Codex
request_user_input questions become one form elicitation for clients
with form support; other clients return no answers. An "Other" question gets
a "None of the above" choice and a note field whose answer is sent as
user_note: ….
- Authentication failures (
unauthorized or HTTP 401) end the prompt with ACP
auth_required instead of a chat message.
Plans
This bridge currently does not forward turn/plan/updated task lists as ACP v1
plan notifications. A completed Codex plan item is surfaced as agent text and
offered to the client via requestPlanImplementation. Approving implementation
starts a separate backend turn with its own event and approval lifecycle.
Live regression tests
live_regressions_test.go drives the installed Codex app-server through ACP
without making model requests. It checks that an unprompted session retains
Plan mode on resume and load, and that an MCP startup wait only completes for
its own session. The MCP test starts two real stdio servers with independently
controlled handshakes and observes the CLI's startup notifications.
Run from the repository root:
CODEX_ACP_LIVE=1 go test ./pkg/acp/codex -run '^TestLiveCodex(UnpromptedPlanMode|MCPStartupSessionIsolation)$' -count=1 -v -timeout=2m
The live usage tests compare prompt usage with the sum of distinct model
requests reported by the CLI. They cover a tool-using prompt, a follow-up after
cancellation, and a resumed thread forced to compact before ordinary output:
CODEX_ACP_LIVE=1 go test ./pkg/acp/codex -run '^TestLiveCodex(PromptUsageCoversEveryRequest|UsageAfterCancellation|UsageWithInitialCompaction)$' -count=1 -v -timeout=8m