你发出一条消息之后,Codex 怎样把它交给模型?
从零开始:先看谁在做什么,再看消息为什么需要排队。每次点击,只走一步。
输入要求、查看结果
消息从这里发出
接收消息、组织工作
根据收到的内容生成
另一个 agent
图中话语为教学简化,不是协议原文。
运行程序里:等待处理的内容
最近一次已交给模型的内容
此处不显示还在等待的消息。
还没有发出模型请求。
只展示与例子有关的内容,省略规则、其他历史等;不是完整 API 请求。
这一步的技术条件
想看实现?展开源码与中文解释
现在可以对照三个暂存区了
它们按用途分开,由运行程序检查。取出后的内容会进入对话历史;“暂存列表为空”不等于历史被删除。
两个容易困惑的边界情况
已经回答“修好了”,测试助手才发来“还有一个失败”
在本例的普通来信模式下,邮件留在会话邮箱里,不会自己启动一轮新工作。后续有新任务时,旧邮件才可能被取出。
启动时可能先把邮件搬到当前任务的追加输入里;这仍不代表首个模型请求已经读到。请求启动新回合的邮件、可唤醒的持久等待等情况另有处理。
我点了“补充当前任务”,但消息到达时任务已经结束
服务端校验时找不到活动回合,就会拒绝这次补充;若活动回合已经换了,也可能回合不匹配。它不会自动变成“下一项任务”。
界面需要处理失败,并根据用户意图决定是否启动新工作。
例子里的代码、测试结果与对话均为教学假设。本页跟踪三类与消息调度有关的暂存区,不是内部所有通道和队列的总清单。规则依据本地源码 50d77959bf92(核查:2026-09-23);具体版本与功能开关可能不同。
进阶参考:完整规则、生命周期与源码依据
01 / 从消息到一次模型请求
用户排队任务
持久化保存 → 等待 thread 空闲 → 启动新 turn
Steer / 追加输入
活动 turn 校验 → turn-local buffer → 下一次允许的请求
Agent 邮箱
投递 → 唤醒或等待 → 按投递阶段加入当前或后续 turn
客户端 / 其他 agent
├─ Queue API ── A 持久队列 ── 空闲调度 ── 新 turn 初始输入
├─ Steer ───── B 当前 turn 追加输入 ──────────────┐
└─ Agent mail ─ C Session 邮箱 ─────────────────┤
↓
允许消费输入的边界:B + C
↓
hooks / 记录 history / 构建模型请求
↓
模型流式输出 → 工具执行及结果
↓
需要继续或仍有输入?继续循环 : 完成 turn按“进入 agent 的业务输入存储”计,这里讲 3 类。只看 core 的 InputQueue 相关内存存储则是 B、C 两个;A 在 queue extension / storage 层。把控制通道、输出通道、工具 future 和前端缓存都算上,数字会变,因此不能笼统说“一个 thread 一共只有 3 个队列”。
02 / 每个队列的完整档案
A · 用户待执行队列Thread queue · 持久化
- 结构 / 入口
- thread/queue/add → QueueService → QueueStore / SQLite
- 消息来源
- 用户选择“排队”提交的下一项任务;具体前端也可能先保存在自己的 UI 队列,只有调用 queue API 的记录才属于这里。
- 什么时候检查
- thread 空闲时,由 queue extension 的 on_thread_idle 调度;add/update 等可唤醒已加载且未中断的 thread;也可手动 thread/queue/start。
- 顺序 / 消费
- 自动调度按 queue_order 取第一条;reorder 可改变顺序,手动 start 可指定一条。启动成功后删除;不能启动时保留。
- 生命周期
- 属于 thread,存储在持久化队列中,可跨 turn 和进程重启保存;重启后不代表无条件立即执行,需恢复/加载和满足调度条件。
- 边界条件
- 一次通常启动一个新 turn,不塞进当前 turn。Interrupted 空闲事件不自动派发;Plan 等启动条件也可阻止自动启动。
- 源码依据
- QueueService ↗ · SQLite 顺序 ↗
B · 当前回合追加输入Turn pending input · 内存
- 结构 / 入口
- TurnState.pending_input → TurnInputQueue.items: Vec<TurnInput>
- 消息来源
- 已接受的 steer 用户输入、特定 FunctionCallOutput、注入的 ResponseItem;新任务启动时也会把已有邮箱内容搬入这里。并非所有普通工具结果都走这个队列。
- 什么时候检查
- 下一次模型请求前,在 can_drain_pending_input 允许时取走。模型步骤结束后检查是否还有待处理输入,从而决定是否继续。
- 顺序 / 消费
- 按 items 的追加顺序取出;与邮箱同批合并时,B 在前,C 在后。这是拼接顺序,不是所有来源按时间戳全局重排。
- 生命周期
- 随 ActiveTurn / TurnState 创建。消费时清空;正常结束会取走剩余 turn-local input 并经过 hooks 记录。中断有取消、等待者清理与 pending 清理路径,不能把它当作持久消息队列。
- 边界条件
- turn/steer 要校验活动 turn 和 expectedTurnId;若已结束则拒绝,不会自动转为下一轮任务。调用方应明确决定是否重新 turn/start。
- 源码依据
- 数据结构与 drain ↗ · steer 校验 ↗
C · Agent 邮箱Session mailbox · 内存
- 结构 / 入口
- InputQueue.mailbox_pending_mails: Mutex<VecDeque<…>>
- 消息来源
- 其他 agent 的 send_message、followup_task,以及子 agent 终态通知等 InterAgentCommunication。完成通知和普通消息进入同一邮箱,不是独立的“完成队列”。
- 什么时候检查
- 常规取输入点;新 task 启动;流式 reasoning / commentary item 完成边界会检查是否有邮箱消息;wait_agent / sleep 订阅活动通知;空闲时检查是否有 trigger_turn 邮件。
- 顺序 / 消费
- push_back,drain 按投递顺序。trigger_turn 是“可触发新 turn”的标记,不是把这封邮件移动到队首。
- 生命周期
- 属于已加载的 Session,跨 turn 保留;drain 后进入 turn 输入/上下文。仅排队不等于已持久化,不能承诺进程崩溃后未消费邮箱自动恢复。
- 边界条件
- 普通 send_message 不主动启动空闲 agent;followup_task 可触发新 turn。存在 durable sleep 时,普通邮箱消息也可唤醒,这是条件例外。
- 源码依据
- 邮箱 ↗ · 空闲唤醒 ↗ · 完成通知 ↗
03 / 优先级是局部规则,不是一张总排名
| 发生位置 | 已确认的规则 | 不能推导成 |
|---|---|---|
| 常规输入 drain | B 的已有输入在前,随后追加 C 的邮箱消息;各自保留其存储顺序。 | 用户消息永远抢占任何 agent 消息。 |
| 订阅活动时的初始检查 | 先看符合条件的 pending steer,再看 mailbox。watch 的后续通知是最新活动值。 | watch 是不会合并事件的 FIFO 消息队列。 |
| 空闲启动竞争 | start_if_idle 遇到 trigger_turn 邮件会拒绝本次启动,让待触发邮箱工作先处理。 | 任何普通邮件都比用户 Queue 优先。 |
| 新 turn 第一轮采样 | 显式初始输入先采样;通常不会先把后来积累的追加输入一起 drain。 | 每次 HTTP / WebSocket 请求前都无条件清空所有输入。 |
| 自动压缩后的续跑 | 若模型/工具还需要 continuation,可先续跑,再消费 steer。 | 压缩会把消息队列全部删除。 |
| Interrupt | 取消走控制/取消令牌路径,影响正在执行的 turn。 | Interrupt 是一个排在 B、C 前面的普通聊天消息。 |
04 / 什么时候检查?
- 接收层:submission loop 持续接收操作;turn 输入也有类型化的提交接口,不要假设所有请求都必须经过同一个入口通道。
- 启动 task:收集已有 mailbox 到 turn-local pending;初始用户输入与这些 pending 的采样时机仍有区别。
- 模型请求前:can_drain_pending_input 为真且投递阶段允许时,取出输入,经过 hooks,写入历史后组装模型请求。
- 模型流式 item 完成:当前快照对 reasoning / commentary 完成项检查 mailbox;有邮件时可结束本次采样步骤,转入下一次请求。不是 token 级检查,也不是任意 item 都触发。
- 工具执行期间:wait_agent / sleep 明确监听输入活动;普通 exec_command 的等待由自己的 yield/进程机制控制,不能承诺邮箱会中止命令。
- 本次模型步骤完成:结合 model_needs_follow_up 和 has_pending_input 决定是否继续;工具返回值也进入后续模型上下文。
- final / turn 完成:调整邮箱投递阶段、记录剩余 turn-local 输入、释放 active turn,再触发空闲扩展与 pending-work 调度。
06 / 队列生命周期与边界
| 事件 | A 用户队列 | B 当前 turn 输入 | C Agent 邮箱 |
|---|---|---|---|
| 创建 | 用户排队写入持久存储 | ActiveTurn 创建 TurnState | Session::InputQueue 创建 |
| 消费 | 成功启动新 turn 后删除对应记录 | 取出并记录到历史 | drain 到输入批次或 turn-local pending |
| 普通 turn 结束 | 满足条件则派发下一项 | 完成流程处理剩余输入;状态释放 | 晚到消息可继续留存至下一轮 |
| 用户中断 | Interrupted 空闲事件不会自动派发 | 执行取消、等待者和 pending 清理;不是可靠消息存储 | Session 级 mailbox 不等同于 turn-local clear;后续启动仍受 trigger/durable sleep 等规则约束 |
| 前端断开 | 后端持久记录不因 UI 断开自动删除 | 取决于后端 session / task 是否仍存活 | 取决于后端 Session 是否仍存活 |
| 进程崩溃 / 重启 | 持久化记录可恢复;还需重新调度 | 未消费内存数据无自动恢复保证 | 未消费内存邮件无自动恢复保证 |
“已入队”“已写历史”“已发给模型”“模型已回答”是不同的时刻。网络 RPC id 用于关联请求响应,turnId / itemId 标识业务实体,都不是队列优先级。
07 / 哪些容易被误算成输入队列?
控制提交通道与输出事件通道
tx_sub / rx_sub 是容量 512 的 bounded async_channel,由 submission_loop 读取控制操作;tx_event / rx_event 是 unbounded 输出事件通道,给宿主/客户端消费。它们不是 B、C 的业务输入优先级层。
审批与提问的等待者表
TurnState 里有按 ID 索引的 pending_approvals、pending_request_permissions、pending_user_input、pending_elicitations、pending_dynamic_tools 等 HashMap + oneshot。返回的审批/答案找到对应等待者,解开那个工具等待;这不是“把用户回答排到普通 steer 队列末尾”。
工具 future、watch 通知、前端草稿
采样过程中可用 FuturesOrdered 管理 in-flight 工具 future;InputQueue 的 watch 通道负责通知“有活动”,正文留在 B/C;前端草稿/待发送队列属于客户端状态。三者都不应与核心三个输入容器混为一谈。
模型实际上看到了什么?
运行时取走队列消息,把它们转换并记录为模型上下文项,然后提交新的模型请求。模型不直接轮询 VecDeque、SQLite 或 HashMap。agent 邮件的模型侧编码可能随模型/供应商/协议能力变化;本页不把内部 InterAgentCommunication 当作通用公开 Responses API 类型。
08 / 核查来源
以下源码链接固定到本次阅读的 commit,便于复核;官方文档用于对照公开接口,不能替代内部调度实现。
阅读建议:先看整体结构,再选择与你的问题相同的消息场景,最后展开对应队列与源码。