dsh-rewind-plugin 0.11.0-beta.1 → 0.12.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/CONTRIBUTING.md +4 -3
- package/README.en.md +19 -2
- package/README.md +18 -3
- package/SECURITY.md +54 -23
- package/docs/architecture.md +12 -6
- package/docs/compat/audit.md +1 -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 +1 -1
- package/lib/index.js +1026 -223
- 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
|
@@ -35,8 +35,11 @@ Every user message carries a **↶ rewind** button in its action row. Clicking i
|
|
|
35
35
|
|
|
36
36
|
## Install
|
|
37
37
|
|
|
38
|
+
Check your local DSH version, then find the matching plugin version in
|
|
39
|
+
[Releases](https://github.com/SiriLee/dsh-rewind/releases).
|
|
40
|
+
|
|
38
41
|
```sh
|
|
39
|
-
dsh plugin --profile web add dsh-rewind-plugin
|
|
42
|
+
dsh plugin --profile web add dsh-rewind-plugin@<version>
|
|
40
43
|
```
|
|
41
44
|
|
|
42
45
|
> ⚠️ The npm name `dsh-rewind` belongs to another author's package — install with `dsh-rewind-plugin`.
|
|
@@ -77,6 +80,20 @@ cleanup** panel (the auto-cleanup switch and the idle-day cutoff), or use the
|
|
|
77
80
|
|
|
78
81
|
<img src="assets/screenshots/cleanup-setting.png" alt="Snapshot cleanup settings: auto-cleanup and idle days" width="600">
|
|
79
82
|
|
|
83
|
+
## Uninstall
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
# Uninstall the plugin
|
|
87
|
+
dsh plugin --profile web remove dsh-rewind-plugin
|
|
88
|
+
|
|
89
|
+
# To also delete local data
|
|
90
|
+
rm -rf <dsh home>/rewind-snapshots
|
|
91
|
+
rm <dsh home>/snapshot-cleanup-last-sweep.json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The plugin's auto-cleanup settings live in the settings document
|
|
95
|
+
(`<dsh home>/settings.yaml`). To remove them completely, delete the key manually.
|
|
96
|
+
|
|
80
97
|
## Why it stands out
|
|
81
98
|
|
|
82
99
|
Compared with the common approaches, here is the trade-off this plugin makes on "rewind":
|
|
@@ -190,7 +207,7 @@ Contributing guide: [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
|
190
207
|
Releases go out through GitHub Actions Trusted Publishing (OIDC, no stored `NPM_TOKEN`): push a `v<version>` tag and CI publishes with Sigstore provenance.
|
|
191
208
|
|
|
192
209
|
```sh
|
|
193
|
-
npm version patch && git push origin
|
|
210
|
+
npm version patch && git push origin <branch> --tags
|
|
194
211
|
```
|
|
195
212
|
|
|
196
213
|
One-time npm-side setup and the full workflow details: [docs/release/release.md](docs/release/release.md).
|
package/README.md
CHANGED
|
@@ -35,8 +35,10 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
|
|
|
35
35
|
|
|
36
36
|
## 安装
|
|
37
37
|
|
|
38
|
+
确认本机 DSH 版本后,在 [Release](https://github.com/SiriLee/dsh-rewind/releases) 找到适配的插件版本。
|
|
39
|
+
|
|
38
40
|
```sh
|
|
39
|
-
dsh plugin --profile web add dsh-rewind-plugin
|
|
41
|
+
dsh plugin --profile web add dsh-rewind-plugin@<版本>
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
> ⚠️ npm 上的 `dsh-rewind` 属于其他作者,请用 `dsh-rewind-plugin` 安装。
|
|
@@ -67,6 +69,19 @@ dsh plugin --profile web add dsh-rewind-plugin
|
|
|
67
69
|
|
|
68
70
|
<img src="assets/screenshots/cleanup-setting.png" alt="快照清理设置:自动清理与失活天数" width="600">
|
|
69
71
|
|
|
72
|
+
## 卸载
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
# 卸载插件
|
|
76
|
+
dsh plugin --profile web remove dsh-rewind-plugin
|
|
77
|
+
|
|
78
|
+
# 如需同时删除本地数据
|
|
79
|
+
rm -rf <dsh home>/rewind-snapshots
|
|
80
|
+
rm <dsh home>/snapshot-cleanup-last-sweep.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
插件的自动清理设置保存在设置文档(`<dsh home>/settings.yaml`)中。如需彻底清理,可手动删除对应键。
|
|
84
|
+
|
|
70
85
|
## 本插件的优势
|
|
71
86
|
|
|
72
87
|
和常见的几种做法相比,本插件在"回退"这件事上的取舍:
|
|
@@ -153,7 +168,7 @@ dsh plugin --profile web add dsh-rewind-plugin
|
|
|
153
168
|
|
|
154
169
|
## 安全
|
|
155
170
|
|
|
156
|
-
本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和代码」时被改写,备份存储于
|
|
171
|
+
本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和代码」时被改写,备份存储于 `<dsh home>/rewind-snapshots/`;还原以备份为唯一来源。不触碰你的 git 仓库,无网络请求,不访问任何凭据。对**长期不活跃**的会话,另有默认关闭的全局自动清理可整目录移除其快照,不影响活动会话与对话日志。完整安全模型:[SECURITY.md](SECURITY.md)。
|
|
157
172
|
|
|
158
173
|
## 开发
|
|
159
174
|
|
|
@@ -177,7 +192,7 @@ node scripts/verify-host.mjs # 端到端验证构建产物
|
|
|
177
192
|
通过 GitHub Actions Trusted Publishing(OIDC,无存储 `NPM_TOKEN`)发布:推送 `v<版本>` tag,CI 即带 Sigstore provenance 发布。
|
|
178
193
|
|
|
179
194
|
```sh
|
|
180
|
-
npm version patch && git push origin
|
|
195
|
+
npm version patch && git push origin <branch> --tags
|
|
181
196
|
```
|
|
182
197
|
|
|
183
198
|
一次性 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:
|
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,19 +104,24 @@ 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
|
-
|
|
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);
|
|
108
110
|
subagent edits are NOT tracked (Claude Code alignment).
|
|
109
111
|
tools/post-execute commitEntry: anchor = latest user/message seq; skip
|
|
110
|
-
failed calls;
|
|
112
|
+
failed calls; publish the staged bytes as the entry's
|
|
113
|
+
sidecar and write the metadata beside them — including
|
|
114
|
+
where the directory resolved (`realpath`), the location
|
|
115
|
+
pin a restore re-checks.
|
|
111
116
|
session/event user/message boundary: reconcileTracked re-reads every
|
|
112
117
|
(user/message) tracked file and records a new before-backup for any
|
|
113
118
|
whose disk state changed since last seen — external
|
|
114
119
|
edits/deletions enter the record this way.
|
|
115
120
|
prune keeps the newest 100 anchor groups per session, storing
|
|
116
121
|
identical before-content as in-place links that are
|
|
117
|
-
materialized before their group is dropped,
|
|
118
|
-
terminal restore
|
|
122
|
+
materialized before their group is dropped, never dropping
|
|
123
|
+
a group a non-terminal restore journal still references,
|
|
124
|
+
and recycles terminal restore journals.
|
|
119
125
|
pruneStale cross-session auto-cleanup (default off): whole
|
|
120
126
|
long-inactive session dirs past the cutoff are removed;
|
|
121
127
|
the active session is never targeted.
|
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
|
|
|
@@ -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
|
|