The Codex app-server integration
How the desktop finds, starts, and supervises the Codex child process: the JSON-RPC pipe, every method the desktop sends, what keeps a child alive, profiles and sign-in, usage limits, and the tool list that is fixed when a thread starts.
Checked against the source on 5 October 2026
On this page
A supervised child process
Codex runs as a child process named codex-app-server. The desktop looks for the binary in the folder of its own executable, then in ../Resources on macOS, then in binaries, src-tauri/binaries, and desktop/src-tauri/binaries below the current directory. It accepts the names codex-app-server and codex-app-server-<target triple>. The binary codex-code-mode-host must be in the same folder. The desktop never falls back to a Codex that is installed on the machine, and a missing sidecar fails the start with an error that names pnpm -C desktop sync:codex-sidecars.
-c features.local_thread_store_compression=false
-c features.background_paginated_rollout_migration=false
-c features.multi_agent_v2.tool_namespace="hbcode"
--listen stdio://
--session-source app-server
-c sqlite_home=<storage-profile-home>
--allowed-storage-root <storage-profile-home>
--auth-home <execution-profile-home>| What the child sees | Value |
|---|---|
| CODEX_ | The storage profile home, <app data>/ |
| CODEX_ | Removed from the environment. |
| Working directory | <app data>/ |
| PATH | Starts with a folder that holds rg: the bundled codex-path folder, the sidecar folder, or a copy staged in the cache folder. HBCODE_ |
| NO_ | 1. |
| RUST_ | warn, unless HBCODE_ |
| stdin, stdout, stderr | Three pipes. The process is killed when the desktop drops its handle. |
Each profile home has a stable opaque UUID. The desktop creates every folder of that path itself and refuses a symlink in any component. The bundled child refuses to open a database unless --allowed-storage-root, CODEX_HOME, and sqlite_home canonicalize to the same directory.
Before each start the desktop reads the pidfile in the profile root. The file is app-server.pid when storage profile and execution profile are equal, and app-server-<execution profile>.pid for a mixed pair. The desktop terminates the recorded process only if it is still a codex-app-server and this desktop did not start it. A process that this desktop runs is never signalled. The desktop then records the new process ID and drains stderr in a separate task.
The handshake is initialize, with client information and experimentalApi enabled, then initialized. The 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, and any other initialization failure shuts it down too. This gate makes an arbitrary upstream binary unusable.
The JSON-RPC pipe
{"id":42,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"hbcode","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. It gets the request (tool, arguments, threadId, turnId, callId) and no actor state, and the actor writes the reply. An approval with no live consumer is declined.
| Limit of the process and the client | Value |
|---|---|
| Child start, handshake included | 35 seconds. |
| Request response | 30 seconds after the write. 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. |
| Place in the actor command queue | 5 seconds. The queue holds 256 commands. |
| Tool replies that wait for the actor | 64. |
| Messages kept while initialize waits | 4,096. A full buffer drops an ordinary event before a critical one. |
| Graceful shutdown before kill | 2 seconds after stdin closes. |
| stderr lines kept for the error text | The last 40. |
The child can send requests too. The desktop answers two kinds: a tool call in the hbcode namespace and the three approval requests. Every other request gets JSON-RPC error -32601 with the text “HB Code desktop does not handle app-server request”, and a tool call for another namespace is such a request. An approval request that arrives while initialize waits is declined. A stdout line that does not start with { or [ is logged and ignored. When stdout closes, the error carries the stderr tail.
Every method the desktop sends
The Codex client is a fixed set of 27 methods. The desktop sends no other method to the child.
| Method | When the desktop sends it |
|---|---|
| initialize, initialized | Once, right after the child starts. |
| thread/ | First turn of a new session, a one-shot text request, and a private worker. Carries dynamic |
| thread/ | Right after thread/ |
| thread/ | Before a turn on a known thread, with the exact path and history sources. Also used to attach to a running turn and before a rollback. |
| thread/ | To read a thread from an exact JSONL path. |
| thread/ | Each history page of the timeline. |
| thread/ | Before a rollback, limit 1, to find the latest turn. |
| thread/ | Rollback of the latest turn, with before |
| thread/ | Existing sessions (source |
| thread/ | After a run, after a one-shot request, and before another child opens the same thread. |
| thread/ | A change of model, effort, or access level while a turn runs. Without a running turn the desktop sends nothing, and the saved settings apply at the next turn. |
| thread/ | A slice of saved command output. |
| thread/ | Goal reads and changes. |
| turn/ | Each new turn. |
| turn/ | A message for the turn that runs. |
| turn/ | Stop and send now takes a pending message back. |
| turn/ | Stop. |
| model/ | The model catalog, with include |
| account/ | Account identity after a sign-in and for the subscription picker. |
| account/ | Usage limits. |
| account/ | Token activity for the usage history. |
| account/ | Browser sign-in. |
The hbcode tool namespace
thread/start carries dynamicTools, which add one namespace called hbcode 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 list holds 4 to 8 tools and follows the settings at that moment.
| Tool | In the list, and its approval class |
|---|---|
| use_ | Always. Class agent: the agent asks its user. |
| maintain_ | Always. Class read-only: no one approves. |
| manage_ | Always. Class agent: the agent asks its user. |
| send_ | Always. Class desktop: the desktop applies its own rule. |
| send_ | When the Desktop and phone notifications setting is on. Class agent: the agent asks its user. |
| synthesize_ | When a Gemini API key is saved. Class agent: the agent asks its user. |
| create_ | When a Gemini API key is saved. Class agent: the agent asks its user. |
| extract_ | When a Gemini API key is saved. Class agent: the agent asks its user. |
thread/start also carries developerInstructions. agents/session_rules.rs joins three texts for them, in this order: the app-tool text of the catalog, the directions rule, and the text of the reply style. The Default style adds no text. A normal send and a goal run use the same instructions. Codex reads them when a thread starts. thread/resume sends them again, and Codex uses them only when a compaction rebuilds the context of the resumed thread. A reply style change therefore reaches new sessions, and an older session only after a compaction.
The approval class says who approves a call for an agent that asks its user before tool calls. Codex runs these tools without an approval request, so for Codex only the desktop class has an effect: a session message that needs the user waits in the queue of the recipient.
Every spec is marked deferLoading and holds no session, run, or project values. The app-tool text is the namespace description, which names the tool groups, and then “Find the hbcode tool for the task. Follow its instructions. For a video, give a brief. Its tool runs a worker.” The namespace description and the app-tool text together stay under 600 characters. For each item/tool/call the desktop looks up the calling thread’s session link and that session’s active run, so a resumed thread or a forked subagent reaches the run in progress. A call to a tool name the desktop doesn’t know gets a result with success: false and the text “Unsupported HB Code app tool.” The JSON-RPC reply itself is a normal result.
create_explainer_video starts a private worker thread, with the namespace explainer_video and the tools save_scenes, render_stills, and finish_explainer_video. The desktop gives each call of a worker thread to the job of that thread. A call in another namespace gets “This tool is not a private tool of this worker.” The desktop checks every 250 milliseconds whether the parent run is still the active run of the session. When it is not, the desktop stops the worker, interrupts its turn, and unloads the worker thread.
Clients per profile pair
- Storage profileCODEX_HOME · sqlite_home
An HB Code-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 child processCodexRuntimePair
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.
- LeaseCodexAppServerLease
Every run, history read, and tool worker takes one: a server lease plus a home lease for the pair. A child start and a probe take a home lease alone. Each ends with its work.
- Subagent threadthread/started · parentThreadId
Holds the child after the root run ends. It lets go on thread/status/changed idle, on turn/completed, or 5 seconds after an error that won’t retry.
- Cap checkMAX_LIVE_APP_SERVERS = 3
Runs after each lease is taken. With more than 3 available children it stops the least recently used idle one. With every child busy the registry grows.
- Probe childaccount/read · account/rateLimits/read
The subscription picker reads a profile that has no child through a throwaway process. It admits no turn and is stopped after the read.
Each pair has its own start lock, so one pair never waits for the start of another. A second start for a pair that is still starting fails with “This Codex subscription is still starting.” A child that is marked retiring closes its turn admission gate, and a turn/start that was prepared on it fails before admission.
After each chat run the desktop unloads the thread with thread/unload. A failed unload, or a subagent path that could not be saved, retires that child, because the process boundary is the only proof that the JSONL writer is gone. A goal run also unloads its thread, but a failed unload there is only logged. Before a thread’s source link changes, every idle copy on every live child is unloaded too.
Profiles and sign-in
- Profilesigned_out → authenticated
A sign-in reuses a signed_out profile that is not active. Without one it creates a profile with a new UUID and its own home.
- Sign-in childaccount/login/start · type chatgpt
Held by a lease for the whole attempt. The attempt is refused while profile Q has a turn, a home lease, or another sign-in.
- Resultaccount/login/completed · loginId
Matched by loginId. The desktop keeps only email, plan, user ID, display name, and workspace fields. It never stores tokens, login IDs, or browser URLs.
- Auth generationper execution profile
Rises after every ended attempt: success, failure, timeout, or cancel. A child with an older generation is not reused for a new turn.
| Rule | Detail |
|---|---|
| Active profile | The setting agent. |
| Send | Needs an active profile with the status authenticated. |
| History read | Needs no sign-in. A linked transcript is read through any local profile. |
| Delete a profile | Allowed when the profile is not active, owns no session link, and no linked transcript lies in its folder. The registry also refuses while the profile has a turn, a lease, a subagent thread, or a sign-in. |
| What a delete stops | Only the children of that profile. It blocks every new lease until the folder is gone, then removes the row and the folder. |
A phone reads the Codex profiles with system.codexAuthProfiles and switches with system.activateCodexAuthProfile. The switch is a mutating method, so it needs an idempotency key. The answer is the new profile list.
Usage limits
Codex limits come from account/rateLimits/read on the child of the active profile. The desktop reads rateLimitsByLimitId.codex, or rateLimits when that entry is missing. It keeps the primary and the secondary window (usedPercent, window minutes, resetsAt), the credits, and the plan. A phone gets the same values with system.codexUsageLimits.
| Read | Cache and limit |
|---|---|
| Limits of the active profile | 60 seconds, one value for the whole desktop. The cache has no profile key and a profile switch does not clear it, so after a switch the old profile’s limits can show for up to 60 seconds. |
| Subscription picker | 60 seconds per profile, signed-in profiles only. Profiles are read one at a time, each with a ceiling of 45 seconds. A failed read is not cached, and a refresh clears the cache. |
| Picker read of a profile with no child | Starts a probe child that admits no turn and stops after the read. |
Requests outside a run
Session titles and text improvement use a one-shot text request. The desktop starts an ephemeral thread with no tools, permissions :read-only, and approval policy never. It starts one turn, waits at most 120 seconds for the text, and unloads the thread. After a timeout or an error it abandons the turn first. An empty answer is an error.
The model catalog comes from model/list with includeHidden false. The desktop caches it for 300 seconds per storage profile and clears the cache when a profile is deleted. A send reads the catalog and checks the selected model and effort against it before the turn is admitted.
The list of existing Codex sessions comes from thread/list on the child of the active profile. The desktop never opens the Codex state database and never scans rollout files for it.
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. 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 and add no other transcript parser.
pnpm -C desktop sync:codex-sidecars:verify