LiveThread 是 Session 对持久化生命周期的句柄;初始化只有在 Session 全部建立后才 commit,任何中途失败都由 InitGuard discard 已注册 writer,避免留下半初始化所有权。
本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:研究 create/resume 的提交边界、Guard 与 append 元数据观察顺序。输入:ThreadStore、Create/Resume 参数和 Session 初始化结果;状态所有者:LiveThreadInitGuard 直到 commit,之后由 Session 持有 LiveThread;成功结果:唯一活动 writer 或无残留的失败收尾。先把这些边界钉住,后面的顺序、失败和恢复才不会混成一句“持久化失败后重试”。
状态边界
| 问题 | 本节答案 |
|---|---|
| 输入 | ThreadStore、Create/Resume 参数和 Session 初始化结果 |
| 状态所有者 | LiveThreadInitGuard 直到 commit,之后由 Session 持有 LiveThread |
| 成功产物 | 唯一活动 writer 或无残留的失败收尾 |
| 研究范围 | 研究 create/resume 的提交边界、Guard 与 append 元数据观察顺序 |
正常路径:先看顺序点
这条路径可以压缩成五步:
- Store 注册 live recorder。
- LiveThread 被 InitGuard 包裹。
- Session 并行初始化其他服务。
- 初始历史写入/物化。
- 成功 commit;失败 discard。
图中的箭头不是“可能调用”的依赖图,而是源码中决定可见性和所有权转移的先后关系。前一步没有确认时,后一步不能替它作出更强的成功承诺。
源码机制拆解
Guard 表达未提交资源
LiveThread 建立后并不立刻交给最终 Session;InitGuard 暂时拥有它。commit 消耗 Guard 并返回 LiveThread,只有这一步之后才算 Session 初始化成功。
Drop 也执行补偿
错误分支若忘记显式 discard,Guard::drop 会在可用 Tokio runtime 上 spawn 清理。显式 discard 仍更可预测,Drop 是取消和早退的最后防线。
Resume 失败不能占住 writer
打开 Store 后还可能加载 history、补全 metadata 或构造 Session 服务。任何后续失败都必须移除 live recorder/跨进程锁,否则同一 Thread 永久报告 active writer conflict。
Append 的观察基于实际持久化子集
LiveThread 先把原始项交给 Store;MetadataSync 只观察按 policy 允许的项,再做 generation-aware update。瞬时 Event 不应意外刷新 preview 或 Git 元数据。
Inherited Paginated 子历史有额外原子边界
create_with_inherited_model_context 计算 subagent_history_start_ordinal,创建后立即持久化继承项;append 失败便 discard,不能留下声称前缀完整的空 child。
Python 风格伪代码
下面的伪代码只保留设计职责、状态和失败顺序;它不逐行翻译 Rust,也不借 Python 语法虚构源码中不存在的事务:
class LiveThreadInitGuard:
def __init__(self, live_thread):
self.live = live_thread
self.committed = False
def commit(self):
self.committed = True
return take(self.live)
async def discard(self):
if self.live:
await self.live.discard()
self.live = None
async def build_session(params):
live = await LiveThread.create_or_resume(params)
guard = LiveThreadInitGuard(live)
try:
services = await initialize_services_in_parallel()
await guard.live.record_initial_history()
await guard.live.ensure_materialized_if_required()
return Session(services, guard.commit())
except BaseException:
await guard.discard()
raise
阅读时要特别看三处:哪个对象拥有可变状态,哪一个 await 是可观察屏障,以及失败后保留的是已提交前缀、未提交后缀,还是完全独立的外部副作用。
失败、取消与恢复
| 故障点 | 已留下的状态 | 可观察结果 | 恢复责任 |
|---|---|---|---|
| Store 已注册、MCP 初始化失败 | writer 所有权已存在 | Session 未产生 | Guard discard recorder |
| 任务取消发生在 create 后 | 普通错误分支未运行 | 可能泄漏 live map | Drop 异步补偿 |
| 继承历史 append 失败 | child meta 可能已建 | 不能提交不完整 child | discard 并返回错误 |
| Metadata 更新慢于下一批 append | 旧 generation 后完成 | 可能覆盖新 preview | generation 检查拒绝 stale apply |
这里没有统一的“回滚一切”。内存状态、日志行、SQLite 投影、父子拓扑和工具造成的文件/网络变化分别有自己的提交点。恢复代码只能根据已经存在的权威证据继续,不能用较弱的投影替较强的事实背书。
必须保持的不变量
- 未 commit 的 LiveThread 必须可补偿清除
- 同一 Thread 不存在两个 LiveThread writer
- metadata 只观察 persisted item
- 子历史前缀失败时不能留下可用 child
这些不变量比“最终能 Resume”更严格:正常路径要成立,Writer 竞争、任务取消、坏尾行、投影落后和旧格式兼容时也必须成立。
设计取舍
Guard 增加一层资源包装和异步 Drop 复杂度,却把“创建 writer”与“提交完整 Session”之间的取消窗口显式建模。
源码可以直接证明字段、分支、调用顺序和测试期望;“为什么这样设计”的表述是基于这些事实作出的工程归纳,不把它包装成未公开的产品承诺。
Mini Codex 复刻
用 async context manager 作为 InitGuard;只有所有服务和初始写入成功才 release 所有权,否则调用 store.discard_thread。
复刻时先验证协议不变量,再补性能优化。一个能在故障注入下说明“留下了什么”的小实现,比一个只在正常路径调用 save() 的演示更接近真实 Runtime。
源码导航
- codex-rs/thread-store/src/live_thread.rs:LiveThread、InitGuard、create/resume/append/commit 补偿
- codex-rs/core/src/session/session.rs:Session 初始化期间持有 Guard 并最终 commit
相邻测试也很重要:
- codex-rs/core/src/session/tests.rs:Resume/Fork 初始化与历史安装
- codex-rs/thread-store/src/local/thread_history_materialization_tests.rs:LiveThread append 与持久化投影
本节结论
LiveThread 是 Session 对持久化生命周期的句柄;初始化只有在 Session 全部建立后才 commit,任何中途失败都由 InitGuard discard 已注册 writer,避免留下半初始化所有权。
评论
登录后即可评论