雨天小六

读懂 Codex(8.34):崩溃点矩阵:模型、工具、日志和投影分别可能留下什么

· 更新于 2026-08-03 · 专栏:读懂 Codex

#Codex#Agent Runtime#持久化#Rollout#软件架构

Codex 的崩溃一致性不是“成功或什么都没发生”;模型流、工具副作用、Context History、Rollout pending、JSONL 和 SQLite Projection 各有独立提交点,恢复必须按留下的证据分层处理。

本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:把本章机制组合成端到端故障矩阵,明确每个崩溃点的残留和恢复动作。输入:一次 Turn 从模型请求到工具执行、规范写入和投影更新的阶段;状态所有者:Session、Tool Runtime、Recorder、Filesystem 和 State DB 各自所有者;成功结果:可判断的残留状态、恢复后的 Prompt 语义与不可自动撤销副作用。先把这些边界钉住,后面的顺序、失败和恢复才不会混成一句“持久化失败后重试”。

状态边界

问题本节答案
输入一次 Turn 从模型请求到工具执行、规范写入和投影更新的阶段
状态所有者Session、Tool Runtime、Recorder、Filesystem 和 State DB 各自所有者
成功产物可判断的残留状态、恢复后的 Prompt 语义与不可自动撤销副作用
研究范围把本章机制组合成端到端故障矩阵,明确每个崩溃点的残留和恢复动作

正常路径:先看顺序点

崩溃点矩阵:模型、工具、日志和投影分别可能留下什么的正常路径时序图,展示模型产生增量/完整 Call、Session 记录 Call 并执行工具、记录 Output/Turn 终态、Recorder 确认 JSONL 前缀、SQLite 追赶投影
图 8.34-1:模型产生增量/完整 Call → Session 记录 Call 并执行工具 → 记录 Output/Turn 终态 → Recorder 确认 JSONL 前缀 → SQLite 追赶投影。这张图标出本节的实际顺序点与状态所有者。

这条路径可以压缩成五步:

  1. 模型产生增量/完整 Call。
  2. Session 记录 Call 并执行工具。
  3. 记录 Output/Turn 终态。
  4. Recorder 确认 JSONL 前缀。
  5. SQLite 追赶投影。

图中的箭头不是“可能调用”的依赖图,而是源码中决定可见性和所有权转移的先后关系。前一步没有确认时,后一步不能替它作出更强的成功承诺。

源码机制拆解

模型增量阶段可能没有规范 Item

只收到 Text/Argument Delta 时客户端可能已经显示内容,但 active ResponseItem 尚未完成,Persistence Policy 不会保存那些分片。重启后 Resume 只能看到上一个完整规范边界。

Call 已写、工具未完成会留下半对

模型 Call 可能已经进入 Context/Rollout,进程在工具 Output 前崩溃。恢复不会重跑工具;for_prompt 在副本上补 aborted Output,承认结果未知。

工具副作用可能领先 Output 落盘

Shell 写文件或网络请求完成后,进程可能在记录 FunctionCallOutput 前终止。外部世界已改变,但会话只知道 Call;自动重试工具会有重复副作用风险,因此框架只修复协议形状。

JSONL 行与 pending 有前缀关系

批量写第 k 行失败时,前 k-1 行可能已耐久,后缀仍在 Recorder 内存。若进程整体崩溃,内存后缀丢失;Loader 只能恢复完整行并报告坏尾/坏行。

Projection 永远可能落后

JSONL flush 成功到 SQLite commit 之间崩溃,Resume 语义仍完整,但分页/搜索暂缺最新 Turn。Materializer 根据 byte offset/ordinal 从旧 checkpoint 追赶。

终态 Flush 决定可观察尾部

TurnComplete/Aborted 已在 Session 处理不等于磁盘确认。显式 flush 缩小进程退出窗口,但普通文件系统和外部设备仍有各自 durability 限制;代码只声称 await 到 writer/file flush 的实现保证。

Python 风格伪代码

下面的伪代码只保留设计职责、状态和失败顺序;它不逐行翻译 Rust,也不借 Python 语法虚构源码中不存在的事务:

