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 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`, plus crash-safety
62
- scenarios in `tests/crash-safety.test.ts` via the test-only `crash` seam
63
- (`RestoreRunOptions.crash`).
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 main --tags
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
- 本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和代码」时被改写,备份存储于 `~/.dsh/rewind-snapshots/`;还原以备份为唯一来源。不触碰你的 git 仓库,无网络请求,不访问任何凭据。对**长期不活跃**的会话,另有默认关闭的全局自动清理可整目录移除其快照,不影响活动会话与对话日志。完整安全模型:[SECURITY.md](SECURITY.md)。
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 main --tags
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` / `path` / `command` fields of write,
16
- `edit`, and `str_replace_editor` tool calls (they name the paths the
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 links**: symlinked and hard-linked paths
95
- (`lstat().nlink > 1`) are skipped and reported, never restored a symlink
96
- would redirect the write outside the checkpoint, and a hard link would
97
- clobber every other name of the same inode (e.g. pnpm-installed files).
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`), so backup accumulation cannot grow the store without
105
- bound (the exact cap is pinned in `docs/format.md`). Across sessions, the
106
- opt-in `pruneStale` sweep removes whole **long-inactive** session directories
107
- (measured by the newest member being idle past `maxAgeDays`); it uses `lstat`
108
- (no symlink following), skips dot-prefixed temp files, and never targets the
109
- active session (`keepActiveId`).
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
- - **Atomic commits**: every JSON write (checkpoint entries, restore journals)
114
- goes to a sibling temp file and is renamed over the target. A host crash
115
- mid-write can leave only an inert `<target>.tmp` never a readable
116
- half-written file and readers never pick up temp files.
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: state files are created with the process
140
- default permissions (no special modes are set — a standard umask applies),
141
- and the host user remains trusted.
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:
@@ -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
- (mutating commands only), read the file's BEFORE state;
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; write the before-backup to the store.
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, and recycles
118
- terminal restore journals.
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.
@@ -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` / `str_replace_editor`) because the plugin snapshots them
13
- before the edit. Command-line (or hand) edits are covered **only when the file
14
- was previously registered** by an editing tool in the same conversation — and,
15
- in that case, the change itself is recorded, but rewinding to a boundary inside
16
- an unchanged interval is only as precise as the next recorded state (see
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, edit lines, insert): each call tells the
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` / `str_replace_editor`)编辑过的文件,因为插件会在编辑前对其快照。**命令行(或手动)编辑**仅当该文件在同一会话中**先前已被某个编辑工具登记**时才会被覆盖——此时变更本身会被记录,但回退到"未变区间中间的边界"时,只能精确到**下一处被记录的状态**(见[已登记文件被编辑工具之外的方式改动时的回退精度](#已登记文件被编辑工具之外的方式改动时的回退精度))。
7
+ 回退**始终能还原**用专用编辑工具(`write` / `edit`)编辑过的文件,因为插件会在编辑前对其快照。**命令行(或手动)编辑**仅当该文件在同一会话中**先前已被某个编辑工具登记**时才会被覆盖——此时变更本身会被记录,但回退到"未变区间中间的边界"时,只能精确到**下一处被记录的状态**(见[已登记文件被编辑工具之外的方式改动时的回退精度](#已登记文件被编辑工具之外的方式改动时的回退精度))。
8
8
 
9
9
  ## 文件变更的分类
10
10
 
11
- **编辑工具**(创建/覆盖、按行编辑、插入):每次调用都会告诉插件*将要改动哪个文件*,因此插件会在编辑前对文件的当前内容**快照**,回退时能还原。这条路径是**确定性**的——用了就一定可还原。
11
+ **编辑工具**(创建/覆盖、按行编辑):每次调用都会告诉插件*将要改动哪个文件*,因此插件会在编辑前对文件的当前内容**快照**,回退时能还原。这条路径是**确定性**的——用了就一定可还原。
12
12
 
13
13
  **命令行或手动编辑**:Windows 上的 PowerShell(`pwsh`)写入、Linux 上的 `sed -i`,或手动保存文件。它们**不告诉**插件动了哪个文件,所以**没有可预先快照的对象**。
14
14