dsh-rewind-plugin 0.20.0-alpha.4 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -34,19 +34,17 @@ Every user message carries a **↶ rewind** button in its action row. Clicking i
34
34
 
35
35
  ## Install
36
36
 
37
- Check your local DSH version, then find the matching plugin version in
38
- [Releases](https://github.com/SiriLee/dsh-rewind/releases).
39
-
40
37
  **Command line**:
41
38
 
42
39
  ```sh
43
- dsh plugin --profile <name> add dsh-rewind-plugin@<version>
40
+ dsh plugin --profile <name> add dsh-rewind-plugin
44
41
  ```
45
42
 
46
- **Graphical interface**: the **Plugins** page in the sidebar → **Add plugin** → enter `dsh-rewind-plugin@<version>` → install → restart DSH.
43
+ **Graphical interface**: the **Plugins** page in the sidebar → **Add plugin** → enter `dsh-rewind-plugin` → install → restart DSH.
44
+
45
+ <img src="assets/screenshots/add-plugin.png" width="600" alt="Add plugin dialog: enter dsh-rewind-plugin in the field"><br>
47
46
 
48
- <img src="assets/screenshots/add-plugin.png" width="600" alt="Add plugin dialog: enter dsh-rewind-plugin@version in the field"><br>
49
- <sub>The screenshot is an example only — use the version that matches your DSH version.</sub>
47
+ The default install is `latest`, which targets the latest stable DSH release. If you are on an older or alpha DSH version, find the matching plugin version on the [Releases](https://github.com/SiriLee/dsh-rewind/releases) page and install that version explicitly.
50
48
 
51
49
  > [!WARNING]
52
50
  > The npm name `dsh-rewind` belongs to another author's package — install with `dsh-rewind-plugin`.
@@ -130,21 +128,21 @@ The whole design rests on two principles, simple but deliberate: **the conversat
130
128
 
131
129
  ### 2. File restore: lightweight checkpointing, "before-write backup"
132
130
 
133
- The file half follows Claude Code's checkpoint semantics — **partial tracking + before-write backup, plus a re-scan of tracked files at each message**, not a whole-tree snapshot. This trade-off saves space, and it's actually more complete:
131
+ The file half follows Claude Code's checkpoint semantics — **partial tracking + before-write backup, plus a re-scan of tracked files at each message**, not a whole-tree snapshot. This trade-off saves space while still covering external changes to tracked files:
134
132
 
135
133
  - **Before-write backup**: tracks only the write-class tools (`write`, `edit`) — backs up the original content before a write and records/tracks the files it touches; it never backs up the whole workspace, so it's lightweight; **a single file that is too large is not backed up**.
136
134
  - **External changes count too**: at every user-message boundary the plugin re-checks all tracked files — external changes such as a command run or a manual edit are recorded as well and restored by a later rewind. "Lightweight" but not "incomplete".
137
135
  - **Unchanged-not-recorded**: an entry is written only when something changed — at the message-boundary re-check, an unchanged file is never backed up (no record); at before-write time, when the new content matches the path's prior record, only a **link to it** (`ref`) is stored instead of a copy.
138
- - **Accurate restore**: backups are the sole standard, checked against the real disk — **only files that actually differ are touched**. Backups are stored byte for byte, so the restored result matches the backups exactly, with no "ghost impact". There is a limit, though: [File-rewind tracking boundary](docs/compat/tracking-boundary.md).
136
+ - **Accurate restore**: backups are the sole standard, checked against the real disk — **only files that actually differ are touched**. Backups are stored byte for byte, so the restored result matches the backups exactly, with no "ghost impact". Coverage is bounded by design: [File-rewind tracking boundary](docs/compat/tracking-boundary.md).
139
137
  - **Safety and integrity**: paths are sanitized so nothing ever escapes the backup root; symlinks / hard links are skipped so one restore can't clobber another name of the same file; a per-file failure never aborts the pass; backups and the restore journal are written atomically and kept across restarts, so a half-applied restore after a crash can be continued or rolled back.
140
138
 
141
139
  > **Design highlight**: this checkpoint's light footprint comes from **recording only what was actually touched and really changed** — before-write backup makes it restorable, unchanged-not-recorded and content-as-link drop the repetition; only the files that differ are touched at restore time.
142
140
 
143
- ## What it deliberately does NOT do
141
+ ## Scope and division of labour
144
142
 
145
- This plugin deliberately stays lightweight and focused on one thing — "conversation rewind". The following are **out of its scope**:
143
+ This plugin deliberately stays lightweight and focused on one thing — "conversation rewind". The following are **out of its scope**, and each has a better owner (see [design boundaries](docs/boundaries.md)):
146
144
 
147
- - **Whole-tree / Git-level snapshots** — only write-class tool edits plus external changes to already-tracked files are backed up; files never touched by a tool are not restored. For a worktree-level full snapshot rollback, use a more specialized snapshot tool (git).
145
+ - **Whole-tree / Git-level snapshots** — only write-class tool edits plus external changes to already-tracked files are backed up; files never touched by a tool are not restored. Workspace history — any file, any moment — belongs to Git; the agent already uses it, and the plugin does not rebuild that layer. For a worktree-level full snapshot rollback, use Git or the snapshot tool the project already uses.
148
146
  - **Subagent edits** — not tracked, and no rewind inside a subagent session (same as Claude Code): a subagent runs its own session, so its backups could never be restored by a rewind of the parent session, and none are kept for one.
149
147
  - **Fork / branch rewind** — DSH already provides this ("branch in new chat"); no need to reinvent the wheel.
150
148
 
@@ -158,12 +156,10 @@ This plugin deliberately stays lightweight and focused on one thing — "convers
158
156
  > This project and DeepSeek Harness are both in developer preview. Pin exact
159
157
  > versions in reproducible environments and review the behavior notes above.
160
158
 
161
- ## Known issues
159
+ ## Known limitations
162
160
 
163
- 1. **Exported logs are complete** — `/export` exports the whole session log (raw JSONL), **withdrawn content included**; this plugin cannot alter exports.
164
- 2. **Lightweight file rewind has a cost** — in specific cases not all changes can be rewound. Consistent with Claude Code. See: [File-rewind tracking boundary](docs/compat/tracking-boundary.md).
165
- 3. **Turn navigation keeps rewound turns** — the right-side turn navigation keeps an "unloaded" tick for each rewound turn, and hovering previews the withdrawn content. Only a display difference; no functional impact.
166
- 4. **A far rewind may show a blank transcript** — when the target predates the loaded history, the conversation area goes blank; click **Load earlier** to page back until the history appears.
161
+ 1. **Turn navigation keeps rewound turns** — the right-side turn navigation keeps an "unloaded" tick for each rewound turn, and hovering previews the withdrawn content. Only a display difference; no functional impact.
162
+ 2. **A far rewind may show a blank transcript** — when the target predates the loaded history, the conversation area goes blank; click **Load earlier** to page back until the history appears.
167
163
 
168
164
  > [!NOTE]
169
165
  > Browser diagnostics are available; see [Browser diagnostics](docs/compat/diagnostics.md).
@@ -185,7 +181,7 @@ node scripts/verify-host.mjs # end-to-end verification of the built artifact
185
181
 
186
182
  `prepack` runs the full build, so `npm pack` / `npm publish` always ship a complete `lib/`. Install from npm or a packed tarball — a git checkout has no built `lib/`.
187
183
 
188
- Maintainers: the module map and harness interface reference live in [docs/harness-reference.md](docs/harness-reference.md).
184
+ Maintainers: the source layout and architecture are in [docs/architecture.md](docs/architecture.md); the harness interface reference is in [docs/harness-reference.md](docs/harness-reference.md).
189
185
 
190
186
  Contributing guide: [CONTRIBUTING.md](CONTRIBUTING.md).
191
187
 
package/README.md CHANGED
@@ -34,18 +34,17 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
34
34
 
35
35
  ## 安装
36
36
 
37
- 确认本机 DSH 版本后,在 [Release](https://github.com/SiriLee/dsh-rewind/releases) 找到适配的插件版本。
38
-
39
37
  **命令行**:
40
38
 
41
39
  ```sh
42
- dsh plugin --profile <name> add dsh-rewind-plugin@<版本>
40
+ dsh plugin --profile <name> add dsh-rewind-plugin
43
41
  ```
44
42
 
45
- **图形界面**:侧栏「插件」页 →「添加插件」→ 填 `dsh-rewind-plugin@<版本>` → 安装 → 重启 DSH。
43
+ **图形界面**:侧栏「插件」页 →「添加插件」→ 填 `dsh-rewind-plugin` → 安装 → 重启 DSH。
44
+
45
+ <img src="assets/screenshots/add-plugin.png" width="600" alt="「添加插件」弹窗:在输入框填入 dsh-rewind-plugin"><br>
46
46
 
47
- <img src="assets/screenshots/add-plugin.png" width="600" alt="「添加插件」弹窗:在输入框填入 dsh-rewind-plugin@版本号"><br>
48
- <sub>图片仅为示例,请以实际适配版本号为准。</sub>
47
+ 默认安装 `latest`,它适配 DSH 最新稳定版本。若使用的是旧版或内测版 DSH,请在 [Releases](https://github.com/SiriLee/dsh-rewind/releases) 页面找到适配的插件版本,并指定版本安装。
49
48
 
50
49
  > [!WARNING]
51
50
  > npm 上的 `dsh-rewind` 属于其他作者,请用 `dsh-rewind-plugin` 安装。
@@ -116,23 +115,23 @@ rm <dsh home>/snapshot-cleanup-last-sweep.json
116
115
 
117
116
  ### 2. 文件还原:轻量检查点,「写前备份」
118
117
 
119
- 文件部分对齐 Claude Code 的检查点语义——**局部追踪、写前备份 + 每条消息重扫已跟踪文件**,而不是整树快照。这项取舍既省空间,又更完整:
118
+ 文件部分对齐 Claude Code 的检查点语义——**局部追踪、写前备份 + 每条消息重扫已跟踪文件**,而不是整树快照。这项取舍省空间,也让已跟踪文件的外部改动同样可还原:
120
119
 
121
120
  - **写前备份**:只追踪写类工具(`write`、`edit`),写前**备份原内容**,并**记录、追踪**被处理的文件——从不备份整个工作区,因此轻量;**单个文件过大时不备份**。
122
121
  - **外部变更也追**:每条用户消息边界,插件重新检查所有已跟踪文件——命令执行、手动修改等外部变更同样被记录,回退时一并还原。这让「轻量」却不「残缺」。
123
122
  - **不变不存**:记录只在有变化时发生——消息边界重扫时无变更的**不备份**(不留记录);写前备份时若与前一条记录一致,只存指向它的链接(`ref`)而非复制内容。
124
- - **还原准确**:以备份为唯一标准,对照真实磁盘,只动真正不一致的文件;备份逐字节存储。还原结果与备份一致,无“幽灵影响”。但存在限制:[文件回退的追踪边界](docs/compat/tracking-boundary.zh.md)。
123
+ - **还原准确**:以备份为唯一标准,对照真实磁盘,只动真正不一致的文件;备份逐字节存储。还原结果与备份一致,无“幽灵影响”。覆盖范围按设计限定:[文件回退的追踪边界](docs/compat/tracking-boundary.zh.md)。
125
124
  - **安全与完整性**:路径经安全化处理,绝不越出备份根目录;符号/硬链接跳过,避免透过一次还原误伤同名的另一份文件;单个文件失败绝不中止整轮还原;备份与还原日志均原子写落盘(跨重启仍在),断电或崩溃后的半还原可续做或回滚。
126
125
 
127
126
  > **设计点睛**:这套检查点的「轻」,来自**只记录被工具动过、且确实变化的文件**——写前备份保证可还原,不变不存与存链接压掉重复;还原时再对照真实磁盘,只动不一致的文件。
128
127
 
129
- ## 明确不做的事
128
+ ## 范围与分工
130
129
 
131
- 本插件刻意保持轻量、聚焦「对话回退」这一件事,以下场景**不属于它的职责**:
130
+ 本插件刻意保持轻量、聚焦「对话回退」这一件事,以下场景**不属于它的职责**,各自有更合适的归属(见[设计边界](docs/boundaries.zh.md)):
132
131
 
133
- - **整树 / Git 级快照**——只跟踪写类工具编辑 + 已跟踪文件的外部改动,从未被工具碰过的文件不还原。需要工作树级的完整快照回退时,请交给更专业的快照工具(git)。
132
+ - **整树 / Git 级快照**——只跟踪写类工具编辑 + 已跟踪文件的外部改动,从未被工具碰过的文件不还原。工作区历史(任意文件、任意时刻)归 Git;Agent 本来就在用 Git,插件不重复造这层。需要工作树级的完整快照回退时,请交给 Git 或项目自选的快照工具。
134
133
  - **子代理的编辑**——不追踪,子代理会话内也不提供回退(同 Claude Code):子代理运行在自己的会话里,其备份无法由父会话的回退还原,因此也不会为子会话保留备份。
135
- - **fork / 分支回退**——DSH 已内置「在新对话中分支」,不重复造轮子。
134
+ - **fork / 分支回退**——DSH 已内置「在新对话中分支」功能,不重复造轮子。
136
135
 
137
136
  ## 兼容性
138
137
 
@@ -144,12 +143,10 @@ rm <dsh home>/snapshot-cleanup-last-sweep.json
144
143
  > 本项目与 DeepSeek Harness 均处于开发者预览阶段。可复现环境请 pin 精确版本,
145
144
  > 并阅读上述行为说明。
146
145
 
147
- ## 已知问题
146
+ ## 已知限制
148
147
 
149
- 1. **导出的日志是完整内容**——`/export` 导出整个会话日志(原始 JSONL),包含**已撤回的内容**;本插件无法改动导出。
150
- 2. **轻量文件回退存在代价**——特定情况可能无法回退所有修改。行为与 Claude Code 一致。详见:[文件回退的追踪边界](docs/compat/tracking-boundary.zh.md)。
151
- 3. **轮次导航保留已回退轮次**——右侧轮次导航为已回退轮次保留「未加载」刻度,悬浮可预览已撤回内容。仅显示差异,无功能影响。
152
- 4. **较远回退可能显示空白**——回退目标早于已加载的历史时,对话区会显示空白;需点击「加载更早」往前翻,才能看到历史对话。
148
+ 1. **轮次导航保留已回退轮次**——右侧轮次导航为已回退轮次保留「未加载」刻度,悬浮可预览已撤回内容。仅显示差异,无功能影响。
149
+ 2. **较远回退可能显示空白**——回退目标早于已加载的历史时,对话区会显示空白;需点击「加载更早」往前翻,才能看到历史对话。
153
150
 
154
151
  > [!NOTE]
155
152
  > 本插件提供浏览器端诊断输出;详见 [浏览器诊断](docs/compat/diagnostics.zh.md)。
@@ -171,7 +168,7 @@ node scripts/verify-host.mjs # 端到端验证构建产物
171
168
 
172
169
  `prepack` 在打包前执行完整构建,所以 `npm pack` / `npm publish` 总会产出完整的 `lib/`。请从 npm 或打好的 tarball 安装——git 检出的源码里没有构建好的 `lib/`。
173
170
 
174
- 维护者:模块地图与 harness 接口参考见 [docs/harness-reference.md](docs/harness-reference.md)
171
+ 维护者:源码布局与架构见 [docs/architecture.md](docs/architecture.md),harness 接口参考见 [docs/harness-reference.md](docs/harness-reference.md)。
175
172
 
176
173
  贡献指南:[CONTRIBUTING.md](CONTRIBUTING.md)
177
174
 
package/SECURITY.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Security model
2
2
 
3
3
  This document is the security model of **dsh-rewind** — how the plugin treats
4
- untrusted input, what it is allowed to mutate, and how it survives crashes.
5
- It is derived from the implementation (`src/index.ts`, `src/snapshot.ts`); if
6
- this document and the code ever disagree, the code wins and this document is
7
- a bug.
4
+ untrusted input, what it is allowed to mutate, how it contains filesystem
5
+ writes, and how it survives crashes. It is derived from the implementation; if
6
+ this document and the code ever disagree, the code wins and this document is a
7
+ bug.
8
8
 
9
9
  ## Trusted boundary
10
10
 
@@ -17,190 +17,167 @@ following are treated as **untrusted inputs**:
17
17
  - **Session log contents** — events are parsed structurally; a hostile or
18
18
  malformed id must never escape the store root.
19
19
  - **Current worktree state** — restore planning reconciles against the live
20
- disk, which may have been changed by anything.
20
+ disk, which anything may have changed.
21
21
  - **Concurrent external modifications** — a restore never assumes the disk
22
22
  still matches its records.
23
23
 
24
- The DSH host (and its other plugins) is trusted; this plugin does not
25
- re-verify the host's own authority boundaries.
24
+ The DSH host (and its other plugins) is trusted; this plugin does not re-verify
25
+ the host's own authority boundaries.
26
26
 
27
27
  ## Mutation gates
28
28
 
29
- When `fileCheckpointingEnabled` is on, the plugin **automatically captures**
29
+ When snapshot backups are on, the plugin **automatically captures**
30
30
  before-backups of tracked mutations, but **never automatically applies** one.
31
- A workspace restore happens only through an explicit user-invoked
32
- `/rewind-plugin @<seq> both` (the per-message ↶ button or the command channel),
33
- and only when all of the following hold:
34
-
35
- 1. **A validated target**: `planRewind` accepts only a `user/message` seq that
36
- is currently on the session surface (`parseRewindTarget` → `RewindPlan`).
37
- 2. **A fresh plan**: the plan is derived from the current `events` + `surface`
38
- at execution time — never cached across events.
39
- 3. **Committed backups exist**: every planned action comes from a committed
31
+ A workspace restore happens only through an explicit user action (the
32
+ per-message ↶ button or `/rewind`), and only when all of the following hold:
33
+
34
+ 1. **A validated target** — a `user/message` seq currently on the session
35
+ surface.
36
+ 2. **A fresh plan** — derived from the current events and surface at execution
37
+ time, never cached across events.
38
+ 3. **Committed backups exist** — every planned action comes from a committed
40
39
  checkpoint entry anchored at or after the target.
41
- 4. **Live-disk reconciliation**: `planRestore` compares each entry against the
42
- current disk and plans only actions that would actually change it — an
40
+ 4. **Live-disk reconciliation** — each entry is compared with the current disk
41
+ and only actions that would actually change it are planned; an
43
42
  already-matching state is a no-op (restores are idempotent).
44
- 5. **Exclusive execution**: a per-session in-flight guard rejects concurrent
45
- rewinds; a running turn is force-cancelled (`keepInbox`) and quiescence is
46
- awaited before the surface is cut.
47
- 6. **Session binding**: the restore reads/writes only the store of the rewound
48
- session (paths are resolved display paths the session's own tools touched).
49
-
50
- **Concurrency scope**: the in-flight guard above only serializes concurrent
51
- *rewinds* of one session; it does not stop other processes from modifying the
52
- workspace. `planRestore` reconciles against the live disk at plan time, but
53
- there is no re-validation between planning and applying — a file changed by
54
- another process in that window is overwritten by the restore. The rescue state
55
- is captured at apply start, so `rollbackRestore` still returns the workspace
56
- to exactly the apply-start state, and per-file failures are reported rather
57
- than hidden.
43
+ 5. **Exclusive execution** — a per-session in-flight guard rejects concurrent
44
+ rewinds; a running turn is cancelled (queued messages are kept) and
45
+ quiescence is awaited before the surface is cut.
46
+ 6. **Session binding** — the restore reads and writes only the rewound
47
+ session's store, over paths that session's own tools touched.
48
+
49
+ **Concurrency scope:** the guard serializes concurrent *rewinds* of one session;
50
+ it does not stop other processes from changing the workspace. The plan is
51
+ reconciled against the live disk at plan time, with no re-validation before
52
+ apply, so a file changed in that window is overwritten by the restore. The
53
+ pre-restore state is captured at apply start, so a rollback still returns the
54
+ workspace to exactly that state, and per-file failures are reported rather than
55
+ hidden.
58
56
 
59
57
  A failed gate fails closed: an invalid target, a missing store, an absent
60
58
  backup, or a cancelled invocation aborts the rewind with an error. A store
61
- written by a **newer** build is one of those gates: it is refused as a whole —
62
- no partial restore and no new entry written into it, and the automatic
63
- format-change clear below is skipped — while the conversation rewind itself
64
- still works. An explicit clear (`run --current --apply`) or the age-based sweep,
65
- being a user-directed deletion of whole session directories, is deliberately not
66
- version-gated.
67
-
68
- **Automatic store deletion**: two things delete session snapshots. The opt-in
69
- `snapshot-auto-cleanup` sweep (default off) removes the whole directories of
70
- **long-inactive** sessions, unrelated to restores. Separately, a session's
71
- snapshots are cleared automatically when the conversation log's session format
72
- has changed (a DSH upgrade), because those backups are anchored to message
73
- positions that no longer line up. Both stay confined to the store root, use
74
- `lstat` (so they never follow a symlink out of the root), the sweep never targets
75
- the active session, and neither touches the conversation log. When auto-cleanup
76
- is disabled (the default), only the format-change clear runs, and only for an
77
- affected session. The store-format guard runs before that clear, so a session
78
- whose store is **newer** than this build understands is not cleared by it (see
79
- `docs/format.md`).
59
+ written by a **newer** build is refused as a whole — no partial restore, no new
60
+ entry, and no automatic format-change clear — while the conversation rewind
61
+ still works. An explicit clear or the age-based sweep, being user-directed
62
+ deletion of whole session directories, is deliberately not version-gated.
63
+
64
+ **Automatic store deletion.** Two things delete session snapshots: the opt-in
65
+ `snapshot-auto-cleanup` sweep (off by default) removes whole directories of
66
+ long-inactive sessions; separately, a session's snapshots are cleared when the
67
+ session format has changed (a DSH upgrade), because the backups are anchored to
68
+ message positions that no longer line up. Both stay confined to the store root,
69
+ never follow a symlink out of it, never touch the conversation log, and the
70
+ sweep never targets the active session. The format guard runs before the clear,
71
+ so a store newer than this build is not cleared by it. See
72
+ [snapshot auto-cleanup](docs/snapshot-auto-cleanup.md) and
73
+ [the store format](docs/format.md).
80
74
 
81
75
  ## Conversation integrity
82
76
 
83
77
  The session log is **append-only** — the plugin never deletes or rewrites
84
- recorded history. A rewind appends a single marker event (a `user/message` whose
85
- content is the constant `(empty message)` placeholder) whose `surfaceOp`
86
- replaces every surface node after the target. The raw log (audit trail, search,
87
- `/export`) is untouched — only the model-visible surface is cut, so the next
88
- request derives its context from the target onward. The marker's content is a
89
- constant, so it carries no untrusted text into the model context; the client
90
- hides the `[target, marker]` span from the rendered transcript.
78
+ recorded history. A rewind appends a single marker event whose content is a
79
+ constant placeholder and whose surface operation replaces every surface node
80
+ after the target. The raw log (audit trail, search, `/export`) is untouched;
81
+ only the model-visible surface is cut, so the next request derives its context
82
+ from the target onward. The marker's constant content carries no untrusted text
83
+ into the model context, and the client hides the withdrawn span from the
84
+ rendered transcript. The marker shape is specified in
85
+ [architecture](docs/architecture.md).
91
86
 
92
87
  ## Filesystem containment
93
88
 
94
- - **Store root**: `<dsh home>/rewind-snapshots/` by default (resolved via
95
- `resolveDshHome`, so `$DSH_HOME` feeds the base and `~/.dsh` is the fallback);
96
- the `snapshotDir` config overrides the whole path. The plugin reads no
97
- environment variable of its own. Deleting the root only removes file backups
98
- and the store rebuilds from scratch.
99
- - **Path sanitization**: session ids and call ids are scrubbed to
100
- `[a-zA-Z0-9._-]` (`safeSessionId` / `safeFileId`) before joining the store
101
- root; `.` and `..` bare values are replaced — hostile ids cannot traverse
102
- out of the root.
103
- - **Links and moved directories are refused**: two checks guard a tracked path.
104
- (1) Planning skips a path whose last component is a symlink or a hard link
105
- (`lstat().nlink > 1`) and reports it, never restoring it — a symlink would
106
- redirect the write outside the checkpoint, and a hard link would clobber every
107
- other name of the same inode (e.g. pnpm-installed files); a link swapped in
108
- after planning is not re-checked. (2) At apply time, every record this build
109
- writes — entry, link and
110
- journal action — carries the `realpath` of the file's directory at commit time
111
- (the location pin; best-effort, and absent on released-v1 data or a commit
112
- whose parent could not be resolved), and a restore re-checks it before
113
- touching the path, including a post-restart continue or rollback, which reads
114
- the pin from the journal: a directory that no longer resolves there (a
115
- repointed or moved ancestor) is refused and reported instead of writing
116
- outside the recorded location, while data with no pin falls back to check (1)
117
- alone. A parent chain that
118
- is gone is still recreated (files whose directory was deleted are restorable),
119
- but only while its nearest surviving ancestor resolves inside the pin. A
120
- stable symlinked ancestor resolves identically on both sides, so a symlinked
121
- workspace or temp root is never a false skip. This matches Claude Code's
122
- checkpoint behavior since v2.1.216.
89
+ - **Store root**: `<dsh home>/rewind-snapshots/` by default (`$DSH_HOME` feeds
90
+ the base, `~/.dsh` is the fallback); the `snapshotDir` config overrides the
91
+ whole path. The plugin reads no environment variable of its own. Deleting the
92
+ root only removes file backups; the store rebuilds.
93
+ - **Path sanitization**: session and call ids are scrubbed to a safe character
94
+ set before joining the store root; bare `.` and `..` are replaced — hostile
95
+ ids cannot traverse out of the root.
96
+ - **Links are refused**: planning skips a path whose last component is a
97
+ symlink or a hard link and reports it — a symlink would redirect the write
98
+ outside the checkpoint, and a hard link would clobber every other name of the
99
+ same inode. A link swapped in after planning is not re-checked.
100
+ - **Recorded locations are re-checked**: every record written by this build
101
+ carries the directory location resolved at commit time; a restore re-checks
102
+ it before touching the path, including after a restart. A directory that no
103
+ longer resolves there is refused and reported; a missing parent chain is
104
+ recreated only inside the recorded location. Data with no recorded location
105
+ falls back to the link check. This matches Claude Code's checkpoint behavior.
106
+ - **Observation sync (best-effort)**: after a restore, the plugin re-emits the
107
+ host's `fs/observed` event for the paths it changed (absent, or present with
108
+ the current version), so a host with the observation policy enabled does not
109
+ judge the next write against a pre-restore observation. It heals only the
110
+ inconsistency the restore itself created; later external changes are still
111
+ seen normally.
123
112
  - **Restores name only recorded paths**: the store contains resolved display
124
113
  paths of the session's own write-class tool calls (plus boundary re-checks
125
114
  over that same tracked set) — a restore can never write an arbitrary path.
126
- - **Path resolution rule**: relative paths resolve against the session's current
127
- working directory, read from the same service the fs tools resolve through.
128
- - **Bounded backups**: `prune` keeps the newest 100 anchor groups per session
129
- (`MAX_ANCHOR_GROUPS`), except groups a non-terminal restore journal still
130
- references (pinned so the op can be finished), so backup accumulation stays
131
- bounded by the cap plus the number of unresolved journals (the exact cap is
132
- pinned in `docs/format.md`). Across sessions, the opt-in `pruneStale` sweep
133
- removes whole **long-inactive** session directories (measured by the newest
134
- member being idle past the configured `autoCleanupMaxAgeDays`); it uses
135
- `lstat` (no symlink following), skips dot-prefixed temp files, and never
136
- targets the active session (`keepActiveId`).
115
+ - **Path resolution**: relative paths resolve against the session's current
116
+ working directory, read from the service the fs tools resolve through.
117
+ - **Bounded backups**: the store keeps a bounded number of anchor groups per
118
+ session, except groups a non-terminal restore still references (pinned so the
119
+ operation can be finished); the exact cap is in
120
+ [the store format](docs/format.md). Across sessions, the opt-in sweep removes
121
+ whole long-inactive session directories and never targets the active session.
137
122
 
138
123
  ## Crash safety
139
124
 
140
- - **Ordered commits**: a checkpoint commit places the before-sidecar (a raw
141
- byte copy) before publishing the entry JSON, so a crash can leave an
142
- unreferenced sidecar but never an entry whose bytes are missing. Every JSON
143
- write (checkpoint entries, restore journals) goes to a sibling temp file and
144
- is renamed over the target: a host crash mid-write can leave only an inert
145
- `<target>.tmp` — never a readable half-written file — and readers never pick
146
- up temp files.
147
- - **Journaled restores**: before mutating anything, the restore captures each
148
- planned path's pre-restore ("rescue") state and persists an intent journal,
149
- then marks each action done as it is applied. A crash at any point leaves
150
- the journal on disk.
151
- - **Disk is truth**: an interrupted op keeps its journal on disk, and the store's
152
- recovery API re-derives the true state from the real disk — `reconcileRestores`
153
- reports which paths already match the goal (restored) and which are pending,
154
- `continueRestore` finishes the op, `rollbackRestore` undoes it to the exact
155
- pre-restore state, and a journal whose goal is already reached auto-heals to
156
- its terminal state. The plugin never runs them on its own: an interrupted op
157
- stays interrupted until an explicit caller resumes or undoes it.
158
- - **Fail-loud vs fail-soft**: a corrupt **journal** is reported
159
- `recovery-required` — never silently dropped (dropping it would erase the
160
- interrupted restore's recovery record). Corrupt **checkpoint entries** are
161
- silently ignored (they only lose one backup, not the recovery path), and a
162
- record whose sidecar is missing or truncated fails only that file — never a
163
- delete of the live file.
164
- - **Journal IO never fails the restore**: if the journal cannot be written the
165
- restore proceeds with pre-journal semantics (crash safety degrades,
166
- behavior does not).
125
+ - **Ordered commits**: a checkpoint writes the before-sidecar (a raw byte copy)
126
+ before publishing the entry, so a crash can leave an unreferenced sidecar but
127
+ never an entry whose bytes are missing. Every JSON write goes to a sibling
128
+ temp file and is renamed over the target: a crash can leave only an inert
129
+ `.tmp`, never a readable half-written file, and readers never pick up temp
130
+ files.
131
+ - **Journaled restores**: before mutating anything, a restore captures each
132
+ planned path's pre-restore state and persists an intent journal, then marks
133
+ each action done as it is applied. A crash at any point leaves the journal on
134
+ disk.
135
+ - **Disk is truth**: an interrupted operation keeps its journal, and recovery
136
+ re-derives the true state from the real disk — which paths already match the
137
+ goal and which are pending, can finish the operation, and can undo it to the
138
+ exact pre-restore state. A journal whose goal is already reached auto-heals.
139
+ The plugin never runs recovery on its own: an interrupted operation stays
140
+ interrupted until an explicit caller resumes or undoes it.
141
+ - **Fail-loud vs fail-soft**: a corrupt journal is reported as
142
+ `recovery-required`, never silently dropped — dropping it would erase the
143
+ recovery record. Corrupt entries are ignored (they lose one backup, not the
144
+ recovery path), and a missing or truncated sidecar fails only that file —
145
+ never a delete of the live file.
146
+ - **Journal IO never fails the restore**: if the journal cannot be written, the
147
+ restore proceeds with pre-journal semantics; crash safety degrades, behavior
148
+ does not.
167
149
 
168
150
  ## Explicit non-goals
169
151
 
170
- - This plugin does **not** sandbox other processes or stop them from changing
171
- files concurrently.
172
- - It does **not** provide confidentiality or tamper resistance against the
173
- same operating-system user: **store** files are created with the process
174
- default permissions (a standard umask applies), and the host user remains
175
- trusted. Restored **workspace** files do carry their recorded permission
176
- bits: a content restore reapplies them (`chmod`, best-effort), while a mode
177
- difference on its own never triggers a restore.
178
- - It does **not** exclude paths from the store, and it never oversteps: the
179
- plugin only adds backup/restore on top of permissions DSH already holds. Two
180
- distinct cases follow:
181
- 1. **Sensitive / personal-information files** (e.g. a `.env` file, which may
182
- sit inside the workspace) the model may read or edit are backed up and
183
- restorable, unless the file is over the size cap.
184
- 2. **Files outside the session workspace** the model is allowed to touch are
185
- likewise backed up and restored.
186
- In both cases the plugin introduces no authority of its own — DSH already
187
- granted the read/edit; the plugin merely records a before-backup and can
188
- restore it. Keeping a path out is a **DSH model-permission** concern (a
189
- per-path deny), not a rewind feature.
190
- - It does **not** touch git (no refs, index, or worktree operations), makes
191
- **no network requests**, and does **not** access credentials.
192
- - It does **not** roll back whole-log state: telemetry, search, and `/export`
193
- still see the withdrawn messages (documented behavior, not a bug).
152
+ This section declares scope, not a gap list: each item names what stays outside
153
+ the plugin and what remains the host's or the user's responsibility. The
154
+ boundary contract is in [design boundaries](docs/boundaries.md).
155
+
156
+ - It does **not** sandbox other processes or stop them from changing files
157
+ concurrently.
158
+ - It does **not** provide confidentiality or tamper resistance against the same
159
+ OS user: **store** files use the process default permissions under the
160
+ process umask, and the host user remains trusted. Restored **workspace** files
161
+ carry their recorded permission bits (a content restore reapplies them,
162
+ best-effort), while a mode difference alone never triggers a restore.
163
+ - It does **not** exclude paths from the store, and it introduces no authority of
164
+ its own: sensitive files (e.g. a `.env` the model may edit) and files outside
165
+ the workspace the model may touch are backed up and restorable, unless over
166
+ the size cap. Keeping a path out is a **DSH model-permission** concern, not a
167
+ rewind feature.
168
+ - It does **not** touch git, make network requests, or access credentials.
169
+ - It does **not** roll back whole-log state: search, telemetry and `/export`
170
+ still see withdrawn messages (by design; see [boundaries](docs/boundaries.md)).
194
171
  - It does **not** restore files written by a cancelled tool call that never
195
172
  committed a backup.
196
- - **Subagent session edits are not tracked** (Claude Code alignment): a
197
- subagent runs its own session, so the files it changes are not backed up and
198
- cannot be restored by a rewind of the parent session; the plugin keeps no
199
- snapshot for a subagent session and offers no rewind inside one.
173
+ - **Subagent session edits are not tracked**: a subagent runs its own session,
174
+ so its files cannot be restored by a rewind of the parent, and the plugin
175
+ offers no rewind inside one. See [boundaries](docs/boundaries.md).
200
176
 
201
177
  ## Reporting
202
178
 
203
179
  Report a vulnerability through the repository's GitHub security channel or to
204
- a repository maintainer. Include: the plugin version/commit, the DSH
180
+ a repository maintainer. Include the plugin version/commit, the DSH
205
181
  (`@deepseek-ai/*`) version, the platform, and a minimal reproduction — and
206
- whether the failure happened before or after workspace mutation.
182
+ whether the failure happened before or after workspace mutation. Do not open a
183
+ public issue for a vulnerability.
package/lib/client.js CHANGED
@@ -1611,7 +1611,7 @@ async function candidatesOverApi(sessionId) {
1611
1611
  }
1612
1612
 
1613
1613
  // src/client/build-info.ts
1614
- var PLUGIN_VERSION = true ? "0.20.0-alpha.4" : "dev";
1614
+ var PLUGIN_VERSION = true ? "0.20.0" : "dev";
1615
1615
  var PLUGIN_PACKAGE = "dsh-rewind-plugin";
1616
1616
  var BUILD_HASH = true ? "c59b80ee" : "dev";
1617
1617
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rewind-plugin",
3
- "version": "0.20.0-alpha.4",
3
+ "version": "0.20.0",
4
4
  "description": "In-window conversation rewind with workspace file restore",
5
5
  "icon": "./assets/icon.svg",
6
6
  "keywords": [
@@ -23,7 +23,7 @@
23
23
  "license": "MIT",
24
24
  "engines": {
25
25
  "node": ">=24.9.0",
26
- "dsh": ">=0.2.2-alpha.2 <0.3.0-0"
26
+ "dsh": ">=0.2.2-rc.1 <0.3.0-0"
27
27
  },
28
28
  "type": "module",
29
29
  "main": "lib/index.js",
@@ -78,24 +78,24 @@
78
78
  "access": "public"
79
79
  },
80
80
  "peerDependencies": {
81
- "@deepseek-ai/cordis": "^4.0.6-alpha.1",
82
- "@deepseek-ai/dsh-client-connection": ">=0.2.2-alpha.2 <0.3.0-0",
83
- "@deepseek-ai/dsh-client-locale": ">=0.2.2-alpha.2 <0.3.0-0",
84
- "@deepseek-ai/dsh-client-ui-commands": ">=0.2.2-alpha.2 <0.3.0-0",
85
- "@deepseek-ai/dsh-client-ui-plugin-manager": ">=0.2.2-alpha.2 <0.3.0-0",
86
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.2.2-alpha.2 <0.3.0-0",
87
- "@deepseek-ai/dsh-client-ui-settings": ">=0.2.2-alpha.2 <0.3.0-0",
88
- "@deepseek-ai/dsh-client-ui-slots": ">=0.2.2-alpha.2 <0.3.0-0",
89
- "@deepseek-ai/dsh-commands": ">=0.2.2-alpha.2 <0.3.0-0",
90
- "@deepseek-ai/dsh-fs": ">=0.2.2-alpha.2 <0.3.0-0",
91
- "@deepseek-ai/dsh-home-paths": ">=0.2.2-alpha.2 <0.3.0-0",
92
- "@deepseek-ai/dsh-llm": ">=0.2.2-alpha.2 <0.3.0-0",
93
- "@deepseek-ai/dsh-sandbox": ">=0.2.2-alpha.2 <0.3.0-0",
94
- "@deepseek-ai/dsh-session": ">=0.2.2-alpha.2 <0.3.0-0",
95
- "@deepseek-ai/dsh-settings": ">=0.2.2-alpha.2 <0.3.0-0",
96
- "@deepseek-ai/dsh-tools": ">=0.2.2-alpha.2 <0.3.0-0",
97
- "@deepseek-ai/dsh-working-directory": ">=0.2.2-alpha.2 <0.3.0-0",
98
- "@deepseek-ai/schemastery": "^3.18.6-alpha.1"
81
+ "@deepseek-ai/cordis": "^4.1.0",
82
+ "@deepseek-ai/dsh-client-connection": ">=0.2.2-rc.1 <0.3.0-0",
83
+ "@deepseek-ai/dsh-client-locale": ">=0.2.2-rc.1 <0.3.0-0",
84
+ "@deepseek-ai/dsh-client-ui-commands": ">=0.2.2-rc.1 <0.3.0-0",
85
+ "@deepseek-ai/dsh-client-ui-plugin-manager": ">=0.2.2-rc.1 <0.3.0-0",
86
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.2.2-rc.1 <0.3.0-0",
87
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.2.2-rc.1 <0.3.0-0",
88
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.2.2-rc.1 <0.3.0-0",
89
+ "@deepseek-ai/dsh-commands": ">=0.2.2-rc.1 <0.3.0-0",
90
+ "@deepseek-ai/dsh-fs": ">=0.2.2-rc.1 <0.3.0-0",
91
+ "@deepseek-ai/dsh-home-paths": ">=0.2.2-rc.1 <0.3.0-0",
92
+ "@deepseek-ai/dsh-llm": ">=0.2.2-rc.1 <0.3.0-0",
93
+ "@deepseek-ai/dsh-sandbox": ">=0.2.2-rc.1 <0.3.0-0",
94
+ "@deepseek-ai/dsh-session": ">=0.2.2-rc.1 <0.3.0-0",
95
+ "@deepseek-ai/dsh-settings": ">=0.2.2-rc.1 <0.3.0-0",
96
+ "@deepseek-ai/dsh-tools": ">=0.2.2-rc.1 <0.3.0-0",
97
+ "@deepseek-ai/dsh-working-directory": ">=0.2.2-rc.1 <0.3.0-0",
98
+ "@deepseek-ai/schemastery": "^3.19.0"
99
99
  },
100
100
  "peerDependenciesMeta": {
101
101
  "@deepseek-ai/cordis": {
@@ -154,45 +154,45 @@
154
154
  }
155
155
  },
156
156
  "devDependencies": {
157
- "@deepseek-ai/cordis": "4.0.6-alpha.2",
158
- "@deepseek-ai/cosmokit": "1.8.7-alpha.2",
159
- "@deepseek-ai/dsh-agent": "0.2.2-alpha.3",
160
- "@deepseek-ai/dsh-api-session-controller": "0.2.2-alpha.3",
161
- "@deepseek-ai/dsh-attachment": "0.2.2-alpha.3",
162
- "@deepseek-ai/dsh-client-connection": "0.2.2-alpha.3",
163
- "@deepseek-ai/dsh-client-locale": "0.2.2-alpha.3",
164
- "@deepseek-ai/dsh-client-store": "0.2.2-alpha.3",
165
- "@deepseek-ai/dsh-client-ui-commands": "0.2.2-alpha.3",
166
- "@deepseek-ai/dsh-client-ui-conversation": "0.2.2-alpha.3",
167
- "@deepseek-ai/dsh-client-ui-input-trigger": "0.2.2-alpha.3",
168
- "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.2-alpha.3",
169
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.2-alpha.3",
170
- "@deepseek-ai/dsh-client-ui-session": "0.2.2-alpha.3",
171
- "@deepseek-ai/dsh-client-ui-settings": "0.2.2-alpha.3",
172
- "@deepseek-ai/dsh-client-ui-slots": "0.2.2-alpha.3",
173
- "@deepseek-ai/dsh-command-compact": "0.2.2-alpha.3",
174
- "@deepseek-ai/dsh-commands": "0.2.2-alpha.3",
175
- "@deepseek-ai/dsh-compaction": "0.2.2-alpha.3",
176
- "@deepseek-ai/dsh-compaction-basic": "0.2.2-alpha.3",
177
- "@deepseek-ai/dsh-compaction-image-offload": "0.2.2-alpha.3",
178
- "@deepseek-ai/dsh-fs": "0.2.2-alpha.3",
179
- "@deepseek-ai/dsh-goal": "0.2.2-alpha.3",
180
- "@deepseek-ai/dsh-home-paths": "0.2.2-alpha.3",
181
- "@deepseek-ai/dsh-llm": "0.2.2-alpha.3",
182
- "@deepseek-ai/dsh-plan-mode": "0.2.2-alpha.3",
183
- "@deepseek-ai/dsh-sandbox": "0.2.2-alpha.3",
184
- "@deepseek-ai/dsh-scope": "0.2.2-alpha.3",
185
- "@deepseek-ai/dsh-session": "0.2.2-alpha.3",
186
- "@deepseek-ai/dsh-session-projection": "0.2.2-alpha.3",
187
- "@deepseek-ai/dsh-session-stats": "0.2.2-alpha.3",
188
- "@deepseek-ai/dsh-session-title": "0.2.2-alpha.3",
189
- "@deepseek-ai/dsh-settings": "0.2.2-alpha.3",
190
- "@deepseek-ai/dsh-token-meter": "0.2.2-alpha.3",
191
- "@deepseek-ai/dsh-tools": "0.2.2-alpha.3",
192
- "@deepseek-ai/dsh-typert-protocol": "0.2.2-alpha.3",
193
- "@deepseek-ai/dsh-user-questions": "0.2.2-alpha.3",
194
- "@deepseek-ai/dsh-working-directory": "0.2.2-alpha.3",
195
- "@deepseek-ai/schemastery": "3.18.6-alpha.2",
157
+ "@deepseek-ai/cordis": "4.1.0",
158
+ "@deepseek-ai/cosmokit": "1.9.0",
159
+ "@deepseek-ai/dsh-agent": "0.2.2-rc.1",
160
+ "@deepseek-ai/dsh-api-session-controller": "0.2.2-rc.1",
161
+ "@deepseek-ai/dsh-attachment": "0.2.2-rc.1",
162
+ "@deepseek-ai/dsh-client-connection": "0.2.2-rc.1",
163
+ "@deepseek-ai/dsh-client-locale": "0.2.2-rc.1",
164
+ "@deepseek-ai/dsh-client-store": "0.2.2-rc.1",
165
+ "@deepseek-ai/dsh-client-ui-commands": "0.2.2-rc.1",
166
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.2-rc.1",
167
+ "@deepseek-ai/dsh-client-ui-input-trigger": "0.2.2-rc.1",
168
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.2-rc.1",
169
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.2-rc.1",
170
+ "@deepseek-ai/dsh-client-ui-session": "0.2.2-rc.1",
171
+ "@deepseek-ai/dsh-client-ui-settings": "0.2.2-rc.1",
172
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.2-rc.1",
173
+ "@deepseek-ai/dsh-command-compact": "0.2.2-rc.1",
174
+ "@deepseek-ai/dsh-commands": "0.2.2-rc.1",
175
+ "@deepseek-ai/dsh-compaction": "0.2.2-rc.1",
176
+ "@deepseek-ai/dsh-compaction-basic": "0.2.2-rc.1",
177
+ "@deepseek-ai/dsh-compaction-image-offload": "0.2.2-rc.1",
178
+ "@deepseek-ai/dsh-fs": "0.2.2-rc.1",
179
+ "@deepseek-ai/dsh-goal": "0.2.2-rc.1",
180
+ "@deepseek-ai/dsh-home-paths": "0.2.2-rc.1",
181
+ "@deepseek-ai/dsh-llm": "0.2.2-rc.1",
182
+ "@deepseek-ai/dsh-plan-mode": "0.2.2-rc.1",
183
+ "@deepseek-ai/dsh-sandbox": "0.2.2-rc.1",
184
+ "@deepseek-ai/dsh-scope": "0.2.2-rc.1",
185
+ "@deepseek-ai/dsh-session": "0.2.2-rc.1",
186
+ "@deepseek-ai/dsh-session-projection": "0.2.2-rc.1",
187
+ "@deepseek-ai/dsh-session-stats": "0.2.2-rc.1",
188
+ "@deepseek-ai/dsh-session-title": "0.2.2-rc.1",
189
+ "@deepseek-ai/dsh-settings": "0.2.2-rc.1",
190
+ "@deepseek-ai/dsh-token-meter": "0.2.2-rc.1",
191
+ "@deepseek-ai/dsh-tools": "0.2.2-rc.1",
192
+ "@deepseek-ai/dsh-typert-protocol": "0.2.2-rc.1",
193
+ "@deepseek-ai/dsh-user-questions": "0.2.2-rc.1",
194
+ "@deepseek-ai/dsh-working-directory": "0.2.2-rc.1",
195
+ "@deepseek-ai/schemastery": "3.19.0",
196
196
  "@types/node": "^24.0.0",
197
197
  "@types/react": "^18.3.31",
198
198
  "@types/react-dom": "^18.3.7",