# Codex app-server protocol How `internal/codex` drives `codex app-server`. The server's own schema is the reference: `codex app-server generate-json-schema --experimental --out DIR` writes it, and `codex_app_server_protocol.schemas.json` in DIR is the whole bundle (top-level definitions plus a nested `v2` set). ## Binary and version byre runs `codex` from `PATH`. On the owner's machine that is the CLI inside the ChatGPT desktop app (`ls -la ~/.local/bin/codex` shows the link), so the version moves when the app updates itself; `codex --version` prints it. byre passes `-c check_for_update_on_startup=false` so the CLI never checks for or offers updates of its own. `SchemaVersion` in `internal/codex/protocol.gen.go` names the version the types were generated from; when it differs from `codex --version`, regenerate the types (below). ## Generated types `go generate ./internal/codex` regenerates `protocol.gen.go` from the CLI's schema; run it after a CLI update. It uses the experimental schema because the server sends experimental fields (such as `environments` on a thread) even to clients that don't opt into them. ## Launch and framing ``` codex app-server -c check_for_update_on_startup=false ``` Shell commands inherit the app-server's environment. NDJSON both ways on stdio. Messages are JSON-RPC 2.0 without the `"jsonrpc"` member: requests `{"id","method","params"}`, responses `{"id","result"}` or `{"id","error":{"code","message"}}`, notifications `{"method","params"}`. The server adds `emittedAtMs` to its notifications. The server reads `config.toml` from `CODEX_HOME` (default `~/.codex`), so the MCP servers, plugins and `notify` program configured there apply to the session; `-c mcp_servers={}` doesn't remove them, a separate `CODEX_HOME` does. ## Session lifecycle 1. `initialize` with `clientInfo` and `capabilities.experimentalApi` true (dynamic tools need it: without it `thread/start` with `dynamicTools` fails with `thread/start.dynamicTools requires experimentalApi capability`), then the `initialized` notification. 2. `thread/start` (`cwd`, `model`, `approvalPolicy` `never`, `sandbox` `danger-full-access`, `developerInstructions` for byre's guidance, `dynamicTools` for the tool schemas), or `thread/resume` with `threadId` and the same settings but no tools, or `thread/fork` (`threadId`, `lastTurnId`, the last turn the fork keeps; the turn must not be in progress) with the same settings. The server picks the thread id, returned as `thread.id`; resuming keeps it, forking makes a new one. A thread's dynamic tools are stored with it, so resumed and forked threads keep them. 3. `turn/start` with `threadId` and `input`, a list of `UserInput`: `{"type":"text","text":...}`, and for each image `{"type":"image","url":"data:;base64,..."}` before the text. Additional turn context uses `additionalContext`, for example `{"byre":{"kind":"application","value":...}}`. `model` on it switches the model for that turn and every later one; `thread/read` reports the thread's current `model`. The response carries the new turn; `turn/started` follows. 4. `turn/steer` with `threadId`, `expectedTurnId` (the active turn) and `input` adds input to the running turn; the model reads it at its next step without the turn ending. 5. `turn/interrupt` with `threadId` and the active `turnId` ends the turn: `turn/completed` arrives with `status` `interrupted` within milliseconds, a running command is killed, and the thread stays usable. 6. `thread/compact/start` runs compaction as a turn of its own: `turn/started`, an item of type `contextCompaction`, `turn/completed`. The thread keeps its id and remembers the conversation in summary. ## Dynamic tools A `dynamicTools` entry of type `namespace` holds tools as `function`s with their names, descriptions and input schemas. Names and namespaces starting with `mcp__` are reserved (`dynamic tool name is reserved`), so byre's tools are `ask_human` and so on in the namespace `byre`; the model refers to them as `byre__ask_human`. A call arrives as the server request `item/tool/call` (`namespace`, `tool`, `arguments`, `callId`, `turnId`); the client answers `{"success":true,"contentItems":[{"type":"inputText","text":...}]}`, or `success` false with the error text. The call also shows as a `dynamicToolCall` item. Closing stdin ends the process. ## Events - `turn/started`, `turn/completed` (`turn.status` `completed`, `interrupted` or `failed` with `turn.error`). A failed request to the model also sends an `error` notification first, with `willRetry`. - `item/started` and `item/completed` with a `ThreadItem`: `userMessage`, `agentMessage` (`text`), `reasoning`, `commandExecution` (`command`, `aggregatedOutput`, `exitCode`), `fileChange`, `mcpToolCall`, `contextCompaction` and others. - `item/agentMessage/delta` (`itemId`, `delta`) streams an agent message; the deltas join to the completed item's `text`. `item/commandExecution/outputDelta` and the reasoning deltas stream the same way. - `thread/tokenUsage/updated` carries `last` and `total` token counts and `modelContextWindow`; `account/rateLimits/updated` the account's rate-limit windows (the same snapshot `account/rateLimits/read` returns on request, as `rateLimits`); `thread/status/changed`, `thread/readState/changed`, `mcpServer/startupStatus/updated`, `account/updated` and `remoteControl/status/changed` also arrive. Besides `item/tool/call`, server requests (approvals, user input) shouldn't arrive with approvals off; if one does, command and file-change approvals are accepted and anything else gets a JSON-RPC error.