dsh-turn-rewind

93Star6Fork4Issue1Watching

为 DSH 提供按 turn 回退的项目文件恢复:每条用户消息多一个图标按钮,自动在 agent 处理前存快照,确认后精准还原。

语言
JavaScript
License
BSD-3-Clause
分支
main
agent-rewindcordis-plugindeepseek-harnessdshdsh-pluginmarisa-pluginrestore-pointturn-rewind

安装

$ dsh plugin --profile web add github:Anionex/dsh-turn-rewind

在终端中运行以上命令,通过 dsh CLI 安装此插件。可在右上角切换 Profile。 第一次用 dsh?看这篇新手教程

一句话定位

为 DeepSeek Harness 的每条用户消息增加一个"恢复到发送这条消息之前"的按钮:点击后看到受影响文件清单,确认即可把项目文件精确还原,并可选择是否再开一个新对话从该条消息继续。

核心能力

  • 在 Web Profile 中,为每条直接用户消息在时间戳和原生复制按钮之后增加一个图标式"回退"按钮,提示文字为"恢复到发送这条消息之前"
  • 启动时通过 agent/pre-step 钩子,在 Agent 开始处理每轮用户消息之前自动捕获一次项目文件快照,作为该 turn 的隐藏恢复点;捕获失败只让该消息无法回退,不会拦截用户本次请求
  • 弹窗内支持两种回退模式:默认"恢复文件并从这里继续"(自动建新会话并把原消息文本填入输入框)或"只恢复文件"(当前对话保持不变)
  • 每次执行前先建一个 rescue 快照,执行后做哈希验证,失败自动按 rescue 快照回滚;DSH 中途崩溃时,启动时自动把未完成操作日志标记为 interrupted 并保留对应的救援点供人工恢复
  • 通过 ctx.changeLedger 公共服务让其他 Cordis 插件注入并复用同一套快照、计划、恢复与对账 API
  • Git 操作、HEAD/分支状态与所选文件在恢复计划生成与执行之间被二次校验,任何相关变化都会使计划失效,不会静默覆盖

技术实现

  • 语言: TypeScript(ESM,严格模式)
  • 关键依赖: @deepseek-ai/cordis(宿主插件框架)、react + @deepseek-ai/dsh-client-ui-primitives(Web UI)、node:fs + node:crypto(快照与内容寻址存储)、git CLI(通过子进程读取仓库事实)
  • 架构模式: 独立 DSH Profile Bundle —— package.json#dsh.bundle.patch 指向 cordis.patch.yml,在宿主启动时挂载 turn-rewind profile layer;服务端通过 ctx.provide('changeLedger', ...) 注册 Cordis 服务,客户端通过 dsh.client.inject 注册到运行时 + 对话 UI 两个包
  • 入口文件: src/index.ts(服务端 Cordis 服务注册)、src/rewind-host.ts(HTTP 接口 + turn checkpoint 协调器)、src/client/index.tsx(Web 弹窗与按钮)

适用场景

  • 用户让 Agent 或外部工具修改了项目文件后,发现方向不对想撤销,只想保留到某条消息之前的工作区状态
  • 想把项目状态退回到某轮对话之前,同时另开一个分支继续讨论,而原对话又想完整保留
  • 团队在 DSH 上跑长会话,担心 Agent 误改文件、工具执行炸了或自己手滑改动代码,需要随时能精准回退工作区而不丢对话

前置依赖与兼容性

依赖最低版本说明
DSH 宿主(Cordis)@deepseek-ai/cordis ^4.0.1package.json 仅声明 Cordis peer 依赖,具体 DSH 版本未明确
Node.js^22.19.0 || >=24.0.0engines 字段要求
git 命令任意主流版本通过 git ls-files / rev-parse / config 等子进程读取仓库事实
平台跨平台(macOS / Windows / Linux)使用 Node node:fsnode:osgit CLI;无平台特定代码
原生模块仅依赖 Node 内置模块

安装包内已声明 dsh.bundle.patch,装好即作为 profile layer 自动加载;本插件无原生模块依赖。

安装方式

dsh plugin --profile web add github:Anionex/dsh-turn-rewind

配置项

配置类型说明默认值
storageDir字符串持久化恢复点、日志、blob 的目录;不得与被管理的 worktree 重叠$DSH_HOME/change-ledger/v1,未设置 DSH_HOME 时回退到 ~/.dsh/change-ledger/v1
maxRestorePoints正整数每个工作区最多保留的用户 + 救援恢复点数量50
maxTurnCheckpointsPerSession正整数每个会话最多保留的自动 turn 检查点数量;只清理自己最旧的,用户/救援点永远不会被静默删除30
maxFiles正整数一个恢复点最多覆盖的文件数,超出直接失败20000
maxFileBytes字节数单个文件最大允许大小,超出直接失败16777216 (16 MiB)
maxSnapshotBytes字节数一个恢复点内全部常规文件累计字节数上限536870912 (512 MiB)
planTtlMs毫秒弹窗生成恢复计划后的有效期;过期失效900000 (15 分钟)
staleLockMs毫秒锁的"失主进程已死但锁文件还在"判定阈值,超过才允许接管30000 (30 秒)

