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。