feat(checkpoint): 会话级文件改动检查点与代码回退(P1) - #504
Conversation
在 fs_write_text / fs_edit_text / fs_delete 落盘前原子捕获前像, 存储于 ~/.liveagent/checkpoints/(blob + 追加式 index.jsonl); 新增 checkpoint_list / checkpoint_diff_stats / checkpoint_rewind_code 三个命令,桌面端顶栏提供按轮回退入口(diff 预览 + 确认)。 - 捕获失败仅记日志,不阻断文件写入 - Cron / WebUI 文件管理器等非对话场景不捕获 - Bash 产生的改动不在范围内(UI 明示);目录删除记不可恢复标记 - 索引追加式不截断,回退后再回退语义自洽 Closes Stack-Cairn#503
|
PR governance checks passed. Awaiting human review. |
yovinchen
left a comment
There was a problem hiding this comment.
整体方向是对的:把 checkpoint 捕获放进 fs_write_text / fs_edit_text / fs_delete 的实际落盘路径,使用前像 blob 与追加索引,不依赖 Git,并在 UI 中明确 Shell 不受覆盖,这些都适合作为 P1 的实现基础。现有单测和 diff 预览也比较完整。
不过结合 Claude Code 的官方 rewind 语义和 LiveAgent 当前的 Worktree、Root Grant、并发会话模型,我认为目前仍有两个阻断合并的问题,以及几项需要明确处理或拆分 follow-up 的问题。
必须修复
1. Rewind 未重新校验路径,可能越过工作区边界写文件
CheckpointRecord 保存绝对路径,rewind_at 恢复时直接通过 PathBuf::from(&record.path) 执行 fs::write / remove_file。这里没有重新经过 Workspace Root、Root Grant 和 scoped path resolver,也没有检查路径当前是否已经变成 symlink/hardlink。
例如:
- Agent 编辑
workspace/a.txt,checkpoint 保存该路径。 - 后续 Bash 将
a.txt替换为指向工作区外文件的符号链接。 - 用户执行 rewind。
fs::write(workspace/a.txt, ...)会跟随链接,覆盖工作区外的文件。
这不是只在未来开放 WebUI 后才存在的问题;当前桌面端配合 Bash 或外部文件变动即可触发。Claude Code 明确跳过 symlink/hardlink restore,正是为了避免这类恢复边界问题。
建议:
- Checkpoint 保存
root identity + relative path,不要把绝对路径作为恢复授权依据。 - 恢复时重新经过当前 Root Grant/path resolver。
- 使用
symlink_metadata检查路径及父目录链,拒绝 symlink;Unix 下检查硬链接数量。 - 比较 preview 后的当前内容哈希,避免确认框与实际写入之间的 TOCTOU。
- 使用临时文件加原子 rename,而不是直接
fs::write。
2. Worktree 子代理的 checkpoint 记录在错误的工作区
buildBuiltinToolRegistry 通过 { ...params } 把主轮 checkpoint 传进了子代理注册表;但 mode=worktree 的子代理实际在临时 worktree.workdir 中运行,完成后再通过 worktree.apply 把改动应用到主工作区。
因此当前流程是:
- 子代理编辑临时 Worktree 文件。
- Checkpoint 保存临时 Worktree 的绝对路径。
- Worktree 补丁被应用到主工作区。
- 临时 Worktree 被清理。
- Rewind 恢复的是已经删除的临时路径,主工作区改动没有被恢复。
对于已有文件,rewind 甚至可能重新创建废弃 Worktree 目录;对于新文件,主工作区文件仍会保留。因此“子代理自动继承 checkpoint,改动一并捕获”目前并不成立。
建议不要向 Worktree 子代理传递父轮 checkpoint,而是在 worktree.apply 修改主工作区之前,对实际 changedPaths/deletedFiles 捕获主工作区前像。
高优先级问题
3. Date.now() 不能作为轮次序列
当前 turnSeq 同时承担顺序标识、同轮去重键和 UI 时间戳,但 Date.now() 不是单调时钟,系统时间回拨会破坏 turnSeq >= target 的恢复语义。仅增加类型注释不足以保证正确性。
建议使用持久化的 conversation turn/message ID,并另外保存 createdAt 供 UI 展示;至少也应使用会话内严格递增序号。
4. 追加式索引没有时间线或 generation 语义
回退到早期轮次后,菜单仍保留后面的旧 checkpoint。继续产生新改动后,再选择旧的未来轮次,会混合已经放弃的旧时间线和当前时间线,形成难以预测的“部分 redo”。
建议引入 checkpoint generation/branch:Rewind 后产生新的 generation;默认隐藏废弃 generation 的未来 checkpoint;如果保留访问入口,需要明确标记为旧时间线,而不是继续用单一 turnSeq >= target 聚合。
5. 捕获失败对用户不可见
capture_pre_image 失败只输出 eprintln!,写入仍继续。这个 best-effort 取舍可以接受,但当前没有记录“本轮 checkpoint 不完整”,因此捕获从未写入索引时,missingBlobs 无法发现;UI 可能报告 rewind 成功,但部分文件实际没有恢复。
建议给每轮增加 manifest/status,例如 complete / incomplete,捕获失败时记录原因,并在恢复确认中明确警告或禁止无提示恢复。
6. Rewind 会静默覆盖手工和并发修改
Diff preview 和实际 rewind 是两次独立调用,中间没有文件锁或内容版本校验。用户、其他会话或后台运行可能在确认后修改文件,随后仍被旧前像覆盖。
建议在 preview 返回 expected-current hash,rewind 时重新比较;发生变化默认跳过并报告 conflict,而不是覆盖。Rewind 期间也应禁止当前会话及同项目运行继续写文件。
建议本 PR 最低补齐
- 修复恢复路径重新授权、symlink/hardlink 和 TOCTOU。
- 修复 Worktree 子代理捕获位置。
- 用稳定 turn identity 替代
Date.now()顺序。 - 增加 incomplete checkpoint 状态。
- Rewind 后失效文件工具缓存,并向 transcript/runtime 写入明确的 rewind 事件。
- 增加最基本的容量限制、文件权限和清理入口。
完整的“代码 / 对话 / 两者 / summarize”可以继续放在 P2;但存储不能无限增长。当前会永久保存完整文件内容,包括可能的密钥和大型文件。即使完整 GC 放在 P3,也建议 P1 至少增加单文件大小、会话总容量、checkpoint 数量限制,以及 0700/0600 权限。
建议补充以下测试:
- 文件在 preview 后被修改。
- 文件或父目录在捕获后被替换为 symlink。
- hardlink 文件恢复。
- Worktree 子代理修改、新建和删除文件。
- 系统时间回拨或重复 turn ID。
- Rewind 后继续工作,再选择旧未来 checkpoint。
- 捕获失败、索引截断、blob 丢失。
- 两个会话同时修改同一文件。
- 大文件删除和存储容量上限。
综上,这个 PR 的 P1 范围本身合理,但当前不能描述为“缩水但自洽的 Claude Code rewind”:主会话线性路径基本自洽,Worktree 子代理和恢复授权路径目前并不自洽。建议修复上述两个阻断问题后再合并,其余能力可以按明确的 P2/P3 issue 继续推进。
阻断 1:检查点改存 root+相对路径(schema v2),恢复前重新解析并 逐级拒绝符号链接/多硬链接,临时文件+原子 rename 落盘;旧 v1 索引 行静默跳过。 阻断 2:worktree 子代理不再继承父轮 checkpoint;subagent_worktree_apply 在改写父工作区前对实际 apply 路径以父仓库根捕获前像。 最低补齐:turnId 稳定标识(UUID,Rust 侧时钟无关分配 turn_seq)、 捕获失败 error 记录与 UI 不完整警示、预览哈希回带的冲突检测 (不一致跳过不覆盖)、回退完成显式通知、容量上限(32MB/512MB/10k) 与 0700/0600 权限、checkpoint_clear 清理命令、rewind 审计标记。
|
评审已全部处理,修复见 0b5635d。逐项对应: 本 PR 已修复阻断 1 — Rewind 路径校验
阻断 2 — Worktree 子代理工作区错位
最低补齐
新增测试(checkpoint 22 个,含):预览后修改冲突跳过、符号链接换目标/换父目录拒绝、多硬链接拒绝、worktree apply 父工作区前像、时钟回拨/重复 turnId、超限 error 记录、blob 缺失不触碰文件、v1 行忽略、rewind 标记惰性。 按 P2/P3 拆分继续推进
验证: |
yovinchen
left a comment
There was a problem hiding this comment.
感谢按上一轮 review 做了系统性修复。我基于 d8ca2ef..a761b8e 做了增量复审,并在隔离 checkout 中执行了:
cargo test --manifest-path crates/agent-gui/src-tauri/Cargo.toml checkpoint --lib:25/25 通过cargo test --manifest-path crates/agent-gui/src-tauri/Cargo.toml subagent_worktree --lib:9/9 通过- PR 当前全部 CI 通过
以下修改已经确认有效:
- Worktree 子代理不再捕获临时目录,父工作区前像改在 apply 前捕获。
Date.now()已替换为稳定turnId,Rust 侧分配单调turn_seq。- 增加了 incomplete/error 记录、哈希冲突结果、Unix 权限、容量上限和清理入口。
- 普通目标 symlink swap、父目录 symlink、hardlink 和 blob 丢失都有对应测试。
不过复审仍发现两个可复现的阻断问题,因此本轮继续 Request changes。
必须修复
1. 根目录本身被替换为 symlink 时,rewind 仍会写到原工作区之外
resolve_rewind_target 先对索引中的 root 执行 fs::canonicalize,随后只检查 rel_path 的路径链。canonicalize 会直接跟随 root 自身的符号链接,因此如果捕获后原工作区目录被移动,并在原路径放置一个指向外部目录的 symlink,rewind 会把外部目录当成新的合法 root。
我在隔离 checkout 中补了最小测试,当前行为可以稳定复现:
- 在
workspace/a.txt捕获v1前像。 - 把
workspace重命名为workspace-old。 - 创建
workspace -> outsidesymlink,outside/a.txt内容为outside-secret。 - 执行 rewind。
- 返回
restored_files=1,且outside/a.txt被覆盖成v1。
因此当前实现仍没有完成“恢复时重新授权”:它把索引里的 root 字符串继续当作授权来源,也没有校验它是否仍属于当前 Workspace Project / Root Grant。
建议至少:
- 在 canonicalize 前先用
symlink_metadata(root)拒绝 root 本身是 symlink。 - 将当前会话仍然有效的 Workspace Project/Root Grant 身份传入后端,恢复时确认记录 root 仍在授权集合中;不能仅依赖索引保存的绝对 root。
- 对 canonicalize 后的 root 与捕获时身份做一致性校验。
- 写入前再次验证父链,避免校验后到 rename 前父目录被替换。
2. 预览时为 clean 的文件没有回传哈希,确认后仍可能覆盖手工修改
前端只把 restore/delete 条目放进 actionable,然后只回传这些条目的 (key, currentHash)。后端则把 expected 做成可选 Map;某个 key 不存在时直接跳过冲突检查。
这会产生以下情况:
- 某文件在 preview 时已经与前像一致,分类为
clean,因此不进入expected。 - 用户在确认框打开期间手工修改该文件。
- rewind 遍历该记录时找不到 expected hash,于是不做冲突检查。
- 用户刚做的修改被前像覆盖。
我同样用临时回归测试复现了当前行为:返回 conflicts=0、restored_files=1,确认后的手工修改被覆盖。
建议:
- 所有可解析的
file记录都必须回传 preview hash,包括clean。 - 后端应 fail closed:只要 expected 集合缺少某个待处理 key,就按 conflict/invalid preview 处理,不能默认为允许覆盖。
expected: None只应保留给明确的内部测试或 force API,不应成为桌面 UI 命令的正常路径。
仍需补齐
3. 32 MB 限制是在整文件读入内存后检查
PreImage::File(None) 当前先执行 fs::read(&abs_path),之后才判断 bytes.len() > MAX_BLOB_BYTES。fs_delete 可以删除任意普通文件,因此删除超大文件时仍会先把整个文件读入内存,容量限制无法防止内存和 I/O 峰值。
现有 oversized_file_records_error_instead_of_blob 测试没有执行这个分支,只是手工追加了一条 error 记录。建议先读取 metadata length,超过限制直接记录 incomplete,不要调用 fs::read,并增加真实的超限分支测试。
4. Rewind marker 仍未解决旧未来时间线问题
kind="rewind" 当前只是审计记录,earliest_records_since 会直接忽略它。回退到早期轮次后,旧的未来 checkpoint 仍显示在菜单中,也仍会与回退后新增的记录一起参与 turn_seq >= target 聚合,之前指出的“部分 redo / 混合时间线”语义仍然存在。
如果 generation/branch 明确留到 P2,本 PR 至少应在 rewind 后隐藏或标记旧未来轮次,并在 UI 明示其语义;当前注释中的“索引不截断且语义自洽”仍不准确。
5. Worktree 遇到 symlink 或 metadata 错误时仍静默跳过捕获
capture_worktree_apply_pre_images 对 symlink/其他类型以及非 NotFound 的 metadata 错误直接 continue,不会写 error 记录。因此 Worktree apply 可以修改这些路径,但该轮不会被标记 incomplete,用户也得不到 Claude Code 类似的 skipped warning。
建议将这些跳过原因写成 kind="error" 或明确的 skip-link 记录,让 diff/result 能如实报告不可恢复路径。
整体上,这次修改已经解决了上一轮大部分问题,Worktree 捕获方向也正确;但上述前两个问题仍会造成工作区外写入或静默覆盖确认后的手工改动,需要修复后再批准。
回退不再把记录里的绝对路径当授权凭据:root 自身是符号链接一律拒绝, 且必须命中调用方给出的当前授权根集合(会话工作区根 + 仍 active 的额外 授权根),写入前紧邻再校验一次整条路径链。工作区被改名后在原路径放符号 链接指向外部目录的换根攻击因此失效。 冲突检测从"只带 restore/delete"改为回传全部可解析条目(含 clean),后端 对缺哈希的条目一律判冲突而非跳过检查——确认期间被手改的干净文件不再被 静默覆盖。 另外:大小上限改为先看 metadata 再读文件,避免为拒绝而先把超限文件读进 内存;完整成功的回退写下剪枝标记,读取侧据此丢弃已撤销的陈旧未来轮次; worktree 前像捕获的失败不再静默 continue,改记 error 记录。 自查补强:授权根的仓库根推导在主目录/文件系统根封顶,避免主目录本身是 dotfiles 仓库时把整个主目录变成可写目标;回退全程持 INDEX_LOCK,防止并发 捕获被剪枝标记连带埋掉;skipped_dirs 计入"未回退干净";rewind 标记不参与 turnId 复用;"unreadable" 哨兵不再自我匹配。
|
5 项已全部处理, Blocker1. 换根攻击(root 符号链接)
授权集合里另有两类后端自行推导的根,不接受调用方传入,所以不构成新入口:Skills 根( 2. clean 条目 fail-open 前端原先只回传 补齐3. 4. 5. 自查补强改完又跑了一轮自审,修掉 5 处(其中前两处是本轮引入的回归):
验证
已知遗留(本轮未动,均为 P1 既有问题,建议另开)
需要的话我可以逐条开 issue。 |
- atomic_write 改为“旧文件挪备份 → rename → 失败挪回”:Windows 上目标被 占用时,原先的 remove+rename 兜底会让新旧内容同时消失 - size 记实际落盘字节数,配额不再因“调用方直接给 bytes”而失真 - 预留 64 条尾部名额给 error 记录,撞条数上限的轮次仍能如实标记不完整 - 捕获前像时一并记 Unix 权限位并在回退时还原,脚本不会回退完就丢 +x - fs 捕获点改用 canonicalize 后的真实相对路径:工作区内符号链接下,前像 不再挂到没被改动的路径上,也不再因逐级拒符号链接而永远回退不了 - worktree 前像捕获移到 empty_patch 提前返回之后,空补丁不再留下一批 existed_before=false 记录、回退时反删父工作区已有文件 - INDEX_LOCK 各处从中毒恢复,一次无关 panic 不会让检查点子系统整体失效 - 菜单受控开合,isSending 期间强制收起;确认框说明手改不在检查点内 - 删除会话时清理检查点目录,此前该数据永不回收
补充一轮:
|
…feat/checkpoint-rewind # Conflicts: # crates/agent-gui/src/pages/ChatPage.tsx
|
已合并 冲突只有一处: 合并后验证: @yovinchen 两轮 review 的意见连同后续全量复审的遗留项都已处理完(见上一条回复),麻烦有空时再看一下。 |
unwrap_err() 要求 Ok 类型实现 Debug,而 PreImage::File 装的就是文件内容, 不该为了一句测试断言让它可以被打印进 panic 信息。改用 let-else,既不需要 Debug,也不会有把前像字节写进日志的路径。 该测试是 #[cfg(unix)] 门控的,Windows 上根本不编译,所以本地检查发现不了。
三个 P1: 1. 不完整检查点会被误判为完整回退。rewind_at 丢弃了 earliest_records_since 返回的 error 计数,checkpoint_rewind_code_sync 据此写下 turn_seq=target 的完整剪枝标记——大文件/捕获失败的轮次回退后 显示成功,error 记录连同"没回退干净"的事实被剪枝永久藏掉。现在 CheckpointRewindResult 携带 capture_errors,完整性判定 (rewind_is_complete)将其计入,有缺口只写 turn_seq=0 的审计标记不剪枝; 前端在部分完成对话框与通知里明示"N 个无前像未回退"。 2. 权限修改绕过预览和冲突检测。current_state_hash 只哈希内容,而回退的 "内容已一致"短路分支会还原权限位——确认框停留期间的 chmod 被静默覆盖, 纯权限漂移还被预览伪装成 clean。状态指纹改为"内容哈希@八进制 mode" (Windows 无 POSIX 位退化为纯内容),预览分类用 mode_differs 把纯权限 漂移判为 restore,rewind 的 TOCTOU 比对自然连权限一起校验。 3. 经工作区内部符号链接新建的文件无法回退。fs_write_text 的 NotFound 分支 直接用未解析的 raw_target 落盘,检查点记下链接路径(link/new.txt);回退 侧逐级拒符号链接,这条"删除该文件"的记录永远不可解析。ensure_parent_dir 现在返回 canonical 父目录,新建文件的落盘目标与检查点相对路径都基于 真实路径(real/new.txt),与 existed_before 分支的 resolve 语义对齐。 新增测试 5 个:capture_error_marks_rewind_partial_and_keeps_timeline / complete_rewind_marker_prunes_timeline / chmod_between_preview_and_rewind_is_a_conflict / mode_only_drift_previews_as_restore_and_is_restored / write_new_file_through_internal_symlink_resolves_real_parent。 验证:cargo test --lib 800 passed;cargo check --tests 零错误; pnpm -C crates/agent-gui build 通过;pnpm test:gui 1883 passed; biome 对改动文件零新增;rustfmt --check 漂移数与基线持平。
|
复审时发现三个 P1 问题,均已确认存在并在 P1-1:不完整检查点会被误判为完整回退
修复: P1-2:chmod 绕过预览与冲突检测
修复:状态指纹改为 P1-3:经工作区内部符号链接新建的文件无法回退
修复: 验证
|
|
三条 P1 我都回到 复核结论P1-1 — 这条最难看: P1-2 — P1-3 — P1-3 和 P1-1 会叠成双重掩埋:新建文件删不掉 → 回退不完整 → 但在 P1-1 修好前,这个不完整连标记都不会体现,被完整剪枝直接藏掉。 共同的失误模式是:我改完一个点就验证"这个点对不对",没有回头问"下游消费者、对偶操作是不是也要跟着变"。 对
|
…feat/checkpoint-rewind
|
基于当前头部 不过目前仍不建议直接合入,剩余三个 P1: P1-1:Root Grant 没有在后端执行时重新授权
因此确认框打开期间若 Grant 被撤销或从 write 降为 read,旧路径集合仍可执行回退写入。建议请求携带稳定 Project/Grant 身份,并在后端真正写入前查询当前授权,逐条确认 P1-2:预览哈希校验与实际替换不是 CAS
所以另一个进程可以在 hash 校验通过后、rename/delete 前修改文件,随后仍被旧前像覆盖;父目录也可在 建议使用稳定目录/文件句柄与 no-follow 语义,并把 expected content+mode+identity 校验绑定到最终替换/删除;至少增加“后端第一次 hash 校验之后发生并发写入”和“最终动作前父目录身份变化”的测试。 P1-3:Worktree checkpoint 没有绑定实际 apply 结果
这会留下本轮实际上没有修改过的幽灵记录,后续 rewind 可能恢复/删除无关文件。现有 建议采用 pending capture,并只对实际成功修改的路径提交记录;noop/already-applied/失败路径应丢弃。补充 另外,GitHub 当前仍是 结论:Git/CI/自动治理层面已干净,但授权、并发写入和 checkpoint 记账语义仍有阻断,因此当前不能直接合入。 |
|
三条我都回到 P1-1 — 拆成两半,一半必修,一半超出本 PR必修的一半: 比基线更糟的地方在于不只是"两边都在前端"。 const canMutate = PROJECT_ROOT_MUTATION_INTENTS.has(options.intent) && root.access === "write";而 超出本 PR 的一半:后端按 grant_id 查库重新授权。 "renderer 提供的路径不应充当最终授权凭据"这个批评我认同,但它适用于整个 fs 工具面,不是 checkpoint 特有:
反过来, 所以它不是绕过了本仓授权体系,而是在本仓最宽松的体系里做得最严、但仍差一道 write 位。要求后端查库重新授权是全仓授权模型改造,我建议单独开 issue,在本 PR 里做会造出两套不一致的授权语义。 「确认框期间 revoke 仍可写入」的窗口,与基线"轮内不复查"( P1-2 — 事实成立,有一处描述需要更正,且是全仓通病先更正: 核心指控站得住:hash 校验( 但对照 这是全仓既有的写入语义,checkpoint 侧反而更严。 在本 PR 里单独给 checkpoint 上 no-follow 句柄绑定 + CAS,会造出与所有其他文件工具不一致的两套语义。建议单独 issue、统一处理。如果你认为必须在本 PR 内解决,请明确,我照做。 P1-3 — 事实全对,但"会恢复/删除无关文件"这个推论不成立事实部分我全部确认:捕获点( 但危害推论要看 let action = if !record.existed_before {
if hash == "absent" { "clean" } else { "delete" }
} else {
// ...
if sha256_hex(&expected) == current_content && !mode_differs(record.mode, &target) {
"clean"
} else { "restore" }
我特意去找「 所以幽灵记录的实际代价是 blob 占盘(计入 512MB 上限)+ UI 多出条目,不是错误回退。我认同补 我自己核出来的一条,你没提到
这是我上一轮 P1-1 修复引入的误报:一个什么都没做的轮次被报成"回退不完整"。属于我该修的范围。 建议范围
@yovinchen 麻烦确认这个范围划分。如果你认为 P1-2 的 CAS 必须在本 PR 内解决(而非跟 |
回退是写操作(覆盖/删除),但授权根集合此前只过滤了 grant.state,把只读根 也一并交给了后端。普通文件工具靠 pathUtils 的 canMutate 拦这一层,而回退 只往后端传路径、access 当场就丢,所以这道门只能在取根这一步补。 另修上一轮 P1 引入的误报:worktree apply 的前像捕获发生在所有 apply 分支 之前,捕获缺口当场记 error 会让 already_applied / fallback_noop(父工作区 一个字节没动)的轮次在 UI 上标 ⚠"回退可能不完整"。改为攒着,只在确认 apply 真改动过父工作区的分支上落账。成功的前像仍立即落盘——过了那一行 就没得捕获了。 新增测试锁住 noop 轮次的回退契约:冗余记录一律判 clean,既不写回也不删除。
|
已推 P1-1(本 PR 内必修的部分) — 顺带修了一个你没报、但确实存在的误报:worktree apply 的前像捕获发生在所有 apply 分支之前,捕获缺口当场记 error 会让 P1-3 复核结论没变:不构成阻断。 P1-2 维持上一条的判断:CAS 缺口真实存在,但 验证
|
Closes #503
Summary
fs_write_text/fs_edit_text/fs_delete落盘前原子捕获前像(pre-image),随文件变更一体完成,不新增 IPC;存储于~/.liveagent/checkpoints/<conversationId>/(原始字节 blob + 追加式index.jsonl)checkpoint.rs模块与三个命令:checkpoint_list(按轮汇总)、checkpoint_diff_stats(回退影响预览)、checkpoint_rewind_code(执行回退)createFsTools→builtinRegistry→runAgentConversationTurn传入{conversationId, turnSeq};子代理注册表经参数展开自动继承CheckpointRewindMenu:列出可回退轮 → diff 统计 + 受影响路径确认框 → 回退,失败逐条报告Screenshots / preview
顶栏入口(主题切换与项目工具开关之间的 History 图标):
空状态(本会话暂无检查点,底部为覆盖范围说明):
有可回退轮时的列表(按轮汇总时间与文件数):
回退确认框(diff 统计 + 受影响路径预览):
设计要点
turnSeq >= 目标轮的记录,每路径保留最早一条前像;索引追加式不截断,回退后继续对话再回退仍自洽eprintln!,绝不阻断文件写入None;符号链接不捕获(恢复语义不明确)Test plan
cargo check --tests通过cargo test checkpoint --lib9 个新单测全绿(往返恢复、删除标记、跨轮最早前像、同轮去重、目录标记、父目录重建、分组/diff 分类)pnpm -C crates/agent-gui build(tsc + vite)通过node --testbuiltin-registry 子代理测试 6/6git diff --check无尾随空白