dsh-rewind-plugin 0.11.0 → 0.12.1
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/CONTRIBUTING.md +4 -3
- package/README.en.md +28 -42
- package/README.md +23 -36
- package/SECURITY.md +56 -24
- package/docs/architecture.md +19 -8
- package/docs/compat/audit.md +1 -1
- package/docs/compat/diagnostics.md +2 -1
- package/docs/compat/diagnostics.zh.md +2 -1
- package/docs/compat/tracking-boundary.md +6 -6
- package/docs/compat/tracking-boundary.zh.md +2 -2
- package/docs/format.md +159 -53
- package/docs/snapshot-auto-cleanup.md +8 -10
- package/docs/snapshot-auto-cleanup.zh.md +2 -4
- package/lib/client.js +77 -51
- package/lib/index.js +1036 -228
- package/lib/types/client/portals.d.ts +34 -0
- package/lib/types/locales.d.ts +1 -0
- package/lib/types/snapshot.d.ts +397 -83
- package/package.json +1 -1
package/CONTRIBUTING.md
CHANGED
|
@@ -58,9 +58,10 @@ npm run verify:host # end-to-end host verification (full check suite)
|
|
|
58
58
|
## Testing expectations
|
|
59
59
|
|
|
60
60
|
- Pure planning (`rewind.ts`, `hidden.ts`) → unit tests in `tests/`.
|
|
61
|
-
- Store behavior (`snapshot.ts`) → `tests/snapshot.test.ts
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
- Store behavior (`snapshot.ts`) → `tests/snapshot.test.ts` plus the focused
|
|
62
|
+
suites (`snapshot-bytes`, `snapshot-mode`, `downgrade-safety`,
|
|
63
|
+
`parent-guard`), and crash-safety scenarios in `tests/crash-safety.test.ts`
|
|
64
|
+
via the test-only `crash` seam (`RestoreRunOptions.crash`).
|
|
64
65
|
- Harness interaction → the compatibility suites
|
|
65
66
|
(`compat-invariants` / `compat-interop` / `compat-gaps`) and
|
|
66
67
|
`scripts/verify-host.mjs`.
|
package/README.en.md
CHANGED
|
@@ -47,8 +47,8 @@ dsh plugin --profile web add dsh-rewind-plugin@<version>
|
|
|
47
47
|
## Usage
|
|
48
48
|
|
|
49
49
|
1. Find the user message you want to rewind to in the conversation, or type `/rewind` (or its alias `/undo`) to open the candidate picker.
|
|
50
|
-
2. **Select it.** A small popover offers the two modes
|
|
51
|
-
3. The rewind takes effect immediately: the conversation returns to how it looked at the target message, and the
|
|
50
|
+
2. **Select it.** A small popover offers the two modes — "conversation only" or "conversation and code".
|
|
51
|
+
3. The rewind takes effect immediately: the conversation returns to how it looked at the target message, and the target message's text is filled back into the composer — edit and re-send.
|
|
52
52
|
|
|
53
53
|
**Keyboard**: both the candidate picker and the mode popover support ↑↓ to move, Enter to confirm, Esc to cancel/back.
|
|
54
54
|
|
|
@@ -56,20 +56,19 @@ dsh plugin --profile web add dsh-rewind-plugin@<version>
|
|
|
56
56
|
<summary><b>Edge notes</b></summary>
|
|
57
57
|
|
|
58
58
|
- Rewinds can be repeated — with no limit on stage or count.
|
|
59
|
-
- A rewind itself **cannot be undone**, but the withdrawn content stays in the session log
|
|
59
|
+
- A rewind itself **cannot be undone**, but the withdrawn content stays in the session log.
|
|
60
60
|
- **Interruptions rewind too** — a `steering` interruption message the model hasn't read yet is also a valid rewind target.
|
|
61
|
-
- **A rewind interrupts the running turn** — to
|
|
61
|
+
- **A rewind interrupts the running turn** — to ensure the rewind runs safely.
|
|
62
62
|
|
|
63
63
|
</details>
|
|
64
64
|
|
|
65
65
|
## Snapshot management
|
|
66
66
|
|
|
67
67
|
Snapshots (the before-write backups) are stored under `<dsh home>/rewind-snapshots/`
|
|
68
|
-
(`~/.dsh/rewind-snapshots/`
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
automatically.
|
|
68
|
+
(the default is `~/.dsh/rewind-snapshots/`). For the **same session**, the plugin
|
|
69
|
+
deduplicates snapshots by content and keeps the newest 100 anchor groups.
|
|
70
|
+
**Deleting that directory manually** only clears the file backups (chat rewinds are
|
|
71
|
+
unaffected) and the plugin rebuilds them automatically.
|
|
73
72
|
|
|
74
73
|
A **global auto-cleanup** (off by default) removes the snapshot directories of
|
|
75
74
|
long-inactive sessions, leaving the active session and chat log untouched. Configure
|
|
@@ -101,57 +100,44 @@ Compared with the common approaches, here is the trade-off this plugin makes on
|
|
|
101
100
|
| Dimension | Common approach | This plugin |
|
|
102
101
|
| --- | --- | --- |
|
|
103
102
|
| Conversation rewind | Fork / branch a new conversation | **In-place rewind** — no new session, no window switch |
|
|
104
|
-
| File restore | No restore feature / git-managed or whole-tree snapshot | **Lightweight before-backups** — auto-captured before writes, one-click restore
|
|
103
|
+
| File restore | No restore feature / git-managed or whole-tree snapshot | **Lightweight before-backups** — auto-captured before writes, one-click restore |
|
|
105
104
|
| Dependencies | Often needs a Git repo or a full snapshot engine | **None** — no git required, works on any directory |
|
|
106
|
-
| Storage footprint | Whole-tree snapshots take space | **Lightweight** — only files touched by write tools are
|
|
105
|
+
| Storage footprint | Whole-tree snapshots take space | **Lightweight** — nothing is stored unless it changed, and only files touched by write tools are tracked |
|
|
107
106
|
|
|
108
107
|
## How it works
|
|
109
108
|
|
|
110
|
-
The whole design rests on two principles, simple but deliberate: **the conversation half "masks, never deletes"
|
|
109
|
+
The whole design rests on two principles, simple but deliberate: **the conversation half "masks, never deletes"**, using DSH's native "hide + replace" mechanism; **the file half "partial tracking, lightweight before-write backup"**, following Claude Code's checkpoint semantics.
|
|
111
110
|
|
|
112
111
|
### 1. Conversation rewind: a single "mask", not a delete
|
|
113
112
|
|
|
114
|
-
`append-only` is a hard rule: the session log only grows and is never rewritten — the foundation of auditability and privacy. A rewind never touches history; it makes a single move: append **one
|
|
113
|
+
`append-only` is a hard rule: the session log only grows and is never rewritten — the foundation of auditability and privacy. A rewind never touches history; it makes a single move: append **one "empty message" marker** to the end of the log and use it to "mask + replace" everything after the target message, so the model and the UI see only the part before it.
|
|
115
114
|
|
|
116
|
-
-
|
|
117
|
-
- The
|
|
118
|
-
-
|
|
115
|
+
- **One and the same log** — the append happens only in the current session's log: no new session, no new branch, so no residue or copy is left behind;
|
|
116
|
+
- **The marker is canonical** — the same "hide + replace" as the official `/compact`: `/compact` compresses a span of history into a summary, while `/rewind` swaps in an "empty message" marker. Because it is canonical, DSH's log replay, compaction, and resume preflight all recognize it and never mistake it for a real message;
|
|
117
|
+
- **The replacement is imperceptible** — the model ignores the marker, with no effect (verified empirically). Together with the plugin's UI handling, what you and the model see is exactly how the conversation looked at the target;
|
|
118
|
+
- **Content is preserved** — because this is "masking, not deleting", the withdrawn content stays in the log — auditable, traceable, and in principle manually recoverable.
|
|
119
119
|
|
|
120
|
-
> **Design highlight**: the entire conversation rewind is **a single append**. It's deterministic, auditable, and — because the log was never broken — a "clean" time-travel. Minimal action, complete semantics. The compatibility subtleties with
|
|
120
|
+
> **Design highlight**: the entire conversation rewind is **a single append**. It's deterministic, auditable, and — because the log was never broken — a "clean" time-travel. Minimal action, complete semantics. The compatibility subtleties with DSH (replicating `/compact`, the empty-message mask) are where this plugin is genuinely professional.
|
|
121
121
|
|
|
122
|
-
### 2. File restore: lightweight checkpointing, "
|
|
122
|
+
### 2. File restore: lightweight checkpointing, "before-write backup"
|
|
123
123
|
|
|
124
124
|
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:
|
|
125
125
|
|
|
126
126
|
- **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.
|
|
127
127
|
- **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".
|
|
128
|
-
- **Unchanged-not-recorded
|
|
129
|
-
- **
|
|
130
|
-
- **Safety
|
|
131
|
-
|
|
132
|
-
> **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;
|
|
133
|
-
|
|
134
|
-
### Design highlights
|
|
135
|
-
|
|
136
|
-
| Design | Why it matters |
|
|
137
|
-
| --- | --- |
|
|
138
|
-
| A single append is a whole rewind | Minimal action, maximal semantics; the log is never mutated |
|
|
139
|
-
| Mask, never delete | History is always auditable and in principle recoverable |
|
|
140
|
-
| Before-backup, grouped by turn, persisted on disk | Space-efficient, survives restarts, Claude Code-aligned |
|
|
141
|
-
| Identical content stored as a link (dedup) | Hundreds of repeated writes cost almost nothing; links are materialized before their group is evicted, never left dangling |
|
|
142
|
-
| Session-level auto-cleanup | Removes only long-inactive sessions' snapshots; the active session and the chat log are never touched |
|
|
143
|
-
| Reconcile against the real disk before restoring | Idempotent, zero side effects, no collateral damage |
|
|
144
|
-
| Empty-message mask + replicating `/compact` | Deeply compatible with the host, pinned by probe tests |
|
|
145
|
-
| Crash safety (atomic writes + restore journal) | Continue or roll back cleanly after a crash |
|
|
146
|
-
| Pure-function planning + probed store | Fully unit-testable without a host; test-driven |
|
|
128
|
+
- **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.
|
|
129
|
+
- **Accurate restore**: backups are the sole standard, checked against the real disk — **only files that actually differ are touched**: modified files restored, newly created files deleted, deleted files recovered. Backups are stored byte for byte, so the restored result matches the backups exactly, with no "ghost impact".
|
|
130
|
+
- **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.
|
|
131
|
+
|
|
132
|
+
> **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.
|
|
147
133
|
|
|
148
134
|
## What it deliberately does NOT do
|
|
149
135
|
|
|
150
136
|
This plugin deliberately stays lightweight and focused on one thing — "conversation rewind". The following are **out of its scope**:
|
|
151
137
|
|
|
152
|
-
- **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
|
|
153
|
-
- **Subagent edits** — not tracked (same as Claude Code): a subagent runs its own session, so its backups could never be restored by a rewind of the parent session.
|
|
154
|
-
- **Fork / branch rewind** —
|
|
138
|
+
- **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).
|
|
139
|
+
- **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.
|
|
140
|
+
- **Fork / branch rewind** — DSH already provides this ("branch in new chat"); no need to reinvent the wheel.
|
|
155
141
|
|
|
156
142
|
## Compatibility
|
|
157
143
|
|
|
@@ -183,7 +169,7 @@ withdrew should consume the stable, locale-independent helpers exported from
|
|
|
183
169
|
|
|
184
170
|
## Security
|
|
185
171
|
|
|
186
|
-
This plugin only appends rewind-marker events to the session log; it never deletes or rewrites logged history. Workspace files are written only when you choose "conversation and code"; backups are stored under `<dsh home>/rewind-snapshots/`; restores draw only from those backups. It never touches your git repository, makes no network requests, and accesses no credentials.
|
|
172
|
+
This plugin only appends rewind-marker events to the session log; it never deletes or rewrites logged history. Workspace files are written only when you choose "conversation and code"; backups are stored under `<dsh home>/rewind-snapshots/`; restores draw only from those backups. It never touches your git repository, makes no network requests, and accesses no credentials. For sessions you've left inactive for a long time, a global auto-cleanup (off by default) can remove their snapshot directory in whole, leaving the active session and the chat log untouched. Full security model: [SECURITY.md](SECURITY.md).
|
|
187
173
|
|
|
188
174
|
## Development
|
|
189
175
|
|
|
@@ -207,7 +193,7 @@ Contributing guide: [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
|
207
193
|
Releases go out through GitHub Actions Trusted Publishing (OIDC, no stored `NPM_TOKEN`): push a `v<version>` tag and CI publishes with Sigstore provenance.
|
|
208
194
|
|
|
209
195
|
```sh
|
|
210
|
-
npm version patch && git push origin
|
|
196
|
+
npm version patch && git push origin <branch> --tags
|
|
211
197
|
```
|
|
212
198
|
|
|
213
199
|
One-time npm-side setup and the full workflow details: [docs/release/release.md](docs/release/release.md).
|
package/README.md
CHANGED
|
@@ -46,8 +46,8 @@ dsh plugin --profile web add dsh-rewind-plugin@<版本>
|
|
|
46
46
|
## 使用
|
|
47
47
|
|
|
48
48
|
1. 在对话中找到要回退的那条用户消息,或输入 `/rewind`(或其别名 `/undo`)打开候选列表选择。
|
|
49
|
-
2. **选中它。**
|
|
50
|
-
3.
|
|
49
|
+
2. **选中它。** 小浮层提供两种模式——「仅回退对话」或「回退对话和代码」。
|
|
50
|
+
3. 回退立即生效:对话回到目标消息当时的样子,目标消息的文本自动回填输入框——改完直接重发。
|
|
51
51
|
|
|
52
52
|
**键盘操作**:候选列表与模式浮层均支持 ↑↓ 移动、Enter 确认、Esc 取消/返回。
|
|
53
53
|
|
|
@@ -55,15 +55,15 @@ dsh plugin --profile web add dsh-rewind-plugin@<版本>
|
|
|
55
55
|
<summary><b>边界说明</b></summary>
|
|
56
56
|
|
|
57
57
|
- 回退可以反复进行——没有阶段或次数限制。
|
|
58
|
-
-
|
|
58
|
+
- 回退本身**无法撤销**,但被撤回的内容仍保留在会话日志中。
|
|
59
59
|
- **插话也能回退**——模型尚未读取的 `steering` 插话消息,同样可作为回退目标。
|
|
60
|
-
-
|
|
60
|
+
- **回退会打断当前正在运行的回合**——确保回退的安全执行。
|
|
61
61
|
|
|
62
62
|
</details>
|
|
63
63
|
|
|
64
64
|
## 存储管理
|
|
65
65
|
|
|
66
|
-
快照(写前备份)存储于 `<dsh home>/rewind-snapshots
|
|
66
|
+
快照(写前备份)存储于 `<dsh home>/rewind-snapshots/`(默认 `~/.dsh/rewind-snapshots/`)。插件对**同一会话**的快照做内容去重并保留最近 100 组锚点;**手动删除该目录**仅清除文件备份(对话回退不受影响),插件会自动重建。
|
|
67
67
|
|
|
68
68
|
另提供**全局自动清理**(默认关闭):把长期不活跃的会话快照整目录移除,不影响活动会话与对话日志。可在 `设置→插件→插件配置→快照清理` 面板查看与配置(自动清理开关、失活天数),也可用 `/snapshot-auto-cleanup` 命令查看、设置和运行。详见:[快照自动清理](docs/snapshot-auto-cleanup.zh.md)。
|
|
69
69
|
|
|
@@ -89,57 +89,44 @@ rm <dsh home>/snapshot-cleanup-last-sweep.json
|
|
|
89
89
|
| 维度 | 常见做法 | 本插件 |
|
|
90
90
|
| --- | --- | --- |
|
|
91
91
|
| 对话回退 | Fork 分支新建对话 | **就地回退**——不新建会话、不切窗口,便捷回退 |
|
|
92
|
-
| 文件还原 | 无还原功能 / git 管理或完整快照 |
|
|
92
|
+
| 文件还原 | 无还原功能 / git 管理或完整快照 | **写前轻量备份**——写文件前自动存原内容,一键还原 |
|
|
93
93
|
| 依赖 | 常依赖 Git 仓库或完整快照引擎 | **无依赖**——不依赖 git,普通目录即可用 |
|
|
94
|
-
| 存储开销 | 整树快照占空间大 |
|
|
94
|
+
| 存储开销 | 整树快照占空间大 | **轻量**——不变不存,且只追踪写类工具改动过的文件 |
|
|
95
95
|
|
|
96
96
|
## 原理
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
整套设计只有两条主线,核心哲学朴素却克制:**对话部分「只遮蔽、不删除」**,使用 DSH 原生的「隐藏 + 替换」机制;**文件部分「局部追踪,写前轻量备份」**,参考 Claude Code 的检查点语义。
|
|
99
99
|
|
|
100
100
|
### 1. 对话回退:一次「遮蔽」,而不是「删除」
|
|
101
101
|
|
|
102
|
-
`append-only`
|
|
102
|
+
`append-only` 是铁律:会话日志只追加、从不改写——这是可审计与信息安全的地基。回退从不动历史,它只做一步:往日志末尾追加一条 **“空消息”标记**,把目标消息之后的全部内容「遮蔽 + 替换」掉,让模型和界面都只看得到目标之前的部分。
|
|
103
103
|
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
104
|
+
- **同一份日志**——只在当前会话的日志做简单追加,不新建会话、不新建分支,因此不会留下残留和副本;
|
|
105
|
+
- **标记是规范的**——采用与官方 `/compact` 相同的「隐藏 + 替换」:`/compact` 把一段历史压缩成摘要,`/rewind` 则换成一条“空消息”标记。由于其规范性,DSH 的日志重放、压缩、续接检查都能正确识别它,绝不会把它误认为真实对话;
|
|
106
|
+
- **替换内容无感**——模型对标记忽略、无感(实测验证)。配合插件对界面显示的处理,模型和你看到的对话就是目标消息当时的样子;
|
|
107
|
+
- **记录完整保留**——因为是「遮蔽」而非「删除」,被撤回的内容完整留在日志里,可审计、可追溯,原则上也能手动恢复。
|
|
107
108
|
|
|
108
|
-
> **设计点睛**:整个对话回退就是**一条**追加。它确定、可审计,且因为日志从未被破坏,回溯是「干净的」——用最小的动作,实现最完整的语义。那些与
|
|
109
|
+
> **设计点睛**:整个对话回退就是**一条**追加。它确定、可审计,且因为日志从未被破坏,回溯是「干净的」——用最小的动作,实现最完整的语义。那些与 DSH 内部的兼容细节(对 `/compact` 的复刻、空消息的遮蔽)正是插件的专业所在。
|
|
109
110
|
|
|
110
|
-
### 2.
|
|
111
|
+
### 2. 文件还原:轻量检查点,「写前备份」
|
|
111
112
|
|
|
112
113
|
文件部分对齐 Claude Code 的检查点语义——**局部追踪、写前备份 + 每条消息重扫已跟踪文件**,而不是整树快照。这项取舍既省空间,又更完整:
|
|
113
114
|
|
|
114
115
|
- **写前备份**:只追踪写类工具(`write`、`edit`),写前**备份原内容**,并**记录、追踪**被处理的文件——从不备份整个工作区,因此轻量。
|
|
115
116
|
- **外部变更也追**:每条用户消息边界,插件重新检查所有已跟踪文件——命令执行、手动修改等外部变更同样被记录,回退时一并还原。这让「轻量」却不「残缺」。
|
|
116
|
-
-
|
|
117
|
-
-
|
|
118
|
-
-
|
|
117
|
+
- **不变不存**:记录只在有变化时发生——消息边界重扫时无变更的**不备份**(不留记录);写前备份时若与前一条记录一致,只存指向它的链接(`ref`)而非复制内容。
|
|
118
|
+
- **还原准确**:以备份为唯一标准,对照真实磁盘,只动真正不一致的文件——被改过的还原、被新建的删除、被删除的恢复;备份逐字节存储。还原结果与备份一致,无“幽灵影响”。
|
|
119
|
+
- **安全与完整性**:路径经安全化处理,绝不越出备份根目录;符号/硬链接跳过,避免透过一次还原误伤同名的另一份文件;单个文件失败绝不中止整轮还原;备份与还原日志均原子写落盘(跨重启仍在),断电或崩溃后的半还原可续做或回滚。
|
|
119
120
|
|
|
120
121
|
> **设计点睛**:这套检查点的「轻」,来自**只记录被工具动过、且确实变化的文件**——写前备份保证可还原,不变不存与存链接压掉重复;还原时再对照真实磁盘,只动不一致的文件。
|
|
121
122
|
|
|
122
|
-
### 设计亮点一览
|
|
123
|
-
|
|
124
|
-
| 设计 | 为什么值得 |
|
|
125
|
-
| --- | --- |
|
|
126
|
-
| 一次追加即一次回退 | 极小动作、极大语义,且日志从不被破坏 |
|
|
127
|
-
| 只遮蔽、不删除 | 历史永远可审计,原则上可恢复 |
|
|
128
|
-
| 改前备份 + 按轮分组 + 落盘 | 省空间、跨重启、对齐 Claude Code |
|
|
129
|
-
| 同内容存为链接(去重) | 上百次重复写入几乎不占空间;淘汰组前先落地链接,绝不悬空 |
|
|
130
|
-
| 会话级自动清理 | 只移除长期不活跃会话的快照,活动会话与对话日志永不触及 |
|
|
131
|
-
| 对照真实磁盘再还原 | 幂等、零副作用、不误伤 |
|
|
132
|
-
| 空消息遮蔽 + 对 `/compact` 的复刻 | 与宿主深度兼容,且被探针测试固化 |
|
|
133
|
-
| 崩溃安全(原子写 + 还原日志) | 断电/崩溃后仍可续做或回滚 |
|
|
134
|
-
| 纯函数规划 + 注入探针的存储 | 无需宿主即可单测,测试驱动 |
|
|
135
|
-
|
|
136
123
|
## 明确不做的事
|
|
137
124
|
|
|
138
125
|
本插件刻意保持轻量、聚焦"对话回退"这一件事,以下场景**不属于它的职责**:
|
|
139
126
|
|
|
140
|
-
- **整树 / Git 级快照**——只跟踪写类工具编辑 +
|
|
141
|
-
-
|
|
142
|
-
- **fork / 分支回退**——
|
|
127
|
+
- **整树 / Git 级快照**——只跟踪写类工具编辑 + 已跟踪文件的外部改动,从未被工具碰过的文件不还原。需要工作树级的完整快照回退时,请交给更专业的快照工具(git)。
|
|
128
|
+
- **子代理的编辑**——不追踪,子代理会话内也不提供回退(同 Claude Code):子代理运行在自己的会话里,其备份无法由父会话的回退还原,因此也不会为子会话保留备份。
|
|
129
|
+
- **fork / 分支回退**——DSH 已内置「在新对话中分支」,不重复造轮子。
|
|
143
130
|
|
|
144
131
|
## 兼容性
|
|
145
132
|
|
|
@@ -168,7 +155,7 @@ rm <dsh home>/snapshot-cleanup-last-sweep.json
|
|
|
168
155
|
|
|
169
156
|
## 安全
|
|
170
157
|
|
|
171
|
-
本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和代码」时被改写,备份存储于
|
|
158
|
+
本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和代码」时被改写,备份存储于 `<dsh home>/rewind-snapshots/`;还原以备份为唯一来源。不触碰你的 git 仓库,无网络请求,不访问任何凭据。对**长期不活跃**的会话,另有默认关闭的全局自动清理可整目录移除其快照,不影响活动会话与对话日志。完整安全模型:[SECURITY.md](SECURITY.md)。
|
|
172
159
|
|
|
173
160
|
## 开发
|
|
174
161
|
|
|
@@ -192,7 +179,7 @@ node scripts/verify-host.mjs # 端到端验证构建产物
|
|
|
192
179
|
通过 GitHub Actions Trusted Publishing(OIDC,无存储 `NPM_TOKEN`)发布:推送 `v<版本>` tag,CI 即带 Sigstore provenance 发布。
|
|
193
180
|
|
|
194
181
|
```sh
|
|
195
|
-
npm version patch && git push origin
|
|
182
|
+
npm version patch && git push origin <branch> --tags
|
|
196
183
|
```
|
|
197
184
|
|
|
198
185
|
一次性 npm 侧配置与完整流程:见 [docs/release/release.zh.md](docs/release/release.zh.md)。
|
package/SECURITY.md
CHANGED
|
@@ -12,9 +12,8 @@ The plugin runs in the DSH host process and therefore holds the host user's
|
|
|
12
12
|
filesystem authority — it reads and writes files with plain `node:fs`. The
|
|
13
13
|
following are treated as **untrusted inputs**:
|
|
14
14
|
|
|
15
|
-
- **Model arguments** — the `file_path`
|
|
16
|
-
|
|
17
|
-
checkpoint store records).
|
|
15
|
+
- **Model arguments** — the `file_path` field of `write` / `edit` tool calls
|
|
16
|
+
(it names the paths the checkpoint store records).
|
|
18
17
|
- **Session log contents** — events are parsed structurally; a hostile or
|
|
19
18
|
malformed id must never escape the store root.
|
|
20
19
|
- **Current worktree state** — restore planning reconciles against the live
|
|
@@ -57,7 +56,13 @@ to exactly the apply-start state, and per-file failures are reported rather
|
|
|
57
56
|
than hidden.
|
|
58
57
|
|
|
59
58
|
A failed gate fails closed: an invalid target, a missing store, an absent
|
|
60
|
-
backup, or a cancelled invocation aborts the rewind with an error.
|
|
59
|
+
backup, or a cancelled invocation aborts the rewind with an error. A store
|
|
60
|
+
written by a **newer** build is one of those gates: it is refused as a whole —
|
|
61
|
+
no partial restore and no new entry written into it, and the automatic
|
|
62
|
+
format-change clear below is skipped — while the conversation rewind itself
|
|
63
|
+
still works. An explicit clear (`run --current --apply`) or the age-based sweep,
|
|
64
|
+
being a user-directed deletion of whole session directories, is deliberately not
|
|
65
|
+
version-gated.
|
|
61
66
|
|
|
62
67
|
**Automatic store deletion**: two things delete session snapshots. The opt-in
|
|
63
68
|
`snapshot-auto-cleanup` sweep (default off) removes the whole directories of
|
|
@@ -68,7 +73,9 @@ positions that no longer line up. Both stay confined to the store root, use
|
|
|
68
73
|
`lstat` (so they never follow a symlink out of the root), the sweep never targets
|
|
69
74
|
the active session, and neither touches the conversation log. When auto-cleanup
|
|
70
75
|
is disabled (the default), only the format-change clear runs, and only for an
|
|
71
|
-
affected session.
|
|
76
|
+
affected session. The store-format guard runs before that clear, so a session
|
|
77
|
+
whose store is **newer** than this build understands is not cleared by it (see
|
|
78
|
+
`docs/format.md`).
|
|
72
79
|
|
|
73
80
|
## Conversation integrity
|
|
74
81
|
|
|
@@ -91,29 +98,49 @@ transcript.
|
|
|
91
98
|
`[a-zA-Z0-9._-]` (`safeSessionId` / `safeFileId`) before joining the store
|
|
92
99
|
root; `.` and `..` bare values are replaced — hostile ids cannot traverse
|
|
93
100
|
out of the root.
|
|
94
|
-
- **Never written through
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
- **Never written through a link or a moved directory**: two checks guard a
|
|
102
|
+
tracked path before any write or unlink. (1) A path whose last component is a
|
|
103
|
+
symlink or a hard link (`lstat().nlink > 1`) is skipped and reported, never
|
|
104
|
+
restored — a symlink would redirect the write outside the checkpoint, and a
|
|
105
|
+
hard link would clobber every other name of the same inode (e.g.
|
|
106
|
+
pnpm-installed files). (2) Every record this build writes — entry, link and
|
|
107
|
+
journal action — carries the `realpath` of the file's directory at commit time
|
|
108
|
+
(the location pin; best-effort, and absent on released-v1 data or a commit
|
|
109
|
+
whose parent could not be resolved), and a restore re-checks it before
|
|
110
|
+
touching the path, including a post-restart continue or rollback, which reads
|
|
111
|
+
the pin from the journal: a directory that no longer resolves there (a
|
|
112
|
+
repointed or moved ancestor) is refused and reported instead of writing
|
|
113
|
+
outside the recorded location, while data with no pin falls back to check (1)
|
|
114
|
+
alone. A parent chain that
|
|
115
|
+
is gone is still recreated (files whose directory was deleted are restorable),
|
|
116
|
+
but only while its nearest surviving ancestor resolves inside the pin. A
|
|
117
|
+
stable symlinked ancestor resolves identically on both sides, so a symlinked
|
|
118
|
+
workspace or temp root is never a false skip. This matches Claude Code's
|
|
119
|
+
checkpoint behavior since v2.1.216.
|
|
98
120
|
- **Restores name only recorded paths**: the store contains resolved display
|
|
99
121
|
paths of the session's own write-class tool calls (plus boundary re-checks
|
|
100
122
|
over that same tracked set) — a restore can never write an arbitrary path.
|
|
101
123
|
- **Path resolution rule**: relative paths resolve against the session
|
|
102
124
|
workspace cwd, mirroring the fs tools' own rule (`src/session-cwd.ts`).
|
|
103
125
|
- **Bounded backups**: `prune` keeps the newest 100 anchor groups per session
|
|
104
|
-
(`MAX_ANCHOR_GROUPS`),
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
126
|
+
(`MAX_ANCHOR_GROUPS`), except groups a non-terminal restore journal still
|
|
127
|
+
references (pinned so the op can be finished), so backup accumulation stays
|
|
128
|
+
bounded by the cap plus the number of unresolved journals (the exact cap is
|
|
129
|
+
pinned in `docs/format.md`). Across sessions, the opt-in `pruneStale` sweep
|
|
130
|
+
removes whole **long-inactive** session directories (measured by the newest
|
|
131
|
+
member being idle past `maxAgeDays`); it uses `lstat` (no symlink following),
|
|
132
|
+
skips dot-prefixed temp files, and never targets the active session
|
|
133
|
+
(`keepActiveId`).
|
|
110
134
|
|
|
111
135
|
## Crash safety
|
|
112
136
|
|
|
113
|
-
- **
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
137
|
+
- **Ordered commits**: a checkpoint commit places the before-sidecar (a raw
|
|
138
|
+
byte copy) before publishing the entry JSON, so a crash can leave an
|
|
139
|
+
unreferenced sidecar but never an entry whose bytes are missing. Every JSON
|
|
140
|
+
write (checkpoint entries, restore journals) goes to a sibling temp file and
|
|
141
|
+
is renamed over the target: a host crash mid-write can leave only an inert
|
|
142
|
+
`<target>.tmp` — never a readable half-written file — and readers never pick
|
|
143
|
+
up temp files.
|
|
117
144
|
- **Journaled restores**: before mutating anything, the restore captures each
|
|
118
145
|
planned path's pre-restore ("rescue") state and persists an intent journal,
|
|
119
146
|
then marks each action done as it is applied. A crash at any point leaves
|
|
@@ -126,7 +153,9 @@ transcript.
|
|
|
126
153
|
- **Fail-loud vs fail-soft**: a corrupt **journal** is reported
|
|
127
154
|
`recovery-required` — never silently dropped (dropping it would erase the
|
|
128
155
|
interrupted restore's recovery record). Corrupt **checkpoint entries** are
|
|
129
|
-
silently ignored (they only lose one backup, not the recovery path)
|
|
156
|
+
silently ignored (they only lose one backup, not the recovery path), and a
|
|
157
|
+
record whose sidecar is missing or truncated fails only that file — never a
|
|
158
|
+
delete of the live file.
|
|
130
159
|
- **Journal IO never fails the restore**: if the journal cannot be written the
|
|
131
160
|
restore proceeds with pre-journal semantics (crash safety degrades,
|
|
132
161
|
behavior does not).
|
|
@@ -136,9 +165,11 @@ transcript.
|
|
|
136
165
|
- This plugin does **not** sandbox other processes or stop them from changing
|
|
137
166
|
files concurrently.
|
|
138
167
|
- It does **not** provide confidentiality or tamper resistance against the
|
|
139
|
-
same operating-system user:
|
|
140
|
-
default permissions (
|
|
141
|
-
|
|
168
|
+
same operating-system user: **store** files are created with the process
|
|
169
|
+
default permissions (a standard umask applies), and the host user remains
|
|
170
|
+
trusted. Restored **workspace** files do carry their recorded permission
|
|
171
|
+
bits: a content restore reapplies them (`chmod`, best-effort), while a mode
|
|
172
|
+
difference on its own never triggers a restore.
|
|
142
173
|
- It does **not** exclude paths from the store, and it never oversteps: the
|
|
143
174
|
plugin only adds backup/restore on top of permissions DSH already holds. Two
|
|
144
175
|
distinct cases follow:
|
|
@@ -159,7 +190,8 @@ transcript.
|
|
|
159
190
|
committed a backup.
|
|
160
191
|
- **Subagent session edits are not tracked** (Claude Code alignment): a
|
|
161
192
|
subagent runs its own session, so the files it changes are not backed up and
|
|
162
|
-
cannot be restored by a rewind of the parent session
|
|
193
|
+
cannot be restored by a rewind of the parent session; the plugin keeps no
|
|
194
|
+
snapshot for a subagent session and offers no rewind inside one.
|
|
163
195
|
|
|
164
196
|
## Reporting
|
|
165
197
|
|
package/docs/architecture.md
CHANGED
|
@@ -81,7 +81,8 @@ Key invariants:
|
|
|
81
81
|
on it, so the marker is appended while idle, outside any turn, as one event.
|
|
82
82
|
- **Restore is reconciled against the live disk** (`planRestore`), so repeated
|
|
83
83
|
rewinds are idempotent and a rewind whose target state already matches is a
|
|
84
|
-
no-op.
|
|
84
|
+
no-op. A path whose directory no longer resolves to its commit-time location
|
|
85
|
+
is skipped and reported, never written through (see `SECURITY.md`).
|
|
85
86
|
|
|
86
87
|
### Marker format history
|
|
87
88
|
|
|
@@ -103,22 +104,32 @@ the `0.9.x` line) and is **removed in the `0.10.x` line**.
|
|
|
103
104
|
## Checkpoint pipeline (Claude Code before-backup model)
|
|
104
105
|
|
|
105
106
|
```
|
|
106
|
-
tools/execute captureBefore: for write / edit
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
tools/execute captureBefore: for write / edit, stage a raw byte copy of
|
|
108
|
+
the file's BEFORE state into the store's .pending/
|
|
109
|
+
(node:fs copyFile, never through a string);
|
|
110
|
+
subagent sessions are NOT tracked and get no rewind
|
|
111
|
+
surface at all (Claude Code alignment — the Harness
|
|
112
|
+
refuses generic Session RPCs for a subagent-owned
|
|
113
|
+
identity, so /rewind cannot execute there).
|
|
109
114
|
tools/post-execute commitEntry: anchor = latest user/message seq; skip
|
|
110
|
-
failed calls;
|
|
115
|
+
failed calls; publish the staged bytes as the entry's
|
|
116
|
+
sidecar and write the metadata beside them — including
|
|
117
|
+
where the directory resolved (`realpath`), the location
|
|
118
|
+
pin a restore re-checks.
|
|
111
119
|
session/event user/message boundary: reconcileTracked re-reads every
|
|
112
120
|
(user/message) tracked file and records a new before-backup for any
|
|
113
121
|
whose disk state changed since last seen — external
|
|
114
122
|
edits/deletions enter the record this way.
|
|
115
123
|
prune keeps the newest 100 anchor groups per session, storing
|
|
116
124
|
identical before-content as in-place links that are
|
|
117
|
-
materialized before their group is dropped,
|
|
118
|
-
terminal restore
|
|
125
|
+
materialized before their group is dropped, never dropping
|
|
126
|
+
a group a non-terminal restore journal still references,
|
|
127
|
+
and recycles terminal restore journals.
|
|
119
128
|
pruneStale cross-session auto-cleanup (default off): whole
|
|
120
129
|
long-inactive session dirs past the cutoff are removed;
|
|
121
|
-
the active session is never targeted
|
|
130
|
+
the active session is never targeted (a subagent's tool
|
|
131
|
+
result never triggers the sweep — it owns no dir and would
|
|
132
|
+
claim that exemption).
|
|
122
133
|
```
|
|
123
134
|
|
|
124
135
|
## Compatibility strategy
|
package/docs/compat/audit.md
CHANGED
|
@@ -45,7 +45,7 @@ version's shape only (no `Session.events` / `[data-time-hover-root]` /
|
|
|
45
45
|
| I3 step/turn structure | Client turn-tail ordering, unique `step/start` (real turns), no ghost turns; the `user/message` rewind marker adds no step frame | `compat-invariants` I3, `helpers.assertTurnTailOrdering` |
|
|
46
46
|
| I4 fold-service safety | stats / title / goal / projection fold a marker-bearing log without throwing, with predictable values | `compat-invariants` I4 |
|
|
47
47
|
| I5 compact interop | A tool-call orphaned by a cancelled turn is pair-balanced once shadowed by a rewind; a rewind across a compaction checkpoint is explicitly refused; a rewind-then-compact transaction stays legal | `compat-interop` I5, `verify-host` 12/14 |
|
|
48
|
-
| I6 tool pipeline | before-snapshot capture/commit/restore is correct (existing `snapshot.test.ts` + `verify-host` 4–8); cancellation timing never hangs | `verify-host` 4–8, 15 |
|
|
48
|
+
| I6 tool pipeline | before-snapshot capture/commit/restore is correct (existing `snapshot.test.ts` + `verify-host` 4–8) for the tracked tools `write` / `edit` — `str_replace_editor` is an optional DSH package that stopped being a default tool in DSH 0.1.3, so it is not tracked; cancellation timing never hangs | `verify-host` 4–8, 15 |
|
|
49
49
|
| I7 client ordering | A log carrying tool turns and rewind markers (a single `user/message` replace) satisfies the client builder ordering | `compat-interop` I7 |
|
|
50
50
|
| I8 runtime safety | `rewind`/`compact` combinations never leave a dangling step/turn frame | `verify-host` 15 |
|
|
51
51
|
|
|
@@ -32,7 +32,8 @@ console filter:
|
|
|
32
32
|
| Scope | What an anomaly here means |
|
|
33
33
|
| --- | --- |
|
|
34
34
|
| `boot` | Startup identity — `loaded v<version> (build <hash>)`, gated by the verbose switch (see below). Confirms the running bundle matches a fix. |
|
|
35
|
-
| `refill` | The composer refill after a rewind (command/wait/refill throws, an outcome that never settles) |
|
|
35
|
+
| `refill` | The composer refill after a rewind (command/wait/refill throws, a rejected or unmatched command, an outcome that never settles) |
|
|
36
|
+
| `preview` | The `/rewind preview` impact probe behind the mode popover (command rejected/unmatched/threw, an outcome that never settles) |
|
|
36
37
|
| `portals` | Per-message button mount issues (e.g. no session binding) |
|
|
37
38
|
| `settings` | The snapshot-cleanup settings card |
|
|
38
39
|
| `hiding` | **Reserved** — no active alert at present. If a future row-hiding diagnostic is added, it belongs in this region. |
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
| scope | 这里的异常含义 |
|
|
23
23
|
| --- | --- |
|
|
24
24
|
| `boot` | 启动身份行——`loaded v<version> (build <hash>)`,由详细输出开关控制(见下)。用于确认运行中的 bundle 是否匹配某个修复。 |
|
|
25
|
-
| `refill` |
|
|
25
|
+
| `refill` | 回退后的输入框回填(命令/等待/回填抛出异常、命令被拒绝或未匹配、等待的结果始终未落定) |
|
|
26
|
+
| `preview` | 模式气泡背后的 `/rewind preview` 影响探测(命令被拒绝/未匹配/抛出异常、等待的结果始终未落定) |
|
|
26
27
|
| `portals` | 每条消息按钮的挂载问题(如无会话绑定) |
|
|
27
28
|
| `settings` | 快照清理设置卡片 |
|
|
28
29
|
| `hiding` | **保留区**——当前无活跃告警。若未来新增行隐藏诊断,应归属此区域。 |
|
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
## One-line summary
|
|
10
10
|
|
|
11
11
|
Rewind **always** restores files edited with the dedicated editing tools
|
|
12
|
-
(`write` / `edit`
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
(`write` / `edit`) because the plugin snapshots them before the edit.
|
|
13
|
+
Command-line (or hand) edits are covered **only when the file was previously
|
|
14
|
+
registered** by an editing tool in the same conversation — and, in that case,
|
|
15
|
+
the change itself is recorded, but rewinding to a boundary inside an unchanged
|
|
16
|
+
interval is only as precise as the next recorded state (see
|
|
17
17
|
[Rewind precision](#rewind-precision-for-a-registered-file-changed-outside-editing-tools)).
|
|
18
18
|
|
|
19
19
|
## How file changes are classified
|
|
20
20
|
|
|
21
|
-
**Editing tools** (create/overwrite,
|
|
21
|
+
**Editing tools** (create/overwrite, line edits): each call tells the
|
|
22
22
|
plugin *which file* it will change, so the plugin snapshots the file's current
|
|
23
23
|
content **before** the edit and can restore it on rewind. This path is
|
|
24
24
|
deterministic — using them always yields a restorable rewind.
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## 一句话概括
|
|
6
6
|
|
|
7
|
-
回退**始终能还原**用专用编辑工具(`write` / `edit
|
|
7
|
+
回退**始终能还原**用专用编辑工具(`write` / `edit`)编辑过的文件,因为插件会在编辑前对其快照。**命令行(或手动)编辑**仅当该文件在同一会话中**先前已被某个编辑工具登记**时才会被覆盖——此时变更本身会被记录,但回退到"未变区间中间的边界"时,只能精确到**下一处被记录的状态**(见[已登记文件被编辑工具之外的方式改动时的回退精度](#已登记文件被编辑工具之外的方式改动时的回退精度))。
|
|
8
8
|
|
|
9
9
|
## 文件变更的分类
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**编辑工具**(创建/覆盖、按行编辑):每次调用都会告诉插件*将要改动哪个文件*,因此插件会在编辑前对文件的当前内容**快照**,回退时能还原。这条路径是**确定性**的——用了就一定可还原。
|
|
12
12
|
|
|
13
13
|
**命令行或手动编辑**:Windows 上的 PowerShell(`pwsh`)写入、Linux 上的 `sed -i`,或手动保存文件。它们**不告诉**插件动了哪个文件,所以**没有可预先快照的对象**。
|
|
14
14
|
|