雨天小六

读懂 Codex(9.15):Resume、Call/Result 修复与不完整 Turn

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

#Codex#Agent Runtime#Python#软件架构

具体问题与边界

进程死在 ToolCall 落盘之后、ToolResult 落盘之前时,恢复怎样继续而不重复未知副作用?

replay_rollout 只重建语义记录与不完整 Turn ID;RuntimeManager 创建新 Session;HistoryManager 只在 Prompt 副本修复 Call/Result。 本节区分“从官方源码得到的生产事实”和“为教学实现作出的 Python 选择”,不会把后一种包装成 Codex 的等价实现。

状态所有权

对象所有者生命周期持久化
JSONL records文件系统跨进程
ResumeStatereplay_rollout一次恢复
effective historyResumeState/HistoryManager新 Session后续继续追加
incomplete_turn_idreplay scanner直到 Host 展示/处理
synthetic aborted resultmodel_view一次 Prompt snapshot

正常路径

Resume、Call/Result 修复与不完整 Turn正常路径图
图 9.15-1:进程死在 ToolCall 落盘之后、ToolResult 落盘之前时,恢复怎样继续而不重复未知副作用?
  1. Loader 顺序读取完整 JSON object 行;坏 JSON 或非 object 行增加 parse_errors 并跳过。
  2. 第一个 session_meta 提供 thread_id;item 追加 history;compacted/rolled_back 用 replacement_history 替换此前有效历史。
  3. turn_started 设置 active_turn_id;匹配的 turn_completed/turn_aborted 清空。文件结尾仍活动就返回 incomplete_turn_id。
  4. RuntimeManager 不反序列化旧 Queue、Task、Token 或连接,而是用 ResumeState.history 创建新的 HistoryManager、Rollout 和 Session。
  5. 构造模型请求时,孤儿 Result 从副本移除;无 Result 的 Call 获得稳定 UUIDv5 aborted Result。它不触发 Tool,也不改写原 Rollout。

顺序为什么不能交换

Loader 顺序读取完整 JSON object 行;坏 JSON 或非 object 行增加 parse_errors 并跳过
→ 第一个 session_meta 提供 thread_id;item 追加 history;compacted/rolled_back 用 replacement_history 替换此前有效历史
→ turn_started 设置 active_turn_id;匹配的 turn_completed/turn_aborted 清空
→ RuntimeManager 不反序列化旧 Queue、Task、Token 或连接,而是用 ResumeState.history 创建新的 HistoryManager、Rollout 和 Session
→ 构造模型请求时,孤儿 Result 从副本移除;无 Result 的 Call 获得稳定 UUIDv5 `aborted` Result

箭头代表可见性与所有权转移,不是松散依赖。Policy、Approval、外部副作用、规范 Item 和 durability 各自有提交点,后一步不能替前一步作更强承诺。

Python 风格伪代码

async def replay(path):
    state = ResumeState()
    for record in tolerant_jsonl_load(path):
        match record["type"]:
            case "session_meta": state.thread_id = record["thread_id"]
            case "item": state.history.append(decode_item(record))
            case "compacted" | "rolled_back":
                state.history = decode_replacement(record)
            case "turn_started": state.active_turn = record["turn_id"]
            case "turn_completed" | "turn_aborted":
                if record["turn_id"] == state.active_turn:
                    state.active_turn = None
    return ResumeState(history=state.history,
                       incomplete_turn_id=state.active_turn,
                       parse_errors=errors)

async def resume_thread(state):
    return Session(
        history=HistoryManager(state.history),
        rollout=JsonlRollout(existing_path, state.thread_id),
        fresh_queues_tasks_tokens=True,
    )

# Only at prompt time:
prompt_history = history.model_view()  # stable aborted repair; no tool rerun

失败、取消与恢复

Resume、Call/Result 修复与不完整 Turn失败路径图
图 9.15-2:每个失败点都列出已经发生的副作用和仍可安全执行的恢复动作。
故障点已留下的状态处理
Call 无 Result工具可能未运行,也可能已产生副作用不自动重试;Prompt-only aborted
孤儿 Result模型看不见对应请求从 Prompt 副本移除
坏 JSON 行部分语义可能丢失计数并继续,Host 可诊断
缺 session_meta无法确定 Thread 身份恢复失败
恢复旧 asyncio 对象跨进程资源无效只重放语义,创建新对象

不变量

恢复不能根据缺失 Result 猜测工具是否执行,更不能自动重跑;协议修复只能作用于模型输入副本。

设计思路与限制

返回 incomplete_turn_id 把不确定性暴露给 Host,比伪造完成更诚实。Mini 不恢复 WorldState full/patch、previous response、上下文窗口身份和分页 lineage。

测试与复现

cd examples/mini-codex
uv run pytest -q -k 'test_resume_reports_incomplete_turn_and_repairs_prompt_only or test_model_recording_replays_only_matching_request'
uv run mypy src
  • test_resume_reports_incomplete_turn_and_repairs_prompt_only
  • test_model_recording_replays_only_matching_request

官方源码导航

Mini Codex 对照

  • src/mini_codex/persistence/resume.py:顺序 replay 与 incomplete Turn
  • src/mini_codex/context/history.py:稳定 Prompt-only repair
  • src/mini_codex/runtime/manager.py:新 Session 恢复

本节结论

恢复不能根据缺失 Result 猜测工具是否执行,更不能自动重跑;协议修复只能作用于模型输入副本。

阅读导航

上一节:9.14 · 下一节:9.16

评论


← 返回文章列表