DeepSeek Harness plugin

dsh-rewind

Conversation rollback for the DeepSeek Harness: rewind a session to the prompt before a chosen turn (restore the files that turn changed) and continue in a fresh forked session.

Jump to install

Source facts

Repository
2501136589/dsh-rewind
Latest update
Aug 14, 2026
Category
Tools & Capabilities
GitHub stars
2
Format
plugin
Catalog evidence
Upstream dsh.bundle evidence
Evidence path
package.json#dsh.bundle
Checked against
0.1.0-rc.8
Upstream check date
2026-08-20

This evidence comes from the upstream catalog. This site has not installed, run, or security-reviewed the plugin.

Install

Start with a prompt that asks an agent to review the GitHub repository and source. Switch to the command if you want to install it yourself.

Copy this prompt into DSH, Codex, or another agent and ask it to review the GitHub repository and source first.

Do not install or run any commands yet. Read this plugin's GitHub repository, README, and relevant source code. Then answer the questions below clearly and directly so I can decide whether it fits my needs:

1. What is this plugin, and what problem does it solve?
2. Who is it for, and what are its typical use cases?
3. How is it used after installation? Include one minimal example.
4. What known limitations or privacy, security, compatibility, or maintenance risks does it have?
5. Give a clear recommendation: recommend, conditionally recommend, or do not recommend, with reasons.

Distinguish statements documented by the repository, inferences from source code, and unknowns. If evidence is insufficient, say so explicitly. Do not guess or simply repeat the README.

GitHub: https://github.com/2501136589/dsh-rewind
Plugin: dsh-rewind
Author: 2501136589

Check the source files

Read the README and other files from this plugin directory before installing.

File explorer3 files
README.mdSource · read only

dsh-rewind — 会话回退插件 (Conversation Rollback for DSH)

一个 100% 开源的 DeepSeek Harness(dsh)插件 bundle,借鉴 opencode / dsh-turn-rewind 的 OpenCode 式架构:为非 git 场景实现内容寻址的文件快照台账,并通过 DSH 官方 sessions.fork 重开一个新会话来回退对话——恢复文件 + 从选中的那一轮输入重新续起。

  • 回退重开(fork):每条用户消息下方出现一个纯图标回退按钮。点击后弹出确认弹窗,预览"回退会撤销的文件",确认后:先恢复这些文件,再通过 DSH 官方 sessions.create / sessions.fork 新建一个会话(从选中的这一轮输入之后继续),并把这条输入的原文填回新会话的输入框,方便你改完重新发送。原会话会自动归档(从会话列表消失;DSH 没有"删除单个会话"的 RPC,归档是官方机制——数据与历史仍在磁盘上,新会话继续可回退)。
  • 内容寻址快照(非 git):每个 agent turn 第一次 step 前,用递归目录遍历(不用 git)把工作区哈希成 sha256 的内容寻址快照,存进 $DSH_HOME/rewind-ledger 台账目录(内容寻址的 blob + per-turn manifest 都放在这里;rewind 的第一版原型数据与之无关,已废弃);回退时把工作区恢复到那个 turn 的快照。支持新增/删除/修改/权限/文本与二进制。

> 为什么是"重开新会话"而不是"原地折叠":DSH 的会话日志是只读追加、不可物理截断;而且 DSH 浏览器端不会根据第三方插件追加的普通 user/message + surfaceOp.replace 折叠对话。所以采取与 turn-rewind 同款的做法——恢复文件后 fork 一个新会话(原会话归档、从列表隐藏)。浏览器打开新会话时天然只显示到选中的那一轮为止,可靠且不需要 DSH 内部支持第三方 surface 折叠。

---

快速开始

安装

要求 dshpnpm 在 PATH 上。

REM 说明:%USERPROFILE% 是当前用户的 home(等价于 PowerShell 的 $env:USERPROFILE)。
REM 以下命令在 cmd 与 PowerShell 中均可运行(PowerShell 请把 %USERPROFILE% 换成 $env:USERPROFILE)。

# 1) 克隆
cd %USERPROFILE%
git clone https://github.com/2501136589/dsh-rewind.git
cd dsh-rewind

# 2) 安装进 web profile
#    推荐(无空格路径):
dsh plugin --profile web add .
#    若路径含空格导致相对锚定解析出错,则进 profile 目录直接 pnpm:
cd %USERPROFILE%\.dsh\profiles\web
pnpm add "link:<dsh-rewind 的绝对路径>"

