# @zoytown/dsh-rewind

[English](README.md) | 中文

**把文件放回去。** 一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件：
每一轮结束时自动保存工作区，agent 改坏了就把文件恢复到任意一个更早的时间点 —— 在设置页点，或者用 `/rewind`。

你自己的 git 仓库不会被改动。不是 git 仓库的目录同样受保护。

![dsh 设置面板中的「回溯」页，按时间倒序列出 DeepSeek Harness 会话的每一轮、改动了几个文件，以及恢复按钮](assets/settings-rewind.webp)

---

## 为什么需要它

agent 连续改了六轮。第 3 轮是对的，第 4 到第 6 轮越改越糟。你想要第 3 轮的状态。

`git` 帮不上忙：你从第 1 轮之后就没提交过，而 agent 的中间状态本来就从来不是 commit。
`git checkout .` 会把所有改动一起丢掉，包括那些改对了的部分。

harness 本身不保存工作区文件 —— 它内建的 `dsh-session-checkpoint-policy` 管的是**会话日志**何时落盘，
与你的文件无关。

这个插件补的就是这块空白。

## 它做什么

- **每一轮都保存。** 一轮结束时把工作区提交进插件自己的影子仓库。没有改动的轮次只花一次 tree 比对，
  不会产生多余的条目。
- **可以恢复到任意一轮。** 之后修改过的文件被还原，之后新增的文件被删除 —— 工作区是**变成**那个状态，
  而不是把那个状态叠上去。
- **每次恢复都可以再撤销。** 被覆盖掉的状态会先存一份，所以恢复错了，再恢复上面那一条就回来了。
- **绝不碰你的 git。** 快照放在 `$DSH_HOME/rewind/`，不在你的 `.git` 里。你的 index、`HEAD`、stash、
  reflog、`gc` 都在影响范围之外；非 git 目录也享受同样的保护。

## 安装

```bash
npx -y @deepseek-ai/dsh plugin --profile web add @zoytown/dsh-rewind
```

然后重启 `dsh`，设置面板左侧会出现「**回溯**」入口。

需要机器上有 `git`。没有的话插件会提示快照已暂停，dsh 的其他功能不受任何影响。

卸载：

```bash
npx -y @deepseek-ai/dsh plugin --profile web remove @zoytown/dsh-rewind
```

快照不会随插件一起消失；要回收空间就删掉 `$DSH_HOME/rewind/`。

## 怎么用

### 设置页

设置 → **回溯**，按时间倒序列出当前工作区的每一个保存点，带上改了几个文件、跑了哪些工具。
点「恢复」会先弹确认，明确告诉你之后新增的文件会被删除，确认后才动手。

徽章标的是**你的文件当前等同于哪个快照** —— 恢复之后它会移到被恢复的那一条上，而不是一直待在最新那条。

![dsh 回溯插件的确认对话框，恢复前明确告知之后新增的文件会被删除](assets/confirm-restore.webp)

### `/rewind` 命令

不消耗 token，不触发模型回合 —— 由 harness 直接处理，不会发给模型。

| 命令 | 作用 |
|---|---|
| `/rewind` | 恢复到本会话的上一轮 |
| `/rewind 3` | 恢复到第 3 轮结束时 |
| `/rewind a1b2c3d4` | 按 id 恢复（唯一前缀，4 位以上即可） |
| `/rewind list` | 列出本工作区的快照 |
| `/rewind save` | 立刻保存当前状态（不必等一轮结束） |

恢复完成后会打印被覆盖状态的 id，用 `/rewind <那个 id>` 就能撤销这次恢复。

## 哪些文件会被保存

快照尊重你的 `.gitignore`，所以被忽略的文件 —— `.env`、构建产物、`node_modules` ——
**既不会被保存，也不会被恢复**。这既让快照保持小体积，也意味着恢复操作不会动这些文件。

