中途插話:這句話進本輪、下一輪,還是被拒
Agent 正在改第三個檔案,你看見方向偏了,補了一句:配置用 YAML。回車之後,這句話是開工、插進當前輪,還是當場被拒,Core 當場拍板,不等模型開口。
當前輪
下一輪
門口拒收
- 按 TurnInputMode 分發,預設走 StartOrSteerturn_input.rs L141
- 先試 steer_input,只有 NoActiveTurn 才開工turn_input.rs L195
- Regular 才收,Review 和 Compact 當場拒turn_input.rs L507
- 空閒則 apply_started,再 spawn_taskturn_input.rs L242
- 插話寫入 pending,並把相位打回 CurrentTurnturn_input.rs L558
- 最終答案把投遞相位 defer 到 NextTurninput_queue.rs L206
- 工具項把相位 accept 回 CurrentTurnstream_events_utils.rs L302
- get_pending_input 看相位,再決定掏不掏信箱input_queue.rs L297
三種日常結局都不好受。立刻打斷,前面兩個檔案的改動可能半成品留在磁碟上。排到下一輪,你只能看著它把剩下的檔案按舊方向改完。塞進當前上下文卻不叫醒迴圈,模型要到下一次自己開口才看得到,你補的約束等於遲到。
Codex 把這件事收成一個入口、三種模式。呼叫方選 StartOrSteer、StartIfIdle 或 Steer,不直接喊 start。Core 按現場忙閒和任務種類判定,立刻回 Started、Steered 或 NotSubmitted。回完決定就結束,不等 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_started 再 spawn_task。其他拒絕原因原樣包裝成 NotSubmitted,不會偷偷開工。TUI 即時語音也走這條,和預設入口共用同一套判定。
出處:codex-rs/core/src/session/turn_input.rs 第 141 至 156 行、第 195 至 249 行
設定不能先改再判定。prepare 先預覽執行緒設定,預覽失敗直接 InvalidRequest。真正寫入發生在 apply_started 或 apply_steered。被拒絕的輸入連設定都不改。插話成功後只落持久設定,當前輪的 TurnContext 不換。開工專用的選項,比如結構化輸出 schema,只在 Started 上用。
出處:codex-rs/core/src/session/turn_input.rs 第 58 至 80 行
開工和插話必須在同一把鎖裡做完。先看有沒有活動輪,再看 kind,再寫入 pending,中間不能讓另一次提交把輪次換掉。設定卻要先預覽後寫入,因為拒絕路徑必須保證執行緒不變。判定和入隊拆成兩次加鎖,使用者回車和子郵件同時到達時,可能出現判定時還空閒、入隊時已經有人佔坑的視窗。
審查任務自己再開一條 one-shot 子對話,壓縮任務在換視窗。使用者在這時候補一句「用 YAML」,沒有當前輪的工具迴圈可以接住它。如果因此新開一輪普通對話,審查結果和壓縮摘要會跟新對話搶同一個 active_turn。
steer_input 拿著 active_turn 鎖做完全部檢查。沒有活動輪,或者有槽沒有 task,都算 NoActiveTurn。TaskKind 只有 Regular、Review、Compact 三個變體。審查和壓縮返回 ActiveTurnNotSteerable。StartOrSteer 也不會因此開工。
出處: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 行
什麼算出使用者可見的最終答案?助手正文,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-turn 和 next-step 兩條持久列表,claim 先掏空 next-step,目標是 next-turn 時再多取一條。
Codex 沒有這三個公開函式。StartOrSteer 把空閒開工和忙時插話焊在一次判定裡。相位這面翻牌,DSH 可以沒有,因為它把等整輪和等下一站寫成兩條列表。代價是答案已經上屏之後,next-step 非空仍會續命當前 Turn。
Claude Code:一條佇列,用優先順序補時機
還原原始碼裡,使用者輸入、任務通知、孤兒權限走同一條 commandQueue。優先順序是 now 大於 next 大於 later,同級 FIFO。使用者命令預設 next,任務通知預設 later,使用者輸入不會被系統訊息餓死。消費發生在當前流結束之後,沒有步級插話。你補的那句 YAML 約束,要等當前生成器收尾才進模型。
答案上屏之後,這封子郵件什麼時候被看見
最終答案送出之後,先入隊一封 trigger_turn: false 的子郵件,再餵一條 FunctionCall。先在紙上推演 get_pending_input 該返回什麼。
對照路徑:正文落盤時相位切到 NextTurn,這封信看不見;工具項到達後相位重開,下一圈才能把它掏出來,並進同輪的下一次模型請求。