宿主拆分:程式掛在誰身上
上一課講 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。