Skip to content

Checkpoint Run-Touched 回退与 Diff 语义收敛 #607

@phantom5099

Description

@phantom5099

状态: Draft
组件: Checkpoint 文件快照层、Runtime Restore / CheckpointDiff / RunDiffSummary、Web/TUI 代码变更展示
日期: 2026-05-10
相关技术: Per-Edit Snapshot, Touched Files Rollback, Checkpoint Restore, Run-Scoped Diff, v_next, Guard Checkpoint


1. 摘要

本 RFC 针对 NeoCode Checkpoint 当前语义过重的问题提出收敛方案:将 checkpoint 文件回退与 run 级 diff 从“历史 tracked-files checkpoint 状态”收敛为 Run-Touched agent 文件回退语义

当前实现中,Finalize 会把 pathToVersions 中所有历史已追踪文件写入 checkpoint meta。这里的“全量”不是全 workspace snapshot,而是 全 tracked files snapshot:它不会扫描整个工作区,但会把所有曾被 checkpoint 系统追踪过的文件都带入后续 checkpoint。这让系统更方便恢复到多轮以前的历史 tracked 状态,但也让语义超出 Claude-like 的 touched-file rollback:未被当前 run agent 触碰的历史文件,可能进入 restore / diff 的候选范围。

本方案选择更明确的产品语义:

  1. 回退范围:只处理当前 run 中 agent 实际触碰过的文件。
  2. Diff 范围:只展示当前 run 中 agent touched files 的净变化。
  3. 用户外部修改保护:未被 agent 触碰的用户修改、删除、创建不被恢复、不进入 run diff。
  4. 保留 per-edit 核心:继续保留 pre-write capture、post-delete、版本链、文件存在性、目录和 mode 元信息。
  5. 删除全 tracked 扩张语义:停止把 pathToVersions 作为 checkpoint/restore/diff 范围扩张来源。

2. 背景与问题

当前 checkpoint 系统最初的核心能力是 per-edit capture:工具准备写文件前,先捕获该文件的旧状态。这个模型天然适合 agent-scoped touched-file rollback:agent 改过什么,就能恢复什么;agent 没碰过什么,就不动什么。

后续为了修复恢复到历史 checkpoint 时的新文件残留、guard 路径遗漏、run diff 被外部修改污染等问题,引入了更强的 tracked-files checkpoint 语义:

  1. Finalize 从 pending-only 改成遍历 pathToVersions,每个 checkpoint 记录所有历史已追踪文件的最新版本。
  2. Restore 在部分路径中合并 target / guard / related checkpoint,甚至曾经合并全局 pathToVersions,以避免遗漏历史 tracked 文件。
  3. FinalizeWithExactStateDiffCheckpointsToCheckpoint 进一步让 checkpoint diff 更接近“目标 checkpoint 精确结束态查询”。
  4. run rollback baseline、workspace drift、run-end state 记录被加入,用来支撑更复杂的 diff 和 restore 展示链路。

这些增强不是全 workspace snapshot,但已经超出了“只处理本 run agent touched files”的最小语义。

2.1 当前语义与 Claude-like 语义不完全一致

如果用户理解的 checkpoint 是:

恢复 agent 在本轮 run 中修改过的文件。

那么当前全 tracked-files checkpoint 会显得过重。因为它关注的不只是本 run touched files,而是所有历史已追踪文件在 checkpoint 之间的状态。

2.2 用户外部修改边界变复杂

示例:工作区原本有 A/B/C/D/E

  1. 用户手动删除 B
  2. 用户手动修改 C
  3. agent 在下一轮修改 C

在 run-touched 语义下:

  • agent 首次触碰 C 前捕获的是“用户已修改后的 C”。
  • rollback 只恢复 C 到该状态。
  • B 没被 agent 触碰,所以不会被恢复。

这是符合用户直觉的:checkpoint 不应撤销用户自己对 B 的删除。

2.3 多轮历史恢复能力与 run-touched 语义存在取舍

全 tracked-files checkpoint 的价值是:每个 checkpoint 更像一个“历史已追踪文件集合的状态点”,恢复到多轮以前时,系统有更多上下文去处理历史 tracked 文件。

