用 dsh-turn-rewind 给 DeepSeek Harness 加上对话与工作区回退

前言

用 DeepSeek Harness(dsh)跑编码智能体时,常见情况是:某一轮提示写偏了,Agent 已经改了一堆文件,对话上下文也跟着跑远。这时只靠 Git 看 diff 不够——diff 能告诉你现在改了什么,但不会按「某一条用户消息发出之前」把工作区和对话一起收回来。dsh 原生的「分支新对话」只分对话、不动项目文件,两边很容易对不上。

dsh-turn-rewind 做的就是这件事:在每条用户消息上挂一个回退入口,按 Turn 把项目文件恢复到发送该消息之前,并可选择从那一轮重新开对话。本文按社区目录页、GitHub 仓库 README / README.zh.md、package.json 与官方 Harness 仓库交叉核对后整理。

DeepSeek Harness 的官方定位是「一切皆插件」,源码在 deepseek-ai/deepseek-harness。下面介绍的社区插件目录(deepseek-harness-plugin.com)是独立站点,与 DeepSeek / 幻方没有官方从属关系,不要把它当成官方应用商店。

这是什么

dsh-turn-rewind 是一款会话与消息类插件,由 Anionex 维护,仓库为 Anionex/dsh-turn-rewind。npm 包名是 @anionex/dsh-turn-rewind,当前 package.json 版本为 0.1.1,许可证 BSD-3-Clause。截至 2026-08-17,GitHub 与目录页均显示 65 stars。

它解决的问题可以概括成一句话:给 DSH Session 加一条围绕工作区改动的安全边界——先建恢复点,Agent / 用户 / 外部程序改完工作树后,再按路径预览差异,由人按下最终按钮才真正恢复。

仓库把两层名字分得很清楚:

  • Turn Rewind:用户看到的功能名、仓库名和 Profile Bundle 名。
  • Change Ledger:底层持久恢复引擎。ctx.changeLedger 服务、磁盘格式和存储路径继续用这个名字,因为它描述的是可复用的快照与恢复层,而不只是 Web 上的回退按钮。

本仓库是标准 DSH Profile Bundle:package.json 声明 dsh.bundle.patchcordis.patch.yml 直接挂载 @anionex/dsh-turn-rewind,不修改 DSH 主仓库。

核心功能

按用户消息锚定恢复点

当 Profile 同时提供 DSH Agent 服务时,插件会在每轮第一次 agent/pre-step、Agent 开始处理用户消息之前,保存一份隐藏的文件状态。保存失败只会让该消息无法回退,不会拦住用户这一轮。

在 Web Profile 中,每条用户消息下方会在时间和原生「复制」之后多一个纯图标「回退」,提示是「恢复到发送这条消息之前」。打开后先检查已保存的文件状态,给出分页预览,再让你选择恢复方式。

两种恢复模式

模式 项目文件 对话
恢复文件并从这里继续(默认) 先自动备份当前状态,再恢复项目文件 创建并打开一个结束于所选消息之前的新 Session,并把该消息原文填回输入框
只恢复文件 同样先备份再恢复 当前 Session 保持原位,内容不变

DSH Session 日志只追加、不改写,所以「从这里继续」会新建 Session,而不是截断原对话。选中第一条消息时,Host 在同一工作目录创建空 Session;选中后续消息时,在上一轮已完成的 turn/end 处分叉。原 Session 始终保留。

如果项目文件已经是发送所选消息之前的状态,Turn Rewind 不会退化为「只开对话分支」,而是提示无需恢复,并引导使用 dsh 原生的分支新对话。仓库明确区分两者:分支新对话只分对话、保持项目文件不变;Turn Rewind 一定恢复项目文件,再由你决定要不要开新对话。

先审阅、后写入

恢复生命周期按仓库说明是这样走的:

创建恢复点
        ↓
agent / 用户 / 外部程序修改工作树
        ↓
预览逐路径变化
        ↓
