dsh-rewind-plugin 0.4.2 → 0.6.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 +72 -0
- package/README.en.md +53 -40
- package/README.md +44 -40
- package/SECURITY.md +160 -0
- package/docs/README.md +30 -0
- package/docs/architecture.md +128 -0
- package/docs/compat/audit.md +149 -0
- package/docs/compat/tracking-boundary.md +103 -0
- package/docs/compat/tracking-boundary.zh.md +55 -0
- package/docs/compat/troubleshooting.md +41 -0
- package/docs/{troubleshooting.zh.md → compat/troubleshooting.zh.md} +15 -0
- package/docs/format.md +151 -0
- package/docs/harness-reference.md +8 -4
- package/docs/release/release.md +69 -0
- package/docs/{release.md → release/release.zh.md} +6 -52
- package/docs/snapshot-auto-cleanup.md +90 -0
- package/docs/snapshot-auto-cleanup.zh.md +57 -0
- package/lib/client.js +7 -64
- package/lib/index.js +688 -45
- package/lib/types/client/index.d.ts +13 -13
- package/lib/types/client/locales.d.ts +0 -2
- package/lib/types/index.d.ts +9 -4
- package/lib/types/locales.d.ts +22 -0
- package/lib/types/snapshot-cleanup.d.ts +145 -0
- package/lib/types/snapshot.d.ts +226 -13
- package/package.json +9 -1
- package/docs/compat-audit.md +0 -137
- package/docs/troubleshooting.md +0 -44
- /package/docs/{client-contract.md → contract/client-contract.md} +0 -0
- /package/docs/{client-contract.zh.md → contract/client-contract.zh.md} +0 -0
package/docs/format.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Durable format
|
|
2
|
+
|
|
3
|
+
The on-disk format of the dsh-rewind checkpoint store, pinned as a spec. The
|
|
4
|
+
implementation is `src/snapshot.ts`; this document is the reference for what
|
|
5
|
+
readers may rely on and what a future incompatible change must do. If the code
|
|
6
|
+
and this spec disagree, the code wins and this spec is a bug.
|
|
7
|
+
|
|
8
|
+
## State root
|
|
9
|
+
|
|
10
|
+
The store root defaults to `<harness home>/rewind-snapshots/` — the dsh data
|
|
11
|
+
directory (`~/.dsh/rewind-snapshots/` when `DSH_HOME` is unset) — overridable in
|
|
12
|
+
order by the `snapshotDir` plugin config, then the `DSH_REWIND_SNAPSHOT_DIR`
|
|
13
|
+
environment variable. It is a sibling of the workspace, never a subtree of it.
|
|
14
|
+
Deleting the root only removes file backups; the store rebuilds from scratch.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
<root>/
|
|
18
|
+
└── <sessionId>/ # safeSessionId(sessionId)
|
|
19
|
+
├── <anchorSeq>/ # decimal seq of the anchoring user/message
|
|
20
|
+
│ └── <callId>.json # one committed before-backup
|
|
21
|
+
└── restore-journal-<opId>.json # one restore-op journal
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `sessionId` is sanitized to `[a-zA-Z0-9._-]`; the bare values `.` and `..`
|
|
25
|
+
are replaced (`safeSessionId`) so a hostile id cannot traverse out of the
|
|
26
|
+
root.
|
|
27
|
+
- `callId` is sanitized to `[a-zA-Z0-9._-]` (`safeFileId`).
|
|
28
|
+
- `<anchorSeq>` is a decimal integer; directories with non-integer names are
|
|
29
|
+
ignored by readers.
|
|
30
|
+
- Journal files are recognized by the `restore-journal-` prefix; everything
|
|
31
|
+
else ending in `.json` under the session dir is treated as a checkpoint
|
|
32
|
+
entry.
|
|
33
|
+
|
|
34
|
+
## Checkpoint entry
|
|
35
|
+
|
|
36
|
+
One JSON file per before-backup, named `<callId>.json`:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
interface CheckpointEntry {
|
|
40
|
+
callId: string // the tool call that mutated the file
|
|
41
|
+
anchorSeq: number // seq of the user message anchoring the turn of the change
|
|
42
|
+
path: string // resolved display path (absolute)
|
|
43
|
+
before: string | null // full content before the change; null = file was created
|
|
44
|
+
time: number // epoch ms, strictly increasing within a store instance
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Semantics:
|
|
49
|
+
|
|
50
|
+
- **`before` is the pre-edit state**: `null` means the call created the file.
|
|
51
|
+
- **`anchorSeq` ties the backup to a user message**: rewinding to message N
|
|
52
|
+
applies every entry anchored at or after N (the boundary is inclusive).
|
|
53
|
+
- **`time` is the ordering key within an anchor group**: it is monotonic per
|
|
54
|
+
store instance (bumped past the previous commit), so same-millisecond
|
|
55
|
+
commits stay capture-ordered and a re-read always picks the same "earliest"
|
|
56
|
+
entry per path.
|
|
57
|
+
- Synthetic re-check entries (external edits/deletions seen at a user-message
|
|
58
|
+
boundary) use `callId = recheck-<anchorSeq>-<sha256(path) first 8 hex>`.
|
|
59
|
+
|
|
60
|
+
### Dedup link entry
|
|
61
|
+
|
|
62
|
+
A tracked file that records the same `before` content as its immediately-prior
|
|
63
|
+
entry for that path is stored as a **link** instead of a full copy: the entry
|
|
64
|
+
carries a `ref` (the `<anchorSeq>/<callId>.json` of that prior entry) and omits
|
|
65
|
+
`before`, so identical content is never duplicated across entries. A reader
|
|
66
|
+
resolves the `ref` back to the terminal real snapshot; `before: null` still
|
|
67
|
+
means "the file was created". A `ref` is validated as a single-level,
|
|
68
|
+
`<digits>/<callId>.json` relative reference (no traversal) so a corrupt or
|
|
69
|
+
hostile ref cannot escape the store root when followed. Because links reference
|
|
70
|
+
prior entries, `prune` materializes a surviving link whose `ref` lands on a
|
|
71
|
+
group it is about to drop before deleting that group, so no kept link is left
|
|
72
|
+
dangling.
|
|
73
|
+
|
|
74
|
+
Real entries (with `before`) are unchanged and read identically before and
|
|
75
|
+
after this addition; a link entry is a distinct kind that lacks `before`.
|
|
76
|
+
|
|
77
|
+
## Restore journal
|
|
78
|
+
|
|
79
|
+
One JSON file per restore operation, written **before any mutation** and
|
|
80
|
+
updated as the pass applies:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
interface RestoreJournal {
|
|
84
|
+
version: 1
|
|
85
|
+
id: string // `op-<base36 ms>-<random>`; file name suffix
|
|
86
|
+
sessionId: string
|
|
87
|
+
targetSeq: number // rewind target the restore belongs to
|
|
88
|
+
startedAt: number // epoch ms
|
|
89
|
+
finishedAt?: number // set on a terminal state
|
|
90
|
+
state: 'running' | 'rollback-running' | 'completed' | 'rolled-back' | 'recovery-required'
|
|
91
|
+
actions: RestoreJournalAction[]
|
|
92
|
+
rollbackError?: string // set when a rollback pass failed partway
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface RestoreJournalAction {
|
|
96
|
+
path: string
|
|
97
|
+
action: 'restore' | 'delete' // restore = write `before` back; delete = unlink
|
|
98
|
+
before: string | null // target content for restore; null for delete
|
|
99
|
+
rescue: string | null // pre-restore disk state; null = file was absent
|
|
100
|
+
rescueError?: string // set when the rescue capture failed (rollback skips it)
|
|
101
|
+
done: boolean // true once the action's fs op completed and was marked
|
|
102
|
+
failed?: string // per-action failure message (the pass never aborts)
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
States: `running` and `rollback-running` are non-terminal; a host restart turns
|
|
107
|
+
them into `interrupted` (or `recovery-required` when the journal is corrupt or
|
|
108
|
+
a rollback could not complete). `completed` / `rolled-back` are terminal.
|
|
109
|
+
|
|
110
|
+
## Write guarantees
|
|
111
|
+
|
|
112
|
+
- **Atomicity**: every JSON write serializes to a sibling `<target>.tmp` and
|
|
113
|
+
renames over the target. A crash between the two steps leaves only the temp
|
|
114
|
+
file — never a readable half-written target — and readers ignore temp files
|
|
115
|
+
(they do not end in `.json`). The next write of the same target overwrites
|
|
116
|
+
a leftover temp.
|
|
117
|
+
- **Journal before mutation**: the rescue state of every planned path is
|
|
118
|
+
captured and the intent journal persisted atomically BEFORE the first fs
|
|
119
|
+
mutation; each action is marked `done` as it is applied.
|
|
120
|
+
- **Disk is truth**: after a restart, reconciliation compares the real disk
|
|
121
|
+
against each action's goal (the restore target for `running` journals, the
|
|
122
|
+
rescue state for `rollback-running` / `recovery-required` ones). A path
|
|
123
|
+
whose disk already matches is marked done without being touched.
|
|
124
|
+
- **Bounded storage**: `prune` keeps the newest 100 anchor groups per session
|
|
125
|
+
(`MAX_ANCHOR_GROUPS`), materializing any surviving dedup link that references
|
|
126
|
+
a group being dropped before deleting whole anchor directories; it also
|
|
127
|
+
recycles terminal journals (`completed` / `rolled-back`). Non-terminal and
|
|
128
|
+
corrupt journals are always kept. Across sessions, `pruneStale` removes whole
|
|
129
|
+
long-inactive session directories whose newest member stamp is older than a
|
|
130
|
+
configurable idle cutoff (default off), so the store root does not grow
|
|
131
|
+
without bound either.
|
|
132
|
+
|
|
133
|
+
## Validation and failure policy
|
|
134
|
+
|
|
135
|
+
- **Entries**: a missing or malformed entry is read as `undefined` (silently
|
|
136
|
+
skipped) — losing one backup, never the recovery path.
|
|
137
|
+
- **Journals**: a corrupt or schema-invalid journal **fails loud** —
|
|
138
|
+
`reconcileRestores` reports it as `recovery-required` and never drops it,
|
|
139
|
+
because dropping it would silently erase the interrupted restore's recovery
|
|
140
|
+
record.
|
|
141
|
+
- **Journal IO**: best-effort by design — if a journal cannot be written, the
|
|
142
|
+
restore proceeds with pre-journal semantics (crash safety degrades,
|
|
143
|
+
behavior does not).
|
|
144
|
+
|
|
145
|
+
## Versioning policy
|
|
146
|
+
|
|
147
|
+
The journal schema is `version: 1`. Checkpoint entries currently carry no
|
|
148
|
+
version field. A future incompatible format must either bump the journal
|
|
149
|
+
`version` (readers reject unknown values — there is no best-effort fallback or
|
|
150
|
+
legacy coercion) or move the state root (e.g. `rewind-snapshots/v2`) and ship
|
|
151
|
+
an explicit migration tool. Old-format data is never silently re-interpreted.
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
Compatibility probes against these subsystems (test-driven investigation, the
|
|
23
23
|
`compat-invariants` / `compat-interop` vitest suites + the `verify-host` real
|
|
24
|
-
`/compact` chain): [compat
|
|
24
|
+
`/compact` chain): [compat/audit.md](compat/audit.md).
|
|
25
25
|
|
|
26
26
|
Also under `docs/` at the repo root: `persistence-catalog.md` (full
|
|
27
27
|
`SessionEventMap`), `tool-catalog.md` (tool inventory), `config-catalog.md`
|
|
@@ -45,20 +45,24 @@ Also under `docs/` at the repo root: `persistence-catalog.md` (full
|
|
|
45
45
|
|
|
46
46
|
## Plugin source layout
|
|
47
47
|
|
|
48
|
+
The module map below duplicates `AGENTS.md` (`Layout at a glance`), which is
|
|
49
|
+
the canonical source; this block only adds the finer-grained client-side files
|
|
50
|
+
and packaging entries.
|
|
51
|
+
|
|
48
52
|
```
|
|
49
53
|
src/index.ts host plugin: /rewind command + checkpoint pipeline (tools/execute|post-execute)
|
|
50
54
|
src/rewind.ts pure planning: target resolution, surface range, candidate listing
|
|
51
55
|
src/snapshot.ts checkpoint store (disk before-backups, restore/preview, bounded prune)
|
|
52
56
|
src/session-cwd.ts session-cwd resolution (fs-tools rule)
|
|
53
|
-
src/client/index.ts client plugin: per-message ↶ button
|
|
57
|
+
src/client/index.ts client plugin: /rewind command decoration + per-message ↶ button portals
|
|
54
58
|
src/client/popover.ts mode-selection popover (both-mode impact confirm)
|
|
55
59
|
src/client/hidden.ts withdrawn-span computation (hiddenSeqsOf), pure
|
|
56
60
|
src/client/locales.ts zh / en copy (LocaleNamespaceMap)
|
|
57
61
|
src/client/styles.ts injected styles (dsh design tokens)
|
|
58
62
|
scripts/build.mjs esbuild: lib/index.js (host ESM) + lib/client.js (loader closure) + .d.ts
|
|
59
|
-
scripts/verify-host.mjs end-to-end host verification (
|
|
63
|
+
scripts/verify-host.mjs end-to-end host verification (full check suite)
|
|
60
64
|
tests/ vitest suites (rewind / snapshot / hidden / session-cwd / integration)
|
|
61
|
-
docs/ maintainer docs:
|
|
65
|
+
docs/ maintainer docs: contract/, compat/, release/ subdirectories
|
|
62
66
|
assets/screenshots/ UI screenshots
|
|
63
67
|
cordis.patch.yml bundle patch (mounts the dual-face plugin row)
|
|
64
68
|
package.json dsh.bundle + dsh.client manifests, optional peerDependencies
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Release
|
|
2
|
+
|
|
3
|
+
[简体中文](release.zh.md)
|
|
4
|
+
|
|
5
|
+
## First release (manual, one-time)
|
|
6
|
+
|
|
7
|
+
Trusted Publisher can only be configured once the package exists, so the first
|
|
8
|
+
version is published locally:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm login
|
|
12
|
+
npm publish --access public
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- If prompted for `EOTP`: complete the browser auth link the CLI prints, or
|
|
16
|
+
retry with a 6-digit code — `npm publish --otp=<code>`.
|
|
17
|
+
- The first version carries no provenance (local path) — acceptable; every CI
|
|
18
|
+
release after that publishes with Sigstore/SLSA provenance automatically.
|
|
19
|
+
|
|
20
|
+
## Configure Trusted Publisher (npmjs.com, one-time)
|
|
21
|
+
|
|
22
|
+
Open `https://www.npmjs.com/package/dsh-rewind-plugin` → package **settings** →
|
|
23
|
+
**Trusted Publisher**:
|
|
24
|
+
|
|
25
|
+
| Field | Value |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| Provider | GitHub Actions |
|
|
28
|
+
| Organization or user | `SiriLee` |
|
|
29
|
+
| Repository | `dsh-rewind` (the GitHub repo, not the npm name) |
|
|
30
|
+
| Workflow filename | `publish.yml` |
|
|
31
|
+
| Environment | empty |
|
|
32
|
+
| Allowed actions | `npm publish` |
|
|
33
|
+
|
|
34
|
+
## Subsequent releases (CI, automatic)
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm version patch
|
|
38
|
+
git push origin main --tags # triggers .github/workflows/publish.yml
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- The workflow verifies the tag matches `package.json`, runs typecheck + tests +
|
|
42
|
+
a full build + artifact verification, publishes with `--provenance`
|
|
43
|
+
(Sigstore), and creates a GitHub Release. It is **idempotent** — an already
|
|
44
|
+
published version is skipped.
|
|
45
|
+
- CI (`.github/workflows/ci.yml`) runs `npm run check` — typecheck + tests +
|
|
46
|
+
build + artifact verification + a `npm pack --dry-run` — on every push / PR
|
|
47
|
+
across both Node engines boundary versions; the tarball layout is guarded by
|
|
48
|
+
`tests/package-layout.test.ts`.
|
|
49
|
+
|
|
50
|
+
## DSH version alignment (peer range maintenance)
|
|
51
|
+
|
|
52
|
+
DSH is still in rc; npm's prerelease matching rules require a peer range to
|
|
53
|
+
share the host version's `[major, minor, patch]` tuple. So `peerDependencies`
|
|
54
|
+
uses an OR-union covering every published rc tuple series
|
|
55
|
+
(e.g. `^0.1.0-rc.6 || ^0.1.1-rc.2`), extended as DSH releases new tuples.
|
|
56
|
+
|
|
57
|
+
- **When to update**: only when DSH releases a new tuple
|
|
58
|
+
(`0.1.1 → 0.1.2 → 0.2.x`); rc rolling within a tuple (`0.1.1-rc.2 → rc.3`)
|
|
59
|
+
needs nothing. All `@deepseek-ai/*` packages release together;
|
|
60
|
+
`npm view @deepseek-ai/dsh version` is the authoritative signal.
|
|
61
|
+
- **Automatic detection**: `node scripts/check-dsh-version.mjs` compares the
|
|
62
|
+
latest npm version against the tuples the peers cover and reports whether an
|
|
63
|
+
extension is needed (exit 0 = nothing to do, exit 1 = update).
|
|
64
|
+
- **Update steps**: append `|| ^<new-tuple>-rc.<n>` to every
|
|
65
|
+
`@deepseek-ai/dsh-*` peer → bump devDependencies to the latest → `npm
|
|
66
|
+
install` → `npm run check` → release.
|
|
67
|
+
- **After DSH goes final**: final releases are not bound by the prerelease
|
|
68
|
+
tuple rule, so the peers can converge to a single stable range (e.g.
|
|
69
|
+
`^0.1.x`); this section can then be deleted.
|
|
@@ -1,53 +1,7 @@
|
|
|
1
|
-
# Release
|
|
2
|
-
|
|
3
|
-
## First release (manual, one-time)
|
|
4
|
-
|
|
5
|
-
Trusted Publisher can only be configured once the package exists, so the first
|
|
6
|
-
version is published locally:
|
|
7
|
-
|
|
8
|
-
```sh
|
|
9
|
-
npm login
|
|
10
|
-
npm publish --access public
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
- If prompted for `EOTP`: complete the browser auth link the CLI prints, or
|
|
14
|
-
retry with a 6-digit code — `npm publish --otp=<code>`.
|
|
15
|
-
- The first version carries no provenance (local path) — acceptable; every CI
|
|
16
|
-
release after that publishes with Sigstore/SLSA provenance automatically.
|
|
17
|
-
|
|
18
|
-
## Configure Trusted Publisher (npmjs.com, one-time)
|
|
19
|
-
|
|
20
|
-
Open `https://www.npmjs.com/package/dsh-rewind-plugin` → package **settings** →
|
|
21
|
-
**Trusted Publisher**:
|
|
22
|
-
|
|
23
|
-
| Field | Value |
|
|
24
|
-
| --- | --- |
|
|
25
|
-
| Provider | GitHub Actions |
|
|
26
|
-
| Organization or user | `SiriLee` |
|
|
27
|
-
| Repository | `dsh-rewind` (the GitHub repo, not the npm name) |
|
|
28
|
-
| Workflow filename | `publish.yml` |
|
|
29
|
-
| Environment | empty |
|
|
30
|
-
| Allowed actions | `npm publish` |
|
|
31
|
-
|
|
32
|
-
## Subsequent releases (CI, automatic)
|
|
33
|
-
|
|
34
|
-
```sh
|
|
35
|
-
npm version patch
|
|
36
|
-
git push origin main --tags # triggers .github/workflows/publish.yml
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
- The workflow verifies the tag matches `package.json`, runs typecheck + tests +
|
|
40
|
-
a full build + artifact verification, publishes with `--provenance`
|
|
41
|
-
(Sigstore), and creates a GitHub Release. It is **idempotent** — an already
|
|
42
|
-
published version is skipped.
|
|
43
|
-
- CI (`.github/workflows/ci.yml`) runs the same checks on every push / PR, plus
|
|
44
|
-
a `npm pack --dry-run` sanity check that the tarball carries `lib/` and
|
|
45
|
-
`LICENSE`.
|
|
46
|
-
|
|
47
|
-
---
|
|
48
|
-
|
|
49
1
|
# 发布流程
|
|
50
2
|
|
|
3
|
+
[English](release.md)
|
|
4
|
+
|
|
51
5
|
## 首次发布(手动,一次性)
|
|
52
6
|
|
|
53
7
|
Trusted Publisher 要求**包已存在**才能配置,因此首个版本走本地发布:
|
|
@@ -86,8 +40,9 @@ git push origin main --tags # 触发 .github/workflows/publish.yml
|
|
|
86
40
|
- workflow 校验 tag 与 `package.json` 版本一致,跑 typecheck + 测试 + 完整
|
|
87
41
|
构建 + 产物验证,以 `--provenance`(Sigstore)发布并创建 GitHub Release。
|
|
88
42
|
**幂等**——已发布的版本会跳过。
|
|
89
|
-
- CI(`.github/workflows/ci.yml`)在每次 push / PR
|
|
90
|
-
`npm pack --dry-run
|
|
43
|
+
- CI(`.github/workflows/ci.yml`)在每次 push / PR 跑 `npm run check`——
|
|
44
|
+
typecheck + 测试 + 构建 + 产物验证 + `npm pack --dry-run`,且覆盖
|
|
45
|
+
engines 两个边界版本;tarball 布局由 `tests/package-layout.test.ts` 守护。
|
|
91
46
|
|
|
92
47
|
## DSH 版本适配(peer 范围维护)
|
|
93
48
|
|
|
@@ -102,7 +57,6 @@ OR 并集覆盖 DSH 已发布的每个 rc 元组系列(如 `^0.1.0-rc.6 || ^0.
|
|
|
102
57
|
- **自动检测**:`node scripts/check-dsh-version.mjs` 对比 npm 最新版本与
|
|
103
58
|
peer 覆盖的元组,输出是否需追加(exit 0 无需动作,exit 1 需要)。
|
|
104
59
|
- **更新步骤**:给每个 `@deepseek-ai/dsh-*` peer 追加 `|| ^<新元组>-rc.<n>`
|
|
105
|
-
→ devDependencies 同步升到最新 → `npm install` → `npm run
|
|
106
|
-
`npm test` / `npm run verify:host` → 发版。
|
|
60
|
+
→ devDependencies 同步升到最新 → `npm install` → `npm run check` → 发版。
|
|
107
61
|
- **正式版后收敛**:DSH 发布 final 版本后,正式版不受 prerelease 元组规则
|
|
108
62
|
限制,peer 可收敛为稳定的 `^0.1.x` 单范围,此节即可删除。
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Snapshot cleanup
|
|
2
|
+
|
|
3
|
+
The plugin saves a backup of each file before it's edited, so you can rewind your
|
|
4
|
+
code to an earlier point. These backups are called **snapshots**, and they're
|
|
5
|
+
grouped by message and stored per session.
|
|
6
|
+
|
|
7
|
+
`/snapshot-auto-cleanup` helps you manage those snapshots — whether you want to
|
|
8
|
+
reclaim disk space from old sessions or clear the **current** session so its
|
|
9
|
+
rewind history starts fresh.
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
**Automatic cleanup** (off by default): the plugin can remember which sessions
|
|
14
|
+
you've stopped using, and now and then remove their snapshots to keep disk usage
|
|
15
|
+
down. It never touches your active session, and it never touches your
|
|
16
|
+
conversation.
|
|
17
|
+
|
|
18
|
+
**Manual:** even with automatic cleanup off, you can run the cleanup yourself:
|
|
19
|
+
|
|
20
|
+
- `/snapshot-auto-cleanup run [--apply]` — preview, then actually remove the
|
|
21
|
+
snapshots of sessions you haven't used for a while.
|
|
22
|
+
- `/snapshot-auto-cleanup run --current [--apply]` — preview, then actually
|
|
23
|
+
clear the **current** session's snapshots. This resets its rewind history to
|
|
24
|
+
"from now on" (your conversation is unaffected). If a turn is currently
|
|
25
|
+
running, the plugin pauses it first, then clears.
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
/snapshot-auto-cleanup show the current settings
|
|
31
|
+
/snapshot-auto-cleanup on|off turn automatic cleanup on or off
|
|
32
|
+
/snapshot-auto-cleanup max-age <days> how many idle days before a session's snapshots are removed
|
|
33
|
+
/snapshot-auto-cleanup run preview what the automatic cleanup would remove
|
|
34
|
+
/snapshot-auto-cleanup run --apply actually remove those snapshots
|
|
35
|
+
/snapshot-auto-cleanup run --current preview clearing this session's snapshots
|
|
36
|
+
/snapshot-auto-cleanup run --current --apply actually clear this session's snapshots
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`run` always starts as a preview; add `--apply` to make the change. `run` works
|
|
40
|
+
whether or not automatic cleanup is on.
|
|
41
|
+
|
|
42
|
+
## Settings
|
|
43
|
+
|
|
44
|
+
The settings live in `<dsh home>/snapshot-cleanup.json`:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{ "enabled": false, "maxAgeDays": 30 }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- `enabled` — whether automatic cleanup runs (default `false`).
|
|
51
|
+
- `maxAgeDays` — how many idle days before a session's snapshots are removed
|
|
52
|
+
(default `30`). Only positive numbers are accepted, so a broken setting can
|
|
53
|
+
never delete everything.
|
|
54
|
+
|
|
55
|
+
You can point the plugin at a different file with the `DSH_SNAPSHOT_CLEANUP_CONFIG`
|
|
56
|
+
environment variable. The file is changed only when you turn cleanup on/off or
|
|
57
|
+
set `max-age`; a missing file reads as the safe default (off).
|
|
58
|
+
|
|
59
|
+
## When automatic cleanup runs
|
|
60
|
+
|
|
61
|
+
Automatic cleanup checks at most **once per run** (a restart lets it check again),
|
|
62
|
+
on the first session activity (a message you send or a tool call that finishes),
|
|
63
|
+
and only when it's enabled and at least 24 hours have passed since the last
|
|
64
|
+
check. The 24-hour clock is saved to `<dsh home>/snapshot-cleanup-last-sweep.json`,
|
|
65
|
+
so restarting doesn't reset it. It runs in the background and never blocks
|
|
66
|
+
what you're doing.
|
|
67
|
+
|
|
68
|
+
If you want a change to take effect right away, use `run`; automatic cleanup
|
|
69
|
+
picks up a fresh setting on the next run.
|
|
70
|
+
|
|
71
|
+
## Safety
|
|
72
|
+
|
|
73
|
+
- Only rewind **snapshots** (the file backups) are ever removed. Your
|
|
74
|
+
conversation is never touched, and the plugin never rewrites or deletes your
|
|
75
|
+
session history.
|
|
76
|
+
- Automatic cleanup never removes your **active** session's snapshots — only
|
|
77
|
+
sessions that have been idle past the cutoff.
|
|
78
|
+
- `run --current` clears the current session's snapshots. This is one-way for
|
|
79
|
+
that session's file-rewind history: you can't rewind code to before the clear,
|
|
80
|
+
but your conversation stays intact, and the session starts recording fresh
|
|
81
|
+
snapshots from now on.
|
|
82
|
+
|
|
83
|
+
## Known limitation
|
|
84
|
+
|
|
85
|
+
The plugin keeps the most recent **100 messages'** snapshots per session. If you
|
|
86
|
+
rewind or compact a lot in one long session, those 100 slots can be taken up by
|
|
87
|
+
messages that are no longer reachable, so you may find you can't rewind as far
|
|
88
|
+
back as you'd like. (Claude Code behaves the same way.) To get back to a clean
|
|
89
|
+
state, run `/snapshot-auto-cleanup run --current --apply` to clear the current
|
|
90
|
+
session and start fresh.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# 快照清理
|
|
2
|
+
|
|
3
|
+
插件会在每次修改文件前保存一份备份,方便你把代码回退到之前的状态。这些备份叫**快照**,按消息分组、按会话存放。
|
|
4
|
+
|
|
5
|
+
`/snapshot-auto-cleanup` 帮你管理这些快照——既可以回收旧会话占用的磁盘,也可以清空**当前**会话、让它从"现在"重新开始记录回退历史。
|
|
6
|
+
|
|
7
|
+
## 它能做什么
|
|
8
|
+
|
|
9
|
+
**自动清理**(默认关闭):插件会记住哪些会话你已不再使用,并定期移除它们的快照以节省磁盘。它**绝不**触碰你当前活跃的会话,也**绝不**触碰你的对话。
|
|
10
|
+
|
|
11
|
+
**手动:**即使自动清理是关闭的,你也可以自己执行清理:
|
|
12
|
+
|
|
13
|
+
- `/snapshot-auto-cleanup run [--apply]` — 先预演,再真正移除你已有一阵子没用的会话的快照。
|
|
14
|
+
- `/snapshot-auto-cleanup run --current [--apply]` — 先预演,再真正清空**当前**会话的快照。这会让它从"现在"重新开始记录回退历史(你的对话不受影响)。若此刻有正在进行中的回合,插件会先暂停它,再清空。
|
|
15
|
+
|
|
16
|
+
## 命令
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
/snapshot-auto-cleanup 查看当前设置
|
|
20
|
+
/snapshot-auto-cleanup on|off 开启/关闭自动清理
|
|
21
|
+
/snapshot-auto-cleanup max-age <天数> 会话失活多少天后移除其快照
|
|
22
|
+
/snapshot-auto-cleanup run 预演:看自动清理会移除什么
|
|
23
|
+
/snapshot-auto-cleanup run --apply 真正移除这些快照
|
|
24
|
+
/snapshot-auto-cleanup run --current 预演:看清空本会话快照会怎样
|
|
25
|
+
/snapshot-auto-cleanup run --current --apply 真正清空本会话快照
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`run` 一开始总是预演;加 `--apply` 才真正执行。`run` 无论自动清理是否开启都能用。
|
|
29
|
+
|
|
30
|
+
## 设置
|
|
31
|
+
|
|
32
|
+
设置保存在 `<dsh home>/snapshot-cleanup.json`:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{ "enabled": false, "maxAgeDays": 30 }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `enabled` — 是否运行自动清理(默认 `false`)。
|
|
39
|
+
- `maxAgeDays` — 会话失活多少天后移除其快照(默认 `30`)。只接受正数,因此一个损坏的设置永远不可能"删掉一切"。
|
|
40
|
+
|
|
41
|
+
你可以用环境变量 `DSH_SNAPSHOT_CLEANUP_CONFIG` 指向别的文件。该文件只在运行 `/snapshot-auto-cleanup` 时写入;文件缺失按安全默认值(关闭)处理。
|
|
42
|
+
|
|
43
|
+
## 自动清理何时运行
|
|
44
|
+
|
|
45
|
+
自动清理每次运行(宿主启动)**最多检查一次**,发生在首次活动时(你发出一条消息或某次工具调用完成),且仅在**已开启**且距上次检查**已满 24 小时**时才执行。这个 24 小时时钟会写入 `<dsh home>/snapshot-cleanup-last-sweep.json`,重启不会重置。它后台运行,从不阻塞你在做的事。
|
|
46
|
+
|
|
47
|
+
想让改动立即生效,请用 `run`;自动清理要到下次运行才会采用新设置。
|
|
48
|
+
|
|
49
|
+
## 安全
|
|
50
|
+
|
|
51
|
+
- 只会移除回退用的**快照**(那份文件备份)。你的对话绝不会被动到,插件也绝不改写或删除你的会话历史。
|
|
52
|
+
- 自动清理**永不**移除你**当前活跃**会话的快照——只清理已失活超过阈值的会话。
|
|
53
|
+
- `run --current` 会清空当前会话的快照。这对该会话的文件回退历史是**不可逆**的:你无法回退到清空之前的代码状态,但你的对话保持完整,会话会从"现在"开始重新记录快照。
|
|
54
|
+
|
|
55
|
+
## 已知限制
|
|
56
|
+
|
|
57
|
+
插件每个会话只保留**最近 100 条消息**的快照。若你在一个很长的会话里多次回退或压缩,这 100 个名额常会被"已经无法再回退到"的消息占满,你可能无法回退到想要的位置。(Claude Code 也是同样行为。)想恢复到一个干净状态,运行 `/snapshot-auto-cleanup run --current --apply` 清空当前会话、重新开始。
|
package/lib/client.js
CHANGED
|
@@ -986,8 +986,7 @@ var zh = {
|
|
|
986
986
|
"popover.impact.restore": "\u8FD8\u539F {path}",
|
|
987
987
|
"popover.impact.delete": "\u5220\u9664 {path}",
|
|
988
988
|
"popover.confirm": "\u786E\u8BA4\u56DE\u9000",
|
|
989
|
-
"popover.back": "\u8FD4\u56DE"
|
|
990
|
-
"guard.hint": "/rewind \u624B\u52A8\u8F93\u5165\u4E0D\u63A5\u53D7\u53C2\u6570\uFF0C\u8BF7\u76F4\u63A5\u8F93\u5165 /rewind \u6253\u5F00\u56DE\u9000\u83DC\u5355"
|
|
989
|
+
"popover.back": "\u8FD4\u56DE"
|
|
991
990
|
};
|
|
992
991
|
var en = {
|
|
993
992
|
"button.aria": "Rewind to this message",
|
|
@@ -1013,8 +1012,7 @@ var en = {
|
|
|
1013
1012
|
"popover.impact.restore": "Restore {path}",
|
|
1014
1013
|
"popover.impact.delete": "Delete {path}",
|
|
1015
1014
|
"popover.confirm": "Confirm rewind",
|
|
1016
|
-
"popover.back": "Back"
|
|
1017
|
-
"guard.hint": "/rewind takes no typed arguments \u2014 enter /rewind to open the rewind picker."
|
|
1015
|
+
"popover.back": "Back"
|
|
1018
1016
|
};
|
|
1019
1017
|
|
|
1020
1018
|
// src/client/index.ts
|
|
@@ -1069,8 +1067,7 @@ function apply(ctx) {
|
|
|
1069
1067
|
const card = textarea?.closest("[data-composer-card]");
|
|
1070
1068
|
return card ?? textarea ?? document.body;
|
|
1071
1069
|
};
|
|
1072
|
-
|
|
1073
|
-
name: "rewind",
|
|
1070
|
+
const rewindPopupSpec = {
|
|
1074
1071
|
// The picker exists exactly while the surface has a reachable user
|
|
1075
1072
|
// message: a fresh session (no candidates) falls through to the host
|
|
1076
1073
|
// command, which fails with "no user messages" — matching the harness's
|
|
@@ -1105,66 +1102,12 @@ function apply(ctx) {
|
|
|
1105
1102
|
});
|
|
1106
1103
|
}
|
|
1107
1104
|
}
|
|
1108
|
-
});
|
|
1109
|
-
const PARAM_REWIND = /^\s*\/rewind\s+\S+/i;
|
|
1110
|
-
const composerTextarea = () => document.querySelector(COMPOSER_SELECTOR2);
|
|
1111
|
-
const hasParamRewindDraft = () => {
|
|
1112
|
-
const textarea = composerTextarea();
|
|
1113
|
-
return textarea !== null && PARAM_REWIND.test(textarea.value);
|
|
1114
|
-
};
|
|
1115
|
-
let guardHintEl = null;
|
|
1116
|
-
let guardHintTimer;
|
|
1117
|
-
const showGuardHint = () => {
|
|
1118
|
-
if (guardHintEl !== null) guardHintEl.remove();
|
|
1119
|
-
if (guardHintTimer !== void 0) window.clearTimeout(guardHintTimer);
|
|
1120
|
-
const textarea = composerTextarea();
|
|
1121
|
-
if (textarea === null) return;
|
|
1122
|
-
const card = textarea.closest("[data-composer-card]");
|
|
1123
|
-
const hint = document.createElement("div");
|
|
1124
|
-
hint.className = "dsh-rewind-guard-hint";
|
|
1125
|
-
hint.setAttribute("role", "status");
|
|
1126
|
-
hint.textContent = t("guard.hint");
|
|
1127
|
-
document.body.appendChild(hint);
|
|
1128
|
-
const rect = card instanceof HTMLElement ? card.getBoundingClientRect() : textarea.getBoundingClientRect();
|
|
1129
|
-
hint.style.left = `${Math.round(rect.left)}px`;
|
|
1130
|
-
hint.style.bottom = `${Math.round(window.innerHeight - rect.top + 8)}px`;
|
|
1131
|
-
guardHintEl = hint;
|
|
1132
|
-
guardHintTimer = window.setTimeout(() => {
|
|
1133
|
-
hint.remove();
|
|
1134
|
-
if (guardHintEl === hint) guardHintEl = null;
|
|
1135
|
-
guardHintTimer = void 0;
|
|
1136
|
-
}, 3200);
|
|
1137
|
-
};
|
|
1138
|
-
const onParamRewindSubmit = (event) => {
|
|
1139
|
-
if (!hasParamRewindDraft()) return;
|
|
1140
|
-
event.preventDefault();
|
|
1141
|
-
event.stopPropagation();
|
|
1142
|
-
showGuardHint();
|
|
1143
|
-
};
|
|
1144
|
-
const onKeyDownGuard = (event) => {
|
|
1145
|
-
if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
|
|
1146
|
-
onParamRewindSubmit(event);
|
|
1147
1105
|
};
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
const button = target.closest("button");
|
|
1153
|
-
if (button === null) return;
|
|
1154
|
-
const card = button.closest("[data-composer-card]");
|
|
1155
|
-
if (card === null) return;
|
|
1156
|
-
const all = card.querySelectorAll("button");
|
|
1157
|
-
if (all[all.length - 1] !== button) return;
|
|
1158
|
-
if (button.querySelector("rect") !== null) return;
|
|
1159
|
-
onParamRewindSubmit(event);
|
|
1160
|
-
};
|
|
1161
|
-
document.addEventListener("keydown", onKeyDownGuard, true);
|
|
1162
|
-
document.addEventListener("click", onClickGuard, true);
|
|
1106
|
+
for (const name2 of ["rewind", "undo"]) {
|
|
1107
|
+
yield commandUi.decorate({ name: name2, ...rewindPopupSpec });
|
|
1108
|
+
}
|
|
1109
|
+
const composerTextarea = () => document.querySelector(COMPOSER_SELECTOR2);
|
|
1163
1110
|
yield () => {
|
|
1164
|
-
document.removeEventListener("keydown", onKeyDownGuard, true);
|
|
1165
|
-
document.removeEventListener("click", onClickGuard, true);
|
|
1166
|
-
if (guardHintEl !== null) guardHintEl.remove();
|
|
1167
|
-
if (guardHintTimer !== void 0) window.clearTimeout(guardHintTimer);
|
|
1168
1111
|
style.remove();
|
|
1169
1112
|
};
|
|
1170
1113
|
}, "dsh-rewind client lifecycle");
|