OpenAI Codex · Code Mode

Host Split: Who Holds the Program

Last lesson was the semantics of exec and wait. This one asks something else: who does this JavaScript hang on, and once that hang-point moves, how do the failure domain and the ownership of state change.

Course goalAfter reading, you can name three things. First, the default is already a separate host process; the main process only holds the session provider. Second, isolate can move; nested-tool approval stays on this machine. Third, if the host crashes or drops, the running cell and the store go with it — reconnect can exec again, it cannot resume that script.
Try it first · The same JS, four hosts
The same script: store, a nested command, then wait
Host
Left is fixed: DSH’s in-process worker. Right swaps Codex providers — watch the isolation boundary, the lifetime, and who is still alive after a crash.
Interrupt kills the cell
Off by default. When it’s off, Ctrl-C only cancels this turn; the script on the host can keep running.
DSH · in-process workerControl; the switches don’t move it
The main-process building
The worker room is empty
IsolationSame process, different thread
LifetimeOne new worker per run
Waiting to start.
Codex · Local subprocessProcessOwned
Codex main process
Holds the provider
stdio
Host process
isolate not up yet
Tool callback hasn’t left yet
IsolationOS process
cell / storeNone yet
Waiting to start.
Logic trail · which source span each animation step maps to
  1. thread manager picks a provider by featurethread_manager.rs L455
  2. Local provider checks that the host binary existsremote_session.rs L70
  3. spawn the host; on Unix, its own process groupconnection.rs L217
  4. After handshake, session/open; host news an in-process sessionlib.rs L602
  5. Nested tools bounce home via RemoteDelegatedelegate.rs L27
  6. app-server swaps to WS or gRPC by URL schemecode_mode_host.rs L32
  7. gRPC drops the lease and closes the sessionsession.rs L105
  8. After reconnect, cell IDs get a generation prefixgeneration.rs L49
  9. Whether interrupt terminates depends on the feature flagtasks/mod.rs L888
  10. When the host is unavailable, tool mode falls back to Directtools/mod.rs L79
Hit Play to see, after the same JS changes host, who is still alive after a crash, and whether the old cell can still wait.
Isolation boundaryDSH’s program and the main process share a building. Codex adds an OS process by default, and a remote host can add another machine.
Approval stays localEvaluation moved; nested exec_command still loops back to the session owner. The host has no approval UI and no execpolicy.
After failureIf the host crashes or drops, cell and store are gone together. Reconnect can exec again; it cannot resume that script. gRPC generation 2 also renames the cell.
Teaching sketch: four hosts map to four session providers. In-process evaluation is still drawn inside the host room; current production wiring no longer exposes it as a main-process option. Line numbers on the logic trail match openai/codex commit 4f39251a01.
Idea 1 · Pull evaluation out of the main process
What problem it solves

The model writes a while (true) {} — last lesson showed that only isolate terminate can cut it. If that program shares a process with the Codex event loop, a hang grows from one cell to the whole session.

V8’s heap, the JIT, a store table with no entry cap — any of them blowing up in the main process takes the TUI or app-server with it. Early notes often wrote the default path as “run isolate directly in the main process.” The current feature comment has already rewritten that sentence.

Source:codex-rs/features/src/lib.rs lines 104–111

What the idea is

Codex leaves a provider interface for “run the code the model wrote.” The business layer depends only on CodeModeSession and CodeModeSessionProvider; it does not new a runtime itself. The protocol folds a session into four verbs: execute, wait, terminate, shutdown. The impl can be in-process or remote. Same session shares a store; different sessions are isolated.

Source:codex-rs/code-mode-protocol/src/session.rs lines 146–167

When the main process picks a provider, there is no longer a production path that news InProcessCodeModeSession directly. If CodeModeHost is on, or disable_in_process_fallback is true, it goes to ProcessOwnedCodeModeSessionProvider. If neither holds, it goes to DisabledCodeModeSessionProvider.

