dsh-undo
Manifest validTurn-level file undo for DeepSeek Harness: every write and edit is snapshotted before it lands, and the model can call the undo tool to roll back all files a conversation turn changed.
dsh-undo — 按轮次回退 agent 对文件的修改
给 DeepSeek Harness 加一个文件回滚能力:每次写入或编辑落盘之前先把原内容快照
下来,模型可以调用 undo 工具把一整轮对话改动过的所有文件一次退回 —— 不是只能退
单个文件。
定位:它和「对话回退」类插件不是一回事
| 本插件 | 对话回退类(如 dsh-rewind-plugin、dsh-recall-plugin) | |
|---|---|---|
| 回滚对象 | 文件(本轮改动过的全部文件) | 对话上下文 + 文件 |
| 谁来触发 | 模型自己调 undo 工具,人也可以直接敲 /undo | 人在界面上点按钮 / 敲命令 |
| 运行环境 | host 侧插件,headless、无 Web UI 的 profile 同样可用 | 主要在 Web GUI 里用 |
| 依赖 | 无(不依赖 git) | 多数依赖 git 或会话 fork 机制 |
| 沙箱 | 不 fork shell:快照在进程内写,workspace-write 会话下无需额外放宽权限 | 取决于实现:经 shell 往工作区之外的存储写入时,会被会话沙箱拒掉 |
需要「把对话切回某条消息之前」请用上面那两个插件;本插件只做文件层,且刻意保持轻量。
沙箱表现:为什么它在受限会话里照样能记录
它的快照与索引是用 Node 的 fs 在进程内写的,不 fork shell、不调用 ctx.shell —— 所以在 workspace-write 这类受限会话里不需要任何权限放宽就能持续记录。这一点在真实环境里验证过:同一台机器上,靠 shell 调 git、把仓库放在工作区之外的实现,其 git add 在 workspace-write 下会因写工作区之外而失败(影子仓库对象数为 0);本插件在同样的会话里正常产出快照并带回轮次标记。
边界(便于评审核对,逐条对应代码):
- 快照与索引(
$DSH_HOME/undo/):fs.writeFileSync,进程内写入,不经过命令沙箱。 - 回滚既有文件:走 harness 的
ctx.fs.writeText,受文件策略约束(只读会话里会被拒 —— 这是期望行为)。 - 删除本轮新建的文件:harness 的文件系统服务没有删除接口,这一步用
fs.unlinkSync,因此不受沙箱策略约束(只读会话里同样会执行)。这是本插件唯一不经策略的写操作,回滚语义需要它:把「本轮新建」还原成「不存在」。 - 不读凭据、不发网络请求、不碰你的 git 仓库。
它怎么工作
两条互补的捕获路径,都只做旁路记录、不改变任何写入语义:
| 路径 | 位置 | 说明 |
|---|---|---|
| ① intent 钩子(主) | fs/write-intent / fs/edit-intent waterfall | DSH 的官方扩展点,能拿到 actor(callId、agent)等工具执行信息 |
| ② fs 服务包装(兜底) | writeText / editText 的实现原型 | 若 ① 因任何原因收不到事件,② 保证仍然有快照 |
三条经验性的结论,都已落在代码里(细节见 lib/index.js 注释):
- intent 钩子必须
prepend: true。 cordis 的 waterfall 语义是「不调用next()的 监听器会否决其后的整条链」,而@deepseek-ai/dsh-fs-observation-policy正是这样一个 监听器(它直接决定 intent 并返回)。profile 补丁里的插件加载晚于内置插件,所以不 prepend 的话钩子永远轮不到执行 —— 实测跑完整个 agent 会话,零事件到达。 - 服务包装是必需的冗余。 真实链路是
SandboxedFileSystem → LocalFileSystem → FileSystem,而ctx.fs每次访问都返回 新的 traceable Proxy,所以包装ctx.fs上的方法没有意义,必须包装方法所属的原型。 - 两条路径会同时命中同一次写入,因此有跨机制去重:同一路径 + 同一事件 + 250 ms 内、 且来自不同机制时只记一条。同一机制内的连续两次写入不会被误合并。
轮次是怎么分出来的
工具执行上下文里只有 callId / rootCallId / agent,没有轮次号,所以轮次边界取自
会话日志中最近一条 user/message 事件的 seq(只扫描尾部 sessionScanEvents 条,
并按 (session, seq) 缓存,代价有界)。读不到会话的写入(例如 fs 服务兜底路径)会归入
「当前正在进行的轮次」,而不是自成一派。
提供的工具
模型可调用 undo:
| 参数 | 说明 |
|---|---|
action | list = 按轮次列出快照;restore = 回滚 |
id | 回滚单个快照(单文件精细回退);给出时优先于 turn |
turn | 回滚哪一轮:last(默认)、从新到旧计数 1/2/…,或 list 显示的 turn 键 |
limit | list 时返回多少轮,默认 10 |
dryRun | restore 时只报告每个文件会变成什么,不写盘 |
人工入口:/undo 命令
模型不是唯一的使用者 —— 你也可以在输入框直接敲:
| 命令 | 作用 |
|---|---|
/undo 或 /undo list | 列出最近的轮次(每轮最多 listFilesPerTurn 条,其余省略汇总) |
/undo restore [轮次] | 回滚该轮;省略轮次 = 最近一轮,轮次用 list 里的序号(1 = 最新) |
/undo dry [轮次] | 只预演:报告每个文件会变成什么,不写盘 |
/undo id <快照id> | 只回滚单个快照(单文件精细回退) |
命令与工具共用同一套执行逻辑(命令只负责把输入解析成工具参数),所以两边行为不会漂移。
命令注册是机会式的:profile 里没有命令注册表时照常加载(只少一个人工入口),
/undo 已被别的插件占用时自动改注册为 /undo-turn,而不会加载失败。
界面按钮:助手消息动作行里的 ↶
Web GUI 里还有一个按钮:已定稿助手消息的动作行(复制、分支旁边)会出现 ↶ 回退本轮。
点击即回退最近一轮;因为回退本身也会被快照,再点一次就能撤销这次回退(等效 redo)。
只想预览时用 /undo dry。
按钮不直接碰文件,而是执行宿主命令 /undo 1 —— 所以三条路径(模型工具、/undo 命令、
界面按钮)共用同一套执行逻辑与同一份结果呈现,不存在"按钮和命令行为不一致"的可能。
实现要点(便于评审核对):
- 客户端半边是
client/client.js,由package.json的dsh.client+exports["./client"]声明; 它是手写的预构建入口(window.__ModuleLoader__.load({ id, factory }),React 与官方客户端包由宿主require提供),不需要打包步骤。 - 注册进
conversation.chat.assistant-actions(官方反馈条目用的同一插槽),order: 20排在它之后。 - 通过插槽的按会话
inject拿到绑定了sessionId的runUndo,组件不需要自己解析会话。
回滚语义
- 一轮里同一文件被多次快照(改了两遍)→ 取最早那条,即该文件在本轮开始前的状态。
- 本轮期间新建的文件 → 回滚 = 删除它。
- 回滚动作本身也会被快照 → 回滚可以再撤销(等效 redo)。
- 单个文件失败不会中止整轮:报告里逐条列出成功/失败。
安装
# 从 GitHub 安装(推荐;package.json 里的 dsh.bundle 让 dsh 自动挂载)
dsh plugin --profile web add github:ruaibeite/dsh-undo
# 或本地克隆后用脚本(自动改用桌面版自带的 CLI)
./install.sh
PROFILE_NAME=headless ./install.sh
安装后重启 dsh 生效。
存储与配置
$DSH_HOME/undo/
├── index.json # 快照索引,每条含 id/time/path/existed/bytes/turn/sessionId
└── files/<快照id> # 快照内容(逐字节)
$DSH_HOME 内部的写入会被排除(那是 harness 自身状态,不是工作区文件)。
cordis.patch.yml 里该条目可配:
| 键 | 默认 | 说明 |
|---|---|---|
maxSnapshots | 300 | 保留快照条数上限,超出淘汰最旧的 |
maxFileBytes | 4194304 | 超过此大小的文件不快照 |
onlyToolWrites | false | true 时只快照带工具 actor 的写入 |
sessionScanEvents | 400 | 为定位轮次边界而扫描的会话日志尾部条数 |
listFilesPerTurn | 10 | action: "list" 每轮最多列出多少条文件,其余用一行省略汇总(list 带上 turn 则打印该轮全量清单) |
debug | false | true 时把每个事件写入 $DSH_HOME/undo/debug.log |
限制(如实说明)
- 只回滚文件,不回滚对话。 对话层回退请用专门的插件。
- 只看得见经
ctx.fs的写入。 通过 shell 等绕过 fs 直接落盘的改动不会产生快照;不过 这些文件在被工具再次写入时,会以「当时已存在」的形态被快照。 - 超过
maxFileBytes的单个文件不快照。 - 轮次分组依赖会话日志里存在
user/message事件;取不到时退化为按工具调用分组。 - 快照只写
$DSH_HOME,不改工作区、不碰你的 git 仓库、无网络请求。
测试
npm install # 官方包从 npm 安装(devDependencies,0.2.0-rc.2 线)
npm test # 业务逻辑:mock fs/tools/agent,36 项断言
npm run test:integration # 真实 cordis 容器 + 真实 waterfall 分发 + 真实 Config 校验,15 项断言
两套测试都用隔离的 DSH_HOME(仓库内 .test-home / .cordis-test-home),跑完自动清理。
许可
MIT
Comments
Loading…
Similar plugins
by LingLambda
Context undo/redo plugin for DeepSeek Harness (dsh): roll the model context back to the last completed step and restore it again.
★ 4
↓ 133/wk
MIT
TypeScript
Aug 15, 2026
dsh plugin --profile web add dsh-undoby zoyluoblue
dsh plugin: snapshot your workspace every turn and restore the files to any earlier point. A rewind/undo for DeepSeek Harness coding sessions — your own git repo is never touched.
★ 0
MIT
TypeScript
Aug 19, 2026
dsh plugin --profile web add @zoytown/dsh-rewindby DDDMUC
Edit any user turn in DeepSeek Harness and re-run it: an inline editor rolls the conversation back through the official surface-replace contract, then re-prompts the agent with the revised text. The append-only session log is never rewritten.
★ 4
↓ 904/wk
MIT
JavaScript
Oct 9, 2026
dsh plugin --profile web add dsh-edit-turnby hu669293657
Adds deliverable-file buttons to the end of each agent turn (opens files with the OS default app) and a floating dot rail for jumping between conversation turns.
★ 0
MIT
JavaScript
Aug 29, 2026
dsh plugin --profile web add dsh-turn-toolsby sharedcare
Codex-style conversation rollback and edit-and-resend plugin for DeepSeek Harness (DSH) Web UI
★ 0
MIT
JavaScript
Sep 11, 2026
dsh plugin --profile web add conversation-rollbackby XSJUSTC
DSH (DeepSeek Harness) conversation rewind plugin - rewind to any user message, interrupt a running turn, hide the tail from chat view and model context, cancel freely before sending.
★ 11
MIT
JavaScript
Sep 22, 2026
dsh plugin --profile web add @xsj/dsh-rewind