dsh-tauri-turnrewind
每次 Agent 改动自动建私有快照,/undo 一键预览红绿 diff 并回滚本轮文件——卡内确认、冲突检测、不动你的 git历史
- Stars
- 0
- Language
- TypeScript
- Created
- Aug 30, 2026
- Updated
- Sep 8, 2026
Introduction
dsh-tauri-turnrewind
本地开发中的 DSH turn 回滚插件(TypeScript 重写版,位于 packages/,遵循 workspace 插件规范;旧 JS 版已移除)。
分发(2026-09-06 起):插件已随桌面构建打包——列入
dsh-tauri-bundle依赖(构建期部署进src-tauri/resources/node_modules/)与src-tauri/resources/internal-plugins.json预装清单,下载桌面版即装即用,无需任何手动配置。定位仍是实验功能:恢复路径直接使用 Node/Git,尚未接入受控的宿主 sandbox/Tauri bridge,请在可丢弃的工作区先行试用。Git 目录模式(当前形态):工作区必须位于 Git worktree(OpenCode 风格)。非 Git 目录显式禁用——turn 记为
skipped(TURNREWIND_GIT_REQUIRED),/undo说明原因,不再为普通目录建快照。ignore 规则(.gitignore/.git/info/exclude/ global excludes /.gitattributes)委托给源仓库;私有 snapshot repo 通过 alternates 借用源对象。设计与进度见docs/TURN_REWIND_GIT_DIR_PROGRESS.md。
当前状态
这是 Host MVP 原型,当前实现:
- 为已领取的 Agent turn 建立私有 Git 快照;
- 将快照映射记录到
$DSH_HOME/ledger.sqlite; - git 子进程模型:快照、diff、恢复走异步 spawn,不阻塞 Host 事件循环;同一会话的捕获/结算按 FIFO 串行;收到 turn 输入后,before snapshot 通过
agent/pre-stepbarrier 完成后才允许模型和工具执行;同一 workspace 被其他 session 占用时,新 turn 会正常运行但记为skipped,避免共享 snapshot 链互相污染。同步开销已收敛:工作区解析合并为单次 rev-parse 子进程并带 60s 缓存(过期后先回缓存值、后台异步刷新),冲突检测的文件读取(上限 64MB)已改异步; - git 可用性探测:系统没有 git 时,turn 显式记为
skipped(原因TURNREWIND_GIT_UNAVAILABLE),而不是静默失败; - 快照链自愈:私有快照仓库被删/损坏后,下一次捕获自动降级重建基线(日志有一条 warning),后续 turn 照常可撤销;被清空前留下来的旧 turn 会在
/undo选目标时自动识别为死快照并标记跳过(snapshot ref missing),不会甩出 git 原始报错; - alternates 失效自愈:私有 repo 通过 alternates 借用源仓库对象,而源仓库
git gc --prune=now(amend/rebase 的日常残留)可能删掉被借用且不可达的对象;每次 capture 后做连通性检查(git rev-list --objects --missing=print),发现缺对象即降级为自包含存储(不再借用、不再复制源 index)并重建基线,旧 turn 走死快照跳过路径; - 工作区资格守卫(Git 目录模式):会话 cwd 必须位于 Git worktree(子目录自动归并到 worktree 根,共享同一快照域);家目录、家目录祖先、盘根等系统目录直接拒绝;非 Git 目录记为
TURNREWIND_GIT_REQUIRED,不再做全目录预算扫描(见「工作区资格」); - 不可用弹窗:客户端半(
src/client/)通过turnrewind会话投影检测到不可用提示时,在 Web UI 内弹出模态对话框(中英双语、跟随应用主题)。单会话只报一次:提示以会话内消息形式永久留档可查,弹窗只对页面存活期间新到达的提示触发,历史提示(重启后重新进入会话)不会重复弹窗; - 两阶段
/undo:先出预览卡(红绿 diff ++x -y徽标 + 文件清单),卡内 ✓/✗ 按钮确认执行或取消——不看预览就不会误执行;计划 5 分钟过期。过期与取消都只锁执行、不抹账本记录:过期的 plan 卡片保留文件清单与 diff 供随时回看(归档视图);取消的 plan 是用户主动放弃,卡片塌缩为一行「已取消」留痕。确认时二次校验磁盘与预览绑定; - 恢复前比较当前文件与 turn 完成时的快照,发现变化则拒绝覆盖,并给出「turn 产物 → 当前磁盘」的冲突 diff;
- 冲突可用
--skip-conflicts(只恢复无冲突文件)或--force(强制覆盖)直接执行; /undo --redo(重做最近一次已应用的 undo)已禁用:入口在解析层直接拒绝,底层恢复路径加固代码保留但未开放(见「已禁用:redo」);- 注册人类命令
/undo; /undo默认处理当前会话最新的单个可恢复 turn,也可指定完整 turn ID;- 同一 workspace 的活动 turn 或 undo 操作互斥;
- 插件重启时将未完成 turn 标记为 abandoned;未完成的 Undo 与 Redo operation 会标记为
needs-recovery,对应 workspace 在清理前拒绝新的 rewind 操作,避免未知磁盘状态被继续覆盖。恢复面板:不可用弹窗在 reason 命中恢复围栏时提供「打开恢复面板」入口,可查看被围 workspace 的中断操作明细,并选择「已检查,保留历史并解锁」(operation 转 recovery-acknowledged 终态)或「清除 rewind 数据并解锁」(等价 purge); /undo --doctor:只读诊断——git 可用性、工作区资格、账本规模与围栏、快照仓库健康(refs / alternates)、最近 turn 状态、备份新旧;- 账本打开时执行
PRAGMA quick_check(损坏显式拒绝加载,TURNREWIND_LEDGER_CORRUPT),并每日滚动备份到ledger.sqlite.bak(VACUUM INTO一致性快照),损坏时可按「还原 .bak → 重启」恢复; - 每次 Undo 的回退提示独立持久化;下一次模型 step 一次性注入全部 pending notice;
- 不修改用户项目的 HEAD、分支、index、stash 或提交历史。
使用方法
在已加载本插件的 DSH 会话中:
/undo
默认进入两阶段流程:先输出预览卡(每个文件的红绿 diff 与 +x -y 徽标),
卡内附两个按钮——✓ 执行撤销 与 ✕ 取消。点 ✓ 才会真正恢复文件;
不点击的话计划 5 分钟后自动过期,不会误执行。确认时还会二次校验磁盘,
预览之后文件又被改动过(冲突)会拒绝执行并提示重新预览。
也可以指定完整 turn ID、或只看计划不动手:
/undo <session-id>:<turn-number>
/undo --dry-run # 仅输出文件清单与分类,不含 diff
/undo --preview # 输出内容与 /undo 的预览卡一致
磁盘在预览后又被改动(冲突)时,可以选择处理策略:
/undo --skip-conflicts # 只恢复无冲突文件,跳过的文件会列出
/undo --force # 强制覆盖冲突文件(执行前仍有回滚点保护)
其他:
/undo --cancel <plan-id> # 取消一个待确认的预览计划
/undo --doctor # 只读诊断报告(不能与其他选项组合)
当前不支持 / 已禁用:
/undo --redo # 已禁用(功能冻结),入口直接拒绝
/undo --subtree # 未实现
父对话递归撤销、消息旁 Undo 按钮和设置页模式切换尚未实现。
工作区资格(Git-only)
曾有用户在 QQ 机器人会话(默认工作目录是家目录、约 250 GB 内容)上触发 git add --all 级别的灾难,早期版本因此引入了全目录预算扫描。当前实现以源仓库的 Git worktree 为快照边界:非 Git 目录禁用(不再做预算扫描,也不再为普通目录建快照)。turn 开始前的资格检查:
- 系统目录直接拒绝:家目录本身、家目录的祖先目录(如
C:\Users、C:\)、以及任何盘符根目录。这类目录无论多小都不做快照。 - 必须是 Git worktree:会话 cwd 不在 Git worktree 内的 turn 记为
skipped(原因TURNREWIND_GIT_REQUIRED)。子目录会话自动归并到 worktree 根(git rev-parse --show-toplevel),共享同一快照域;同一仓库的 linked worktree 各自成域。 - 快照范围委托给源仓库 ignore 规则:
.gitignore、.git/info/exclude(每次 capture 重同步)、global excludes 与.gitattributes语义与源仓库一致;插件自身只额外排除.git元数据与 turnrewind 临时文件。不再有自定义敏感文件名单——未写进 ignore 的文件(含.env、密钥类文件)会被快照;这是有意的取舍:token.ts、credentials.module.ts等合法源码曾被旧规则静默排除,导致 undo 永远无法恢复它们。若不希望某些文件进入快照,请把它们加进源仓库的 ignore 规则。
被拒绝的 turn 仍正常执行,只是不提供 undo。同时该会话会收到一条一次性提示([Turn rewind unavailable]),说明工作区被拒绝的原因;每个会话只提示一次,后续 turn 不再重复打扰。提示会以两种形态呈现:
- 会话内消息:插件来源的上下文注入消息,模型和用户都可见、可审计;
- Web UI 弹窗:宿主端
turnrewind会话投影(src/host/service/dialog-projection.ts)把提示折叠进会话列表快照,客户端半(src/client/register/dialog.ts)从sessions.list的projectionValues.turnrewind读到后弹出模态对话框。去重不在浏览器存储里做(localStorage 会因换端口/清存储丢「已读」):按「单会话一次」种子逻辑,进入会话时已存在的提示视为历史留档(会话内消息永久可见)不弹,只有页面存活期间新到达的提示弹一次。
- 敏感文件提醒:会话首次追踪一个工作区时做一次浅层启发式扫描(根 + 两层、上限 500 文件、跳过 node_modules),命中
.env/密钥类且未被 ignore 的文件时发一条一次性[Turn rewind privacy notice](每会话+工作区一条),列出会被快照的具体文件与退出方式(加 ignore)。扫描失败静默跳过——这是提醒,不是门禁。 大工作区的取舍:不再做预算预扫描,快照耗时与磁盘占用随仓库规模增长(Git ignore 能排除node_modules等,但容量/性能风险仍由使用者自行承担);单文件快照/恢复上限为 64 MB——超限文件仍会被捕获进快照,/undo预览会以[too large]标注并在执行后单文件报告为「未恢复」,不会导致整次 undo 失败,其余文件照常恢复。若项目里有此类大文件且不希望被追踪,请把它们加进源仓库的 ignore 规则。
清理已膨胀的快照数据
如果某个工作区在旧版本下已经生成过巨大快照,先停止 DSH Host 进程,再执行:
node packages\dsh-tauri-turnrewind-ts\purge-workspace.mjs "C:\Users\<user>\Desktop\test" # release 数据目录 ~/.dsh
node packages\dsh-tauri-turnrewind-ts\purge-workspace.mjs "C:\Users\<user>\Desktop\test" --home "$env:USERPROFILE\.dsh.dev" # debug
(需先 pnpm --filter dsh-tauri-turnrewind build 产出 dist/——CLI 从构建产物导入引擎。)
该命令删除该工作区对应的私有快照仓库($DSH_HOME/snapshots/<hash>.git)及其全部账本记录(turns / operations / notices / plans / workspaces),其他工作区的数据不受影响。workspace 被运行中的 Host 占用时命令会拒绝执行(workspace lock),请先停止对应 Host 进程。
注意:这个 CLI 只在两种场景需要——①账本损坏前的彻底清理,②命令行批量操作。解除恢复围栏的日常路径已产品化:在「打开恢复面板」里选「清除 rewind 数据并解锁」即可(同一 purge 逻辑、同一把 workspace 锁,占用时在 UI 内报 409),见「当前状态」的恢复面板条目。
Undo 的工作区范围
Undo 的范围是:
当前 session 绑定的 workspace(session header 的 cwd)
例如当前 session 的工作区是:
C:\Users\<user>\Desktop\test
插件只会比较和恢复该目录以内、且被快照规则允许的文件。它不是“恢复整台电脑”,也不是“恢复所有 Agent 访问过的路径”。
一次 turn 的文件范围是:
turn 开始时的 workspace 快照
↓
Agent 执行
↓
turn 结束时的 workspace 快照
↓
两次快照之间发生变化的文件
Undo 只恢复两次快照之间发生变化的文件:
- Agent 修改的文件:恢复到 turn 开始前;
- Agent 新建的文件:Undo 时删除;
- Agent 删除的文件:Undo 时恢复;
- 未发生变化的文件:不处理。
Workspace 外的文件
如果 Agent 访问或修改了 workspace 外的文件,当前版本不会捕获,也不会 Undo。
例如当前 workspace 是:
C:\Users\<user>\Desktop\test
Agent 修改了:
C:\Users\<user>\Desktop\test\a.txt
C:\Users\<user>\Desktop\other\b.txt
执行 /undo 时:
test\a.txt → 可能被恢复
test 之外的 b.txt → 不会被处理
workspace 外的修改不会:
- 写入 turn 的 before/after snapshot;
- 出现在
/undo --dry-run文件列表; - 被
/undo恢复; - 出现在 rewind notice 的文件列表中。
在 workspace-write 权限模式下,宿主 sandbox 通常会阻止 Agent 写出 workspace;但如果使用 danger-full-access 或其他方式允许外部写入,Agent 能写成功不代表 turnrewind 能回退这些文件。
插件自己的恢复操作会拒绝以下路径:
../outside.txt
C:\OtherProject\file.txt
workspace 内指向外部的符号链接路径
如果确实需要管理多个项目,应将它们作为独立 workspace 分别运行,不要把多个项目目录混成一个恢复域。
不同修改方式的捕获规则
当前插件按 workspace 在 turn 前后的实际差异捕获,不按工具名称判断来源。因此以下 Agent 修改方式通常都会被纳入当前 turn:
- Agent
write; - Agent
edit; - Agent 使用 Git Bash/bash;
- Agent 使用 PowerShell/pwsh;
- Agent 运行格式化工具;
- Agent 运行生成脚本;
- Agent 通过 Git 命令间接修改文件;
- Agent 执行其他会改写 workspace 文件的程序。
准确语义是:
该 workspace 在本 turn 期间发生的允许范围内变化
不是:
百分之百证明该变化由 AI 直接写入
如果用户或其他程序在 Agent turn 执行期间同时修改同一个 workspace,当前 MVP 可能无法区分来源,也可能把这些变化一起记录到该 turn。建议等待 Agent 完成或打断后,再手动编辑同一 workspace。
如果用户是在 turn 完成后修改文件,Undo 预检会将当前内容与 Agent turn 完成时的快照进行比较。内容不一致时会显示冲突,并默认拒绝覆盖。
冲突是什么意思
Undo 不是无条件覆盖。它会检查:
当前文件状态 == Agent turn 完成时的文件状态?
如果相等,文件可以安全恢复到 turn 开始前。
如果不相等,就会显示类似:
Undo preflight: turn <turn-id>; 5 file(s); 5 conflict(s): 4.txt, 5.txt, 6.txt, 7.json, 8.md.
含义是:
- 目标 turn 涉及 5 个文件;
- 当前文件与该 turn 完成时的快照不一致;
- 插件无法确认这些变化是否来自用户、其他 turn 或外部程序;
- 默认不会覆盖,文件保持当前状态。
Windows 的 CRLF 与 Unix 的 LF 换行差异会被文本状态比较规范化,不应单独造成冲突。二进制文件仍按原始字节比较。
冲突可用 --skip-conflicts(只恢复无冲突文件)或 --force(强制覆盖)处理;默认遇到冲突则拒绝并给出「人改了什么」的 diff。
多次 Undo 与模型提示
每次 Undo 都会产生一条独立的 pending rewind notice:
Undo C → notice C
Undo B → notice B
Undo A → notice A
如果在发送下一条普通消息之前连续执行多次 Undo,下一次模型 step 会一次性收到全部 notice,顺序为:
C、B、A
每条 notice 会列出:
- 被撤销的 turn;
- 该次 Undo 涉及的文件;
- “当前磁盘文件是权威状态”;
- “不要假设旧修改仍存在,请重新读取文件”。
notice 只消费一次。下一次模型 step 后不会重复注入。
Git 与快照存储
快照边界与对象借用(Git 目录模式)
私有 snapshot repo 保存于 $DSH_HOME/snapshots/<workspace-hash>.git,与用户项目的 .git 完全隔离:capture 使用临时 GIT_INDEX_FILE(finally 中删除),snapshot refs 仅允许 refs/turnrewind/ 前缀;不修改用户项目的 HEAD、branch、index、stash 或提交历史(有逐字节不变测试钉住)。初始化时通过 objects/info/alternates 借用源仓库对象以减少重复存储,并同步源仓库 .git/info/exclude;源仓库 gc --prune=now 删掉被借用对象时,下一次 capture 的连通性检查会检测到并降级为自包含存储(见「当前状态」中的 alternates 失效自愈)。
家目录防护与 Git worktree 要求互为兜底:会话 cwd 为家目录/盘根等系统目录时直接拒绝;不在 Git worktree 内的目录记为 TURNREWIND_GIT_REQUIRED,不做全目录预算扫描,也没有可配置的 guard 预算项(旧版本的 config.guard / TURNREWIND_MAX_* 已随本模式移除)。
快照存放在插件私有目录:
$DSH_HOME/snapshots/<workspace-hash>.git
账本存放在:
$DSH_HOME/ledger.sqlite
插件不会调用用户项目的:
git reset --hard
git checkout .
git clean -fd
也不会修改用户项目的:
HEAD
当前分支
index
stash
commit history
快照范围与敏感文件(Git 目录模式)
⚠️ 插件不提供敏感文件保护。 旧版本的自定义排除清单(
.env、*.pem、credentials.*、*token*等)已随 Git 目录模式全部移除。当前快照范围完全委托给源仓库的 ignore 规则:未写进.gitignore/.git/info/exclude/ global excludes 的文件——包括.env、密钥、证书——都会被捕获进私有快照,并可被/undo恢复。
插件自身只额外排除(不属于项目内容的部分):
.git/ # Git 元数据
.turnrewind/ # 插件临时目录
**/*.turnrewind-*.tmp # 恢复过程中的临时文件
node_modules/、dist/、build/ 等不再由插件排除——由你仓库自己的 .gitignore 决定(普通项目都会忽略它们,快照语义因此与源仓库一致)。
两点边界:
- 被源仓库 ignore 的文件不会进入快照,也不会被 undo 恢复;ignore 规则每次 capture 重新读取(
.git/info/exclude还会同步进私有 repo); - 曾经使用的
*token*、*secret*等子串规则已移除——它们会静默排除token.ts、tokenizer.py这类正当源码,且被排除路径不出现在 dry-run 列表中,导致 undo 静默漏恢复。
如果不希望秘密文件进入快照:把它们加进源仓库的 ignore 规则(对 git 和本插件同时生效)。也可以在 /undo --dry-run 的文件清单里核对实际被追踪的范围。插件仍是实验原型,不应把它当作秘密信息保护工具。
辅助提示:会话首次追踪一个工作区时,插件会对未被 ignore 的疑似秘密文件发一次性 [Turn rewind privacy notice](见「工作区资格」的敏感文件提醒)——它是提醒而非保护。
已禁用:redo(功能冻结)
/undo --redo 已在解析层禁用:任何包含 --redo 的输入都会得到「temporarily disabled」错误,不会触发快照校验或文件恢复。
禁用原因(2026-09-03 审查报告 P0-4):redo 的执行段此前未接入 operation 记录与失败回滚,执行中任一文件恢复失败会留下「部分 redo」状态,无 needs-recovery 围栏。
底层加固已经完成并保留(未开放):redo 现在与 undo 共用同一套机制——createOperation 登记 applying operation → 单路径失败计入 notRestored 明细 → completeRedoTransaction 在单事务内结算旧 undo operation / turn / 新 operation / notice,事务失败时 redo operation 落 needs-recovery 由启动围栏拦截。重新开放只需移除 applyUndo 中的 redo 冻结闸门(parsed.redo 分支)并恢复对应测试。
Debug 安装
TS 版插件位于 packages/dsh-tauri-turnrewind-ts,构建产物在 dist/。在当前 checkout 中执行:
pnpm install
pnpm --filter dsh-tauri-turnrewind build
pnpm --filter dsh-tauri-turnrewind test
插件通过 pnpm add / dsh plugin add 安装(debug 桌面端启动时会自动以 link: 方式安装全部内部插件)。修改 Host 代码后需要重新 pnpm --filter dsh-tauri-turnrewind build 并重启 DSH debug Host 进程;修改 Client 代码后同理(client bundle 在 Host 启动时发现)。
发布分发:仓库根的 pnpm build(prebuild → build:plugins)会把本插件的 dist/ 部署进 src-tauri/resources/node_modules/,安装包启动时按 internal-plugins.json 自动预装并激活——终端用户无需 pnpm/构建/任何配置。
测试
运行插件 lint 与测试:
pnpm --filter dsh-tauri-turnrewind exec eslint src test
pnpm --filter dsh-tauri-turnrewind typecheck
pnpm --filter dsh-tauri-turnrewind test
当前 25 个测试文件、130 个测试,覆盖:Git 快照(增删改恢复、中文路径、CRLF、路径逃逸、工作区根拒绝、absent 路径上的非空目录拒绝与空目录移除、symlink 路径拒绝与快照 symlink 策略、ignore 委托、alternates 复用与自愈)、原子 bak-swap 恢复与崩溃清扫、Git 状态零污染(HEAD/branch/index/status/refs/stash 不变)、linked worktree 隔离、oversized blob 单文件报告、容量治理(保留条数过期、超限两阶段重建、不可达 loose object 回收)、账本生命周期(含 needs-recovery 围栏与跨连接 notice 单次消费、legacy schema 迁移重放、quick_check 拒载与 .bak 还原、恢复面板明细与 acknowledge 终态)、/undo --doctor 报告、pending plan 原子 claim 与预览绑定漂移校验、interrupted turn、barrier 时序、跨进程 workspace 锁、undo 入口与 redo 冻结、client 纯函数(输出解析/plan 状态判定/会话归属/通道 latest-owner-wins 语义/模态 Escape 与焦点陷阱)、敏感文件扫描过滤与提醒去重。
当前限制
这是实验性版本,正式启用前仍需要:
- 接入受控的宿主 sandbox/Tauri bridge;
- 完成真实 DSH lifecycle integration tests;
- 容量治理为两级粗粒度策略(保留条数过期 + 超限整仓重建 + 不可达 loose object 回收),尚未做按 turn 的细粒度 ref 清理;
- 实现父对话递归 undo;
- 实现消息旁 Undo 按钮;
- 明确重命名与特殊文件(submodule、设备文件)策略(symlink 与 mode/权限位已落地:mode 进快照状态、POSIX 下恢复可执行位,Windows 按 git filemode=false 约定统一 100644);
- 设置面(retention / TTL / 逐 workspace 开关)尚未提供,调优走
TURNREWIND_RETAIN_TURNS/TURNREWIND_MAX_SNAPSHOT_MB环境变量; - 重新开放 redo:移除解析层禁用分支、恢复端到端 redo 测试(底层加固已完成,见「已禁用:redo」)。
符号链接策略(P1-3)
- 工作区内指向外部的 symlink 路径:从不遍历、从不写入(
TURNREWIND_SYMLINK_UNSUPPORTED); - 快照中记录的 symlink 条目(git mode
120000):stateAt报告为unsupported,预览卡与 dry-run 标注[unsupported]并单独列出;undo 执行时跳过该路径并计入「未恢复」清单,绝不把 link target 文本伪装成普通文件写回;restorePath内有同规则双保险; - turn 把 symlink 替换为普通文件(或反向)时,类型变化按 modified 处理;含 symlink 的 undo 结果中该文件始终出现在「未恢复」明细与 notice 里,需要用户手工重建。