雨天小六

读懂 Codex(8.28):Copied Fork 的历史筛选和新 SessionMeta

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

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

Copied Fork 不是原样复制父 Rollout:它先构造对 child 合适的模型历史,过滤 Reasoning/工具/旧通信和非最终 assistant,再由 child Create 路径写自己的 SessionMeta。

本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:研究 Agent Fork 的内容筛选、指令替换和 copied persistence,不讨论引用式冻结。输入:父模型上下文、FullHistory/LastNTurns、子角色配置与 destination history mode;状态所有者:AgentControl spawn + Session ForkPersistence::Copied;成功结果:拥有新 ThreadId/SessionMeta 的 child Rollout 和经过筛选的 Context History。先把这些边界钉住,后面的顺序、失败和恢复才不会混成一句“持久化失败后重试”。

状态边界

问题本节答案
输入父模型上下文、FullHistory/LastNTurns、子角色配置与 destination history mode
状态所有者AgentControl spawn + Session ForkPersistence::Copied
成功产物拥有新 ThreadId/SessionMeta 的 child Rollout 和经过筛选的 Context History
研究范围研究 Agent Fork 的内容筛选、指令替换和 copied persistence,不讨论引用式冻结

正常路径:先看顺序点

Copied Fork 的历史筛选和新 SessionMeta的正常路径时序图,展示父 Thread materialize+flush、加载 Legacy 全史或 Paginated latest model context、可选截取最后 N Turns、筛选 item 并替换子指令、创建 child Meta 后写 copied prefix/settings
图 8.28-1:父 Thread materialize+flush → 加载 Legacy 全史或 Paginated latest model context → 可选截取最后 N Turns → 筛选 item 并替换子指令 → 创建 child Meta 后写 copied prefix/settings。这张图标出本节的实际顺序点与状态所有者。

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

  1. 父 Thread materialize+flush。
  2. 加载 Legacy 全史或 Paginated latest model context。
  3. 可选截取最后 N Turns。
  4. 筛选 item 并替换子指令。
  5. 创建 child Meta 后写 copied prefix/settings。

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

源码机制拆解

Fork 前先固定父前缀

父 record path 可能异步排队,因此 spawn_forked_thread 先 ensure_rollout_materialized 和 flush,再从 Store 取历史,避免 child 漏掉刚完成的父消息。

只保留适合 child 的长期语义

普通 Message 保留 system/developer/user 与 phase=FinalAnswer 的 assistant;AdditionalTools、AgentMessage、Reasoning、Call/Output、Web/Image、旧 inter-agent communication 被丢弃。Compacted 与必要 Event/Meta 可保留。

截断 Fork 清除 diff 基线

只有 FullHistory 才可能保留 TurnContext/WorldState。LastNTurns 丢掉部分 prompt prefix,child 第一 Turn 必须重新注入上下文,不能沿用父 reference baseline。

父协作提示和指令需要消毒

V2 会过滤父 root/subagent usage hint;父 developer instruction fragment 被替换为 child 角色指令。Compacted.replacement_history 内部也执行同一 retain_mut 规则,不能只清洗顶层旧行。

新物理 Thread 先写 child SessionMeta

Forked history 中可能还含父 SessionMeta 作为历史记录,但 Create recorder 的首行是新 child meta,携带新的 ThreadId、parent/forked_from、source 与配置。Loader 永远以首行确认当前身份。

Python 风格伪代码

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

async def copied_fork(parent, options, child_config):
    await parent.ensure_materialized()
    await parent.flush()
    items = await load_model_context_for_mode(parent.id, parent.history_mode)

    if options.last_n_turns is not None:
        items = truncate_to_last_n_effective_turns(items, options.last_n_turns)
        preserve_baseline = False
    else:
        preserve_baseline = not newest_checkpoint_is_legacy(items)

    filtered = []
    for item in items:
        item = sanitize_compacted_and_top_level_messages(item, child_config)
        if keep_for_child(item, preserve_baseline):
            filtered.append(item)

    child = await create_new_thread(
        session_meta=new_child_meta(parent_id=parent.id, config=child_config),
        initial_history=filtered,
        fork_persistence=COPIED,
    )
    await child.persist(filtered + [child.effective_settings_event()])
    await child.ensure_materialized()
    return child

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

失败、取消与恢复

Copied Fork 的历史筛选和新 SessionMeta的失败路径图,区分Fork 前未 flush、只过滤顶层、漏 compact replacement、LastNTurns 保留 TurnContext baseline、父 SessionMeta 成为 child canonical
图 8.28-2:同一操作在不同故障点留下的状态不同,恢复责任必须回到拥有该状态的组件。
故障点已留下的状态可观察结果恢复责任
Fork 前未 flush父尾部只在内存队列child 历史截断过早先 materialize+flush
只过滤顶层、漏 compact replacement旧工具/父提示藏在 checkpointchild 仍继承不合适内容两层使用同一 sanitizer
LastNTurns 保留 TurnContext baseline前缀已被裁掉首 Turn 只发 diff截断模式删除 TurnContext/WorldState
父 SessionMeta 成为 child canonical身份/path 不匹配恢复错误 ThreadCreate 首行写新 child meta

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

必须保持的不变量

  • child ThreadId 与 canonical SessionMeta 全新
  • Copied Fork 不继承工具执行中间态
  • FullHistory 才可能保留 reference baseline
  • 父角色指令与 usage hint 不原样泄漏到 child

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

设计取舍

复制使 child Rollout 自包含、删除简单,却复制历史字节;筛选还意味着它不是父日志的取证副本,而是为 child 推理构造的语义快照。

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

Mini Codex 复刻

ForkFilter 作为纯函数输入 parent items + child role + mode,输出 child history;ChildRecorder 永远自行创建 meta header。

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

源码导航

相邻测试也很重要:

本节结论

Copied Fork 不是原样复制父 Rollout:它先构造对 child 合适的模型历史,过滤 Reasoning/工具/旧通信和非最终 assistant,再由 child Create 路径写自己的 SessionMeta。

阅读导航

上一节:8.27 · 下一节:8.29

评论


← 返回文章列表