dsh-rewind-plugin 0.4.1 → 0.5.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 +174 -0
- package/README.md +105 -101
- package/SECURITY.md +154 -0
- package/docs/README.md +29 -0
- package/docs/architecture.md +128 -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 +150 -0
- package/docs/harness-reference.md +7 -3
- package/docs/release/release.md +69 -0
- package/docs/{release.md → release/release.zh.md} +6 -52
- package/docs/snapshot-auto-cleanup.md +71 -0
- package/docs/snapshot-auto-cleanup.zh.md +42 -0
- package/lib/client.js +1 -1
- package/lib/index.js +982 -50
- package/lib/types/client/candidates.d.ts +5 -4
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/index.d.ts +3 -1
- package/lib/types/locales.d.ts +17 -0
- package/lib/types/rewind.d.ts +3 -2
- package/lib/types/snapshot-cleanup.d.ts +125 -0
- package/lib/types/snapshot.d.ts +374 -14
- package/package.json +6 -3
- package/README.zh.md +0 -159
- package/docs/troubleshooting.md +0 -44
- /package/docs/{compat-audit.md → compat/audit.md} +0 -0
- /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,150 @@
|
|
|
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 `~/.dsh/rewind-snapshots/` (the dsh data
|
|
11
|
+
directory), overridable via the `DSH_REWIND_SNAPSHOT_DIR` environment
|
|
12
|
+
variable. It is a sibling of the workspace, never a subtree of it. Deleting
|
|
13
|
+
the root only removes file backups; the store rebuilds from scratch.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
<root>/
|
|
17
|
+
└── <sessionId>/ # safeSessionId(sessionId)
|
|
18
|
+
├── <anchorSeq>/ # decimal seq of the anchoring user/message
|
|
19
|
+
│ └── <callId>.json # one committed before-backup
|
|
20
|
+
└── restore-journal-<opId>.json # one restore-op journal
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- `sessionId` is sanitized to `[a-zA-Z0-9._-]`; the bare values `.` and `..`
|
|
24
|
+
are replaced (`safeSessionId`) so a hostile id cannot traverse out of the
|
|
25
|
+
root.
|
|
26
|
+
- `callId` is sanitized to `[a-zA-Z0-9._-]` (`safeFileId`).
|
|
27
|
+
- `<anchorSeq>` is a decimal integer; directories with non-integer names are
|
|
28
|
+
ignored by readers.
|
|
29
|
+
- Journal files are recognized by the `restore-journal-` prefix; everything
|
|
30
|
+
else ending in `.json` under the session dir is treated as a checkpoint
|
|
31
|
+
entry.
|
|
32
|
+
|
|
33
|
+
## Checkpoint entry
|
|
34
|
+
|
|
35
|
+
One JSON file per before-backup, named `<callId>.json`:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
interface CheckpointEntry {
|
|
39
|
+
callId: string // the tool call that mutated the file
|
|
40
|
+
anchorSeq: number // seq of the user message anchoring the turn of the change
|
|
41
|
+
path: string // resolved display path (absolute)
|
|
42
|
+
before: string | null // full content before the change; null = file was created
|
|
43
|
+
time: number // epoch ms, strictly increasing within a store instance
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Semantics:
|
|
48
|
+
|
|
49
|
+
- **`before` is the pre-edit state**: `null` means the call created the file.
|
|
50
|
+
- **`anchorSeq` ties the backup to a user message**: rewinding to message N
|
|
51
|
+
applies every entry anchored at or after N (the boundary is inclusive).
|
|
52
|
+
- **`time` is the ordering key within an anchor group**: it is monotonic per
|
|
53
|
+
store instance (bumped past the previous commit), so same-millisecond
|
|
54
|
+
commits stay capture-ordered and a re-read always picks the same "earliest"
|
|
55
|
+
entry per path.
|
|
56
|
+
- Synthetic re-check entries (external edits/deletions seen at a user-message
|
|
57
|
+
boundary) use `callId = recheck-<anchorSeq>-<sha256(path) first 8 hex>`.
|
|
58
|
+
|
|
59
|
+
### Dedup link entry
|
|
60
|
+
|
|
61
|
+
A tracked file that records the same `before` content as its immediately-prior
|
|
62
|
+
entry for that path is stored as a **link** instead of a full copy: the entry
|
|
63
|
+
carries a `ref` (the `<anchorSeq>/<callId>.json` of that prior entry) and omits
|
|
64
|
+
`before`, so identical content is never duplicated across entries. A reader
|
|
65
|
+
resolves the `ref` back to the terminal real snapshot; `before: null` still
|
|
66
|
+
means "the file was created". A `ref` is validated as a single-level,
|
|
67
|
+
`<digits>/<callId>.json` relative reference (no traversal) so a corrupt or
|
|
68
|
+
hostile ref cannot escape the store root when followed. Because links reference
|
|
69
|
+
prior entries, `prune` materializes a surviving link whose `ref` lands on a
|
|
70
|
+
group it is about to drop before deleting that group, so no kept link is left
|
|
71
|
+
dangling.
|
|
72
|
+
|
|
73
|
+
Real entries (with `before`) are unchanged and read identically before and
|
|
74
|
+
after this addition; a link entry is a distinct kind that lacks `before`.
|
|
75
|
+
|
|
76
|
+
## Restore journal
|
|
77
|
+
|
|
78
|
+
One JSON file per restore operation, written **before any mutation** and
|
|
79
|
+
updated as the pass applies:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
interface RestoreJournal {
|
|
83
|
+
version: 1
|
|
84
|
+
id: string // `op-<base36 ms>-<random>`; file name suffix
|
|
85
|
+
sessionId: string
|
|
86
|
+
targetSeq: number // rewind target the restore belongs to
|
|
87
|
+
startedAt: number // epoch ms
|
|
88
|
+
finishedAt?: number // set on a terminal state
|
|
89
|
+
state: 'running' | 'rollback-running' | 'completed' | 'rolled-back' | 'recovery-required'
|
|
90
|
+
actions: RestoreJournalAction[]
|
|
91
|
+
rollbackError?: string // set when a rollback pass failed partway
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface RestoreJournalAction {
|
|
95
|
+
path: string
|
|
96
|
+
action: 'restore' | 'delete' // restore = write `before` back; delete = unlink
|
|
97
|
+
before: string | null // target content for restore; null for delete
|
|
98
|
+
rescue: string | null // pre-restore disk state; null = file was absent
|
|
99
|
+
rescueError?: string // set when the rescue capture failed (rollback skips it)
|
|
100
|
+
done: boolean // true once the action's fs op completed and was marked
|
|
101
|
+
failed?: string // per-action failure message (the pass never aborts)
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
States: `running` and `rollback-running` are non-terminal; a host restart turns
|
|
106
|
+
them into `interrupted` (or `recovery-required` when the journal is corrupt or
|
|
107
|
+
a rollback could not complete). `completed` / `rolled-back` are terminal.
|
|
108
|
+
|
|
109
|
+
## Write guarantees
|
|
110
|
+
|
|
111
|
+
- **Atomicity**: every JSON write serializes to a sibling `<target>.tmp` and
|
|
112
|
+
renames over the target. A crash between the two steps leaves only the temp
|
|
113
|
+
file — never a readable half-written target — and readers ignore temp files
|
|
114
|
+
(they do not end in `.json`). The next write of the same target overwrites
|
|
115
|
+
a leftover temp.
|
|
116
|
+
- **Journal before mutation**: the rescue state of every planned path is
|
|
117
|
+
captured and the intent journal persisted atomically BEFORE the first fs
|
|
118
|
+
mutation; each action is marked `done` as it is applied.
|
|
119
|
+
- **Disk is truth**: after a restart, reconciliation compares the real disk
|
|
120
|
+
against each action's goal (the restore target for `running` journals, the
|
|
121
|
+
rescue state for `rollback-running` / `recovery-required` ones). A path
|
|
122
|
+
whose disk already matches is marked done without being touched.
|
|
123
|
+
- **Bounded storage**: `prune` keeps the newest 100 anchor groups per session
|
|
124
|
+
(`MAX_ANCHOR_GROUPS`), materializing any surviving dedup link that references
|
|
125
|
+
a group being dropped before deleting whole anchor directories; it also
|
|
126
|
+
recycles terminal journals (`completed` / `rolled-back`). Non-terminal and
|
|
127
|
+
corrupt journals are always kept. Across sessions, `pruneStale` removes whole
|
|
128
|
+
long-inactive session directories whose newest member stamp is older than a
|
|
129
|
+
configurable idle cutoff (default off), so the store root does not grow
|
|
130
|
+
without bound either.
|
|
131
|
+
|
|
132
|
+
## Validation and failure policy
|
|
133
|
+
|
|
134
|
+
- **Entries**: a missing or malformed entry is read as `undefined` (silently
|
|
135
|
+
skipped) — losing one backup, never the recovery path.
|
|
136
|
+
- **Journals**: a corrupt or schema-invalid journal **fails loud** —
|
|
137
|
+
`reconcileRestores` reports it as `recovery-required` and never drops it,
|
|
138
|
+
because dropping it would silently erase the interrupted restore's recovery
|
|
139
|
+
record.
|
|
140
|
+
- **Journal IO**: best-effort by design — if a journal cannot be written, the
|
|
141
|
+
restore proceeds with pre-journal semantics (crash safety degrades,
|
|
142
|
+
behavior does not).
|
|
143
|
+
|
|
144
|
+
## Versioning policy
|
|
145
|
+
|
|
146
|
+
The journal schema is `version: 1`. Checkpoint entries currently carry no
|
|
147
|
+
version field. A future incompatible format must either bump the journal
|
|
148
|
+
`version` (readers reject unknown values — there is no best-effort fallback or
|
|
149
|
+
legacy coercion) or move the state root (e.g. `rewind-snapshots/v2`) and ship
|
|
150
|
+
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,6 +45,10 @@ 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
|
|
@@ -56,9 +60,9 @@ 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,71 @@
|
|
|
1
|
+
# Snapshot auto-cleanup
|
|
2
|
+
|
|
3
|
+
The rewind store writes one on-disk `before` backup per tracked file change,
|
|
4
|
+
grouped by its anchor message. Snapshots are deduped within a session and capped
|
|
5
|
+
at the newest 100 anchor groups, but across sessions that are no longer active
|
|
6
|
+
the store can
|
|
7
|
+
still grow without bound. `snapshot-auto-cleanup` is an OPTIONAL global policy
|
|
8
|
+
(off by default) that removes the whole snapshot directory of a session that has
|
|
9
|
+
been **long-inactive** — untouched past a configurable idle cutoff.
|
|
10
|
+
|
|
11
|
+
It only ever removes the whole snapshot **directory** of a long-inactive session.
|
|
12
|
+
It never touches the active session's snapshots, never touches the conversation
|
|
13
|
+
log, and leaves snapshot data within the idle cutoff alone.
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
/snapshot-auto-cleanup show status (enabled, max-age, config path)
|
|
19
|
+
/snapshot-auto-cleanup on|off enable/disable the automatic sweep
|
|
20
|
+
/snapshot-auto-cleanup max-age <days> set the idle cutoff (positive integer)
|
|
21
|
+
/snapshot-auto-cleanup run dry-run: list what would be removed
|
|
22
|
+
/snapshot-auto-cleanup run --apply actually remove those sessions
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`run` is a manual escape hatch and works whether or not the automatic cleanup is
|
|
26
|
+
`on`. `run` defaults to a dry-run; add `--apply` to execute.
|
|
27
|
+
|
|
28
|
+
## Config file
|
|
29
|
+
|
|
30
|
+
The policy is persisted to `~/.dsh/snapshot-cleanup.json`:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{ "enabled": false, "maxAgeDays": 30 }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- `enabled` — whether the automatic sweeps run (default `false`).
|
|
37
|
+
- `maxAgeDays` — how many idle days before a long-inactive session's snapshot dir is
|
|
38
|
+
removed (default `30`; `0`/negative are rejected, so a broken config can never
|
|
39
|
+
delete everything).
|
|
40
|
+
|
|
41
|
+
Override the path with the `DSH_SNAPSHOT_CLEANUP_CONFIG` environment variable.
|
|
42
|
+
The file is written only by the `/snapshot-auto-cleanup` command. A missing file
|
|
43
|
+
reads as the safe default (off); a missing or corrupt file makes a sweep
|
|
44
|
+
**fail-closed** (delete nothing) and log a warning, and is surfaced when you run
|
|
45
|
+
the command again.
|
|
46
|
+
|
|
47
|
+
## When it runs
|
|
48
|
+
|
|
49
|
+
The 24h window is anchored on a **persisted** last-sweep time
|
|
50
|
+
(`~/.dsh/snapshot-cleanup-last-sweep.json`), so a host restart does not reset
|
|
51
|
+
it: the auto-sweep checks **once per process run**, on the first session
|
|
52
|
+
activity of a window (a user message or a completed tool call), and cleans only
|
|
53
|
+
when enabled **and** >=24h since the last sweep. It runs in the background and
|
|
54
|
+
never blocks the activity that triggered it. Because the check happens once per
|
|
55
|
+
run, a change that takes effect immediately is best applied with
|
|
56
|
+
`/snapshot-auto-cleanup run`; editing the config file by hand (or enabling after
|
|
57
|
+
the run's first activity) takes effect on the next run.
|
|
58
|
+
|
|
59
|
+
## Safety and boundaries
|
|
60
|
+
|
|
61
|
+
- Removes only whole **long-inactive** session dirs; the active session and the
|
|
62
|
+
conversation log are never touched.
|
|
63
|
+
- "Inactive" is judged by mtime: a session still being written to keeps scrolling
|
|
64
|
+
its newest member stamp forward, so it is never old enough to be pruned.
|
|
65
|
+
- Dedup `ref` links are session-relative, so removing a whole dir cannot dangle a
|
|
66
|
+
link elsewhere.
|
|
67
|
+
- Trade-off: enabling auto-cleanup means a session resumed after a long idle gap
|
|
68
|
+
will rewind only from its remaining (newest 100) anchors; its old snapshots are
|
|
69
|
+
gone. The conversation log is never affected.
|
|
70
|
+
- Deleting the whole store dir manually stays safe (it is recreated on the next
|
|
71
|
+
capture); auto-cleanup just scopes that removal to long-inactive sessions.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# 快照自动清理
|
|
2
|
+
|
|
3
|
+
回退插件为每次被跟踪的文件修改写入一份磁盘 `before` 备份,并按其锚点消息分组。快照在**同一会话**内去重、且保留最近 100 组锚点,但跨**已不再活跃的会话**时存储仍会无界增长。`snapshot-auto-cleanup` 是**可选**的全局策略(默认关闭):把**长期不活跃**(失活超过可配置阈值)的会话快照**整目录**移除。
|
|
4
|
+
|
|
5
|
+
它只会移除**长期不活跃**会话的整个快照目录,绝不触碰活动会话的快照、绝不触碰对话日志,也保留仍在阈值内的快照数据。
|
|
6
|
+
|
|
7
|
+
## 命令
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
/snapshot-auto-cleanup 查看状态(是否开启、max-age、配置路径)
|
|
11
|
+
/snapshot-auto-cleanup on|off 开启/关闭自动清理
|
|
12
|
+
/snapshot-auto-cleanup max-age <天数> 设置失活阈值(正整数)
|
|
13
|
+
/snapshot-auto-cleanup run 预演:列出将移除哪些会话
|
|
14
|
+
/snapshot-auto-cleanup run --apply 真正移除这些会话
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`run` 是手动逃生舱,无论自动清理是否为 `on` 都能用;默认只预演,加 `--apply` 才执行。
|
|
18
|
+
|
|
19
|
+
## 配置文件
|
|
20
|
+
|
|
21
|
+
策略持久化到 `~/.dsh/snapshot-cleanup.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{ "enabled": false, "maxAgeDays": 30 }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- `enabled` — 是否运行自动清理(默认 `false`)。
|
|
28
|
+
- `maxAgeDays` — 失活多少天后移除某**长期不活跃**会话的快照目录(默认 `30`;`0`/负数会被拒绝,因此损坏的配置不可能导致"删除一切")。
|
|
29
|
+
|
|
30
|
+
可用环境变量 `DSH_SNAPSHOT_CLEANUP_CONFIG` 覆盖路径。该文件只由 `/snapshot-auto-cleanup` 命令写入。文件缺失按安全默认(关闭)处理;缺失或损坏会令一次清扫**fail-closed**(不删任何东西)并写入警告,再次运行命令时会有所提示。
|
|
31
|
+
|
|
32
|
+
## 运行时机
|
|
33
|
+
|
|
34
|
+
24 小时窗口锚定在**持久化**的上次清扫时间(`~/.dsh/snapshot-cleanup-last-sweep.json`)上,因此宿主重启不会重置:自动清扫在**每次运行只检查一次**,于某个窗口的首次会话活动(一条用户消息或一次工具调用完成)时执行,且仅在**已启用**且距上次清扫 **>=24 小时**时才清理。它后台运行,从不阻塞触发它的活动。由于每次运行只检查一次,想让改动立即生效最好用 `/snapshot-auto-cleanup run`;手动编辑配置文件(或在本次运行的首次活动之后才开启)会到下次运行才生效。
|
|
35
|
+
|
|
36
|
+
## 安全与边界
|
|
37
|
+
|
|
38
|
+
- 只移除**长期不活跃**会话的整目录;活动会话与对话日志永不触碰。
|
|
39
|
+
- "不活跃"以 mtime 判定:仍在写入的会话会不断把"最新成员"时间戳往后推,因此永远到不了可被清理的失活阈值。
|
|
40
|
+
- 去重 `ref` 链接是会话内相对的,所以移除整目录不会让别处产生悬空链接。
|
|
41
|
+
- 权衡:开启自动清理后,某个长时间闲置才被重新打开的会话,只能从剩余(最近 100 组)锚点回退,旧快照已不存在;对话日志永不受影响。
|
|
42
|
+
- 手动删除整个存储目录依然安全(下次捕获时自动重建);自动清理只是把这种删除限定到长期不活跃的会话。
|
package/lib/client.js
CHANGED
|
@@ -92,7 +92,7 @@ function hiddenSeqsOf(snap) {
|
|
|
92
92
|
|
|
93
93
|
// src/client/candidates.ts
|
|
94
94
|
var PREVIEW_CHARS = 80;
|
|
95
|
-
var DEFAULT_CANDIDATE_LIMIT =
|
|
95
|
+
var DEFAULT_CANDIDATE_LIMIT = 100;
|
|
96
96
|
function messagePreviewOf(message) {
|
|
97
97
|
const text = message.content.map((block) => block.type === "text" && typeof block.text === "string" ? block.text : "").join("").replace(/\s+/g, " ").trim();
|
|
98
98
|
return text.length <= PREVIEW_CHARS ? text : `${text.slice(0, PREVIEW_CHARS - 1)}\u2026`;
|