Codex 怎样接收、暂存并处理消息?

先看架构:谁负责接收,消息存在哪,什么时候交给模型。再用动画走一遍。

01 / 设计架构

先看全貌:模型外面,还有一个负责组织工作的程序

界面接收你的操作。Codex 运行程序保存输入、请求模型并执行工具。模型只根据本次收到的内容生成下一步。

消息来源

你 / 前端界面

发送新任务、补充当前要求,或把下一项任务排队。

消息来源

其他 agent

独立工作的测试助手等,发回发现、进度或完成通知。

把输入交给运行程序 →
程序管理的范围 · app-server / core 及相关扩展

Codex 运行程序

新任务条件允许时开始一轮工作
中途补充 / 助手来信 / 排队任务进入对应的暂存区,等待各自的处理时机
本次任务的补充等当前工作接收
助手来信等接收或下一轮处理
下一项任务等空闲后启动新工作

队列 = 程序保存的待处理列表。三类暂存区的规则不同,模型不能直接翻看。

工作循环(agent loop)

在允许的位置取输入 → 更新对话历史 → 准备下一份模型请求

拿到模型输出后:需要工具就执行工具并继续;完成任务就收尾。

对话历史存放已接收的任务、消息和工具结果,用来组织后续模型输入。它与“还没处理的队列”是两回事。
请求内容 → ← 生成结果

OpenAI 模型

读取本次请求,生成回答或工具调用。新到消息由运行程序带入后续请求。

执行指令 → ← 工具结果

工具执行环境

运行命令、读写文件等。结果先回到运行程序,再交给模型。

这是职责与数据流示意,不表示工具一定运行在本机,也不表示连接的数量。

一条会话,一轮工作,多次模型请求

会话(thread):保存连续的工作历史
第一轮工作(turn):修复登录

请求模型 → 执行测试 → 带结果再请求模型 → … → 完成

下一轮工作:写文档

沿用会话历史,开始新任务

“下一次请求模型”可能还在做同一件事;“下一轮工作”才是开始另一项任务。

三类暂存区:分别收什么,什么时候取?

像三份用途不同的待办列表。它们都由运行程序管理,但不共用一套取出规则。

当前工作要接收新输入时

本次任务的补充→ 然后接上 →助手来信

这是一个允许接收输入的位置上的合并顺序。不是所有消息按到达时间统一排序,也不是能随时抢占正在执行的工具。

下一项任务什么时候开始?

会话空闲且启动条件允许时,取队首任务,开始新一轮工作。启动成功后才删除排队记录;它不会混进当前工作的普通输入合并。

最重要的边界:程序收到,不等于模型读到。

进入暂存区 → 程序取出 → 加入对话历史 → 随后续模型请求发出。暂存区变空,只说明消息被取走,不能单凭这一点判断模型已读。

本页只讨论这三类与消息调度有关的暂存区;不代表 Codex 内部总共只有三个队列。下面的动画采用同一套名称。

02 / 动画演示

带着上面的架构,看一条具体消息怎样流动

选择场景,然后点击“下一步”。默认暂停;每次只推进一个事件。

默认暂停 · ← → 可逐步看

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

输入要求、查看结果

消息从这里发出

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,便于复核;官方文档用于对照公开接口,不能替代内部调度实现。

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