审阅全部或部分恢复计划
        ↓
在回退弹窗中按下最终恢复按钮
        ↓
先建救援点 → 恢复 → 哈希验证

弹窗预览会基于当前工作树生成一份短期、会话绑定的恢复计划,这一步不改任何文件。真正修改前会再检查所选路径和仓库状态,并先创建自动备份。预览之后出现的相关变化会使计划失效。同一工作树只要还有正在运行的 Agent(包括当前来源 Session),恢复会被阻止;空闲 Session 不阻止。

插件不会自动 commit、stash、reset、切分支、改 Git index,也不会替你判断某项改动「应该回滚」。恢复后的内容可能表现为当前 HEAD 上的未提交修改,因为 commit、分支、HEAD 和 index 都保持原样。Git 正在合并、变基等操作时仍会阻止恢复。

Change Ledger 负责什么

普通 Git 面板可以展示当前 diff,但不拥有完整、持久的恢复生命周期。Change Ledger 独立负责:

  • 内容寻址的恢复点 manifest
  • Git worktree、HEAD、分支和进行中 Git 操作的状态围栏
  • 从审阅到执行之间的 stale plan 检测
  • 短期确认码与 DSH 人工批准双门槛
  • 每次恢复前自动建立救援点
  • 恢复后的内容哈希验证
  • 恢复失败后的自动回滚
  • DSH 重启时对未完成操作日志进行对账
  • 可供其他插件依赖的 ctx.changeLedger 公共服务

持久格式见仓库 docs/FORMAT.md,安全与故障模型见 SECURITY.md

安装与启用

社区目录页给出的安装命令如下,在 DeepSeek Harness 终端中运行即可:

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

dsh CLI 会从 GitHub 解析插件并安装到当前配置。如需可复现安装,目录页建议固定 commit 哈希:

dsh plugin add github:Anionex/dsh-turn-rewind#<commit>

<commit> 换成仓库里实际的 commit 哈希。目录页同时提醒:插件以当前 dsh 进程的权限运行,安装时可能执行代码。安装前请检查源代码仓库和许可证。

仓库 README 还给出了按 Profile 挂载的写法。先在检出的插件目录构建,再分别加到需要暴露该服务的 Profile:

pnpm install --frozen-lockfile
pnpm run check

dsh plugin --profile web add @anionex/dsh-turn-rewind
dsh plugin --profile headless add @anionex/dsh-turn-rewind

dsh --profile web --dump-config | grep turn-rewind

修改 Profile Bundle 后需要重启对应 DSH 进程。package.json 声明的 Node 引擎是 ^22.19.0 || >=24.0.0

默认配置可在 Profile 的 patch 层覆盖,仓库给出的字段如下:

- id: turn-rewind
  config:
    storageDir: ~/.dsh/change-ledger/v1
    maxRestorePoints: 50
    maxTurnCheckpointsPerSession: 30
    maxFiles: 20000
    maxFileBytes: 16777216
    maxSnapshotBytes: 536870912
    planTtlMs: 900000
    staleLockMs: 30000

未配置时,storageDir 使用 $DSH_HOME/change-ledger/v1,未设置 DSH_HOME 时回退到 ~/.dsh/change-ledger/v1。仓库要求它不得与被管理的 worktree 重叠。容量与用户恢复点数量限制都是 fail loud:超限会直接失败,而不是悄悄丢掉文件。自动 Turn 检查点有独立的每会话保留窗口,只清理自己最旧的检查点;用户和救援恢复点不会被静默删除。

典型用法