但 run-touched 语义要求更窄:

  • 当前 run 没碰的文件,不应被 restore/diff 处理。
  • checkpoint 不表达整个 tracked workspace 的历史状态。
  • diff 不回答“世界最后变成什么”,只回答“agent 对 touched files 造成了什么净变化”。

3. 典型用户场景

场景 1:用户手动删除未 touched 文件,restore 不应恢复

当前风险: 旧 tracked 文件可能被纳入 restore / diff 候选,系统需要大量 guard、related checkpoint、drift 逻辑避免误处理。

新行为: 如果本 run agent 没触碰 B,restore run 时不处理 B。用户删除的 B 保持删除状态。

场景 2:用户先修改文件,agent 后修改同一文件

旧行为目标: 系统试图把 checkpoint 表达为历史 tracked 状态,需要推导目标 checkpoint 的结束态。

新行为: agent 修改 C 前捕获 C_before_agent。restore 时写回 C_before_agent。用户在 agent 前做的修改被保留。

场景 3:agent 创建新文件后 restore

新行为: agent 创建 new.go 前捕获 Existed=false。restore run 时删除 new.go

场景 4:agent 删除文件后 restore

新行为: agent 删除 old.go 前捕获内容、mode 和文件类型。删除后补录 post-delete。restore run 时恢复 old.go 的原内容和权限。

场景 5:run diff 只展示 agent touched files

新行为: run diff 的文件集合来自本 run per-edit checkpoints 的 touched hashes。未 touched 文件的用户修改、删除、创建都不进入 diff。


4. 设计目标

本方案要求同时满足:

  1. checkpoint restore 默认遵循 agent-touched 语义,不恢复未 touched 文件。
  2. run diff 只展示 agent 在当前 run 中触碰过的文件净变化。
  3. 用户外部修改默认受保护:未 touched 文件不进入 affected set。
  4. 保留 per-edit 版本链和 pre-write capture 的核心优势。
  5. 保留 undo restore,但 undo 的范围也必须是本次 restore affected set。
  6. 删除或停止使用全 tracked-files checkpoint 作为范围扩张机制。
  7. 保持公开 RPC wire shape 尽量稳定,只改变语义和字段填充。

5. 非目标

本 RFC 不处理:

  1. 不实现全 workspace snapshot。
  2. 不尝试恢复用户在 run 外或 run 中对未 touched 文件的修改。
  3. 不保证任意历史 checkpoint 能表达所有历史 tracked files 的完整状态。
  4. 不删除 pathToVersions 内部索引;它仍用于版本编号、v_next 查询、索引重建。
  5. 不重做 Web/TUI 的视觉设计,只调整它们消费 diff/restore 语义的预期。
  6. 不改变 session checkpoint 的消息/head 恢复语义。

6. 核心设计

6.1 Checkpoint 范围:从 tracked-files full snapshot 收敛为 run touched set

当前 Finalize 的“全量”含义是:遍历 pathToVersions,把所有历史已追踪文件写入 checkpoint meta。

新设计将 checkpoint 文件范围收敛为 affected set:

type RunTouchedSet struct {
    RunID  string
    Hashes map[string]struct{}
}

affected set 来源:

  1. 本 run 中所有 CapturePreWrite 成功的路径。
  2. 本 run 中所有 CapturePostDelete 记录的路径。
  3. 由文件工具明确报告的 moved/copied/deleted source/target 路径。

pathToVersions 仍保存全局版本链,但不能作为 restore/diff 范围来源。

这样设计的原因是:范围来源必须是“agent 实际触碰过什么”,而不是“checkpoint 系统历史上知道什么”。这能避免历史 tracked 文件污染当前 run 的 restore/diff。

6.2 Finalize:恢复为 pending / touched-only

Finalize 不再遍历 pathToVersions

新行为:

  • Finalize(checkpointID) 只写当前 pending hashes。
  • FinalizeWithExactState(checkpointID) 只为当前 pending hashes 写 pre-write baseline 和 exact end state。
  • 如果 pending 为空,不创建 code checkpoint。
  • Reset() 清空 pending,避免跨 turn 污染。

保留 FinalizePending 的概念,但语义调整为“为指定 affected set 创建 restore guard”,不能再依赖全 tracked checkpoint fallback。

这样设计的原因是:pending 是本轮工具执行确实捕获到的 touched files。用 pending 作为 checkpoint 范围,可以保证 checkpoint meta 不携带历史无关文件。