无论哪种方式,最后都要让 @xiaoxin/dsh-rewind 出现在 web profile 的 package.json 里:

"dsh": { "profile": { "bundles": [ "@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "@xiaoxin/dsh-rewind" ] } },
"dependencies": { "@xiaoxin/dsh-rewind": "link:<路径>" }

> 注意:bundle 行必须用 - insert: 包裹在 bundle 的 cordis.patch.yml 里(本仓库已配好),否则 loader 会把它当“按 id 覆盖已有 entry”而静默忽略,导致按钮不出现。

然后完全停掉再启动 dsh web(装载器与浏览器客户端 bundle 在新进程里才注册),再 Ctrl+F5 强刷。成功后,每条已完成的助手消息旁会出现一个纯图标回退按钮。

通过 npm 安装(可选)

发布到 npm 后,可用 dsh plugin --profile web add @xiaoxin/dsh-rewind 直接安装 package 名。

常见问题

  • declares no dsh.bundle in its package.jsondsh plugin 没把这个包识别为 bundle。确认 package.json 里有 "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
  • client bundle not found:确认 exports["./client"] 指向真实存在的 lib/client.js
  • 重启了但没有回退按钮:先确认 profile 的 dsh.profile.bundles 里有 @xiaoxin/dsh-rewind;再确认 cordis.patch.yml 用的是 - insert: 包裹 loader 行(本仓库已配好)。然后完全停掉再启动 dsh web(不是只刷新页面),再 Ctrl+F5。

---

目录结构

dsh-rewind/
├─ package.json          # bundle 清单:dsh.bundle.patch + dsh.client(web)
├─ cordis.patch.yml      # 装载 host 插件的 loader 行(必须用 - insert:)
├─ lib/
│  ├─ index.js           # host 侧入口:装配快照采集、/rewind 命令与 HTTP 路由
│  ├─ snapshot.js        # 非 git 工作区遍历 + sha256 内容寻址快照/差异
│  ├─ ledger.js          # 内容寻址 blob/manifest 台账($DSH_HOME/rewind-ledger)
│  ├─ host-rewind.js     # turn 采集、消息→fork 边界解析、恢复、HTTP handler
│  ├─ client.js          # 浏览器侧:用户消息回退按钮 + 重开会话
│  └─ types/*.d.ts       # 类型声明
├─ test/                 # smoke 测试(node 直跑,无外部依赖)
├─ LICENSE               # MIT
└─ README.md

一个包同时承载 host 逻辑(dsh.bundle.patchcordis.patch.yml)和浏览器 UI(dsh.client.platform:"web" + exports["./client"])。dsh-client-modules 会扫描 loader 行里的包,发现 dsh.client.* 声明后把 lib/client.js 喂给浏览器。host 侧不依赖 git:快照靠 snapshot.js 的目录遍历 + ledger.js 的内容寻址存储。

---

用法

浏览器界面

1. 打开一个会话,在每条用户输入下方的时间/操作栏里会看到一个纯图标回退按钮(向后/撤销箭头)。 2. 点击后弹出确认弹窗:先列出"回退会撤销的文件改动"(对比选中那一轮快照与当前工作区)。 3. 确认后:插件先恢复这些文件,然后通过 DSH 官方 sessions.fork/create 新建一个会话(从选中那一轮之后继续),并把这条输入的原文填回新会话输入框。 4. 在新会话输入框里编辑这条输入并发送——agent 从已恢复文件、且只带选中轮之前历史的状态继续。原会话会被自动归档,从会话列表隐藏(数据与历史仍在磁盘上;可在 cordis.patch.yml 里设 archiveOriginalSession: false 保留原会话)。

命令方式(等价,便于脚本/无 UI 调用)

/rewind <messageSeq|messageId> [rewind|list-files|dry-run]

  • messageSeq —— 目标用户消息的 seq;也可传 messageId(会按 user/messagedata.message.id 解析)。必须是某轮的开场用户消息
  • 默认 rewind(恢复文件 + fork 新会话);list-files / dry-run 只预览不执行。
  • /rewind list(或直接 /rewind):列出本会话所有可回退的用户消息 —— 每条显示 seq | turn | 是否有快照 | 消息预览,照着抄 seq 即可。
  • ⚠️ seq 不是"第几条消息":seq 是会话日志的原始事件流水号user/messageturn/starttool/call 等所有事件共用一条递增计数器,所以用户消息的 seq 往往不连续(例如 1, 47, 92…)。/rewind list 会直接给出可用值;浏览器按钮弹窗里显示的也是这套 seq。

---

回退的语义细节(务必了解)

  • 会话与文件是两套独立的回退:文件靠内容寻址快照回退;对话靠官方 sessions.fork/create 重开新会话。两者各自可靠,不需要 DSH 内部支持第三方表面折叠。
  • 重开新会话:确认后,插件调用 apiProxy.sessions.fork({ sessionId, atSeq: 选中轮之前的 turn/end })(首轮则 sessions.create({ cwd }))新建一个会话,其历史天然只到选中的那一轮为止;然后调用 apiProxy.workspace.archiveSession 归档原会话(默认,可在 cordis.patch.yml 关掉)——DSH 没有"删除单个会话"的 RPC,归档会把原会话从侧栏列表和搜索中排除,但它的数据与台账快照仍在磁盘上,分叉后的新会话继续回退不受影响。浏览器打开新会话即看到折叠后的效果。
  • 采集边界 = turn:每个 agent turn 的第一次 step(agent/pre-stepstep===1)前,对工作区做一次快照(哈希成 sha256 内容寻址树存进台账)。这是 opencode 式"step 前打 baseline",而不是每条用户消息/每次写入。
  • 内容寻址、几乎不复制:每个文件按内容 sha256 去重存储(相同内容只存一份),台账只在 $DSH_HOME/rewind-ledger/ 下按工作区 sha256 分目录。支持新增/删除/修改/权限/文本与二进制;超大或超总量会明确失败,不静默漏。
  • 只能回退到"开启采集"之后的 turn:启用插件之前的轮次没有快照,无法回退,会明确提示。
  • 快照边界与对话边界一致:一个 turn 的"开场用户消息"才能在 Web 上回退(对应它的快照 + 它之前的 turn/end 作为 fork 点)。
  • 分叉会话也能继续回退(血缘解析):fork 出来的新会话会原样拷贝父会话的事件(seq 逐条一致)直到 header.seedLength。在这些"拷贝来的消息"上回退时,插件沿 header.parentSession 血缘链向上找到真正录制这些快照的会话,用它的台账(checkpointOwnerFor)。所以 A→回退→A2,在 A2 里继续回退 A 的消息、甚至回退 A2 自己续写的新轮次,都成立;消息在血缘链上每一层的 seedLength 之内,就继续向上找。
  • 文件无改动也能回退:回退的核心是"重开会话";当工作区已经与该快照一致时,弹窗仍提供"确认回退"(只做 fork + 填回原文),不会卡死。
  • 回退只动本会话自己的文件(跨会话隔离):多个会话共享同一工作区时,快照是"整个目录"的状态。回退范围被限制为该会话自己碰过的文件——从会话事件里的 fs 写工具调用(write/edit 一族,tool/call)∪ 该会话连续快照之间的漂移(覆盖 shell 副作用)。所以 B 会话回退时,A 会话后来改的文件不会出现在预览里、也不会被还原。会话事件完全取不到时才回退到"全量差异"(scope=workspace,旧行为)。

---

架构

Host 侧(lib/index.js + host-rewind.js + snapshot.js + ledger.js

  • 采集TurnCheckpointCoordinatorPREPENDEDagent/pre-step 监听器,在 step===1 时对 session.header.cwd 做一次快照(walkWorkspace 递归遍历,跳过 .git/node_modules/台账目录;snapshot.js 哈希成 sha256 树),并把内容寻址 blob + per-turn manifest 写入 ledger.jsLedgerStore$DSH_HOME/rewind-ledger)。绝不阻塞/否决 agent。
  • 回退/rewind <messageSeq|messageId> 命令 + 同名 /rewind HTTP 路由。

1. messageTargetFor 把目标用户消息解析为 { turn, turnStartSeq, previousTurnEndSeq }(必须是该轮的开场用户消息); 2. 用 diffTrees(快照entries, 当前树) 得到改动文件预览; 3. engine.applyRestoreFiles 把工作区恢复到快照(删新增/恢复修改/找回删除,路径做越界防护); 4. createConversationRestartapiProxy.sessions.fork(有 previousTurnEndSeq 时)或 sessions.create(首轮;先经 workspace.list 解析源会话工作区、把 workspaceId 带进载荷,避免生成"无工作区"的新会话)得到新会话 id。

浏览器侧(lib/client.js

  • apply(ctx) 注入 conversation.session.header.actions 槽,用一个 session 作用域组件经过 React portal 把回退按钮放进每条用户消息的 action 行(data-chat-flow-kind="user" + data-chat-anchor-key + data-time-hover-root)。
  • 点击弹窗 → GET /rewind?sessionId=..&messageSeq=.. 预览文件改动 → 确认后 POST /rewind 恢复文件并 fork 新会话。
  • 成功后 openSessionWithDraft: ctx.sessions.open(新会话id) + 在该会话 scopectx.conversation.input.for(scope).setDraft(原文),把这条输入原文填回新会话输入框。

为什么走官方 sessions.fork + 同源 HTTP,而不是 session.command + 表面替换

之前版本想走 session.command + surfaceOp.replace 原地折叠,但 DSH 浏览器端不渲染第三方 surface-replace,聊天永远不动。改用官方的 sessions.fork(新建会话)是平台原生支持、必定在浏览器端正确显示历史截断的方式;同源 HTTP 路由让浏览器能预览恢复前的文件改动,无需第三方 Remote 命名空间。

---

局限与后续

  • 重开会话而非原地折叠:回退会新建一个会话,并把原会话归档(从列表隐藏,默认行为,可用 archiveOriginalSession: false 关闭)。DSH 目前没有"删除单个会话"的 RPC,也没有在 UI 里取消归档的入口——归档后原会话从侧栏/搜索消失,但其数据仍在磁盘上(~/.dsh/sessions/...)。需要"物理删除"时,那要 DSH 平台支持第三方删除会话,超出本插件(也是一切第三方 bundle 的边界)。
  • 快照边界 = turn:只能在"开启采集之后"的轮次回退;首轮没有上一轮 turn/end,会走 sessions.create首轮回退会先通过 workspace.list 找到源会话所在的工作区,把新会话挂进同一工作区——不会出现"没有工作区"的新会话。
  • 工作区遍历的取舍walkWorkspace 跳过 .gitnode_modules 和台账目录,不回退这些内容;超大文件/超总量会明确失败而不是静默漏。
  • 跨会话隔离是启发式的:回退范围来自 fs 写工具调用 + 连续快照漂移。若 agent 用 shell 命令在最后一个 turn 里改文件(没有后续快照可对、也没有写工具调用),这类改动可能不被归因;会话事件缺失时整体退回"全量差异"(不隔离)。两个会话改过同一个文件时,后回退的一方可覆盖另一方的该文件改动(真正的冲突,无法自动裁决)。
  • 并发/漂移:演示级实现没做 turn-rewind 那样的恢复点细锁、rescue 点、hash 验证后 rollback 与操作日志。若用于生产,建议参考 dsh-turn-rewind 补齐这些护栏(它已含完整的 Change Ledger)。

台账维护(scripts/repair-ledger.mjs

内容寻址台账是只增不减的:采集绝不删除 blob(避免"UI 还列着某轮、blob 已被 GC"的 BLOB_NOT_FOUND)。需要瘦身时手动跑一次修复:

node scripts/repair-ledger.mjs            # 默认修 $DSH_HOME/rewind-ledger(无 DSH_HOME 则 ~/.dsh/rewind-ledger)
node scripts/repair-ledger.mjs <其他目录> # 指定台账目录

它会删除:① 包含 node_modules/.git 路径的旧时代污染 manifest;② 引用了缺失/损坏 blob 的 manifest;然后 collectGarbage 清掉不再被任何 manifest 引用的 blob。

测试

cd dsh-rewind
node test/client-smoke.mjs      # 客户端:槽注册 + openSessionWithDraft 回填 helper
node test/render-smoke.mjs      # 组件渲染:纯图标按钮 + Modal
node test/host-smoke.mjs        # host:快照采集/差异/恢复 + messageTargetFor + fork
node test/host-undo-smoke.mjs   # 端到端:恢复新增/删除/修改 + blob 哈希校验 + fork
node test/host-fork-lineage-smoke.mjs # fork 血缘:A2/A3 里回退父会话消息的台账解析
node test/host-scoped-rewind-smoke.mjs # 跨会话隔离:B 回退不碰 A 改过的文件
# 或一次跑全部:
npm test   # = 上面 6 个脚本依次执行

测试在临时工作区 + 临时 DSH_HOME 下直跑,不触网、不读真实 DSH 环境,node 即可。

---

License

[MIT](./LICENSE)