Source:codex-rs/core/src/thread_manager.rs lines 455–462

CodeModeHost is already Stable and on by default. What the user sees as default is already a local subprocess. In-process evaluation is still there — it just sank inside the host process: when the host opens a session, what it news is InProcessCodeModeSession. isolate lives in the small room; the landlord is now the standalone binary codex-code-mode-host.

Source:codex-rs/features/src/lib.rs lines 921–925 · codex-rs/code-mode-host/src/lib.rs lines 599–608

Codex thread Main process Session provider Holds the interface only codex-code-mode-host Standalone OS process InProcess session V8 isolate lives in this room Nested tool callbacks come home; approval stays in the main process Input is a stretch of JS. What happens is cross-process evaluation. Output is a cell id and the text you hand over.
Teaching diagram: on the default path, V8 lands in the host process; the main process only holds the provider.

When the process is spawned, stdin / stdout / stderr are all piped; on Unix it gets its own process group; the environment is scrubbed first. If the binary is missing, availability() fails outright — it will not fall back to newing an isolate in the main process. The real fallback is one layer up, at tool mode: when the host is unavailable, the request is ordinary CodeMode, and fallback has not been turned off, the effective tool mode becomes Direct. The model sees ordinary tools again.

Source:codex-rs/code-mode/src/remote_session.rs lines 69–83 · codex-rs/core/src/tools/mod.rs lines 79–89

The name disable_in_process_fallback makes people think “fall back to in-process V8” still exists. The config comment says something else: when the host is unavailable, let Code Mode fail closed. Today it controls “after the host is gone, should Code Mode fall back to ordinary tools.”

Source:codex-rs/core/src/config/mod.rs lines 1089–1096

Why it lasts

Don’t let an untrusted language runtime share fate with the agent main process. Swap in Python’s subprocess, swap in another isolate — the question is the same: if this code crashes, who is still alive.

Idea 2 · Tool callbacks must come home
What problem it solves

You point app-server at a remote machine’s --code-mode-host and it’s easy to think the whole agent moved — even the exec_command approval dialog should pop on the far side. Current source doesn’t match. The remote host only moved evaluation. Nested-tool approval, execpolicy, and Guardian stay on the local session.

What the idea is

The host turns CodeModeSessionDelegate into a RemoteDelegate and bounces it home to the Codex main process over IPC. Only the dispatch broker on the main process walks nested tools. isolate moved; policy did not. JS runs elsewhere; side effects loop back and ask you.

Source:codex-rs/code-mode-host/src/delegate.rs lines 26–50

WebSocket and gRPC are one evaluation, two wires. The WebSocket listener rejects requests with an Origin header, blocking a browser page from cross-origin to the local host. gRPC splits tool subscribe, complete, and execute streams; drop the OpenSession lease stream and the session closes, running cells included. app-server’s --code-mode-host treats http / https as gRPC and ws / wss as WebSocket. Several threads share one remote connection; the store is still sliced by session.

Source:codex-rs/code-mode-host/src/transport.rs lines 288–302 · codex-rs/app-server/src/code_mode_host.rs lines 32–40

Local · session owner Approval dialog execpolicy Guardian Policy didn’t move Host · evaluation only V8 isolate + store No approval UI exec invoke_tool comes home
Teaching contrast: what moved is the untrusted JS world; what stayed is the policy world that has a UI.
Evaluation can move; policy stays local.
Why it lasts

What’s untrusted is the JS world; what’s trusted is approval and policy. Move the first, leave the second on the side that has a UI. Without this loop, a remote host would have to copy your whole permission system.

Idea 3 · A drop equals losing isolate and store
What problem it solves

A common expectation: you hit interrupt, V8 dies with it, and the next wait sees termination at once. The current default doesn’t match. Another: after reconnect, that script is still running in the same cell. The source recognizes neither.

What the idea is

