为 DSH 提供按 turn 回退的项目文件恢复:每条用户消息多一个图标按钮,自动在 agent 处理前存快照,确认后精准还原。
- 语言
- JavaScript
- License
- BSD-3-Clause
- 分支
- main
安装
$ 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(快照与内容寻址存储)、gitCLI(通过子进程读取仓库事实) - 架构模式: 独立 DSH Profile Bundle ——
package.json#dsh.bundle.patch指向cordis.patch.yml,在宿主启动时挂载turn-rewindprofile 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.1 | package.json 仅声明 Cordis peer 依赖,具体 DSH 版本未明确 |
| Node.js | ^22.19.0 || >=24.0.0 | engines 字段要求 |
git 命令 | 任意主流版本 | 通过 git ls-files / rev-parse / config 等子进程读取仓库事实 |
| 平台 | 跨平台(macOS / Windows / Linux) | 使用 Node node:fs、node:os、git 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=20000、maxFileBytes=16 MiB、maxSnapshotBytes=512 MiB) 偏保守,工作区超过上限需主动调大 - 同一轮"恢复文件并从这里继续"如果新对话创建失败,插件会自动从救援点回滚文件;但回滚再失败的话需要走
listRecovery人工处理
Message-anchored project-file recovery for DeepSeek Harness, with an option to restart from the restored request.
Turn Rewind is the user-facing feature, repository, and Profile Bundle name. Change Ledger is the durable restore engine underneath it: the ctx.changeLedger service, on-disk format, and storage path keep that name because they describe the reusable snapshot and recovery layer rather than the Web action alone.
Change Ledger gives a DSH session an explicit safety boundary around workspace mutations:
create restore point
↓
agent / user / external tools modify the worktree
↓
preview exact path-level drift
↓
review a full or selective restore plan
↓
press the final restore button in the rewind dialog
↓
create rescue point → restore → verify
It never commits, stashes, resets, switches branches, edits the Git index, or decides automatically that a change should be reverted.
Preview
Rewind appears as an icon-only third action under each user message, after its timestamp and native Copy action:

Opening it shows the affected files and offers two choices: restore the files and restart from before that message, or restore only the files:

