宿主拆分:程序挂在谁身上
上一课讲 exec 和 wait 的语义。这一课问另一件事:这段 JavaScript 到底挂在谁身上,挂点换了之后,故障域和状态归属怎么变。
- 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 或 gRPCcode_mode_host.rs L32
- gRPC 丢掉 lease 就关会话session.rs L105
- 重连后 cell ID 加世代前缀generation.rs L49
- 中断是否 terminate 看特性开关tasks/mod.rs L888
- 宿主不可用时,工具模式退回 Directtools/mod.rs L79
exec_command 仍绕回 session owner。宿主里没有审批窗,也没有 execpolicy。模型写一段 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 行
拉起进程时,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 行
不可信的是 JS 世界,可信的是审批和策略。把前者搬走,后者留在有 UI 的那边。没有这条回路,远端宿主就必须复制你的整套权限系统。
一种常见预期是:你按了中断,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 行
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、世代和掉线语义。
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 仍然可以再走进去,那是下一层的执行拆分。两条路不要收成同一个远端。
codex-rs/exec-server/README.md 第 1 至 5 行
旧 cell 还能不能 wait
同一段脚本在 gRPC 宿主上跑到一半,连接断了又连上。模型拿着原来的 cell_id 去 wait,会看到什么。本地子进程路径会不会给这个编号改名。两种路径的 store 还在不在。
然后把 CodeModeInterrupt 拨到关,按中断再 wait 一次。答案会不会变,为什么用户按停止并不自动等于 terminate。