When a turn is marked Interrupted, the task cancel token always cancels. Whether it also terminates a still-running cell depends on CodeModeInterrupt. That flag is still in development and off by default. Off, interrupt only cancels this turn’s tool calls and approvals. The isolate on the host can keep running until it finishes, is stopped by wait(terminate: true), or the session shuts down. The user hitting Ctrl-C is not automatically that terminate.

Source:codex-rs/core/src/tasks/mod.rs lines 888–899

A drop is the same. gRPC drops the lease and closes the session. The client can open another lease; generation counts up from 1. Generation 1 still uses the raw cell ID outward; generation 2 becomes g{generation}:{cell_id}. If the model waits on a generation-1 id, it gets a stale generation. The local-subprocess path has no such prefix — it just snaps the state machine back to New and allocates a new session-N. Outward cell IDs still count from 1. Both reconnects lose the running cell and that store.

Source:codex-rs/code-mode/src/grpc_session/generation.rs lines 49–67

Local subprocess / WebSocket Open Connection died Back to New Reopen the session; cells still count from 1 gRPC lease 1 Streams dropped lease 2 Outward ID becomes g2:1; old wait is void
Teaching state diagram: both reconnects lose the old world; gRPC uses generation to expose “this is a new world” to the caller.

The store table follows the host-side SessionRuntime. No disk, no cross-process share, no TTL. Restart the remote machine and the table is gone. Reusing a session ID is rejected — the old table is not quietly reattached. What reconnect restores is “you can exec again,” not “that script.”

Why it lasts

A cell’s lifetime is per session, not per turn. Interrupting a turn and killing the program are two things. Reconnect opens a new world; the old ID is void. If you build an Agent and expect Stop to kill the program at once, default to terminate. Codex does not kill by default because it still treats a cell as something that can continue across turns.

Side-by-side · Another answer to the same question

Topology: DeepSeek Harness stays in the same building

DSH puts the program in an in-process worker_threads.Worker. The first sentence of the crate header nails the stance: this is containment, not a security boundary. Model code is treated as bash-equivalent. Each run() starts a new Worker; the environment is empty; the heap is capped. The program world dies with the worker; there is no cross-run state.

If one worker crashes, the main-process room is still there — but a V8 bug or a native crash can still take the whole Node process. Codex’s isolate is already narrower than a Node worker: no fs, no net, no import. It still does not trust “a narrow isolate sharing fate with the main process.” Default adds an OS process. The cost is one more codex-code-mode-host that must ship with the package, plus session IDs, generations, and drop semantics.

Both sides verified against source · 2026-08-22 · Source:packages/code-runtime/code-runtime-worker-thread/src/index.ts lines 1–6 · README.md line 23 · DSH · Code Mode

Counterpart: Claude Code and Grok leave this to the shell

Neither side has a counterpart for “the model writes a program and orchestrates tools in a separate runtime.” Claude Code’s isolation shows up as a git worktree and a remote CCR session; Grok’s isolation shows up as a sub-agent worktree. That’s workspace isolation, not a JS host split. The missing counterpart is itself the finding: both leave running model-written code to ordinary shell tools.

The repo also has exec-server: what it moves is shell, PTY, and filesystem RPC, not JavaScript. Nested tools.exec_command can still walk in — that’s the next layer of exec split. Don’t fold the two roads into one remote.

No counterpart found · 2026-08-22 · Source:codex-rs/exec-server/README.md lines 1–5
Classroom Exercise
01

Can the old cell still wait

The same script is halfway on a gRPC host; the connection drops and comes back. The model waits on the original cell_id — what does it see? Does the local-subprocess path rename that id? Is the store still there on either path?

Then flip CodeModeInterrupt off, hit interrupt, and wait again. Does the answer change — and why doesn’t the user hitting Stop automatically equal terminate?

Takeaway:In the default topology V8 is already out of the main process. Remote only moves evaluation; approval still comes home. If the host crashes or drops, cell and store are gone together. Ctrl-C does not terminate a running cell by default.