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.
- thread manager picks a provider by featurethread_manager.rs L455
- Local provider checks that the host binary existsremote_session.rs L70
- spawn the host; on Unix, its own process groupconnection.rs L217
- After handshake, session/open; host news an in-process sessionlib.rs L602
- Nested tools bounce home via RemoteDelegatedelegate.rs L27
- app-server swaps to WS or gRPC by URL schemecode_mode_host.rs L32
- gRPC drops the lease and closes the sessionsession.rs L105
- After reconnect, cell IDs get a generation prefixgeneration.rs L49
- Whether interrupt terminates depends on the feature flagtasks/mod.rs L888
- When the host is unavailable, tool mode falls back to Directtools/mod.rs L79
exec_command still loops back to the session owner. The host has no approval UI and no execpolicy.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
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
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
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.
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.
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
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.
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.
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
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.”
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.
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.
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.
codex-rs/exec-server/README.md lines 1–5
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?