具体问题与边界
进程死在 ToolCall 落盘之后、ToolResult 落盘之前时,恢复怎样继续而不重复未知副作用?
replay_rollout 只重建语义记录与不完整 Turn ID;RuntimeManager 创建新 Session;HistoryManager 只在 Prompt 副本修复 Call/Result。 本节区分“从官方源码得到的生产事实”和“为教学实现作出的 Python 选择”,不会把后一种包装成 Codex 的等价实现。
状态所有权
| 对象 | 所有者 | 生命周期 | 持久化 |
|---|---|---|---|
| JSONL records | 文件系统 | 跨进程 | 是 |
| ResumeState | replay_rollout | 一次恢复 | 否 |
| effective history | ResumeState/HistoryManager | 新 Session | 后续继续追加 |
| incomplete_turn_id | replay scanner | 直到 Host 展示/处理 | 否 |
| synthetic aborted result | model_view | 一次 Prompt snapshot | 否 |
正常路径
- 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 清空。文件结尾仍活动就返回 incomplete_turn_id。
- RuntimeManager 不反序列化旧 Queue、Task、Token 或连接,而是用 ResumeState.history 创建新的 HistoryManager、Rollout 和 Session。
- 构造模型请求时,孤儿 Result 从副本移除;无 Result 的 Call 获得稳定 UUIDv5
abortedResult。它不触发 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
失败、取消与恢复
| 故障点 | 已留下的状态 | 处理 |
|---|---|---|
| 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_onlytest_model_recording_replays_only_matching_request
官方源码导航
- codex-rs/core/src/session/rollout_reconstruction.rs:Rollout 反向定位与前向重建
- codex-rs/core/src/context_manager/normalize.rs:Call/Output Prompt 规范化
- codex-rs/core/src/context_manager/history.rs:
for_prompt副本边界 - codex-rs/rollout/src/recorder.rs:容错行读取与记录边界
Mini Codex 对照
src/mini_codex/persistence/resume.py:顺序 replay 与 incomplete Turnsrc/mini_codex/context/history.py:稳定 Prompt-only repairsrc/mini_codex/runtime/manager.py:新 Session 恢复
本节结论
恢复不能根据缺失 Result 猜测工具是否执行,更不能自动重跑;协议修复只能作用于模型输入副本。
评论
登录后即可评论