OpenAI Codex · 代码模式

宿主拆分:程序挂在谁身上

上一课讲 exec 和 wait 的语义。这一课问另一件事:这段 JavaScript 到底挂在谁身上,挂点换了之后,故障域和状态归属怎么变。

课程目标读完能说清三件事。第一,默认已经是独立宿主进程,主进程只握着会话提供方。第二,isolate 可以搬家,嵌套工具的审批还在本机。第三,宿主崩了或掉线,正在跑的 cell 和 store 一起没了,重连只能再 exec,不能接着刚才那段脚本。
先玩一遍 · 同一段 JS,四种宿主
同一段脚本:store、嵌套命令、再 wait
宿主
左边固定 DSH 的进程内 worker。右边换 Codex 的提供方,看隔离边界、寿命和崩了谁还活着。
中断杀 cell
默认关。关掉时,Ctrl-C 只取消这一轮,宿主上的脚本还能接着跑。
DSH · 进程内 worker对照,不随开关变
主进程这栋楼
worker 小房间空着
隔离同一进程,不同线程
寿命一次 run 一个新 worker
等待开始。
Codex · 本地子进程ProcessOwned
Codex 主进程
握着提供方
stdio
宿主进程
isolate 还没起来
工具回调还没出门
隔离操作系统进程
cell / store还没有
等待开始。
逻辑轨迹 · 动画每一步对应源码里的哪一段
  1. thread manager 按特性挑选提供方thread_manager.rs L455
  2. 本地提供方检查宿主文件是否存在remote_session.rs L70
  3. spawn 宿主,Unix 上单独进程组connection.rs L217
  4. 握手后 session/open,宿主里 new 进程内会话lib.rs L602
  5. 嵌套工具经 RemoteDelegate 打回本机delegate.rs L27
  6. app-server 按 URL 方案换成 WS 或 gRPCcode_mode_host.rs L32
  7. gRPC 丢掉 lease 就关会话session.rs L105
  8. 重连后 cell ID 加世代前缀generation.rs L49
  9. 中断是否 terminate 看特性开关tasks/mod.rs L888
  10. 宿主不可用时,工具模式退回 Directtools/mod.rs L79
点播放,看同一段 JS 换宿主之后,崩了谁还活着、旧 cell 还能不能 wait。
隔离边界DSH 的程序和主进程住同一栋楼。Codex 默认再加一层操作系统进程,远端还可以再加一台机器。
审批还在本机求值搬家了,嵌套 exec_command 仍绕回 session owner。宿主里没有审批窗,也没有 execpolicy。
失败之后宿主崩了或掉线,cell 和 store 一起丢。重连能再 exec,不能接着刚才那段脚本。gRPC 第二代还会给 cell 改名。
教学示意:四种宿主对应四个会话提供方。进程内求值仍画在宿主屋子内部,当前生产接线不再把它做成主进程上的一档。逻辑轨迹右侧行号对应 openai/codex 仓库 commit 4f39251a01。
思路一 · 求值从主进程拆出去
它解决什么问题

模型写一段 while (true) {},上一课见过,要靠 isolate 的 terminate 才能打断。这段程序如果和 Codex 事件循环抢同一个进程,卡死会从单个 cell 扩大到整条会话。

V8 的堆、JIT、没有条目上限的 store 表,任意一项在主进程里爆炸,都会带走 TUI 或 app-server。早期资料常把默认路径写成「主进程里直接跑 isolate」。当前特性注释已经把这句话改掉了。

出处:codex-rs/features/src/lib.rs 第 104 至 111 行

思路是什么

Codex 给「跑模型写的代码」留了一个提供方接口。业务层只依赖 CodeModeSessionCodeModeSessionProvider,自己不去 new 运行时。协议把会话收成四件事:executewaitterminateshutdown。实现可以进程内,也可以远端。同会话共享 store,异会话隔离。

出处:codex-rs/code-mode-protocol/src/session.rs 第 146 至 167 行

主进程选提供方时,已经没有直接 new InProcessCodeModeSession 这条生产路径。CodeModeHost 打开,或者 disable_in_process_fallback 为真,都走向 ProcessOwnedCodeModeSessionProvider。两条都不成立,走向 DisabledCodeModeSessionProvider

出处:codex-rs/core/src/thread_manager.rs 第 455 至 462 行