在此之外，下面的 `excludes` 列表对**每一个**工作区都生效，无论它是不是 git 仓库。
它的存在是为了让没有忽略规则的目录不会把 `node_modules` 整个吞进快照，但它**不是条件性的**：
一个刻意把 `dist/` 或 `build/` 提交进 git 的项目，这些文件同样不会被保存。
只想依赖自己的 `.gitignore` 就把 `excludes` 设成 `[]`。

`.git` 本身永远被排除，且不可配置：把它存进快照会让每个快照的体积翻倍，
而恢复它会用一份过期的副本覆盖掉你真正的提交历史。

## 配置

编辑 profile 的 `cordis.yml` 里 `dsh-rewind` 那一行：

```yaml
- id: dsh-rewind
  name: '@zoytown/dsh-rewind'
  config:
    gitTimeoutMs: 60000
    retentionDays: 30
    maxSessions: 50
    listLimit: 200
    exposeTool: 'off'
```

| 配置项 | 默认值 | 含义 |
|---|---|---|
| `gitTimeoutMs` | `60000` | 单条 git 命令的超时。超大工作区可以调高。 |
| `excludes` | 常见依赖与产物目录 | 在你的 `.gitignore` 之外额外不保存的路径，对每个工作区都生效。设成 `[]` 则只依赖你自己的 `.gitignore`。 |
| `retentionDays` | `30` | 最新快照早于这个天数的会话会被清理。 |
| `maxSessions` | `50` | 每个工作区保留多少个会话，按时间倒序。 |
| `listLimit` | `200` | 一次最多展示多少条快照。 |
| `exposeTool` | `'off'` | 模型能不能用回溯。见下。 |

### 让 agent 自己回溯

`exposeTool` 默认是 `'off'`，也就是回溯只属于你。

| 取值 | agent 可以 |
|---|---|
| `'off'` | 什么都做不了 —— 回溯只归你 |
| `'read-only'` | 查看自己的历史（`rewind_list`） |
| `'full'` | 还可以恢复工作区（`rewind_restore`） |

`'full'` 确实有用 ——「这条路走不通，把文件放回去重来」是再多提示词也替代不了的止损动作；
但也确实有风险：agent 判断失误就可能丢掉你想留的工作。
每次恢复仍然会先保存被覆盖的状态，所以是可撤销的，但开不开这个开关，应该是你自己的、明确的决定。

## 常见问题

### agent 刚把我的文件改坏了，怎么撤销？

输入 `/rewind`，工作区会恢复到上一轮结束时的状态。想回到具体某一点，用 `/rewind list` 看所有保存点，
再用 `/rewind 3` 恢复到第 3 轮。

### 它会往我的 git 仓库里提交东西吗？

不会。快照存在 `$DSH_HOME/rewind/` 下一个独立的仓库里，你的 `.git` 从不会被写入。
你的 index、`HEAD`、stash、reflog 都不变，`git status` 的输出和之前一模一样。

### 我的项目不是 git 仓库，能用吗？

能。影子仓库是插件自带的，你的目录不需要是 git 仓库 ——
而且这恰恰是回溯保护得最好的场景，因为这种情况下本来就没有别的东西在保护你。

### 回溯会把对话也一起撤销吗？

不会。回溯**只恢复文件**。DeepSeek Harness 的会话日志在设计上是 append-only 的，聊天记录原样保留 ——
所以恢复之后 agent 仍然记得它做过什么，这通常正是你想要的：让它换一种思路重来，而不是失忆。

### 装了但设置里没有「回溯」，怎么办？

先重启 `dsh` —— 插件在启动时挂载。还是没有的话，跑
`npx -y @deepseek-ai/dsh --profile web --dump-config`，确认输出里有 `dsh-rewind` 这一行；
没有就说明这个包被当成普通依赖装上了，而不是作为 bundle 生效。

### 会拖慢每一轮吗？

不会。快照在一轮结束之后才跑，且不会被 agent 循环 await；
没有改动文件的轮次只花一次 tree 比对，什么都不存。

### 占多少磁盘？

快照像 git commit 一样共享存储：没变的文件只存一份。
一个工作区的成本大致是「它的一份副本 + 之后的改动」。
超过 `retentionDays`（默认 30 天）的会话会被自动删除。