所有容量与用户恢复点数量限制都采用 fail loud 策略,超限直接报错而不是自动清理。
在 profile 的 patch 层按 id: turn-rewind 覆盖,例如:config: { storageDir: "~/.dsh/change-ledger/v1", maxRestorePoints: 50, ... }

常见问题

Q: 安装后需要额外配置吗?

A: 不需要。包内已经声明 dsh.bundle.patch,cordis.patch.yml 直接挂载,装上即生效;存储目录有默认值,装好后重启 profile 即可在每条用户消息下方看到回退图标。

Q: 恢复会不会动 Git 的分支、HEAD、index、stash、commit?

A: 不会。安全契约明文规定只恢复工作区文件,不修改 Git index、分支、HEAD、stash 和 commit。已审阅过的 HEAD/分支变化也不会阻止恢复,恢复后的内容会表现为当前 HEAD 的未提交修改。

Q: 同一项目目录还有别的 Agent 在运行,能恢复吗?

A: 不能,会被阻止。恢复时会扫描其他 Agent 的工作区,只要有任何 running 状态的 Session 共用同一 worktree,本次恢复就会因 WORKSPACE_IN_USE 失败;空闲 Session 不会阻止恢复。

Q: 恢复失败会自动还原吗?

A: 是。每次执行前都会先建一个 rescue 快照;执行失败会立即按 rescue 快照回滚;如果回滚也失败,操作状态会记为 recovery-required,可通过 listRecovery 找到对应的救援点做人工处理。

Q: 支持哪些 Git 仓库?哪些不支持?

A: 仅支持普通 Git worktree。sparse checkout、submodule gitlink、ignored 文件、socket/设备/FIFO 等特殊文件、扩展属性/ACL/所有者/时间戳/hard-link 拓扑、Git index 和仓库元数据都不在快照范围,遇到这些情况创建恢复点会直接失败。

Q: 其他 Cordis 插件能调用这个能力吗?

A: 可以。插件通过 inject: ['changeLedger'] 拿到 ctx.changeLedger,就可以调用 create / list / inspect / planRestore / applyRestore / delete / listRecovery 这套完整生命周期 API;完整类型从 @anionex/dsh-turn-rewind/format 导出,引擎本身可从 @anionex/dsh-turn-rewind/core 单独使用。

Q: DSH 中途崩溃,未完成的恢复操作会怎样?

A: 启动时自动对账:把那些 workspace 锁未被持有的非终态操作日志标记为 interrupted;若另一个仍存活的 DSH 进程持有 workspace 锁,则不会误判。救援点始终作为普通可检查的恢复点保留,直到显式删除。

Q: 恢复时会重写 Git 历史吗?

A: 不会。仓库元数据(index、分支、HEAD、stash、commit)全程保持原样,插件只读取 git ls-files / rev-parse 等命令的事实信息用于校验,不会发起 commit / stash / reset / checkout / branch 等任何写操作。

上手难度

入门 — 用户视角只需安装后重启 profile,无需任何配置或命令;Agent 开发者要调用 ctx.changeLedger 才需要看 Service API。

已知问题与限制

  • 不支持 sparse checkout 工作树,创建恢复点会直接失败(sparse 路径在快照中无法定义)
  • 不支持 submodule:遇到任何 gitlink 时整棵工作树被拒绝,要求进入每个 submodule 单独建恢复点
  • 不跟踪 ignored 文件、特殊文件类型(socket/设备/FIFO)、扩展属性/ACL/所有者/时间戳/hard-link 拓扑,这些对象的恢复失败而非静默丢弃
  • 恢复时如果目标路径下有被 ignored 或未管理的文件占据,会直接拒绝覆盖(避免递归删除工作树内容)
  • 拒绝穿过符号链接父目录,也拒绝覆盖非空目录
  • 同一 worktree 有任何 running 状态的 Agent 时,本次恢复被 WORKSPACE_IN_USE 阻止;空闲 Session 不阻止
  • 处于 merge / rebase / cherry-pick / revert / bisect 等进行中 Git 操作时,本次恢复被阻止
  • 默认容量上限(maxFiles=20000maxFileBytes=16 MiBmaxSnapshotBytes=512 MiB) 偏保守,工作区超过上限需主动调大
  • 同一轮"恢复文件并从这里继续"如果新对话创建失败,插件会自动从救援点回滚文件;但回滚再失败的话需要走 listRecovery 人工处理