The Codex app-server integration
How the desktop spawns, drives, and supervises the Codex child process: the JSON-RPC pipe, the handshake, timeouts, notifications and approvals, per-run tool injection, and clients scoped to profile pairs.
Checked against the source on 17 September 2026
On this page
A supervised child process
The spawn path resolves the bundled command, applies the app-owned runtime configuration, and opens piped stdin, stdout, and stderr. It first terminates the orphan recorded in the profile root’s app-server.pid if that process is still a codex-app-server, drains stderr separately, records the new process ID, and uses kill-on-drop. Initialization sends initialize with client information and experimentalApi enabled, then initialized; an initialization failure shuts the child down.
-c features.local_thread_store_compression=false
-c features.background_paginated_rollout_migration=false
-c features.multi_agent_v2.tool_namespace="plantocode"
-c sqlite_home=<storage-profile-home>
--allowed-storage-root <storage-profile-home>
--auth-home <execution-profile-home>
--listen stdio:// --session-source app-serverThe initialize reply must list every one of 15 required protocol extensions, among them threadItem.createdAtMs, threadItem.commandOutputSource, threadItemsList.path, threadUnload, and threadTurn.executionMetadata. One missing entry shuts the child down as incompatible with this build. This gate is what makes an arbitrary upstream binary unusable.
{"id":42,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"plantocode","version":"<build-version>"}}}- Replyid
Matched to the request that waits for it. The turn/start reply carries the turn ID that opens the turn’s channel.
- EventthreadId · turnId
Routed into that turn’s channel. While a turn/start for the thread waits, its events are held, up to 4,096, and replayed before live ones.
- Request from the childitem/tool/call · …/requestApproval
A tool call runs in its own task with only the tool name and arguments, and the actor writes the reply. An approval with no live consumer is declined.
The child runs inside an app-owned Codex profile home with a stable opaque UUID, created by the desktop below its application data. The owned runtime setup derives and validates that home, cleans inherited storage configuration, and supplies the selected home to the child, and the bundled child refuses to open any database unless --allowed-storage-root, CODEX_HOME, and sqlite_home canonicalize to the same directory.
| Deadline | Value |
|---|---|
| Child start | 35 seconds. |
| Request response | 30 seconds after the write, fixed. The caller gives up after 31 seconds. Only initialize restarts its 30 seconds each time the child sends a request before the reply. |
| stdin write | 5 seconds. |
| Command-queue reservation | 5 seconds. |
| Graceful shutdown before kill | 2 seconds. |
| Unclaimed approval | Declined automatically after 30 seconds. |
Approvals shown on a phone resolve through run.codexChatResolveApproval, which requires an idempotency key and passes the decision to the authoritative app-server resolver.
The plantocode tool namespace
When a run opens its thread with thread/start or thread/resume, the desktop passes dynamicTools and developerInstructions that add a namespace called plantocode next to the shell and file tools Codex already has. The child is launched with features.multi_agent_v2.tool_namespace set to the same name, so Codex’s own multi-agent tools share it. The set is rebuilt per run: use_user_browser, maintain_sessions, and create_html_document are always present, send_mobile_notification appears when the Desktop and phone notifications setting is on, and synthesize_speech and extract_video_context appear when a Gemini API key is saved. Every spec is marked deferLoading and embeds the current appSessionId, runId, and project directory, and the namespace description with its instructions stays under 600 characters. A call to a tool name the desktop doesn’t know gets a result with success: false and the text “Unsupported PlanToCode app tool.” instead of a protocol error.
Clients per profile pair
- Storage profileCODEX_HOME · sqlite_home
A PlanToCode-owned Codex home named by an opaque UUID. A new session links to the home of the account active at its first turn, and a switch never copies, moves, or relinks the transcript.
- Account--auth-home
The signed-in ChatGPT profile, called the execution profile, that is active when a turn is admitted. The turn keeps it until it ends.
- Codex childCodexRuntimePair
One per storage and account pair, started on demand. Before a child resumes a thread, every other child unloads its idle copy, so a running turn never moves.
The source link stores a thread ID and the exact rollout path. thread/start returns only the ID, so the desktop reads the absolute path with a separate thread/read before it publishes the link and before turn/start; a child thread’s path arrives with its thread/started notification. Changing credentials never changes the conversation source.
The cap of three never stops a busy child, so with every child busy the registry grows instead of stalling. Every sign-in attempt that ends, including a timeout or cancellation, bumps the execution profile’s auth generation, and a child started under the old generation finishes its work and retires once idle. After each run the desktop unloads the thread with thread/unload, and a failed unload retires that child, because the process boundary is the only proof that the JSONL writer is gone. Before a thread’s source link changes, every idle copy on every live child is unloaded too.
The vendored sidecar
- Fetched mainrefs/remotes/origin/main
Written only by reconcile:codex-source into the local mirror. Sync compares against this snapshot, so a newer commit on official main changes nothing until the next reconcile.
- Accepted revisionvendor/codex/upstream-revision
Moves only through accept:codex-source, once the reconciled candidate equals fetched main, has no conflicts, and passes its tests. The complete override files move with it.
- Syncsync:codex-sidecars
Stops before building unless the accepted revision equals fetched main. Otherwise it checks out that revision, copies the overrides on top, builds with --locked, and verifies the manifest.
tauri:dev runs sync:codex-sidecars first, so the child is built from the vendored source rather than taken from a globally installed Codex. Expect a Rust build and the network access described in desktop/BUILD.md.
The sync is a freshness gate. It fails unless desktop/vendor/codex/upstream-revision equals the main that the last reconcile:codex-source fetched into the local mirror, so the accepted revision is as current as that fetch. The 309 override files under desktop/vendor/codex/source implement the protocol extensions and the storage boundary the desktop relies on, which is why an unrelated upstream binary fails the initialize gate. Keep the protocol transport as the history boundary instead of adding another transcript parser.
pnpm -C desktop sync:codex-sidecars:verify