### agent 能自己回溯吗？

只有你打开它才能。`exposeTool` 默认是 `'off'`；设成 `'full'` 让 agent 能恢复工作区，
设成 `'read-only'` 则只让它看历史、不能恢复。

## 已知限制

- **被 `.gitignore` 忽略的文件不保存也不恢复。** agent 改坏了 `.env` 或 `node_modules` 里的东西，
  回溯救不回来。
- **恢复不影响对话。** 文件会回去，聊天记录不会。
- **保留策略是按会话粒度，不是按单个快照。** 一个特别长的会话会保留它的全部快照；
  限额到了是整个会话一起删。
- **0.1 不支持沙箱 / 远端 subprocess provider。** 影子仓库建在 dsh 所在的机器上，
  而在别处执行工具的 provider（如 E2B）会导致快照打在错误的文件系统上。本版本面向默认的本地 provider。
- **必须装有 `git`。** 没有的话页面会显示快照已暂停，dsh 其他功能完全不受影响。
- **嵌套 git 仓库里的文件不会被保存。** 子仓库是按 git 记录子仓库的方式记的 —— 一个指针，不是文件本身，
  所以回溯既不保存也不恢复它里面的任何内容。
- **从设置页或 `/rewind` 发起的恢复，在 agent 跑一轮的过程中会被拒绝。** 这两条路径会等 agent 空闲。
  可选开启的 `rewind_restore` 工具是刻意的例外：它由 agent 在自己的回合内调用，所以不等待
  （改用「禁止与其他工具调用并行」来防并发写）。
- **恢复过程被中途打断（git 超时、进程被杀）可能让工作区停在两个状态之间。**
  safety 快照总是先写，所以不会丢东西：重跑一次恢复，或恢复那条 safety 记录，就能收敛。
  但不要把「恢复报错」理解成「磁盘上一定什么都没动」。
- **`excludes` 对每个工作区都生效，不只是对没有 `.gitignore` 的目录。**
  把 `dist/` 或 `build/` 提交进 git 的项目，这些文件不会被保存也不会被恢复，除非把 `excludes` 设成 `[]`。

## 实现方式

每个工作区在 `$DSH_HOME/rewind/<key>/repo.git` 下有一个 git 仓库，用 `--work-tree` 指向你的目录。
保存走 `add` → `write-tree` → `commit-tree` → `update-ref`，**只往影子仓库里写**：
不碰工作区任何文件，也不移动 `HEAD`。恢复走 `read-tree -m -u`，让工作区**等于**那个快照 ——
包括删掉快照里没有的文件。

每条 git 命令都关掉了 hooks、关掉了签名、屏蔽了你的全局 excludes，
所以你仓库自己的 hook 永远不会被触发，也不会突然向你要签名密码。
git 通过 harness 的 subprocess 服务执行，因此它总是跑在你的文件真正所在的地方。

不同会话在同一个工作区仓库里是各自独立的链，既共享存储，又能单独恢复、单独过期。

## 隐私

没有任何数据离开你的机器。插件没有遥测，也不发起任何对外网络请求。

它的两条 HTTP 路由是给设置页用的本机接口：要求请求既来自回环网卡，**又**以回环名称访问服务器
（`Host` 头）—— 后一项正是 DNS rebinding 页面无法满足的。恢复接口还额外要求 POST 并拒绝跨站标记的请求。
当然，任何已经能在你机器上以你的身份执行代码的东西都能访问它们，就像它同样能访问 dsh 本身一样。

## 事实核验

以上描述已于 **2026-08-19** 在 DeepSeek Harness `0.1.0-rc.6`/`0.1.0-rc.7` 与 git `2.50.1` 上实测，
包含一次从设置页发起的完整「保存 → 恢复 → 撤销」流程。
DeepSeek Harness 处于 developer preview，harness 升级后请重新核对这些结论。

## 环境要求

- dsh 运行工具的环境里有 `git`
- Node `^22.19 || >=24`
- DeepSeek Harness（developer preview，会有破坏性变更）

## 许可

MIT
