OpenAI Codex · Turn 循環

中途插話:這句話進本輪、下一輪,還是被拒

Agent 正在改第三個檔案,你看見方向偏了,補了一句:配置用 YAML。回車之後,這句話是開工、插進當前輪,還是當場被拒,Core 當場拍板,不等模型開口。

課程目標讀完能説清三件事。第一,為什麼提交必須立刻回 Started、Steered 或 NotSubmitted。第二,審查和壓縮為什麼拒收插話,並且不會因此新開一輪。第三,最終答案上屏之後,遲到的子郵件為什麼預設躺到下一輪,用戶再插一句又為什麼能把門打開。
先玩一遍 · 同一句話,四個時機
信箱分揀台:同一句約束,投遞時刻不同,落點就不同
時機
四個時機共用同一句輸入。空閒會開工,採樣中會插話,答案上屏後子郵件先躺着,審查中會被擋在門口。
時刻 · 空閒 任務 · 無 相位 · —

當前輪

pending_input,相位 CurrentTurn 時會被掏走

下一輪

會話信箱,相位 NextTurn 時只排隊不掏

門口拒收

NotSubmitted,設定也不會落地
等待投遞。
邏輯軌跡 · 動畫每一步對應源碼裏的哪一段
  1. 按 TurnInputMode 分發,預設走 StartOrSteerturn_input.rs L141
  2. 先試 steer_input,只有 NoActiveTurn 才開工turn_input.rs L195
  3. Regular 才收,Review 和 Compact 當場拒turn_input.rs L507
  4. 空閒則 apply_started,再 spawn_taskturn_input.rs L242
  5. 插話寫入 pending,並把相位打回 CurrentTurnturn_input.rs L558
  6. 最終答案把投遞相位 defer 到 NextTurninput_queue.rs L206
  7. 工具項把相位 accept 回 CurrentTurnstream_events_utils.rs L302
  8. get_pending_input 看相位,再決定掏不掏信箱input_queue.rs L297
選一個時機,點播放。看同一句話進當前輪、躺到下一輪,還是被擋在門口。
用戶插話的落點空閒是 Started,採樣中是 Steered。審查中是 NotSubmitted,不會偷偷新開一輪普通對話。
子郵件的落點答案已經上屏時,遲到的子郵件先躺在會話信箱。本輪認為沒有待處理輸入。
門什麼時候重開用戶再插一句,或模型再發出工具調用,相位翻回 CurrentTurn,積壓的子郵件跟着進下一次採樣。
教學示意:筐與紙條是課程化隱喻,對應 turn 內 pending_input 與會話級 mailbox。邏輯軌跡右側行號對應 openai/codex 倉庫 commit 4f39251a01。
思路一 · 提交立刻回判定
它解決什麼問題

三種日常結局都不好受。立刻打斷,前面兩個檔案的改動可能半成品留在磁盤上。排到下一輪,你只能看着它把剩下的檔案按舊方向改完。塞進當前上下文卻不叫醒循環,模型要到下一次自己開口才看得到,你補的約束等於遲到。

思路是什麼

Codex 把這件事收成一個入口、三種模式。調用方選 StartOrSteerStartIfIdleSteer,不直接喊 start。Core 按現場忙閒和任務種類判定,立刻回 StartedSteeredNotSubmitted。回完決定就結束,不等 user-prompt hook,不等歷史落盤,不等模型開始採樣。

出處:codex-rs/core/src/session/turn_input.rs 第 1 至 9 行;codex-rs/protocol/src/turn_input.rs 第 127 至 136 行

StartOrSteer 的順序和函式名一致。先試插話。只有返回 NoActiveTurn,才 apply_startedspawn_task。其他拒絕原因原樣包裝成 NotSubmitted,不會偷偷開工。TUI 即時語音也走這條,和預設入口共用同一套判定。

出處:codex-rs/core/src/session/turn_input.rs 第 141 至 156 行、第 195 至 249 行

設定不能先改再判定。prepare 先預覽綫程設定,預覽失敗直接 InvalidRequest。真正寫入發生在 apply_startedapply_steered。被拒絕的輸入連設定都不改。插話成功後只落持久設定,當前輪的 TurnContext 不換。開工專用的選項,比如結構化輸出 schema,只在 Started 上用。

出處:codex-rs/core/src/session/turn_input.rs 第 58 至 80 行

用戶提交 TurnInputMode handle 先試 steer,空閒再開工 Started · 新開 Regular 輪 Steered · 插進當前 Regular 輪 NotSubmitted · 綫程保持原樣
教學化結構圖:入口只做判定,hook、落盤和採樣都還沒開始。
為什麼長期成立

開工和插話必須在同一把鎖裏做完。先看有沒有活動輪,再看 kind,再寫入 pending,中間不能讓另一次提交把輪次換掉。設定卻要先預覽後寫入,因為拒絕路徑必須保證綫程不變。判定和入隊拆成兩次加鎖,用戶回車和子郵件同時到達時,可能出現判定時還空閒、入隊時已經有人佔坑的窗口。

思路二 · 只有 Regular 收插話
它解決什麼問題

審查任務自己再開一條 one-shot 子對話,壓縮任務在換窗口。用戶在這時候補一句「用 YAML」,沒有當前輪的工具循環可以接住它。如果因此新開一輪普通對話,審查結果和壓縮摘要會跟新對話搶同一個 active_turn

思路是什麼