CodeModeHost 已经是 Stable,默认打开。用户看见的默认已经是本地子进程。进程内求值还在,只是沉到宿主进程内部:宿主打开会话时 new 的就是 InProcessCodeModeSession。isolate 活在小屋里,房东换成了独立二进制 codex-code-mode-host

出处:codex-rs/features/src/lib.rs 第 921 至 925 行 · codex-rs/code-mode-host/src/lib.rs 第 599 至 608 行

Codex thread 主进程 会话提供方 只握接口 codex-code-mode-host 独立操作系统进程 InProcess 会话 V8 isolate 在这间屋里 嵌套工具回调回家,审批仍在主进程 输入是一段 JS。发生的是跨进程求值。输出是 cell 编号和主动交出去的文本。
教学化结构图:默认路径上,V8 落在宿主进程里,主进程只握着提供方。

拉起进程时,stdin / stdout / stderr 全管道化,Unix 上单独一个进程组,环境变量先 scrub 一遍。找不到可执行文件,availability() 直接失败,不会改去主进程里 new isolate。真正的回退发生在工具模式这一层:宿主不可用、请求的是普通 CodeMode、并且没有关掉回退时,有效工具模式变成 Direct。模型重新看见普通工具。

出处:codex-rs/code-mode/src/remote_session.rs 第 69 至 83 行 · codex-rs/core/src/tools/mod.rs 第 79 至 89 行

disable_in_process_fallback 这个名字容易让人以为还存在「回退到进程内 V8」。配置注释写的是另一件事:宿主不可用时,让 Code Mode 闭门失败。今天它控制的是「宿主没了以后,要不要从 Code Mode 退回普通工具」。

出处:codex-rs/core/src/config/mod.rs 第 1089 至 1096 行

为什么长期成立

不要让不受信任的语言运行时和 agent 主进程同命运。换成 Python 的 subprocess,换成别的 isolate,该问的还是同一句:这段代码崩了,谁还活着。

思路二 · 工具回调必须回家
它解决什么问题

你把 app-server 指到一台远端机器的 --code-mode-host,容易以为整段 agent 都搬家了,连 exec_command 的审批弹窗都该出现在远端。当前源码对不上。远端宿主只搬走了求值。嵌套工具的审批、execpolicy、Guardian 仍在本机会话上。

思路是什么

宿主把 CodeModeSessionDelegate 做成 RemoteDelegate,经 IPC 打回 Codex 主进程。主进程上的 dispatch broker 才去走嵌套工具。isolate 搬家了,策略没有搬家。JS 在别处跑,副作用要绕回来问你。

出处:codex-rs/code-mode-host/src/delegate.rs 第 26 至 50 行

WebSocket 和 gRPC 是同一套求值、两套线。WebSocket 监听器拒绝带 Origin 头的请求,挡住浏览器页面跨源连到本机宿主。gRPC 把工具订阅、完成、执行流拆开,丢掉 OpenSession 那条租约流,会话关闭,正在跑的 cell 一并终止。app-server 的 --code-mode-hosthttp / https 为 gRPC,认 ws / wss 为 WebSocket。多个 thread 共享同一条远端连接,store 仍按会话切开。

出处:codex-rs/code-mode-host/src/transport.rs 第 288 至 302 行 · codex-rs/app-server/src/code_mode_host.rs 第 32 至 40 行

本机 · session owner 审批弹窗 execpolicy Guardian 策略没搬家 宿主 · 只负责求值 V8 isolate + store 没有审批 UI exec invoke_tool 回家
教学化对照:搬走的是不可信的 JS 世界,留下的是有 UI 的策略世界。
求值可以搬家,策略还在本机。
为什么长期成立

不可信的是 JS 世界,可信的是审批和策略。把前者搬走,后者留在有 UI 的那边。没有这条回路,远端宿主就必须复制你的整套权限系统。

思路三 · 掉线等于丢掉 isolate 和 store
它解决什么问题

一种常见预期是:你按了中断,V8 一起掐掉,下一轮 wait 立刻看到终止。当前默认对不上。另一种预期是:重连之后,刚才那段脚本还在原来的 cell 里接着跑。两种预期,源码都不认。

思路是什么

