雨天小六

读懂 Codex(8.31):Archive、Delete 与被引用历史

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

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

Archive 是在 active/archived 集合之间移动 Rollout 并更新元数据;Delete 是不可恢复移除,必须先锁住生命周期、预检引用,并在批量场景区分内部引用与外部引用。

本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:对比 archive/unarchive/delete 的锁、路径和引用语义。输入:一个或一组 ThreadId、writer/lifecycle 状态和 ReferenceIndex;状态所有者:LocalThreadStore archive_thread/delete_thread;成功结果:移动后的可恢复 Rollout,或安全删除的文件与投影。先把这些边界钉住,后面的顺序、失败和恢复才不会混成一句“持久化失败后重试”。

状态边界

问题本节答案
输入一个或一组 ThreadId、writer/lifecycle 状态和 ReferenceIndex
状态所有者LocalThreadStore archive_thread/delete_thread
成功产物移动后的可恢复 Rollout,或安全删除的文件与投影
研究范围对比 archive/unarchive/delete 的锁、路径和引用语义

正常路径:先看顺序点

Archive、Delete 与被引用历史的正常路径时序图,展示解析精确 Thread 集合、按稳定顺序获取生命周期和 writer locks、检查 live/cross-process owner 与引用、执行 move 或 delete、best-effort/严格更新 State DB
图 8.31-1:解析精确 Thread 集合 → 按稳定顺序获取生命周期和 writer locks → 检查 live/cross-process owner 与引用 → 执行 move 或 delete → best-effort/严格更新 State DB。这张图标出本节的实际顺序点与状态所有者。

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

  1. 解析精确 Thread 集合。
  2. 按稳定顺序获取生命周期和 writer locks。
  3. 检查 live/cross-process owner 与引用。
  4. 执行 move 或 delete。
  5. best-effort/严格更新 State DB。

图中的箭头不是“可能调用”的依赖图,而是源码中决定可见性和所有权转移的先后关系。前一步没有确认时,后一步不能替它作出更强的成功承诺。

源码机制拆解

Archive 不等于删除

archive 把 sessions 下的 canonical path rename 到 archived_sessions,保留同一文件名与内容,并在 State DB 标记 archived_at/path。Unarchive 根据文件名日期恢复层级目录并 touch mtime。

活动 Writer 阻止管理操作

批量 archive 先锁 lifecycle,再锁每 Thread writer 并检查 live_recorders/cross-process locks。不能在 Recorder 仍追加时移动它的路径。

单删必须没有外部直接引用

Delete 构建 RolloutReferenceIndex,发现任何其他 child 的 history_base 指向 source 就返回 Conflict;否则 child 的逻辑历史会立即不可读。

批量删除先全量预检

删除 parent+child 时,child 对 parent 的引用属于 batch 内部,可允许;仍在 batch 外的 referrer 会在删除任何文件前阻止整个操作,避免只删一半才发现依赖。

路径删除与投影清理协调

成功预检和锁定后,Store 删除 State DB thread/history projection 与 active/archived plain/zst Rollout,并移除 live recorder。NotFound 的幂等语义与严格批量计数由接口区分。

Python 风格伪代码

下面的伪代码只保留设计职责、状态和失败顺序;它不逐行翻译 Rust,也不借 Python 语法虚构源码中不存在的事务:

async def archive_many(store, ids, extra_lock_ids=[]):
    ordered = sorted(unique(ids + extra_lock_ids))
    async with acquire_lifecycle_locks(ordered):
        async with acquire_writer_locks(ordered):
            require(no_live_or_external_writer(ordered))
            results = []
            for thread_id in ids:
                results.append(await move_to_archive_and_mark_db(thread_id))
            return results

async def delete_many(store, ids):
    deleting = set(ids)
    async with acquire_lifecycle_and_writer_locks(sorted(deleting)):
        refs = await build_complete_reference_index_or_fail()
        for source in deleting:
            external = refs.referrers(source) - deleting
            if external:
                raise Conflict(f'{source} is referenced by {external}')
        # preflight complete: no file has been deleted yet
        for thread_id in ids:
            await store.delete_projection_and_rollout(thread_id)

阅读时要特别看三处:哪个对象拥有可变状态,哪一个 await 是可观察屏障,以及失败后保留的是已提交前缀、未提交后缀,还是完全独立的外部副作用。

失败、取消与恢复

Archive、Delete 与被引用历史的失败路径图,区分Archive 时有 active writer、单删被引用 parent、批量中存在外部 referrer、Unarchive 文件名无日期
图 8.31-2:同一操作在不同故障点留下的状态不同,恢复责任必须回到拥有该状态的组件。
故障点已留下的状态可观察结果恢复责任
Archive 时有 active writer文件可能继续追加Conflict先正常 Shutdown Thread
单删被引用 parentchild 仍依赖前缀Conflict保留 parent 或一起删除 child
批量中存在外部 referrer依赖不在删除集合删除前整体拒绝扩展集合或取消
Unarchive 文件名无日期无法恢复 canonical 目录InvalidRequest保留 archive 并修复命名

这里没有统一的“回滚一切”。内存状态、日志行、SQLite 投影、父子拓扑和工具造成的文件/网络变化分别有自己的提交点。恢复代码只能根据已经存在的权威证据继续,不能用较弱的投影替较强的事实背书。

必须保持的不变量

  • Archive 保留 Rollout 内容和 ThreadId
  • 任何文件操作发生前先完成批量外部引用预检
  • 管理操作不与 active writer 并发
  • 删除后不能留下指向已删除物理前缀的 durable child

这些不变量比“最终能 Resume”更严格:正常路径要成立,Writer 竞争、任务取消、坏尾行、投影落后和旧格式兼容时也必须成立。

设计取舍

严格预检与全局元数据扫描让删除更慢,但删除是不可恢复操作,必须优先保护所有仍可读取的引用历史。

源码可以直接证明字段、分支、调用顺序和测试期望;“为什么这样设计”的表述是基于这些事实作出的工程归纳,不把它包装成未公开的产品承诺。

Mini Codex 复刻

archive 用 rename,delete 用 preflight-plan-then-execute;引用检查返回具体 referrers,批量集合先闭包验证再修改文件。

复刻时先验证协议不变量,再补性能优化。一个能在故障注入下说明“留下了什么”的小实现,比一个只在正常路径调用 save() 的演示更接近真实 Runtime。

源码导航

相邻测试也很重要:

本节结论

Archive 是在 active/archived 集合之间移动 Rollout 并更新元数据;Delete 是不可恢复移除,必须先锁住生命周期、预检引用,并在批量场景区分内部引用与外部引用。

阅读导航

上一节:8.30 · 下一节:8.32

评论


← 返回文章列表