雨天小六

读懂 Codex(8.20):Resume 怎样创建新 Session 而不是还原旧对象

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

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

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 分支与初始化提交,不展开历史重放算法

正常路径:先看顺序点

Resume 怎样创建新 Session 而不是还原旧对象的正常路径时序图,展示按 ThreadId 定位 StoredThread、并行建立新服务和持久化句柄、LiveThread 获取新的 writer 所有权、加载历史并重建内存状态、InitGuard commit 新 Session
图 8.20-1:按 ThreadId 定位 StoredThread → 并行建立新服务和持久化句柄 → LiveThread 获取新的 writer 所有权 → 加载历史并重建内存状态 → InitGuard commit 新 Session。这张图标出本节的实际顺序点与状态所有者。

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

  1. 按 ThreadId 定位 StoredThread。
  2. 并行建立新服务和持久化句柄。
  3. LiveThread 获取新的 writer 所有权。
  4. 加载历史并重建内存状态。
  5. 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 是可观察屏障,以及失败后保留的是已提交前缀、未提交后缀,还是完全独立的外部副作用。

失败、取消与恢复

Resume 怎样创建新 Session 而不是还原旧对象的失败路径图,区分旧 Session 正在另一进程写、历史加载失败、当前模型不同、把旧任务状态当可恢复
图 8.20-2:同一操作在不同故障点留下的状态不同,恢复责任必须回到拥有该状态的组件。
故障点已留下的状态可观察结果恢复责任
旧 Session 正在另一进程写跨进程 lock 被占用Resume Conflict不能克隆活动 writer
历史加载失败新服务可能已创建Session 不 commitGuard 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。

源码导航

相邻测试也很重要:

本节结论

Resume 保留 Thread 的 durable identity,却重新创建任务、channel、服务连接、锁和 Session 状态;旧进程内对象从不被序列化复活。

阅读导航

上一节:8.19 · 下一节:8.21

评论


← 返回文章列表