async def run_and_persist_turn(session, user_input):
    await session.record_user_boundary(user_input)
    response = await model.stream_until_complete()       # deltas may be UI-only
    call = response.tool_call
    await session.record_conversation_items([call])      # canonical candidate

    try:
        output = await tool.execute(call)                 # external commit point
    except Cancelled:
        output = aborted_output(call)
    await session.record_conversation_items([output])
    await session.record_turn_terminal()
    await session.flush_rollout()                         # JSONL confirmation
    # projection runs only after JSONL; it may fail without replay loss

def recover_after_crash(thread_id):
    complete_lines, parse_errors = tolerant_load_jsonl(thread_id)
    state = reconstruct(complete_lines)
    prompt = state.history.for_prompt_copy()              # repair call/output only here
    schedule_projection_catchup(thread_id)
    return state, prompt, parse_errors
    # Never claim external tool effects were rolled back or are safe to repeat.

阅读时要特别看三处:哪个对象拥有可变状态,哪一个 await 是可观察屏障,以及失败后保留的是已提交前缀、未提交后缀,还是完全独立的外部副作用。

失败、取消与恢复

崩溃点矩阵:模型、工具、日志和投影分别可能留下什么的失败路径图,区分崩溃在完整 ResponseItem 前、崩溃在 Call 后/工具前、崩溃在工具副作用后/Output 前、崩溃在 JSONL 后/SQLite 前、崩溃写出半行、崩溃在 Rollback 热应用后/marker flush 前
图 8.34-2:同一操作在不同故障点留下的状态不同,恢复责任必须回到拥有该状态的组件。
故障点已留下的状态可观察结果恢复责任
崩溃在完整 ResponseItem 前可能只有 UI DeltaRollout 无该半截消息从上一规范边界继续
崩溃在 Call 后/工具前Rollout 有 Call、无 OutputPrompt 补 aborted不自动执行未知工具
崩溃在工具副作用后/Output 前外部状态已变、Rollout 无结果同样补 aborted人工/工具特定幂等键核对副作用
崩溃在 JSONL 后/SQLite 前规范历史完整、查询投影落后Resume 正常,列表/分页暂旧增量 materialize 追赶
崩溃写出半行尾部不是完整 JSONLoader 跳过/追加前补 newlineparse error 提示语义可能缺失
崩溃在 Rollback 热应用后/marker flush 前当前进程内已回退、耐久 marker 未确认若进程消失可能回到旧历史Warning/重试;不能声称已耐久

这里没有统一的“回滚一切”。内存状态、日志行、SQLite 投影、父子拓扑和工具造成的文件/网络变化分别有自己的提交点。恢复代码只能根据已经存在的权威证据继续,不能用较弱的投影替较强的事实背书。

必须保持的不变量

  • JSONL 完整前缀是恢复上限,SQLite 不能扩展它
  • 协议修复不证明工具执行结果
  • 外部副作用没有通用 rollback
  • 任何“已保存”声明必须指出对应屏障层级

这些不变量比“最终能 Resume”更严格:正常路径要成立,Writer 竞争、任务取消、坏尾行、投影落后和旧格式兼容时也必须成立。

设计取舍

逐层提交让系统能在大多数崩溃后继续,但它拒绝提供虚假的跨模型、文件系统、网络与数据库全局事务。正确设计依赖幂等工具、显式终态和可诊断缺口。

源码可以直接证明字段、分支、调用顺序和测试期望;“为什么这样设计”的表述是基于这些事实作出的工程归纳,不把它包装成未公开的产品承诺。

Mini Codex 复刻

写 fault-injection test 在每个 await 后崩溃;断言 event log、外部 fake tool、prompt repair 和 read-model checkpoint 的组合,而不是只测最终 happy path。

复刻时先验证协议不变量,再补性能优化。一个能在故障注入下说明“留下了什么”的小实现,比一个只在正常路径调用 save() 的演示更接近真实 Runtime。

源码导航

相邻测试也很重要:

本节结论

Codex 的崩溃一致性不是“成功或什么都没发生”;模型流、工具副作用、Context History、Rollout pending、JSONL 和 SQLite Projection 各有独立提交点,恢复必须按留下的证据分层处理。

阅读导航

上一节:8.33 · 下一节:第九章总览

评论


← 返回文章列表