fix(engines): rewrite the Codex adapter against the real app-server protocol (#914) - #1009
Merged
Conversation
…rotocol (#914) The adapter spoke an invented protocol: `initialize` without the required `clientInfo`, id-less `thread/start`/`turn/start` with made-up params, and an event loop waiting on `session_started`/`notification`/`tool_call` — none of which exist in `ServerNotification`. Every run died at the handshake. Verified against the live `codex app-server` (codex-cli 0.141.0) and its generated JSON Schema: - handshake is `initialize` (with clientInfo) -> `initialized` notification -> `thread/start` -> `turn/start`, all id-carrying requests correlated by id, with the threadId taken from the thread/start response instead of a uuid; JSON-RPC error responses are surfaced verbatim - approvals arrive as server *requests* and are answered by their own id; any unimplemented server request gets an explicit -32601 reply so the turn can never hang waiting on us - terminal state comes from `turn/completed`'s `turn.status`, tokens from `thread/tokenUsage/updated`, output from the final agentMessage item Transport: a single reader thread drains stdout into a queue, replacing the `select()`-plus-buffered-`TextIOWrapper` mix that stranded a burst of lines until the 5-minute stall timeout. Malformed JSON is logged and skipped, now distinct from EOF, so one stray log line no longer aborts a live turn. Tests: a burst-then-silence case over a real OS pipe asserts no stall, a stray non-JSON line is proven non-fatal, and a contract test validates every outbound message against a checked-in `generate-json-schema` fixture.
…e timeouts (#914) codex review [P2]: a JSON-RPC error on the turn/start id was treated as a harmless late ack, so an invalid cwd/threadId or an auth/model refusal sat until the stall or turn timeout and lost the server's reason. Verified live: a bad threadId returns {"id":3,"error":{"code":-32600,...}} and no turn/completed ever follows.
4 tasks
Contributor
|
Claude finished @frankbria's task in 6m 4s —— View job GLM Precision Bug Review — Codex app-server adapter (#1009)Scope: concrete defects only (logic errors, null/unhandled paths, security, data loss/races, contract breaks, resource leaks). Style/architecture/coverage are covered by the general review.
✅ GLM review: no defects found. No inline findings. What I verified against the checked-in
|
3 tasks
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.
Closes #914.
--engine codexcould not work at all: the adapter spoke a protocol that does not exist.initializewas sent without the requiredclientInfo, so the server returned a JSON-RPC error and the'"result" not in response'check killed every run at the handshake.thread/startandturn/startwent out id-less with invented params, and the event loop waited onsession_started/notification/tool_call— none of which appear inServerNotification— replying with an inventedtool_call/approved.What the protocol actually is
Verified two ways: against
codex app-server generate-json-schemaand against a livecodex app-server(codex-cli 0.141.0) session.Two details that bite if you assume plain JSON-RPC 2.0: the wire format carries no
jsonrpcfield (confirmed live — the server accepts messages without it, andJSONRPCRequestrequires onlyid+method), and approvals arrive as requests that must be answered by their own id. There is noturn/failedand noturn/cancelled; the terminal state isturn.status ∈ {completed, failed, interrupted}.Changes
Handshake and turn flow — real
initialize(withclientInfo) →initializednotification →thread/start→turn/start, all id-carrying requests correlated by id. ThethreadIdcomes from thethread/startresponse instead of a locally-invented uuid. JSON-RPC error responses are surfaced verbatim with their code, at every step.Approvals answered by request id —
item/commandExecution/requestApprovalanditem/fileChange/requestApprovalget{"id": <same>, "result": {"decision": "accept"|"decline"}}per the approval policy. Any server request we do not implement gets an explicit-32601reply, because an unanswered request wedges the turn. The v1 callbacks (applyPatchApproval,execCommandApproval) use a different decision enum and are deliberately not guessed at — we are on the v2 path.Transport — a single reader thread drains stdout into a
queue.Queue, replacing theselect()-on-the-fd plus buffered-TextIOWrappermix. That mix was the stall bug: once a burst was slurped into the Python buffer,selectreported "no data" and a run that had already receivedturn/completedsat until the 5-minute stall timeout. Malformed JSON is now logged and skipped, distinct from EOF, so one stray non-JSON log line no longer aborts a live turn as "Process terminated unexpectedly".Result mapping — terminal status from
turn/completed'sturn.status(failedcarriesturn.error.message), tokens fromthread/tokenUsage/updated, andAgentResult.outputfrom the finalagentMessageitem.Sandbox default —
sandbox_modenow defaults toworkspace-writerather thanNone. The adapter exists to write code into the workspace; inheriting whatever~/.codex/config.tomlhappens to say could silently produce a read-only run.Acceptance criteria
TestCodexHandshake,TestCodexApproval(7 tests)select()and buffered reads; malformed JSON skipped and distinct from EOF; burst-then-silence asserts no stallTestCodexTransport— the burst test writes 50 lines +turn/completedinto a realos.pipe(), leaves it open, and asserts completion in < 2 s against a 2 s stall timeoutgenerate-json-schemafixtureTestCodexSchemaContractvalidates every outbound message againstClientRequest/ClientNotification/ the two approval*Responseschemas intests/core/adapters/fixtures/codex_app_server/engine_registryuntouched; proven working end-to-end belowDemo — real binary, real turn
Driving the actual
CodexAdapteragainst the realcodexCLI in a scratch git repo:The file on disk is the outcome evidence — the adapter completed a turn, the delegated agent wrote real code, and
modified_filespicked it up.Review
codex review --base main(cross-family, pre-PR) raised one [P2]: a JSON-RPC error on theturn/startid was treated as a harmless late ack, so a rejected turn would sit until the stall/turn timeout and lose the server's reason. Verified live before fixing — a badthreadIdreturns{"id":3,"error":{"code":-32600,"message":"invalid thread id: …"}}and noturn/completedever follows. Fixed in fe93933 with a regression test asserting the failure surfaces in < 2 s against a 30 s stall timeout.Tests
tests/core/adapters/— 194 passed (28 for codex, up from 20).ruff checkclean.Known limitations
cf work start --engine codex --executestill hard-requiresOPENAI_API_KEYeven though thecodexCLI works fine on a ChatGPT login — the live demo above ran with noOPENAI_API_KEYset. That gate lives incli/app.py→require_openai_api_key(), outside this issue's scope; filed as a follow-up.codexin CI) is [P0.21] Run every shipped engine adapter against its real CLI in a binary-gated smoke/contract tier #915 (P0.21). This PR verifies against the real binary manually and ships the schema fixture that tier will reuse.