turn 被标成 Interrupted 时,任务取消令牌一定会取消。会不会再去 terminate 还在跑的 cell,要看 CodeModeInterrupt。这个开关还在开发,默认关。关掉时,中断只取消本轮工具调用和审批。宿主上的 isolate 可以继续跑,直到自己结束、被 wait(terminate: true) 停掉,或会话 shutdown。用户按 Ctrl-C,并不自动等于那条 terminate。

出处:codex-rs/core/src/tasks/mod.rs 第 888 至 899 行

掉线也一样。gRPC 丢掉 lease 就关会话。客户端可以再开一条租约,generation 从 1 往上加。第一代对外仍用原始 cell ID,第二代变成 g{generation}:{cell_id}。模型拿着第一代的编号去 wait,会收到 stale generation。本地子进程路径没有这套前缀,只是把状态机打回 New,再分配一个新的 session-N。对外 cell ID 仍从 1 数。两种重连都丢运行中的 cell 和那份 store。

出处:codex-rs/code-mode/src/grpc_session/generation.rs 第 49 至 67 行

本地子进程 / WebSocket Open 连接死了 回到 New 再开会话,cell 仍从 1 数 gRPC lease 1 丢掉流 lease 2 对外 ID 变成 g2:1,旧 wait 作废
教学化状态图:两种重连都丢旧世界,gRPC 用世代把「这是新世界」暴露给调用方。

store 表跟着宿主侧的 SessionRuntime。没有落盘,没有跨进程共享,没有 TTL。远端机器重启,表就没了。会话 ID 复用会被拒绝,不会把旧表偷偷接回来。重连恢复的是「还能再 exec」,不是「刚才那段脚本」。

为什么长期成立

cell 的寿命按会话算,不按 turn 算。中断轮次和杀掉程序是两件事。重连开的是新世界,旧身份证作废。自己做 Agent 时,如果用户按停止就期望程序立刻死,默认应该 terminate。Codex 默认不杀,是因为它还把 cell 当成可跨轮续跑的对象。

横向对比 · 同一道题的另一种答法

拓扑:DeepSeek Harness 选择同一栋楼

DSH 把程序放进进程内的 worker_threads.Worker。crate 头注释第一句把立场写死:这是 containment,不是 security boundary。模型代码按 bash 等价来对待。每次 run() 拉起一个新 Worker,环境是空的,堆有上限。程序世界随 worker 一起死,没有跨 run 状态。

一个 worker 崩了,主进程房间还在,可 V8 漏洞或原生崩溃仍可能带走整个 Node 进程。Codex 的 isolate 已经比 Node worker 窄,没有 fs、没有 net、没有 import,仍然不信任「窄 isolate 和主进程同命运」。默认再加一层操作系统进程。代价是多了一个必须随包装分发的 codex-code-mode-host,多了会话 ID、世代和掉线语义。

两侧均已核对源码 · 2026-08-22 · 出处:packages/code-runtime/code-runtime-worker-thread/src/index.ts 第 1 至 6 行 · README.md 第 23 行 · DSH · Code Mode

对位物:Claude Code 与 Grok 把这件事留给 shell

两边都没有「模型写一段程序、在独立运行时里编排工具」的对位实现。Claude Code 的 isolation 出现在 git worktree 和远端 CCR 会话,Grok 的 isolation 出现在子 agent 的 worktree。那是工作区隔离,不是 JS 宿主拆分。没有对位物本身就是结论:这两家把跑模型写的代码留给了普通 shell 工具。

仓库里还有 exec-server,搬走的是 shell、PTY 和文件系统 RPC,不跑 JavaScript。嵌套 tools.exec_command 仍然可以再走进去,那是下一层的执行拆分。两条路不要收成同一个远端。

检索未找到对位实现 · 2026-08-22 · 出处:codex-rs/exec-server/README.md 第 1 至 5 行
课堂练习
01

旧 cell 还能不能 wait

同一段脚本在 gRPC 宿主上跑到一半,连接断了又连上。模型拿着原来的 cell_idwait,会看到什么。本地子进程路径会不会给这个编号改名。两种路径的 store 还在不在。

然后把 CodeModeInterrupt 拨到关,按中断再 wait 一次。答案会不会变,为什么用户按停止并不自动等于 terminate

Takeaway:默认拓扑里 V8 已经不在主进程里。远端只搬走求值,审批仍回本机。宿主崩了或掉线,cell 和 store 一起没了。Ctrl-C 默认不 terminate 还在跑的 cell。