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 追赶投影。
图中的箭头不是“可能调用”的依赖图,而是源码中决定可见性和所有权转移的先后关系。前一步没有确认时,后一步不能替它作出更强的成功承诺。
源码机制拆解
模型增量阶段可能没有规范 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 前 | 可能只有 UI Delta | Rollout 无该半截消息 | 从上一规范边界继续 |
| 崩溃在 Call 后/工具前 | Rollout 有 Call、无 Output | Prompt 补 aborted | 不自动执行未知工具 |
| 崩溃在工具副作用后/Output 前 | 外部状态已变、Rollout 无结果 | 同样补 aborted | 人工/工具特定幂等键核对副作用 |
| 崩溃在 JSONL 后/SQLite 前 | 规范历史完整、查询投影落后 | Resume 正常,列表/分页暂旧 | 增量 materialize 追赶 |
| 崩溃写出半行 | 尾部不是完整 JSON | Loader 跳过/追加前补 newline | parse 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-rs/core/src/session/turn.rs:Turn 中模型、工具、WorldState 与终态的调用顺序
- codex-rs/core/src/context_manager/normalize.rs:Call/Output Prompt-only 修复
- codex-rs/rollout/src/recorder.rs:pending 前缀、flush、坏尾和重试
- codex-rs/thread-store/src/local/live_writer.rs:JSONL-first/SQLite-later
- codex-rs/core/src/session/handlers.rs:Rollback marker 的热应用与 flush warning
相邻测试也很重要:
- codex-rs/core/src/context_manager/history_tests.rs:不完整 Call/Output 与 stable prompt repair
- codex-rs/rollout/src/recorder_tests.rs:尾部、部分写、重开和 parse error
- codex-rs/thread-store/src/local/thread_history_materialization_tests.rs:projection lag/catch-up 与原子 checkpoint
- codex-rs/core/src/session/tests.rs:Turn rollback、终态和 persistence failure 行为
本节结论
Codex 的崩溃一致性不是“成功或什么都没发生”;模型流、工具副作用、Context History、Rollout pending、JSONL 和 SQLite Projection 各有独立提交点,恢复必须按留下的证据分层处理。
评论
登录后即可评论