中途插话:这句话进本轮、下一轮,还是被拒
Agent 正在改第三个文件,你看见方向偏了,补了一句:配置用 YAML。回车之后,这句话是开工、插进当前轮,还是当场被拒,Core 当场拍板,不等模型开口。
本页解决的问题
先给结论「中途插话:这句话进本轮、下一轮,还是被拒」要解决的关键问题是什么?
Agent 正在改第三个文件,你看见方向偏了,补了一句:配置用 YAML。回车之后,这句话是开工、插进当前轮,还是当场被拒,Core 当场拍板,不等模型开口。
跟着交接处走,不要只看 Demo。 系统是否可靠,往往取决于模型、工具、状态、权限和人的交接处。把每次交接都当成可以观察、测试和恢复的地方。
为一个自动化步骤写清输入、负责人、审批和恢复动作。
一次运行成功了,却说不清发生了什么,也无法安全重放。
当前轮
下一轮
门口拒收
- 按 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,这封信看不见;工具项到达后相位重开,下一圈才能把它掏出来,并进同轮的下一次模型请求。
「先玩一遍 · 同一句话,四个时机」的能力藏在每次交接里
「三种日常结局都不好受。立刻打断,前面两个文件的改动可能半成品留在磁盘上。排到下一轮,你只能看着它把剩下的文件按旧方向改完。塞进当前上下文却不叫醒循环,模型要到下一次自己开口才看得到,你补的约束等于迟到」说明,Agent 的表现不只由模型决定。模型、上下文、工具、状态、权限和人之间的每次交接,都会改变任务能否继续以及出了问题能否恢复。
先写清状态,再增加能力
从「Codex 把这件事收成一个入口、三种模式。调用方选 StartOrSteer 、 StartIfIdle 或 Steer ,不直接喊 start。Core 按现场忙闲和任务种类判定,立刻回 Started 、 Steered 或 NotSubmitted 。回完决定就结束,不等 user-prompt hook,不等历史落盘,不等模型开始采样」出发,可以把流程拆成起始状态、下一步动作、工具返回、状态更新和停止条件。这样调试时找的是第一处丢失信息或权限的位置,而不是笼统地说“模型变笨了”。
- 按 TurnInputMode 分发,默认走 StartOrSteer turn_input.rs L141
- 先试 steer_input,只有 NoActiveTurn 才开工 turn_input.rs L195
- Regular 才收,Review 和 Compact 当场拒 turn_input.rs L507
成功路径不能代表系统可靠
用「对照路径:正文落盘时相位切到 NextTurn ,这封信看不见;工具项到达后相位重开,下一圈才能把它掏出来,并进同轮的下一次模型请求」重放一次成功和一次失败,记录每一轮真正传入的上下文、工具结果和负责人;只要第二个人能复述这条链,系统才有可维护性。
从「先玩一遍 · 同一句话,四个时机」走到「当前轮」
「先玩一遍 · 同一句话,四个时机」先把问题落在「信箱分拣台:同一句约束,投递时刻不同,落点就不同 播放 单步 重置 时机 空闲 采样中 答案上屏 审查中 四个时机共用同一句输入。空闲会开工,采样中会插话,答案上屏后子邮件先躺着,审查中会被挡在门口。 这句话 时刻 · 空闲 任务 · 无 相位 · —」上;到了「当前轮」,讨论继续推进到「pending_input,相位 CurrentTurn 时会被掏走 空」。两段连起来,重点就不只是记住一个结论,而是看清它成立所依赖的条件。
把这条判断带到下一个场景
分析 Agent 时,沿着状态、动作、工具结果和下一步的顺序走一遍;每次交接都要能说明信息从哪里来、由谁确认、失败时停在哪里。
- 「先玩一遍 · 同一句话,四个时机」:信箱分拣台:同一句约束,投递时刻不同,落点就不同 播放 单步 重置 时机 空闲 采样中 答案上屏 审查中 四个时机共用同一句输入。空闲会开工,采样中会插话,答案上屏后子邮件先躺着,审查中会被挡在门口。 这句话 时刻 · 空闲 任务 · 无 相位 · —
- 「当前轮」:pending_input,相位 CurrentTurn 时会被掏走 空
- 「最后的要点」:插话写入 pending,并把相位打回 CurrentTurn turn_input.rs L558
最后的「最后的要点」把讨论落到「插话写入 pending,并把相位打回 CurrentTurn turn_input.rs L558」。回看这条线索时,最值得保留的是:当输入、规模或风险改变,哪些判断需要重新做一遍。
我把这篇文章里的一个判断改写成了今天可以验证的小实验。比记住结论更有用的是,知道下一步要观察什么。
读完以后我先回头找它成立的条件,而不是直接把方法搬进项目。这个顺序让后面的取舍清楚很多。
如果把这个判断放到真实工作里,最先需要补的约束是什么?我想知道从阅读到第一次实践之间,哪一步最值得先做。
还没有这篇文章的讨论。