OpenAI Codex · 代码模式 宿主拆分:程序挂在谁身上 上一课讲 exec 和 wait 的语义。这一课问另一件事:这段 JavaScript 到底挂在谁身上,挂点换了之后,故障域和状态归属怎么变。 一句话速览 上一课讲 exec 和 wait 的语义。这一课问另一件事:这段 JavaScript 到底挂在谁身上,挂点换了之后,故障域和状态归属怎么变。
课程目标 读完能说清三件事。第一,默认已经是独立宿主进程,主进程只握着会话提供方。第二,isolate 可以搬家,嵌套工具的审批还在本机。第三,宿主崩了或掉线,正在跑的 cell 和 store 一起没了,重连只能再 exec,不能接着刚才那段脚本。
先玩一遍 · 同一段 JS,四种宿主
同一段脚本:store、嵌套命令、再 wait
播放 单步 重置
宿主
本地子进程 WebSocket gRPC 禁用
左边固定 DSH 的进程内 worker。右边换 Codex 的提供方,看隔离边界、寿命和崩了谁还活着。
中断杀 cell
关 · 默认 开 · CodeModeInterrupt
默认关。关掉时,Ctrl-C 只取消这一轮,宿主上的脚本还能接着跑。
DSH · 进程内 worker 对照,不随开关变
主进程这栋楼 worker 小房间空着
隔离 同一进程,不同线程 寿命 一次 run 一个新 worker
等待开始。
Codex · 本地子进程 ProcessOwned
Codex 主进程 握着提供方
stdio
宿主进程 isolate 还没起来
工具回调还没出门
隔离 操作系统进程 cell / store 还没有
等待开始。
逻辑轨迹 · 动画每一步对应源码里的哪一段
thread manager 按特性挑选提供方 thread_manager.rs L455 本地提供方检查宿主文件是否存在 remote_session.rs L70 spawn 宿主,Unix 上单独进程组 connection.rs L217 握手后 session/open,宿主里 new 进程内会话 lib.rs L602 嵌套工具经 RemoteDelegate 打回本机 delegate.rs L27 app-server 按 URL 方案换成 WS 或 gRPC code_mode_host.rs L32 gRPC 丢掉 lease 就关会话 session.rs L105 重连后 cell ID 加世代前缀 generation.rs L49 中断是否 terminate 看特性开关 tasks/mod.rs L888 宿主不可用时,工具模式退回 Direct tools/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 给「跑模型写的代码」留了一个提供方接口。业务层只依赖 CodeModeSession 和 CodeModeSessionProvider ,自己不去 new 运行时。协议把会话收成四件事: execute 、 wait 、 terminate 、 shutdown 。实现可以进程内,也可以远端。同会话共享 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-host 认 http / 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_id 去 wait ,会看到什么。本地子进程路径会不会给这个编号改名。两种路径的 store 还在不在。 然后把 CodeModeInterrupt 拨到关,按中断再 wait 一次。答案会不会变,为什么用户按停止并不自动等于 terminate 。
Takeaway: 默认拓扑里 V8 已经不在主进程里。远端只搬走求值,审批仍回本机。宿主崩了或掉线,cell 和 store 一起没了。Ctrl-C 默认不 terminate 还在跑的 cell。