Web 界面回退

  1. 确认当前 Profile 已挂上 @anionex/dsh-turn-rewind,并且提供 DSH Agent 服务,这样每轮开始前才会留下可用的恢复点。
  2. 在 Web 界面找到某条用户消息,点击时间和「复制」后面的回退图标。
  3. 弹窗会列出受影响文件,可展开「查看全部文件」。文件会按实际结果显示为「恢复之前的版本」「找回文件」「移除后来新增的文件」「恢复文件权限」或「恢复之前的文件类型」。
  4. 选择「恢复文件并从这里继续」或「只恢复文件」,审阅后按下最终恢复按钮。弹窗本身就是确认流程,不再要求重复勾选。
  5. 若选择「从这里继续」,新 Session 创建失败时,Change Ledger 会自动从备份把文件恢复到操作前的状态。

Web Profile 提供同源 /turn-rewind 接口:根据所选 user/message 序号找到对应状态,分页返回文件预览,生成短期、会话绑定的恢复授权,并把新对话创建交给 dsh 官方 Host 的 create / fork 生命周期。插件绝不会自动恢复文件。

给其他插件调用 Change Ledger

其他 Cordis 插件可以注入 changeLedger,走同一套结构化 API。仓库示例:

export const inject = ['changeLedger']

export async function apply(ctx: Context) {
  const point = await ctx.changeLedger.create({
    cwd: '/absolute/git/worktree',
    sessionId: 'session-id',
    label: 'before refactor',
  })
  // point.id 是持久恢复点 ID。
}

完整格式类型从 @anionex/dsh-turn-rewind/format 导出;可信集成和测试可以从 @anionex/dsh-turn-rewind/core 使用独立 Engine。

中途崩溃时,恢复也走同一套 API:用 listRecovery 找到操作的 rescuePointId,用 inspect 审阅该救援点,再对相关路径走 planRestore / applyRestore。任何路径写入前,插件都会先创建救援点和持久 operation journal。如果 dsh 在非终态操作期间退出,下次启动会把该操作标记为 interrupted;另一个仍存活的 dsh 进程持有工作树锁时,不会误判其操作。

适用场景与注意事项

适合这类情况:

  • 编码 Agent 已经改坏工作区,希望回到某条提示发出之前,而不是只开一条新对话
  • 只想把文件收回去,当前对话先留着对照
  • 需要给其他插件提供可检查、可回滚的工作区恢复点

0.1 系列只支持普通 Git worktree,范围内包括:tracked 文件(含创建恢复点时已经缺失的 tracked 路径)、未被 .gitignore 或 Git 标准 excludes 忽略的 untracked 文件、文本和二进制普通文件、符号链接,以及可执行位等可移植权限位。

明确不支持或会直接拒绝的对象包括:sparse checkout、submodule gitlink(应分别进入每个 submodule 建恢复点)、ignored 文件、socket / 设备 / FIFO 等特殊文件、扩展属性 / ACL / 所有者 / 时间戳 / hard-link 拓扑、Git index 和仓库元数据、非 Git 目录。如果 ignored 或其他未受管理的文件占据了待恢复路径,插件会拒绝恢复,不会递归删除它。

还需要注意:

  • 插件以当前 dsh 进程权限运行,安装前应检查源码与许可证。
  • 它不沙箱化其他进程,也不能阻止外部程序同时改文件。
  • 它对同一操作系统用户不提供保密或防篡改保证;状态文件默认仅所有者可读写,但宿主用户仍被当作可信方。
  • 「从这里继续」在创建子 Session 时有 seedLength 围栏:只有所选 user/message 和对应的精确 turn/start 都低于每一层持久围栏时,才能复用祖先保存的状态;子 Session 自己的状态优先,兄弟分支绝不混用。

小结

智能体改文件是不可逆的操作,除非事先留下可验证的恢复点。dsh-turn-rewind 把这件事做成 Turn 级的显式回退:消息上挂入口,Change Ledger 管快照、审阅、救援和失败回滚,Git 的 commit / 分支 / index 保持不动。当前版本把范围收在普通 Git worktree,自动恢复被明确排除。

目录页:https://deepseek-harness-plugin.com/zh-CN/plugins/dsh-turn-rewind/

GitHub:https://github.com/Anionex/dsh-turn-rewind

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