解除对 rewind 的依赖 —— 设计记录(2026-08-17)

August 17, 2026 · View on GitHub

实施状态Phase B(轨迹重放)已实现(0.5.0,本会话完成)——lib/trace/*

  • GUI "Trace (session log)" 范围 + /diff --trace 命令 + 三个 trace HTTP 端点; 单测 33 项 + 集成 trace 流程全绿,真实会话日志端到端验证通过。Phase A (自建快照捕获)未动,按 §4 排期。README/ARCHITECTURE 已按实现更新。

用途:回答"能不能解除对 dsh-checkpoint-rewind 的依赖"。结论:,分两个 阶段——轨迹重放(零写路径,立即可用)与自建快照捕获(完全自足,需扩展 写路径边界)。rewind 存在时继续消费(向后兼容、时间线合并),不存在时插件独立工作。 本文件所有"已核实"事实均于 2026-08-17 在本机(Node v24.9.0 / DSH 0.1.0-rc.5 部署) 与 harness 源码(../deepseek-harness,只读)验证。

0. 现状与目标

现状:本插件是 rewind 的纯消费者——读它的 checkpoints 存储域记录 + 快照目录 布局(git 未引用对象 / copy 目录)。没有 rewind 就没有检查点,时间线为空。 用户观察到其它插件各有方案(dsh-snapshot 自建快照、turn-rewind 变更台账、Claude Code 回合级快照、Aider 自动 commit),问能否解除依赖。

目标:让本插件在 rewind 缺席时也能回答问题("这次对话改了哪些文件、何时改的、 如何退回去"),同时保留现有能力;rewind 在场时行为不变(不重复捕获、时间线合并)。

1. 已核实事实(证据)

1.1 捕获层:事件对任何插件开放(rewind 不是唯一能捕获的)

  • fs/write-intent / fs/edit-intent 是 Cordis waterfall 事件,由 packages/fs/tool-fs(write.ts/edit.ts)与 packages/fs/tool-str-replace-editor 在写盘前派发(ctx.waterfall('fs/write-intent', target, actor, next))。
  • packages/fs/fs-observation-policy 占用决策槽(监听且不调 next()),但 观察型监听器(调 next() 放行)可共存——rewind 正是 prepend 直通监听 (lib/gate.mjs/index.mjs,0.4.0 安装包源码已读)。
  • tools/pre-execute 覆盖 bash/write/edit/str_replace_editor/pwsh/terminal_send 等任意命令(rewind mutationTools 名单)——bash 类变更只有靠"执行前整工作区快照"。
  • 结论:自建捕获在技术上与 rewind 等价(同一组事件、同一"变更前"语义), 不需要 rewind 的任何代码。

1.2 轨迹层:session.jsonl.zstd 可读,内容型工具参数携带完整内容

  • 轨迹持久化在 $DSH_HOME/sessions/<workspaceKey-->/<sessionId>/session.jsonl.zstd (本机实测存在;harness 默认压缩 = zstd)。
  • 格式为多帧拼接:header 帧 + 追加帧;node:zlib.zstdDecompressSync 对拼接流 只解首帧(实测),须按帧扫描后逐帧解码——harness 的 scanZstdFrames 算法 (packages/session/session-persistence-jsonl/src/zstd.ts)约 60 行、零依赖、可复用。
  • Node 版本node:zlib 的 zstd 需 Node ≥ 23.5(本机 v24.9.0 实测 roundtrip OK;^22.19 无此 API——直接解析需运行时探测 typeof zstdDecompressSync, 缺失则降级)。
  • 事件形状(真实轨迹实测)tool/call = {type, seq, time, data:{turn, step, callId, name, arguments}}arguments 为 JSON 字符串;write 参数 {file_path, content}(实测含 8844 字符全量内容)、edit 参数 {file_path, old_string, new_string}(实测)。tool/result 同 seq 对。
  • 冷会话读取sessionQuery.readSession(sessionId) 返回完整事件日志 ({session, events},replay 校验后克隆,live/cold 皆可)——首选路径,无需自解 zstd;本插件 labels 已依赖它。zstd 直接解析仅为 session-query 缺席时的兜底。
  • 已知边界:轨迹不含 fs/*-intent 事件(rc.6 事件门关闭,已知事实)—— 因此 bash/pwsh/terminal_send 等任意命令对文件的修改无法从参数推断(除非有 快照)。这是轨迹重放的固有盲区,不是实现问题。

2. 方案对比

方案机制写路径bash 盲区历史会话(未装插件时)回滚质量成本
B 轨迹重放session.jsonl.zstd(或 readSession),重放 write/edit 重建任意时刻文件内容(纯读)有(bash 修改不可见)可用(轨迹天然持久,与是否装插件无关)中(只能恢复内容型工具碰过的文件;bash 改过的文件报"不可重建"跳过)
A 自建捕获监听同一组事件(fs/write-intent、fs/edit-intent、tools/pre-execute),变更前自存快照(copy 语义,自有目录 + 自有域)新写路径(自有存储,须扩展 AGENTS.md #1)(与 rewind 同质量)不可用(未装则无捕获)高(与 rewind copy provider 等价,复用现有回滚)
C git 自动 commit每次变更后向私有 ref 提交写 git(违"绝不写 git";未引用对象仍会被 gc)不可用高(git 原生)低-中,但破坏现有安全边界、污染仓库、丢非 git 场景
现状(仅消费 rewind)读 rewind 域 + 快照目录不可用零,但依赖不解除

3. 推荐架构:B 先行,A 跟进,双源时间线

数据源(按优先级合并进同一时间线):
1) rewind checkpoints 域(在场时)      ── 快照节点(现行为,不变)
2) 自有快照域 'cdp-snapshots'(A 落地后)── 快照节点(rewind 缺席时捕获)
3) 会话轨迹(B 落地后,兜底)            ── 轨迹节点(虚拟节点,重放内容)
   ├─ 首选 sessionQuery.readSession(live/cold,replay 校验)
   └─ 兜底 zstd 帧扫描 + node:zlib(session-query 缺席;node<23.5 降级)

节点模型:快照节点 = {id, time, trigger, turn, step, ref/provider}(现有);
          轨迹节点 = {id: trace:<seq>, time, trigger: tool name, turn, step,
                     content: 重放到该点后的文件状态(write/edit 全量)}
diff:任意两节点(同源或跨源)→ 逐文件逐行(现有 LCS 引擎,复用)
回滚:快照节点 → 现有 rollback(copy 语义);轨迹节点 → 只写"可重建"文件
      (内容型工具 track 的),其余文件报告"不可重建"跳过(绝不删除,同现有语义)

关键决策

  • rewind 在场时不重复捕获(检测 checkpoints 域有本会话记录即跳过自建捕获)—— 避免双份快照与事件竞争;rewind 缺席时自建捕获自动接管。
  • B 与 A 互补而非替代:B 解决"历史与无生产者"(纯读、零风险);A 解决 "bash 盲区与回滚质量"(需写路径)。B 先落地即达成"可脱离 rewind 独立回答 diff", A 再补"独立快照 + 独立回滚"。
  • 轨迹节点的语义:节点 = 工具调用边界;/diff <trace节点A> <trace节点B> 呈现 重放内容差异;快照与轨迹节点可混排(按 time 排序),寻址(id 前缀/latest)沿用。
  • 回滚安全契约不变:预览先行、绝不删除(轨迹节点缺内容的文件 = 不可重建,跳过 并报告)、路径校验、只写会话工作区、单次撤销(撤销记录复用现有 undoData 结构)。

4. 分阶段实施计划

Phase B:轨迹重放(0.5.x,零写路径,先做)

  1. lib/trace/ 新模块:
    • frames.mjs:zstd 多帧扫描 + 逐帧解码(算法同构 harness scanZstdFrames, 零依赖;typeof zstdDecompressSync === 'function' 门控,node 22 → 明确报错降级);
    • replay.mjs:从事件流重放 write/edit/str_replace_editor → 每文件内容序列 (Map<relPath, Array<{seq, after}>> 或按文件的内容版本表);
    • read.mjs:数据源选择——sessionQuery.readSession 优先,zstd 兜底;
  2. 服务层:traceTimeline(sessionId) / traceDiff(from, to) / traceRollback(...) (复用 lib/diff/engine.mjs LCS 与 lib/rollback.mjs 写路径);
  3. 时间线合并:现有 timeline 在"快照记录为空"时回落到轨迹节点;或始终追加 "会话轨迹视图"(设计评审定:先做回落,合并混排后置);
  4. 端点/命令/GUI:/diff --traceGET /api/trace-timeline、面板 scope 切换增加 "Trace" 视图(复用现有面板组件,节点标 ⤷ trace);
  5. 测试:真实轨迹 fixture(本机 3 会话 90 tool/call 已采样的形状)+ 帧扫描单测 + 重放正确性(write/edit 序列 → 重建内容 == 实际文件)+ 冷会话 readSession 路径 + node 22 降级路径 + 集成(真 trace + 无 rewind 环境全流程)。

Phase A:自建快照捕获(0.7.x,需边界扩展,走设计评审)

  1. AGENTS.md 非协商条款 #1 扩展:"自建快照"写路径——写自有存储域 cdp-snapshots(version 1,schema 自声明)与自有快照目录 $DSH_HOME/dsh-checkpoint-diff/snapshots/<workspaceKeyHash16>/<uuid>/ (copy 语义:变更前拷贝 + manifest,与 workspace.mjs 同构算法;不 fork rewind 代码);
  2. 捕获器 lib/capture.mjs:prepend 直通监听 fs/write-intent(目标文件)、 fs/edit-intent(目标文件)、tools/pre-execute(mutationTools 名单 → 整工作区); rewind 在场(checkpoints 域有当前会话记录)→ 捕获器停用(避免双份);
  3. 配额/清理自管:独立 maxSnapshots/maxBytes(默认宽松,如 200/1 GiB)+ 手动清理命令,不参与 rewind 逐出、不写 git refs(绕开 gc 问题);
  4. 时间线合并:自有快照节点 + rewind 节点(若在)+ 轨迹节点(兜底)三源混排 (按 time 稳定排序;provider 标注 cdp 自有/rewind/trace);
  5. 测试:捕获器事件顺序(intent 前捕获 = 变更前内容断言)、rewind 在场跳过、 配额清理、三源合并、回滚全流程(复用现有 copy provider 语义 + 集成)。

收尾(两阶段共用)

  • README/ARCHITECTURE/contract.md/competitive-analysis.md 更新:"可脱离 rewind 独立工作(轨迹重放 + 自建快照)"写进定位与对比表;
  • 发布流程照旧(npm → profile → tag → dist-tag);host 变更需重启 harness。

5. 边界与风险

  • Phase A 必须走 AGENTS.md #1 扩展评审:新增写路径(自有快照存储)是 "只读设计,唯一例外是回滚"的第二次扩展;约束保持可审计:只写自有域与自有目录、 绝不碰 rewind 存储/git/会话、路径校验同 rollback、配额有上限。
  • B 的固有盲区:bash/pwsh 修改不可见——文档必须写明"轨迹视图只能回答 内容型工具引起的变更";要完整答案请装 rewind 或等 Phase A。
  • Node 版本门控:zstd 直接解析需 ≥23.5;readSession 路径无版本限制(优先)。
  • 节点寻址歧义:轨迹节点 id 用 trace:<seq> 前缀防与快照 id 冲突; 跨源 diff(快照×轨迹)按时间对齐内容,无对齐点时拒绝并说明。
  • 性能:轨迹帧扫描 + 重放为全量读(万行级轨迹秒级),加缓存(按 sessionId
    • 文件大小写敏感键);轨迹节点数远多于快照节点,GUI 默认折叠/限流。
  • 不重复捕获(A 与 rewind 共存)依赖"checkpoints 域有记录"检测——rewind 清了 记录(/rewind clear)但快照目录还在时,自建捕获会接管,无冲突(语义 = 变更前)。
  • 安全边界不变:HTTP 面仍 GET-only(除 rollback/rollback-undo); 轨迹读取只读;回滚路径校验照旧。

6. 结论

解除依赖可行且分两步走:Phase B(轨迹重放)零写路径、立即落地,让插件在 rewind 缺席时也能回答"改了哪些文件、怎么改的"(内容型工具全覆盖,bash 盲区 文档化);Phase A(自建捕获)让插件成为独立的生产者+消费者,与 rewind 同质量 的变更前快照与回滚,同时保留对 rewind 的向后兼容消费。竞品对照:dsh-snapshot 只有 A(自建捕获,无行级 diff、无轨迹兜底);turn-rewind 只有"变更台账 + git 恢复"; 本插件两阶段后 = A + B + 行级 diff + 血缘 + 安全回滚,且历史会话无需任何生产者 即可审计(B 独有)。