Resume 保留 Thread 的 durable identity,却重新创建任务、channel、服务连接、锁和 Session 状态;旧进程内对象从不被序列化复活。
本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:追踪 Session::new 的 Resume 分支与初始化提交,不展开历史重放算法。输入:thread_id、rollout_path/StoredHistory、当前 Config 与运行时服务;状态所有者:ThreadManager + Session::new + LiveThread::resume;成功结果:使用同一 conversation/thread identity 的全新 Session。先把这些边界钉住,后面的顺序、失败和恢复才不会混成一句“持久化失败后重试”。
状态边界
| 问题 | 本节答案 |
|---|---|
| 输入 | thread_id、rollout_path/StoredHistory、当前 Config 与运行时服务 |
| 状态所有者 | ThreadManager + Session::new + LiveThread::resume |
| 成功产物 | 使用同一 conversation/thread identity 的全新 Session |
| 研究范围 | 追踪 Session::new 的 Resume 分支与初始化提交,不展开历史重放算法 |
正常路径:先看顺序点
这条路径可以压缩成五步:
- 按 ThreadId 定位 StoredThread。
- 并行建立新服务和持久化句柄。
- LiveThread 获取新的 writer 所有权。
- 加载历史并重建内存状态。
- InitGuard commit 新 Session。
图中的箭头不是“可能调用”的依赖图,而是源码中决定可见性和所有权转移的先后关系。前一步没有确认时,后一步不能替它作出更强的成功承诺。
源码机制拆解
Durable identity 与 runtime identity 分开
Resume 的 conversation_id/thread_id 沿用历史 identity;若首个 SessionMeta 提供 session id,兼容逻辑据此设置会话标识。Sender、Receiver、CancellationToken 和任务句柄全部重新创建。
配置来自“现在”与“过去”的合成
当前启动 Config 决定 provider、功能开关和服务实例;Rollout Reconstruction 恢复 previous turn settings、reference context 与窗口身份。模型不同时会发 warning,而不是偷偷切回旧对象。
持久化与其他服务并行初始化
State DB、MCP/auth 等工作可以与 LiveThread::resume 并行,但 LiveThread 在最终 commit 前由 InitGuard 保护。任何分支失败都释放 writer。
历史应用是状态重建
record_initial_history 对 Resumed history 调用 reconstruction,用结果替换 ContextManager、恢复 token info、WorldState 与 auto-compact window;这不是逐个重新执行过去的工具。
恢复后仍需建立当前边界
非 subagent Resume 会显式 flush 初始化相关记录;第一个新 Turn 再根据恢复的 reference context 与当前 Step 决定全量注入还是 diff。
Python 风格伪代码
下面的伪代码只保留设计职责、状态和失败顺序;它不逐行翻译 Rust,也不借 Python 语法虚构源码中不存在的事务:
async def resume_thread(manager, thread_id, current_config):
stored = await manager.thread_store.read_thread(thread_id, include_history=False)
live_future = LiveThread.resume(manager.thread_store, stored)
services_future = initialize_current_services(current_config)
live, services = await gather(live_future, services_future)
guard = LiveThreadInitGuard(live)
try:
history = await guard.live.load_history(include_archived=True)
session = Session.new_runtime_object(
thread_id=thread_id,
channels=new_channels(),
services=services,
config=current_config,
)
session.apply_rollout_reconstruction(history.items)
await session.record_resume_boundary_if_needed()
session.live_thread = guard.commit()
return session
except BaseException:
await guard.discard()
raise
阅读时要特别看三处:哪个对象拥有可变状态,哪一个 await 是可观察屏障,以及失败后保留的是已提交前缀、未提交后缀,还是完全独立的外部副作用。
失败、取消与恢复
| 故障点 | 已留下的状态 | 可观察结果 | 恢复责任 |
|---|---|---|---|
| 旧 Session 正在另一进程写 | 跨进程 lock 被占用 | Resume Conflict | 不能克隆活动 writer |
| 历史加载失败 | 新服务可能已创建 | Session 不 commit | Guard discard 并取消其余初始化 |
| 当前模型不同 | 历史仍可重建 | 发出 mismatch warning | 由用户/配置决定是否切换 |
| 把旧任务状态当可恢复 | Cancellation/进程句柄已失效 | 幽灵 active turn | 只从持久化 Turn 边界计算状态 |
这里没有统一的“回滚一切”。内存状态、日志行、SQLite 投影、父子拓扑和工具造成的文件/网络变化分别有自己的提交点。恢复代码只能根据已经存在的权威证据继续,不能用较弱的投影替较强的事实背书。
必须保持的不变量
- Resume 不复用旧 channel/task/connection 对象
- ThreadId 与 durable history identity 保持
- 新 writer 获取成功后才允许追加
- 过去工具副作用不会在重建中重执行
这些不变量比“最终能 Resume”更严格:正常路径要成立,Writer 竞争、任务取消、坏尾行、投影落后和旧格式兼容时也必须成立。
设计取舍
重建而非对象快照要求明确记录语义检查点,却避免把不可序列化的锁、socket 和任务生命周期绑进长期格式。
源码可以直接证明字段、分支、调用顺序和测试期望;“为什么这样设计”的表述是基于这些事实作出的工程归纳,不把它包装成未公开的产品承诺。
Mini Codex 复刻
SessionSnapshot 只保存领域记录;resume() 总是调用 Session() 构造器,再用 reducer 恢复 history/settings,不 pickle event loop 对象。
复刻时先验证协议不变量,再补性能优化。一个能在故障注入下说明“留下了什么”的小实现,比一个只在正常路径调用 save() 的演示更接近真实 Runtime。
源码导航
- codex-rs/core/src/session/session.rs:Resume 分支的 identity、并行初始化和 InitGuard commit
- codex-rs/thread-store/src/live_thread.rs:LiveThread::resume 的 writer 与 metadata 恢复
- codex-rs/core/src/session/mod.rs:record_initial_history 应用恢复状态
相邻测试也很重要:
- codex-rs/core/src/session/tests.rs:Resume 初始化、模型差异和历史应用
- codex-rs/thread-store/src/local/writer_lock_tests.rs:活动 writer 阻止第二个 Resume owner
本节结论
Resume 保留 Thread 的 durable identity,却重新创建任务、channel、服务连接、锁和 Session 状态;旧进程内对象从不被序列化复活。
评论
登录后即可评论