第 5 章:线程、会话与持久状态
阅读契约: 用本章理解持久工作。把 thread identity、queues、history、rollout 和 session ownership 分别看成 state、progress 与 resumability 的答案。

源码边界: 本章只有在链接到固定 Codex commit 或本章源码地图的 files、types、functions、tests、schemas、request/event shapes 时,才把说法视为 verified source。像 runtime、owner、projection、contract 这类架构归纳是从可见 anchors 得出的 surrounding contract inference,不是对 OpenAI 服务内部的断言。
第 4 章把协议边界讲清楚了:客户端可以提交 operation、接收 event、 交换模型 item,却不需要知道 runtime 内部如何工作。本章越过这条边界, 看 Codex 如何把这些协议消息变成一个可恢复、可 fork、可回滚的活线程。
问题:Agent 必须既像一段连续对话,又能恢复、分叉、中断和观测。
主张:Codex 通过区分 live runtime handle 与 durable thread record 来做到这一点。
心智模型:thread 是长期工作账本;session 是当前正在服务这个账本的运行时进程。
Runtime Stack
Codex 不是围绕一个“聊天对象”组织起来的。它更像一组逐层收窄权限的 handle:越靠近外部,接口越小;越靠近内部,状态和调度责任越多。
ThreadManager 拥有 live threads 集合,以及创建 session 所需的共享服务。 CodexThread 是线程创建或恢复之后交给客户端的稳定 handle。Codex 本身刻意保持很小:一条提交路径,一条事件路径。Session 才是内部的 大对象,保存解析后的配置、持久身份、active turn、mailbox、realtime 状态、目标状态、review 状态和各种 service handle。
这层拆分很关键。客户端不应该直接改调度器内部状态。客户端提交一个 operation;session 决定它是启动新 task、steer 当前 task、记录 pending input、更新持久 metadata,还是关闭线程。
第一个事件就是契约
一个新打开的 runtime,第一个客户端可见事件是 session setup event。它 携带 thread identity、session identity、工作目录、模型、provider、 审批策略、权限 profile、初始消息和其他已解析设置。
这个顺序不是偶然的。有些启动工作可以在 session 已经可见后继续进行: MCP server 可能还在初始化,plugin 能力可能稍后才报告,prewarm 工作也 可能在后台运行。setup event 给所有客户端一个确定的流式锚点,后续事件 才有共同参照。
// Pseudocode - illustrative pattern.
function open_runtime_thread(request):
resolved = resolve_configuration(request)
live_store = open_thread_persistence(resolved.thread_identity)
session = build_session(resolved, live_store)
emit_event(SessionConfigured(resolved.public_metadata))
record_initial_history_if_resuming(session)
start_background_capability_initialization(session)
start_submission_loop(session)
return client_thread_handle(session)
重要的不是源码里每一步的具体位置,而是这个性质:客户端可以把 setup 当作事件流的起点,并且在可选能力还没有全部加载完时,也能先渲染一个 恢复出来的线程。
三种历史
Agent 架构里很多混乱,都来自把“历史”当成一个东西。Codex 同时维护三种 历史,它们回答的问题不同。
| 历史 | 主要 owner | 回答的问题 |
|---|---|---|
| 模型可见上下文 | ContextManager | 下一次模型请求应该看到什么? |
| Rollout JSONL | thread persistence | 按 replay 顺序到底发生了什么,以便恢复或 fork? |
| SQLite projection | state layer | 哪些信息需要高效查询,而不是每次 replay 全量文件? |
模型可见上下文会被规范化为 model items。它不是 UI transcript,也不是 raw log。它只包含未来 inference 应该参与的内容,并且会受到模型输入 模态、compaction、context injection、工具观察结果和 pending input 的影响。
rollout 文件是 replay spine。它按顺序记录持久 runtime 事实:session metadata、turn context snapshot、模型可见 item、event record、compaction record、rollback marker,以及其他可 replay 的 item。线程恢复时,它是 重建过程的事实来源。
SQLite state 是 projection layer。它保存 thread metadata、列表和搜索索引、 日志、memory job、graph edge 等适合查询的状态。在设计允许的地方, projection 可以从持久记录修复或重建。rollout 是账本;数据库是索引和 运行时辅助状态。
记录一个 Item 可能影响三层
一次 turn 记录一个新 item 时,runtime 必须判断哪些 surface 需要知道它。 一个工具结果可能是模型可见的、可 replay 的、客户端可渲染的、analytics 关心的,也可能需要进入 trace 诊断证据。这些关系很近,但并不是同一件事。
// Pseudocode - illustrative pattern.
function record_runtime_item(turn, item):
if item.belongs_in_model_context:
context_manager.append(item.as_model_item)
if item.belongs_in_replay:
rollout.append(item.as_rollout_item)
if item.updates_queryable_state:
sqlite_projection.apply(item.as_projection_delta)
if item.should_be_seen_by_clients:
event_stream.emit(item.as_event)
这种设计保护的是 replay。客户端可以断开再重连;数据库行可以过期后修复; 模型请求可以从规范化 context 重新构造,而不是依赖屏幕文本。持久状态是 这些 surface 之间的共同语言。
Start、Resume、Fork、Rollback
线程操作本质上都是同一个原则的变体:加载或选择一个 replay 前缀,在它 之上构造 session,然后用相同的词汇继续追加未来 item。
resume 不是重新加载一段字符串。它要从 durable items 重建模型历史和 session metadata。fork 不是复制 UI transcript。它选择一个一致的前缀, 打开一个新的 thread,让未来从源线程分叉。rollback 也不是简单删除可见 消息;它要从保留下来的 durable record 重建 active model context,并考虑 compaction 和未完成 turn。
这就是为什么 turn context snapshot 必须持久化。它告诉后来的 session: 当时的工作目录、权限、模型设置、网络策略、memory mode 和环境选择是什么。 没有这些上下文,replay 就只剩文本,没有执行语义。
Session State 是分层的
live session 内部有多类状态,不应该被揉成一个 map。
| 层次 | 生命周期 | 示例 |
|---|---|---|
| 配置 | session 生命周期,可受控更新 | model、provider、permissions、cwd、service tier |
| 可变 session state | session 生命周期 | token usage、当前 metadata、connector selection、rate limits |
| Active turn state | 一个 in-flight task | cancellation token、tool futures、pending approvals、streamed items |
| Mailbox 与 pending input | 跨调度边界 | 另一个 turn 正在运行时到达的消息 |
| Durable store handle | thread 生命周期 | rollout writer、thread metadata、state database handle |
这层拆分让 Codex 能回答一个细问题:“这个 operation 改的是 durable thread、 当前 live turn、下一次 turn,还是只是客户端视图?”这些承诺完全不同。 如果 runtime 分不清,迟早会丢输入、重复工具结果,或者让 resume 变得不一致。
Metadata 是抽取出来的,不是凭空造的
线程列表和搜索视图不能每次请求都全量 replay。Codex 因此维护 metadata projection:标题或 preview、model provider、memory mode、archive state、 working directory、git 信息、创建和更新时间、分页 anchor 等。
projection 的价值在于可查询;它的安全性来自背后仍然有 durable rollout。 当列表行缺失或过期时,系统可以扫描对应 rollout 的头部,或者 replay 携带 metadata 的 items 来修复索引。这就是持久状态模式:用 projection 优化读取,但让 event record 保持权威。
应用到实践
- 明确 live-handle stack,让客户端提交 operation,而不是直接修改调度器内部。
- 区分 thread identity 与 session execution,因为一个 durable thread 可能经历多次 runtime 生命周期。
- 在可选启动工作之前先发确定的 setup event,让所有客户端拥有同一个事件流锚点。
- 分开维护模型历史、replay 历史和查询历史,不要强迫一个 transcript 承担所有职责。
- 让 resume、fork 和 rollback 复用正常 turn 使用的 replay vocabulary。
小结
第 5 章把第 4 章的协议变成了活的 runtime:thread 是持久的,session 是 运行中的,turn 是 session 内被调度的工作。第 6 章会进入一次 turn, 看 Codex 如何构造上下文、采样模型、执行工具、处理 interruption,并判断 Agent 是否真的完成。
源码地图
| 概念 | 源码锚点 |
|---|---|
| Thread manager boundary | codex-rs/core/src/thread_manager.rs |
| Client-facing thread handle | codex-rs/core/src/codex_thread.rs |
| Queue-pair runtime facade | codex-rs/core/src/session/mod.rs |
| Model-visible history | codex-rs/core/src/context_manager/history.rs |
| Accepted prompt recording | codex-rs/core/src/session/mod.rs |