6.3 Run restore:只恢复本 run affected files

restore 到 run 起点时:

  1. 收集目标 run 的 affected hashes。
  2. 对每个 hash,读取该 run 内首次触碰前的版本作为 before state。
  3. 写回 before state:
    • Existed=false → 删除路径。
    • IsDir=true → 创建目录并恢复 mode。
    • 文件存在 → 写回内容和 mode。
  4. 未出现在 affected set 的文件一律不处理。

restore 到 run 内某个 checkpoint 时:

  1. affected hashes = 该 run 中从目标 checkpoint 之后到当前仍需要撤销的 touched hashes。
  2. 对目标 checkpoint 前已经 touched 的文件,恢复到目标 checkpoint 的 exact end state。
  3. 对目标 checkpoint 后才首次 touched 的文件,恢复到首次触碰前状态。
  4. 未 touched 文件不处理。

这样设计的原因是:restore 的目标不是重建工作区世界态,而是撤销 agent 对 touched files 的影响。

6.4 Undo restore:对 affected set 做 restore 前精确快照

当前 guard 逻辑依赖 FinalizePending 和 fallback checkpoint,语义复杂。

新设计:执行 restore 前,先对本次 restore 的 affected set 做精确快照:

guardID := NewCheckpointID()
CaptureExactState(guardID, affectedHashes)

undo restore 时:

  • 使用 RestoreExact(guardID)
  • 只恢复 guard 中记录的 affected files。
  • 不依赖最近 end-of-turn checkpoint fallback。

这样设计的原因是:undo restore 应恢复“restore 前被本次 restore 影响的文件状态”,不需要扩大到历史 tracked files。

6.5 Run diff:before = 首次触碰前,after = run-end touched state

CheckpointDiff(scope=run) 新语义:

  1. 收集该 run 的 affected hashes。
  2. before = 每个 hash 在该 run 内首次触碰前的 version。
  3. after = run 结束时该 hash 的 exact state。
  4. before 与 after 相同则不展示。
  5. 未 touched 文件不参与。

RunEndCapture 保留,但只针对 affected hashes。

这样设计的原因是:run diff 应回答“agent 对自己触碰过的文件造成了什么净变化”,不应回答“整个工作区最后世界态是什么”。

6.6 删除跨 run tracked-file 过滤与 drift baseline

停止使用这些逻辑作为 run diff 正确性的基础:

  1. prevFileVersions 过滤历史文件。
  2. run rollback baseline 持久化。
  3. workspace drift rebase。
  4. 通过全 tracked checkpoint 推导 target checkpoint diff。

这些逻辑是为“历史 tracked checkpoint 状态”服务的。run-touched 语义下,affected set 已经把范围限定到当前 run agent touched files,不需要再靠跨 run baseline 修正。

6.7 保留 v_next,但降低其职责

v_next 仍可用于:

  1. 读取某个 pre-write version 之后的状态。
  2. 兼容旧 checkpoint 缺少 exact end state 的场景。
  3. 测试和历史数据迁移时的 fallback。

但新路径优先使用 exact state:

  • before 使用首次 pre-write capture。
  • after 使用 run-end exact state 或 target checkpoint exact state。
  • v_next 只作为兼容 fallback,不再作为主要语义来源。

7. 与现有模块的关系

checkpoint

  • Finalize 改回 pending-only。
  • FinalizeWithExactState 仅处理 pending / affected hashes。
  • Restore 移除 pathToVersions 范围扩张和全 tracked 语义。
  • RunAggregateDiff 改为 run touched diff,删除 prevFileVersions 过滤和全 tracked 相关分支。
  • DiffCheckpointsToCheckpoint 若保留,只能基于传入 run affected hashes;否则删除并由 run-touched diff 替代。

runtime

  • createEndOfTurnCheckpoint 继续只在有 workspace write 时创建 checkpoint,但 checkpoint 内容必须是本 turn pending。
  • restoreCheckpointCore 先计算 affected set,再创建 exact guard,再 restore。
  • CheckpointDiff(scope=run) 调用 run-touched diff,不再围绕 target checkpoint 的全 tracked 精确态查询。
  • EventRunDiffSummary 使用同一套 run-touched diff。
  • 删除 run rollback baseline / workspace drift 对 run diff 的核心依赖。