Why it has a Change Ledger engine
A diff button can show current changes, but it does not own a durable restore lifecycle. Change Ledger owns:
- content-addressed restore-point manifests;
- Git worktree, HEAD, branch, and in-progress-operation fences;
- stale-plan detection between review and mutation;
- exact two-step confirmation plus DSH human approval;
- automatic pre-restore rescue points;
- post-restore hash verification;
- rollback after a failed restore;
- startup reconciliation of interrupted restore journals;
- a public
ctx.changeLedgerservice that other plugins can consume.
The durable format is documented in docs/FORMAT.md. The security and failure model is documented in SECURITY.md.
Safety contract
- Explicit only: nothing is restored automatically — every restore starts from the user pressing the final button in the Web dialog, or from an explicit call through the service API.
- Read before write: the dialog preview generates an expiring, session-bound plan from the current tree and changes no files.
- Human gate: the dialog's reviewed impact plus the final restore button is the human decision; direct mutation requests without a live session-bound plan pair fail closed.
- Rescue before mutation: every restore captures the current eligible tree as a durable rescue point before changing a path.
- No silent omission: unsupported submodules, sparse checkouts, oversized files, aggregate limits, and unsupported file types fail point creation.
- No path escape: every durable path is canonical and workspace-relative; restore refuses symlink parents and non-empty directory replacement.
- No stale overwrite: selected paths and the reviewed HEAD/branch/operation fence are checked again at apply time. Any relevant post-review change invalidates the plan.
- No Git control-plane mutation: the index, branch, HEAD, stash, and commits remain untouched.
Scope
Version 0.1 intentionally supports normal Git worktrees only:
- tracked files, including currently missing tracked paths;
- untracked files not excluded by
.gitignoreor other standard Git excludes; - regular files, binary or text;
- symbolic links;
- executable and other portable permission bits.
The following are rejected or deliberately outside the snapshot:
- sparse checkouts;
- submodule gitlinks (create a restore point inside each submodule instead);
- ignored files;
- special files, sockets, devices, and named pipes;
- extended attributes, ACLs, ownership, timestamps, and hard-link topology;
- the Git index and repository metadata;
- non-Git directories.
If an ignored or otherwise unmanaged file occupies a path that restoration would replace, the restore fails rather than deleting it.
Install
Build the checked-out plugin, then add it to each DSH profile that should expose the service:
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
Restart a running profile after changing its bundle list.
The package is a DSH Profile Bundle. package.json declares dsh.bundle.patch, and cordis.patch.yml mounts @anionex/dsh-turn-rewind without a DSH core patch.
When the profile also provides the DSH Agent service, the plugin captures a hidden checkpoint in the first agent/pre-step waterfall before the Agent processes the opening user message. Capture failures are reported but do not reject the user's turn; the corresponding message simply has no usable rewind point. In Web profiles, the same-origin /turn-rewind endpoint resolves the selected user/message sequence, exposes a paged file preview, mints a short-lived session-bound restore plan, and delegates child creation to DSH's official Host create/fork lifecycle. It never restores files automatically.
User flow
In the Web profile, each direct user message gains a compact, icon-only Rewind action after its timestamp and native Copy control. The tooltip reads “Return to before sending this message.” Opening Rewind checks the saved file state, shows a concise preview with a “view all files” action, and offers two modes:
| Mode | Code | Conversation |
|---|---|---|
| Restore files and restart (default) | Restores the project files after automatically backing up their current state. | Creates and opens a Session ending before the selected message, then puts that message's text back in the composer. |
| Restore files only | Restores the project files after automatically backing up their current state. | Leaves the current Session open and unchanged. |
The dialog itself is the confirmation: there is no duplicate checkbox. It describes each file as restoring an earlier version, finding a deleted file, removing a later-added file, or restoring permissions/type. If the project files already match the state before the selected message, Turn Rewind performs no action and directs the user to the native Branch button for conversation-only branching.
Before mutation, Turn Rewind rechecks the selected files and repository state, then creates an automatic backup. Changes made after preview invalidate the operation. Any running Agent using the same worktree, including the source Session, blocks restoration; idle Sessions do not block. A reviewed HEAD or branch difference does not block restoration: commits, refs, branch, and index remain unchanged, so restored content may appear as ordinary uncommitted changes against the current HEAD. An in-progress Git operation still blocks. If child creation fails after “restore and restart,” Change Ledger automatically restores the pre-operation files from the backup.
DSH Session logs are append-only, so “restart” creates a new Session instead of truncating the original. For the first message, the Host creates a blank Session in the same working directory; for later messages, it forks at the previous completed turn/end. A child may reuse an ancestor's prompt checkpoint only while both the selected user/message and its exact turn/start remain inside every durable seedLength fence. Direct child checkpoints take priority and sibling checkpoints never mix. Branch creates only a conversation branch and keeps project files unchanged; Turn Rewind always restores project files, optionally followed by a new conversation with the selected prompt restored to the composer. The original Session is always retained.
Configuration
Override configuration in the profile patch layer:
- id: turn-rewind
config:
storageDir: ~/.dsh/change-ledger/v1
maxRestorePoints: 50
maxTurnCheckpointsPerSession: 30
maxFiles: 20000
maxFileBytes: 16777216
maxSnapshotBytes: 536870912
planTtlMs: 900000
staleLockMs: 30000
All size and user-point retention limits fail loudly. Automatic turn checkpoints have a separate per-session retention window and prune only their own oldest checkpoints; user and rescue restore points are never silently pruned. When omitted, storageDir resolves to $DSH_HOME/change-ledger/v1 and falls back to ~/.dsh/change-ledger/v1; it must not overlap the managed worktree.
Recovery
Before writing any path, a restore creates a rescue point and a durable operation journal. If DSH stops with a non-terminal journal, the next plugin startup marks it interrupted unless another live DSH process still owns that workspace lock.
Recovery uses the public ctx.changeLedger service API: listRecovery finds the operation's rescuePointId, inspect reviews that rescue point, then planRestore/applyRestore handle the affected paths. Rescue points remain ordinary, inspectable restore points until explicitly deleted.
Public service
Other Cordis plugins can inject changeLedger and call the same lifecycle through the structured service 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 is a durable restore-point id.
}
The complete exported types are available from @anionex/dsh-turn-rewind/format; the engine is available from @anionex/dsh-turn-rewind/core for non-Cordis tests and trusted integrations.
Development
pnpm install --frozen-lockfile
pnpm run check
The test suite creates real temporary Git repositories and covers full/selective restore, stale plans, ignored-path collision refusal, HEAD drift, rescue rollback, crash reconciliation, active-lock preservation, durable-state integrity, symlinks, size limits, sparse checkouts, submodules, deletion, and blob garbage collection.
About
DSH Turn Rewind is maintained by anionex. If you would like to follow my future work, follow me on X or GitHub.
License
BSD-3-Clause. See LICENSE.