This package uses the bundled @openai/codex dependency by default.
Set CODEX_PATH to run a different Codex binary; versions other than the one specified in package.json may not be compatible.
CODEX_API_KEY- API key used when the API-key auth method is selected. Takes precedence overOPENAI_API_KEY.OPENAI_API_KEY- fallback API key used when the API-key auth method is selected.CODEX_PATH- run a specific Codex executable instead of the bundled package dependency.CODEX_CONFIG- JSON object merged into the Codex session config.hooksmove to App Server startup (-c hooks=...) for AIR review; Codex rejects malformed hooks at startup andinitializereports its error.MODEL_PROVIDER- model provider to pass to Codex for new sessions.DEFAULT_AUTH_REQUEST- ACP auth request JSON used when Codex requires authentication.INITIAL_AGENT_MODE- initial mode id:read-only,workspace-write,agent, oragent-full-access.NO_BROWSER- hide browser-based ChatGPT auth when set.APP_SERVER_LOGS- directory for adapter logs.CODEX_ACP_APP_SERVER_CRASH_LIMIT- how many crashes of the Codex app-server in the crash window stop its automatic restart (default5).CODEX_ACP_APP_SERVER_CRASH_WINDOW_MS- the crash window in milliseconds (default300000, 5 minutes).CODEX_ACP_PROMPT_SUGGESTIONS- set tofalseto turn off the predicted follow-up prompt for AIR. It costs one extra model call per turn.
- Download and install C++ redistributable package
Run from sources
- Install dependencies
npm install - Adjust ACP client config
{
"agent_servers": {
"Codex (app-server)": {
"command": "npm",
"args": ["run", "start", "--prefix", "/path/to/project/"],
"env": {
"CODEX_PATH": "node_modules/.bin/codex",
"APP_SERVER_LOGS": "optional/path/to/existing/log/directory"
}
}
}
}Run from binaries
- Download a
codex-acp-<platform>.ziparchive from https://github.com/agentclientprotocol/codex-acp/releases (<platform>is one of:linux,darwin,win32) - Unzip the archive:
unzip codex-acp-<platform>.zip
- Adjust ACP client config
{
"agent_servers": {
"Codex (app-server)": {
"command": "/path/to/codex-acp",
"env": {
"CODEX_PATH": "/path/to/codex"
}
}
}
}Building standalone binaries requires bun.
Build single-file executables in dist/bin directory:
npm run bundle:allPackage binaries into zip archives:
npm run package:all- Update the
@openai/codexversion inpackage.json(underdependencies). - Regenerate Codex types in
src/app-server/:npm run generate-types - Ensure there are no type errors or failed tests:
npm run typecheckandnpm run test
npm test: unit tests with the Codex app-server mocked.npm run test:integration: real codex-acp plus a realcodex app-server. The model is a local mock of the OpenAI Responses API (src/__tests__/integration/mock-responses-server.ts) reached through thegatewayauth method, so no API key or network is needed. Tests script model replies withtextResponse,execCommandResponseandapplyPatchResponse.npm run test:e2e: real OpenAI model. Builds first and requiresRUN_E2E_TESTS=true(set by the script) plusOPENAI_API_KEYorCODEX_API_KEYin the environment.
The adapter implements Session Notices
for Codex warnings, configuration warnings, deprecation notices, model rerouting, and the legacy
thread/compacted advisory when the client advertises clientCapabilities.session.notices: {}.
These are live session/update notifications with
sessionUpdate: "notice", a severity, a plain-text title, and optional description.
They are not replayed from session history and repeated notices remain independent events.
Without that capability (including absent or null capability objects), the adapter preserves
the existing assistant/thought text or AIR sessionFailure advisory records. When notices are
enabled, they take precedence over AIR advisory records. Clients control their presentation;
the adapter does not rely on notices being displayed.
Command replies, review results, and terminal/retrying errors retain their existing response or failure channels. Clients advertising session compaction support continue to receive the dedicated compaction lifecycle instead of the legacy completion advisory.
The adapter disables Codex skills whose output ACP clients cannot show, and leaves them out of the skill commands and
/skills. The list is in src/UnsupportedSkills.ts. Today it holds Codex's bundled Visualize skill: it ends a reply
with a content reference that only the OpenAI apps render, so ACP clients show raw visualize{...} text.
The adapter matches skills by name, so it needs no skills/list request when a session starts. The cost: a skill from
another plugin with the same qualified name, such as a visualize skill in some other plugin named visualize, is
disabled too.
The adapter restarts the Codex app-server when it dies (for example, killed for lack of memory). Its code is in
src/app-server-recovery/.
- A request that was waiting on the dead app-server fails with error
1001and the cause, such as "was killed by SIGKILL, which usually means it ran out of memory". A running prompt ends at once: open tool calls becomefailed, open permission dialogs are cancelled, and an AIR client gets thetransport_lostsession failure. - An app-server that exits before its
initializehandshake succeeded, such as one that rejects its startup config, fails the request with "Codex process has exited with code N:" and its stderr, and no promise of a restart. - The next request starts a new app-server with the same
initializehandshake and provider routing. A session that was open is resumed on its next use, with the model, mode and settings that the adapter kept. - Codex writes a thread to disk only with its first message, so a session without messages cannot be resumed in a new app-server, after a crash or a provider restart. Its next use fails with "had no messages yet and was lost"; the client starts a new session. A provider restart still completes for the agent and the other sessions.
- After
CODEX_ACP_APP_SERVER_CRASH_LIMITcrashes in the crash window the adapter stops restarting it and says so, with the stderr of the last crash. A session that the app-server died opening twice in 30 minutes is not opened again for the rest of that time, so one session that is too large for the memory cannot take the other sessions down with it.
The agent speaks both ACP v1 and the unstable ACP v2 (Draft) protocol from the same process; the
version is negotiated on the first initialize request and fixed for the connection's lifetime.
The v2 chain registers: initialize, session/new|list|close|delete|resume (v2 folds session/load
into session/resume with replayFrom), session/set_config_option, session/cancel,
auth/login|logout, session/prompt (plus $/cancel_request), session/fork, and
providers/list|set|disable. Unstable extension methods on top of that: _session/steering,
_session/goal (set/pause/resume/clear), _session/async_task/stop, and the AIR session index
requests (_session/rename, _session/archive|unarchive, _session/list/subscribe|unsubscribe).
session/fork drops the
v1 modes field (v2 removed the modes API in favor of config options), and forked sessions do not
get an available_commands_update, matching v1. There are no new environment/config knobs specific
to v2; it reuses the runtime environment variables above.
Both ACP versions advertise systemPrompt: {}: text-only append mode. Override and non-text
content are rejected with -32602 before a session is created or a fork is made.
Text blocks are joined with a blank line and appended to the existing developer-instruction
baseline: AIR customInstructions when supplied, otherwise configured Codex developer instructions.
Codex's base instructions and workspace context remain intact. Requests without systemPrompt
keep the legacy AIR behavior.
New sessions use the instructions on their first turn. A changed prompt on an idle, persisted session detaches and reloads that thread under the same session ID. The retained model context changes at the next compaction, not immediately after resume; the adapter does not force a compaction. Forks with changed instructions have the same deferred behavior.
Changed prompts are rejected while session work or background terminals are running, when a live session has no saved rollout yet, or when Codex cannot confirm a reload. Existing work is not cancelled. Retry after work finishes, or create a new session for immediate instructions. New prompts, steering, and config changes cannot race an instruction reload.
Omitted/null prompts keep the instructions known to the currently open adapter session, including through app-server recovery and provider restarts. After closing a session or restarting the adapter, clients should resend their prompt on load/resume/fork; without it, Codex's retained context and configured defaults determine the instructions.
/compact finishes foreground work only after Codex reports turn/completed for the compact
turn. Item completion still reports compaction progress, but does not release the next prompt.
The v1 prompt response and v2 idle state wait for the turn to finish; v2's early prompt-acceptance
response is unchanged.
These are behaviors the AIR (JetBrains) ACP client relies on that aren't obvious from the wire schema alone:
- Initial commands come from
availableCommandsin v2session/new,session/resume, andsession/forkresponses, not an initialavailable_commands_update. Later command changes still use notifications. Text inputs usetype: "text", and AIR command actions and skill paths are preserved. Skill discovery is best-effort: setup succeeds without initial commands if it fails. - Failed foreground work ends with
state: "idle",stopReason: "error"and a standarderrorobject containingcodeand a user-facingmessage. Before insertion, the prompt also fails with a JSON-RPC error; after insertion, its successfulmessageIdresponse is not revoked. Cancellation remainscancelled. Retries, individual failed tools, and idle background errors do not become terminal session failures. Raw process stderr and opaque error data are not sent in the standard error object. - On a failed prompt, read
sessionFailure(_meta.jetbrains.air.sessionFailure) andquota(_meta.quota) from the idlestate_updatethat ends the turn, not from a separate error channel. The same_metav1 attaches toPromptResponseis copied onto the v2 idle update. - Custom session updates are renamed on v2:
subagent_spawned/subagent_state_updatebecome a single_subagent_update(gated on the AIRnativeSubagentSessionscapability; this contract is retained independently of standard subagent sessions), andasync_task_spawned/async_task_state_updatebecome_async_task_spawned/_async_task_state_update(gated on the AIRasyncTaskscapability). Payload shapes are unchanged. - Foreground state is independent of prompt acceptance.
runningcan precede the user-message echo, including while Codex runs hooks. A successful prompt response still waits for insertion. - Every successful
session/resumesends a current-state snapshot after any replay and before its response. An idle snapshot is not another completed turn and repeats no usage or failure metadata. - Permission and elicitation requests affect aggregate foreground state: another runnable item keeps the session running, and answering one wait does not resume work that remains blocked. Withdrawing a request on cancellation does not resume the workflow; late answers cannot revive an ended turn.
- A steer (
_session/steering) shows up as auser_messageonce it actually lands in the turn. - On cancel, any permission or elicitation request still waiting on a client response is withdrawn
with
$/cancel_request, not just the underlying Codex turn. - Turns Codex starts on its own (goal auto-turns, e.g. after
session/resumeorsession/new) are rendered the same as prompted turns (runningthen oneidle) but with no correspondingsession/promptcall — clients should not assume everyrunning/idlepair has a matching outbound prompt. - After a tool call streams terminal output as
terminal_output_chunks, completion does not resend a fulloutputsnapshot; the streamed chunks are the only copy of the output. - The AIR tool call contract (
docs/air-extensions.md) does not apply on v2: every v2 client gets the standard tool call reports, sorawInputRendering,planContentDeltaanddiffPatchare not offered there. A read, search or list command has no terminal; its output arrives ascontenttext when it completes. A file change sends one diff per file whose Git patch is built from the Codex diff. - The AIR
sessionIndexextension (docs/air-extensions.md, "Session index") works as on v1: a client that declaressessionIndexgets it,session/listreads_meta.jetbrains.air.list, and rows,session_info_updates and_session/list/changescarry the same_meta. - Other AIR
_metakeys (goal, message phase, mode kind, permission presentation) go only to AIR clients, as on v1. Every v2 permission request still gets its prompttitleanddescription.
Non-AIR v1 clients opt in with clientCapabilities.subagents: {}. Non-AIR v2 clients use
standard subagent_update associations automatically. AIR keeps its existing opt-in, updates,
and generation-based identities; clients do not receive both contracts.
A standard child keeps one opaque conversation ID across assignments and parent handler changes. Its ancestors are announced before its output, tool updates, or interactions. Permissions and elicitation use the child's ID and survive parent replay; actual work endings withdraw obsolete requests locally, even if the client returns a late answer. Child tools, terminals, plans, titles, and observed configuration stay separate from the parent.
Only cancellation is advertised, and only while an owned, observable child's current turn can be
identified. Send session/cancel with its opaque ACP child ID to interrupt that work and observed
owned descendant work, not its parent or siblings. Raw provider IDs cannot bypass the restriction.
Prompting, loading, configuration changes, closing, and other child mutations remain disabled.
Parent cancellation still interrupts observed live descendants.
An observed work ending reports idle; loss of observability reports unknown, not a fabricated
failure or a permanently closed conversation.
Cancellation withdraws affected permission/elicitation requests, but an interrupt acknowledgement does not fabricate a cancelled outcome. Actual provider completion reports the ending; the same conversation may receive later work. Idle or unobservable children have no cancel capability. Reattachment suspends child controls until the latest successful response is actually written, including complete JSON-RPC batches; historical or copied child records never grant control.
Child config_option_update notifications contain complete reported snapshots. Observed model and
reasoning effort use singleton selects, including changes between assignments. Unknown or unset values
are omitted; no values are inferred from the parent or requested spawn arguments. These reports do not
enable configuration changes, even to the current value. Replayed configuration is historical, not
confirmed current. Reattachment drops cached confirmation and refreshes loaded children after the
response. Optional reads do not block child delivery; newer observations or attachment changes discard
stale results.
Replay follows both recorded spawn formats and replays available child transcripts once per conversation, including nested children, using legacy or paginated history. Announcements and all selected history precede the response. Completed snapshots reconcile tool and rendering state before duplicate live payloads are suppressed; live bookkeeping still runs. In-progress commands retain classification without resetting streamed output. Incomplete text has no safe delta cursor; it and other records without a safe partial representation are omitted rather than replayed over newer content. Agent-owned input is not rendered as human input. Missing, omitted, or incomplete child history produces a parent advisory; an empty transcript alone does not invent a gap.
Replay reports the latest recorded child turn's known success, cancellation, or failure as historical
idle before the response. A missing or still-running turn invents no outcome. Legacy history reuses
its turn data; paginated history reads the latest turn summary separately, retaining available text
and reporting a gap if that summary is unavailable. Recorded outcomes do not enter the live foreground
cache, withdraw current interactions, confirm runtime ownership, or grant controls.
Rejoining a loaded child still requires current provider parentage; copied fork history can be displayed
but cannot acquire the original runtime's subscriptions.
After replay, current child work/configuration snapshots follow the actual response write, including whole
JSON-RPC batches. Parent setup operations serialize without waiting on each other's batch delivery.
providers/set lists the models of the gateway (GET {baseUrl}/models, with the headers Codex sends to it)
before it responds. The models reported to the client are then only the Codex models whose model or id the
gateway lists, matched without case and date suffix. A session whose thread model the gateway does not list uses
the default of these models; a thread model that the gateway lists but Codex does not know is kept. A gateway that
lists none of the Codex models leaves no model option at all, and the session keeps the thread's model as current.
Sessions already open get the models of the new routing when the provider restart resumes them, after
providers/disable too. A 4xx answer means the gateway has no listing: providers/set succeeds and nothing is
narrowed. A failed listing (network error, a status other than 2xx or 4xx, malformed body, or the timeout) fails
providers/set with internal_error and keeps the current routing; a base URL that is not http(s) fails with
invalid_params. A listing that finishes after a newer providers/set or providers/disable was applied is
dropped. The gateway auth method and providers from
the Codex config are not listed.
- After
providers/disable("openai"),providers/liststill reports native OpenAI routing as the current provider. The providers RFD says a disabled provider'scurrentshould benull; this is left as-is and documented here rather than fixed. - Open question for AIR: AIR's fork points match Codex item ids, but on v2 a user message's
rendered id is the client-minted
clientId(when the client supplied one), not the Codex item id. A fork at a user message would not resolve. This is flagged for AIR, not fixed here.