雨天小六

读懂 Codex(8.30):Rollout Lineage 的祖先拼接和删除约束

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

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

一个 Paginated child 的逻辑历史是有序的物理 segment 列表;Lineage resolver 沿 history_base 追溯、验证每个 cutoff 并反转顺序,所有 Reader 只能消费这些有界段。

本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:研究 lineage 构造、循环/身份/边界校验及其对读取和删除的影响。输入:requested child ThreadId 与每个 SessionMeta.history_base;状态所有者:RolloutLineage;成功结果:ancestor→child 排列的 RolloutLineageSegment[]。先把这些边界钉住,后面的顺序、失败和恢复才不会混成一句“持久化失败后重试”。

状态边界

问题本节答案
输入requested child ThreadId 与每个 SessionMeta.history_base
状态所有者RolloutLineage
成功产物ancestor→child 排列的 RolloutLineageSegment[]
研究范围研究 lineage 构造、循环/身份/边界校验及其对读取和删除的影响

正常路径:先看顺序点

Rollout Lineage 的祖先拼接和删除约束的正常路径时序图,展示从 child 开始读取 canonical meta、记录当前 segment 的 frozen end、跟随 history_base.thread_id 到祖先、检测 cycle/identity/mode/bounds、反转为祖先到 child 顺序
图 8.30-1:从 child 开始读取 canonical meta → 记录当前 segment 的 frozen end → 跟随 history_base.thread_id 到祖先 → 检测 cycle/identity/mode/bounds → 反转为祖先到 child 顺序。这张图标出本节的实际顺序点与状态所有者。

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

  1. 从 child 开始读取 canonical meta。
  2. 记录当前 segment 的 frozen end。
  3. 跟随 history_base.thread_id 到祖先。
  4. 检测 cycle/identity/mode/bounds。
  5. 反转为祖先到 child 顺序。

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

源码机制拆解

Segment 同时有 start 与可选 end

start_ordinal 由该物理文件自己的 history_base.end_ordinal_exclusive+1 推出,无 base 时从 1 开始;end 来自后代指向它的 HistoryPosition,child 当前段通常没有 end。

每层必须验证物理身份

resolved path 的首个 SessionMeta.id 必须等于正在追踪的 ThreadId,history_mode 必须是 Paginated。路径同名或 SQLite 指错不能静默拼进历史。

seen set 阻止引用环

A→B→A 或 self cycle 会返回 invalid paginated history lineage。没有这个检查,恢复器会无限追溯或重复同一物理段。

Cutoff 不能包含源 Meta 或越过文件

end_ordinal_exclusive=0 会把祖先 SessionMeta 当继承正文;end_byte_offset 大于 file len 指向不存在内容。两者在建立 segment 时立即拒绝。

删除约束来自物理引用而非逻辑可见项

即便 child 只继承祖先很短前缀,该 source 文件仍必须存在;单独删除 ancestor 会被 ReferenceIndex 阻止。只有把所有 referrer 一起纳入 batch 才能安全清除。

Python 风格伪代码

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

async def resolve_lineage(requested_id, representation='existing'):
    reverse_segments = []
    seen = set()
    thread_id, frozen_end = requested_id, None

    while True:
        if thread_id in seen:
            raise InvalidLineage('cycle detected')
        seen.add(thread_id)

        path = await resolve_rollout_path(thread_id, include_archived=True)
        if representation == 'plain-for-reference':
            path = await materialize_under_writer_lock(path)
        meta = await read_canonical_meta(path)
        require(meta.id == thread_id and meta.history_mode == PAGINATED)
        if frozen_end:
            validate_cutoff(path, frozen_end)

        start = meta.history_base.end_ordinal_exclusive + 1 if meta.history_base else 1
        reverse_segments.append(Segment(thread_id, path, start, frozen_end))
        if meta.history_base is None:
            break
        thread_id, frozen_end = meta.history_base.thread_id, meta.history_base

    return Lineage(list(reversed(reverse_segments)))

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

失败、取消与恢复

Rollout Lineage 的祖先拼接和删除约束的失败路径图,区分history_base 形成环、源文件 Meta.id 不符、祖先不是 Paginated、单删被 child 引用的祖先
图 8.30-2:同一操作在不同故障点留下的状态不同,恢复责任必须回到拥有该状态的组件。
故障点已留下的状态可观察结果恢复责任
history_base 形成环seen 已含 ThreadIdInvalidRequest停止追溯
源文件 Meta.id 不符路径指向别的 Thread拒绝 lineage修复索引/路径而非拼接
祖先不是 Paginatedposition 语义不兼容拒绝 lineage使用 copied 兼容路径
单删被 child 引用的祖先child segment 悬空Conflict保留祖先或批量删除整个引用集合

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

必须保持的不变量

  • Lineage 中 ThreadId 不重复
  • segment 顺序从最老祖先到 requested child
  • 每个 frozen end 在对应物理文件边界内
  • Reader 不读取 segment.end 之后的后来追加

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

设计取舍

指针链减少复制,却把一个逻辑 Thread 的可用性扩展为多个物理文件的共同可用性,删除和归档必须理解依赖图。

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

Mini Codex 复刻

Lineage 是唯一跟随 parent pointer 的模块;分页、搜索、模型上下文都只接收已验证 Segment[],不各自重新追指针。

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

源码导航

相邻测试也很重要:

本节结论

一个 Paginated child 的逻辑历史是有序的物理 segment 列表;Lineage resolver 沿 history_base 追溯、验证每个 cutoff 并反转顺序,所有 Reader 只能消费这些有界段。

阅读导航

上一节:8.29 · 下一节:8.31

评论


← 返回文章列表