pref(checkpoint):修复 Checkpoint 恢复与 Run 级 Diff 完整性#572
pref(checkpoint):修复 Checkpoint 恢复与 Run 级 Diff 完整性#572phantom5099 wants to merge 3 commits into1024XEngineer:mainfrom
Conversation
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
|
/review 检查一遍还有没有存在checkpoints创建出错、run级diff存在遗漏或是参杂外部修改、代码回退出现遗漏等问题 |
| } | ||
| afterContent, afterIsDir, afterExists, degraded := s.contentAfterLastVersionLocked(hash, vr.maxV, display) | ||
| if degraded { | ||
| if info, err := os.Stat(display); err == nil && info.ModTime().After(runEndTime) { |
There was a problem hiding this comment.
RunAggregateDiff 这里仍然会把 run 结束之后发生的删除 误算进来。contentAfterLastVersionLocked 退化到 workdir 后,如果文件已经被后续操作删掉,os.Stat(display) 会直接报 ENOENT,这段保护逻辑就失效了,随后 afterExists=false 会把它分类成“本次 run 删除了该文件”。也就是说,这次改动虽然过滤了“run 后修改但文件仍存在”的场景,却没有过滤“run 后删除”的场景,scope=run 仍然会被后续工作区变更污染。
| if err != nil { | ||
| return fmt.Errorf("per-edit: read bin v%d: %w", nextVersion, err) | ||
|
|
||
| if toIsDir { |
There was a problem hiding this comment.
Restore 现在仍然没有处理文件/目录类型切换。若当前 display 是文件而目标是目录,os.MkdirAll(display, ...) 会失败;反过来如果当前是目录而目标是文件,后面的 writeFileAtomic(..., display) 也会因为目标路径已被目录占用而失败。这个 PR 强化了目录恢复语义,但这里缺少“类型不一致时先移除旧节点”的处理,历史里出现 file<->dir 变更时 restore 还是会中断。
| } | ||
| perEditID := checkpoint.PerEditCheckpointIDFromRef(r.CodeCheckpointRef) | ||
| perEditIDs = append(perEditIDs, perEditID) | ||
| if firstPerEditCheckpointID == "" { |
There was a problem hiding this comment.
这里把第一个遍历到的记录当成 firstPerEditCheckpointID,但 ListCheckpoints 在 SQLite 实现里是按 created_at_ms DESC 返回的。所以线上路径会把 run 中最新的 checkpoint 填进结果,而不是变量名、测试期望和接口语义里的“第一个 checkpoint”。当前测试里的 spy 恰好按升序喂数据,所以看不出来;真实存储下这个字段会反过来。
问题
1. 恢复后新文件残留
Agent 在运行过程中创建了新文件,restore 到更早的 checkpoint 时,新文件未被删除。根本原因是两层:
Finalize只写本 turn 的pending子集。当某 turn 没有文件修改时,生成CodeCheckpointRef=""的 session-only checkpoint;restoreCheckpointCore遇到非 per-edit ref 时整段跳过代码恢复路径。Restore,当无 guard checkpoint 时,hashSet仅包含目标 checkpoint 自身的FileVersions,不包含后续创建的文件 -> for 循环遍历不到 -> 文件残留。2. Restore 语义模糊
旧
Restore使用 v_next 单快照推断:对 checkpoint 中记录的版本号 v_A,查找其后的下一个版本作为"checkpoint 时刻状态"。这种方式对 post-delete 版本、删除后重新创建、目录操作等场景需要复杂的启发式判断,容易出错。3. move 操作源文件残留
filesystem_move_file执行后未调用CapturePostDelete标记源路径已删除,restore 时源文件错误地回到原地。4. Run 级 diff 外部污染
CheckpointDiff的scope=run未实现。RunAggregateDiff 的 after-side 退化到readWorkdirContent,会混入 run 结束后外部进程或用户手动做的修改。方案
1. Finalize 全量快照 + FinalizePending 保护 Guard
Finalize改为遍历pathToVersions全量索引,为每个 hash 取最新非 PostDelete 版本号,生成完整快照。这样每个 checkpoint 都有完整的CodeCheckpointRef,restore 路径始终生效。FinalizePending,仅写当前 turn 的pending子集,专用于 pre-restore guard checkpoint。避免 guard 包含多轮前的旧 pre-write 内容,导致RestoreExact在 undo 时写错状态。2. 双快照对比恢复
Restore(targetID, guardID)改为对比两个明确的状态快照:contentAtCheckpointLocked+ v_next 语义)os.RemoveAll;目标为目录则MkdirAll;否则写回内容消除了 v_next 单快照推断的歧义。
3. 无 guard 时合并全量索引
当用户主动 restore 到任意历史 checkpoint(无 guard)时,
hashSet额外合并pathToVersions中所有已知文件。这样新创建的文件被纳入遍历 ->toExists=false-> 正确删除。4. IsPostDelete 标记区分
CapturePostDelete写入IsPostDelete=true的 meta。Finalize构建全量快照时跳过这些标记版本,确保 checkpoint 记录的是文件内容的最近版本号。pre-create 版本(Existed=false, IsPostDelete=false)仍然保留,使新建文件在 checkpoint 中可见。5. RunAggregateDiff + RunID 过滤
RunAggregateDiff:对每个文件取最小版本作为 before baseline,取最大版本号的 v_next 作为 after-side(通过contentAfterLastVersionLocked),避免 run 结束后外部修改污染。ListCheckpointOpts增加RunID字段,runDiff按run_id精确收集 checkpoint,替代原来Limit: 500的粗暴方式。CheckpointDiffInput增加Scope和RunID字段。6. move 操作补录 CapturePostDelete
toolexec.go中为filesystem_move_file添加CapturePostDelete(touchedPaths[0]),标记源文件已删除。修改范围
internal/checkpoint/per_edit_snapshot.goFinalize全量、FinalizePending、Restore双快照、RestoreExact、RunAggregateDiff、contentAfterLastVersionLockedinternal/checkpoint/per_edit_snapshot_test.goRestore调用更新为三参数签名)internal/checkpoint/checkpoint_manager.goListCheckpointOpts增加RunIDinternal/runtime/checkpoint_restore.gorestoreCheckpointCore;guard 使用FinalizePending;Restore双参数调用;runDiff实现internal/runtime/checkpoint_gate.golastEndOfTurnCheckpointID记录;findPreviousEndOfTurnCheckpoint辅助方法internal/runtime/toolexec.goCapturePostDeletefor move;bash side effect 路径记录internal/repository/fingerprint.gocheckpoint包迁移ScanWorkdir/DiffFingerprintsdocs/reference/gateway-rpc-api.mdCheckpointDiffInput增加Scope/RunID文档预期收益
FinalizePending精确固化 restore 前状态,undo restore行为一致。scope=run模式可展示一次完整 run 的端到端代码变更,不被外部修改干扰。TUI/GUI 接入方式
GUI(Web)已接入
CheckpointInlineMark组件内联展示每个 checkpoint,支持 Restore 和 Undo Restore 操作,通过gatewayAPI.restoreCheckpoint/gatewayAPI.undoRestoreRPC 调用。eventBridge消费CheckpointCreated/CheckpointRestored/CheckpointUndoRestore事件,更新 insight store 和 UI toast 提示。gatewayAPI.checkpointDiff展示 checkpoint 间差异(目前通过CheckpointDiffParams传session_id+checkpoint_id)。GUI 待接入点
CheckpointDiffParams接口需扩展scope和run_id字段,即可接入scope=run的 run 级聚合 diff。接入后可在 Run 结束时自动展示本次 run 的端到端代码变更摘要。EventRunDiffSummary已定义但未在 eventBridge 中消费,待接入后可在消息流中内联展示 run 级 diff 卡片。TUI 接入
EventCheckpointCreated/EventCheckpointRestored/EventCheckpointUndoRestore事件(remote_runtime_adapter转发)。runDiff已暴露为CheckpointDiffRPC 方法,TUI 侧扩展参数即可调用。