session

  • checkpoint record / session checkpoint 结构保持不变。
  • CodeCheckpointRef 仍指向 per-edit checkpoint。
  • session messages/head restore 保持原行为。

gateway

  • CheckpointDiffInput.Scope / RunID wire shape 保持。
  • CheckpointDiffResult 尽量保持字段兼容。
  • 与 drift/baseline 相关字段若存在,停止填充或返回空值,并更新文档说明。

web / tui

  • diff 展示语义改为 agent touched files。
  • restore / undo restore 文案应避免暗示“恢复整个工作区”。
  • run diff summary 只展示 agent touched files 的净变化。

docs

  • 更新 pr-checkpoint-restore-fixes.md,删除“Finalize 全量快照”作为目标设计的表述。
  • 更新 award/checkpoint 材料:说明 phantom5099 原始核心是 per-edit touched-file 版本链;全 tracked checkpoint 是后续为历史 restore 完整性引入的增强,不作为最终产品语义。

8. 行为变化

场景 当前行为 新行为
restore 到多轮以前 checkpoint 尝试恢复历史已追踪文件状态 只恢复目标 run / affected set touched files
run diff 可能包含历史 tracked files 的过滤/推导结果 只包含本 run agent touched files
未 touched 文件被用户删除 可能需要 drift/guard 逻辑避免误恢复 永远不处理
checkpoint meta 可包含所有历史 tracked files 只包含本 turn/run touched files
undo restore guard/fallback/related checkpoint 复杂链路 exact guard 记录本次 affected set
多轮历史恢复能力 更强,能表达历史 tracked 状态 减弱,只撤销 agent touched files

9. 测试场景

  1. 用户删除未 touched 文件 B,agent 修改 C;restore run 后只恢复 C,不恢复 B
  2. 用户先修改 C,agent 后修改 C;restore 后 C 回到用户修改后的状态。
  3. agent 创建 new.go;restore 后删除 new.go
  4. agent 删除 old.go;restore 后恢复 old.go 的内容和 mode。
  5. agent 移动文件;restore 后源路径和目标路径回到 agent 首次触碰前状态。
  6. 同一 run 中 cp1 修改 Acp2 修改 B;restore 到 cp1 时撤销 B,并使 A 回到 cp1 结束态。
  7. restore 后 undo restore 能恢复 restore 前 affected files。
  8. run diff 只展示本 run touched files。
  9. 未 touched 文件的用户修改、删除、创建不进入 run diff。
  10. 文件在 run 内改了又改回原值,run diff 为空。
  11. agent 创建后又删除 transient file,run diff 为空或不展示该文件。
  12. 旧 checkpoint 缺少 exact state 时,v_next fallback 仍可兼容读取。
  13. CheckpointDiff(scope=run) 缺少 run_id 时仍返回参数错误。
  14. Web/TUI 对 CheckpointDiffResult 的解析测试保持通过。

10. 假设与默认决策

  1. 产品语义选择 Run-Touched:只回退和展示当前 run 中 agent 实际触碰过的文件。
  2. NeoCode 不再追求 checkpoint 表达所有历史 tracked files 的完整状态。
  3. 不实现全 workspace snapshot。
  4. pathToVersions 保留为内部索引,但不能作为 restore/diff 范围扩张来源。
  5. 新 checkpoint 只保证 agent touched files 的 rollback/diff 正确性。
  6. 旧 checkpoint 数据需要兼容读取,但可以降级为 best-effort v_next fallback。
  7. 与 drift/baseline 相关的公开字段如果已经存在,优先保留 wire shape,停止填充复杂语义。

11. 一句话结论

Checkpoint 当前的全 tracked-files checkpoint 设计并不是全 workspace snapshot,但它确实超出了“只回退 agent 本 run touched files”的 Claude-like 语义。新方案将 checkpoint restore 和 run diff 收敛为 Run-Touched agent 文件回退模型:agent 碰过什么,就记录什么、展示什么、恢复什么;agent 没碰过的用户修改不进入系统处理范围。这样会牺牲恢复到多轮以前历史 tracked 状态的能力,但换来更清晰、更安全、更符合用户直觉的 rollback / diff 语义。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions