解除对 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 等任意命令(rewindmutationTools名单)——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,零写路径,先做)
lib/trace/新模块:frames.mjs:zstd 多帧扫描 + 逐帧解码(算法同构 harnessscanZstdFrames, 零依赖;typeof zstdDecompressSync === 'function'门控,node 22 → 明确报错降级);replay.mjs:从事件流重放 write/edit/str_replace_editor → 每文件内容序列 (Map<relPath, Array<{seq, after}>>或按文件的内容版本表);read.mjs:数据源选择——sessionQuery.readSession优先,zstd 兜底;
- 服务层:
traceTimeline(sessionId)/traceDiff(from, to)/traceRollback(...)(复用lib/diff/engine.mjsLCS 与lib/rollback.mjs写路径); - 时间线合并:现有 timeline 在"快照记录为空"时回落到轨迹节点;或始终追加 "会话轨迹视图"(设计评审定:先做回落,合并混排后置);
- 端点/命令/GUI:
/diff --trace、GET /api/trace-timeline、面板 scope 切换增加 "Trace" 视图(复用现有面板组件,节点标⤷ trace); - 测试:真实轨迹 fixture(本机 3 会话 90 tool/call 已采样的形状)+ 帧扫描单测 + 重放正确性(write/edit 序列 → 重建内容 == 实际文件)+ 冷会话 readSession 路径 + node 22 降级路径 + 集成(真 trace + 无 rewind 环境全流程)。
Phase A:自建快照捕获(0.7.x,需边界扩展,走设计评审)
- AGENTS.md 非协商条款 #1 扩展:"自建快照"写路径——写自有存储域
cdp-snapshots(version 1,schema 自声明)与自有快照目录$DSH_HOME/dsh-checkpoint-diff/snapshots/<workspaceKeyHash16>/<uuid>/(copy 语义:变更前拷贝 + manifest,与 workspace.mjs 同构算法;不 fork rewind 代码); - 捕获器
lib/capture.mjs:prepend 直通监听 fs/write-intent(目标文件)、 fs/edit-intent(目标文件)、tools/pre-execute(mutationTools 名单 → 整工作区); rewind 在场(checkpoints 域有当前会话记录)→ 捕获器停用(避免双份); - 配额/清理自管:独立
maxSnapshots/maxBytes(默认宽松,如 200/1 GiB)+ 手动清理命令,不参与 rewind 逐出、不写 git refs(绕开 gc 问题); - 时间线合并:自有快照节点 + rewind 节点(若在)+ 轨迹节点(兜底)三源混排 (按 time 稳定排序;provider 标注 cdp 自有/rewind/trace);
- 测试:捕获器事件顺序(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 独有)。