你发出一条消息之后,Codex 怎样把它交给模型?

从零开始:先看谁在做什么,再看消息为什么需要排队。每次点击,只走一步。

默认暂停 · ← → 可逐步看

固定看这四个角色绿色边框=本步正在处理
你 / 使用界面

输入要求、查看结果

消息从这里发出

Codex 运行程序

接收消息、组织工作

OpenAI 模型

根据收到的内容生成

工具

图中话语为教学简化,不是协议原文。

运行程序里:等待处理的内容

最近一次已交给模型的内容

此处不显示还在等待的消息。

    还没有发出模型请求。

    只展示与例子有关的内容,省略规则、其他历史等;不是完整 API 请求。

    这一步的技术条件

    这一节只记住一件事

    试着判断一下(可跳过)

    想看实现?展开源码与中文解释

    例子里的代码、测试结果与对话均为教学假设。本页跟踪三类与消息调度有关的暂存区,不是内部所有通道和队列的总清单。规则依据本地源码 50d77959bf92(核查:2026-09-23);具体版本与功能开关可能不同。

    进阶参考:完整规则、生命周期与源码依据

    01 / 从消息到一次模型请求

    A · 下一轮

    用户排队任务

    持久化保存 → 等待 thread 空闲 → 启动新 turn

    B · 当前轮

    Steer / 追加输入

    活动 turn 校验 → turn-local buffer → 下一次允许的请求

    C · 跨轮

    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 / 优先级是局部规则,不是一张总排名

    发生位置已确认的规则不能推导成
    常规输入 drainB 的已有输入在前,随后追加 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 / 什么时候检查?

    1. 接收层:submission loop 持续接收操作;turn 输入也有类型化的提交接口,不要假设所有请求都必须经过同一个入口通道。
    2. 启动 task:收集已有 mailbox 到 turn-local pending;初始用户输入与这些 pending 的采样时机仍有区别。
    3. 模型请求前:can_drain_pending_input 为真且投递阶段允许时,取出输入,经过 hooks,写入历史后组装模型请求。
    4. 模型流式 item 完成:当前快照对 reasoning / commentary 完成项检查 mailbox;有邮件时可结束本次采样步骤,转入下一次请求。不是 token 级检查,也不是任意 item 都触发。
    5. 工具执行期间:wait_agent / sleep 明确监听输入活动;普通 exec_command 的等待由自己的 yield/进程机制控制,不能承诺邮箱会中止命令。
    6. 本次模型步骤完成:结合 model_needs_follow_up 和 has_pending_input 决定是否继续;工具返回值也进入后续模型上下文。
    7. final / turn 完成:调整邮箱投递阶段、记录剩余 turn-local 输入、释放 active turn,再触发空闲扩展与 pending-work 调度。

    06 / 队列生命周期与边界

    事件A 用户队列B 当前 turn 输入C Agent 邮箱
    创建用户排队写入持久存储ActiveTurn 创建 TurnStateSession::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,便于复核;官方文档用于对照公开接口,不能替代内部调度实现。

    阅读建议:先看整体结构,再选择与你的问题相同的消息场景,最后展开对应队列与源码。