steer_input 拿着 active_turn 鎖做完全部檢查。沒有活動輪,或者有槽沒有 task,都算 NoActiveTurnTaskKind 只有 Regular、Review、Compact 三個變體。審查和壓縮返回 ActiveTurnNotSteerableStartOrSteer 也不會因此開工。

出處:codex-rs/core/src/session/turn_input.rs 第 507 至 519 行;codex-rs/core/src/state/turn.rs 第 67 至 72 行

檢查過關之後,用戶輸入被推進 pending_input,同時把信箱相位打回 CurrentTurn。窮盡 match 在這裏有業務後果:新加一種任務類型,編譯器會逼你回答能不能插話。

出處:codex-rs/core/src/session/turn_input.rs 第 546 至 564 行

為什麼長期成立

內部任務有自己的生命週期。把用戶插話焊進審查輪,等於讓兩種工作搶同一條執行槽。調用方收到拒絕,這條輸入不會被默默排進一條新對話。換個語言重寫,該守的仍是:能接住插話的任務和不能接住的任務,必須在類型上分開。

思路三 · 一面翻牌決定能不能續寫
它解決什麼問題

主 agent 已經在螢幕上打出一段看起來像最終答案的話,子 agent 同時發回一條進度。並進去,用戶已經看見的答案會被續寫。一律等到下一輪,子 agent 的結果可能要隔一次採樣才進模型。

思路是什麼

用戶插話進的是 turn 內的 pending_input。子 agent 的信走會話級 mailbox_pending_mails。兩套存貨能不能並進當前輪,由 MailboxDeliveryPhase 決定。相位從 CurrentTurn 起步。用戶已經看見最終答案之後切到 NextTurn。用戶再插一句,或者模型又發出工具調用,相位會重開。

出處:codex-rs/core/src/state/turn.rs 第 37 至 56 行

切到 NextTurn 有一條例外:pending_input 裏只要還有一條不是排隊不叫醒的子郵件,就保持當前相位。取信時也看這面牌。NextTurn 時 turn 內 pending 也不拿,會話信箱更不掏。CurrentTurn 時先拿走 turn 內 pending,再掏會話信箱,拼在後面。所以最終答案落地之後,子郵件可以躺在信箱裏,循環卻認為沒有待處理輸入,本輪會收束。

出處:codex-rs/core/src/session/input_queue.rs 第 206 至 227 行、第 284 至 336 行

CurrentTurn pending 加信箱,並進本輪 NextTurn 兩套存貨都不掏 最終答案上屏 用戶插話,或工具調用,或模型還要 follow-up
教學化狀態圖:答案上屏後關閘,明確的同輪工作再開門。

咩先算用戶見到嘅最終答案?助手正文,phase 是 Commentary 的不算,trim 之後為空的也不算。未打標的助手消息按最終答案處理。未打標的提供方預設走更安全的那條:先把信箱關到下一輪。審批、權限、提問、elicitation、動態工具是五張獨立的 oneshot 表,不進這套信箱,各等各的回執。

出處:codex-rs/core/src/stream_events_utils.rs 第 486 至 501 行;codex-rs/core/src/state/turn.rs 第 87 至 103 行

答案已經給用戶看過,遲到的信預設不續寫。
為什麼長期成立

「答案已經給用戶看過」是一條產品邊界,不依賴 Rust 或某個信箱實現。換語言重寫,仍然需要一面翻牌:遲到的附屬消息預設不續寫已經上屏的答案;明確的同輪工作,比如用戶再插一句或模型再調工具,再把門打開。

橫向對比 · 同一道題的另一種答法

DSH:兩個參數,兩條軌道

DSH 對外是三個別名,底層共用 send。目標隊列和喚不喚醒是兩個正交參數。followup 自己獨佔一輪並叫醒,steer 插進下一站並叫醒,inject 上車不催司機。Inbox 是 next-turnnext-step 兩條持久列表,claim 先掏空 next-step,目標是 next-turn 時再多取一條。

Codex 沒有這三個公開函式。StartOrSteer 把空閒開工和忙時插話焊在一次判定裏。相位這面翻牌,DSH 可以沒有,因為它把等整輪和等下一站寫成兩條列表。代價是答案已經上屏之後,next-step 非空仍會續命當前 Turn。

兩側均已核對源碼 · 2026-08-22 · DSH · Inbox

Claude Code:一條隊列,用優先級補時機

還原源碼裏,用戶輸入、任務通知、孤兒權限走同一條 commandQueue。優先級是 now 大於 next 大於 later,同級 FIFO。用戶命令預設 next,任務通知預設 later,用戶輸入不會被系統消息餓死。消費發生在當前流結束之後,沒有步級插話。你補的那句 YAML 約束,要等當前生成器收尾才進模型。

已核對還原源碼 · 2026-08-22 · messageQueueManager.ts
課堂練習
01

答案上屏之後,這封子郵件什麼時候被看見

最終答案落地之後,先入隊一封 trigger_turn: false 的子郵件,再餵一條 FunctionCall。先在紙上推演 get_pending_input 該返回什麼。

對照路徑:正文落盤時相位切到 NextTurn,這封信看不見;工具項到達後相位重開,下一圈才能把它掏出來,並進同輪的下一次模型請求。

Takeaway:提交立刻回判定,回的是收下還是拒絕,採樣還沒開始。只有 Regular 收插話,審查和壓縮當場拒。答案上屏後預設不續寫,用戶再插或工具再調,才把門打開。