dsh-rewind-plugin 0.5.0 → 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/README.en.md +17 -2
- package/README.md +3 -3
- package/SECURITY.md +9 -3
- package/docs/README.md +1 -0
- package/docs/compat/audit.md +134 -122
- package/docs/format.md +5 -4
- package/docs/harness-reference.md +1 -1
- package/docs/snapshot-auto-cleanup.md +75 -56
- package/docs/snapshot-auto-cleanup.zh.md +36 -21
- package/lib/client.js +7 -64
- package/lib/index.js +199 -39
- package/lib/types/client/index.d.ts +12 -12
- package/lib/types/client/locales.d.ts +0 -2
- package/lib/types/index.d.ts +6 -3
- package/lib/types/locales.d.ts +5 -0
- package/lib/types/snapshot-cleanup.d.ts +28 -8
- package/lib/types/snapshot.d.ts +80 -2
- package/package.json +6 -1
package/README.en.md
CHANGED
|
@@ -40,7 +40,7 @@ dsh plugin --profile web add dsh-rewind-plugin
|
|
|
40
40
|
|
|
41
41
|
## Usage
|
|
42
42
|
|
|
43
|
-
1. Find the user message you want to rewind to in the conversation, or type `/rewind` to open the candidate picker.
|
|
43
|
+
1. Find the user message you want to rewind to in the conversation, or type `/rewind` (or its alias `/undo`) to open the candidate picker.
|
|
44
44
|
2. **Select it.** A small popover offers the two modes ("conversation and code" is only shown when there are restorable changes after the target).
|
|
45
45
|
3. The rewind takes effect immediately: the conversation returns to how it looked at the target message, and the withdrawn message's text is filled back into the composer — edit and re-send.
|
|
46
46
|
|
|
@@ -56,6 +56,21 @@ dsh plugin --profile web add dsh-rewind-plugin
|
|
|
56
56
|
|
|
57
57
|
</details>
|
|
58
58
|
|
|
59
|
+
## Snapshot management
|
|
60
|
+
|
|
61
|
+
Snapshots (the before-write backups) are stored under `<dsh home>/rewind-snapshots/`
|
|
62
|
+
(`~/.dsh/rewind-snapshots/` when `$DSH_HOME` is unset). For the **same session**,
|
|
63
|
+
the plugin deduplicates snapshots by content (an unchanged file is stored as a link)
|
|
64
|
+
and keeps the newest 100 anchor groups. **Deleting that directory manually** only
|
|
65
|
+
clears the file backups (chat rewinds are unaffected) and the plugin rebuilds them
|
|
66
|
+
automatically.
|
|
67
|
+
|
|
68
|
+
A **global auto-cleanup** (off by default) removes the snapshot directories of
|
|
69
|
+
**long-inactive** sessions, leaving the active session and chat log untouched. Use
|
|
70
|
+
`/snapshot-auto-cleanup` to **view, configure, and run** it; the settings live in
|
|
71
|
+
`<dsh home>/snapshot-cleanup.json` and the last-sweep time in
|
|
72
|
+
`<dsh home>/snapshot-cleanup-last-sweep.json`. See: [Snapshot cleanup](docs/snapshot-auto-cleanup.md).
|
|
73
|
+
|
|
59
74
|
## Why it stands out
|
|
60
75
|
|
|
61
76
|
Compared with the common approaches, here is the trade-off this plugin makes on "rewind":
|
|
@@ -140,7 +155,7 @@ withdrew should consume the stable, locale-independent helpers exported from
|
|
|
140
155
|
|
|
141
156
|
## Security
|
|
142
157
|
|
|
143
|
-
This plugin only appends rewind-marker events to the session log; it never deletes or rewrites logged history. Workspace files are written only when you choose "conversation and code"; backups and restores stay under
|
|
158
|
+
This plugin only appends rewind-marker events to the session log; it never deletes or rewrites logged history. Workspace files are written only when you choose "conversation and code"; backups and restores stay under `<dsh home>/rewind-snapshots/`. It never touches your git repository, makes no network requests, and accesses no credentials. Delete `~/.dsh/rewind-snapshots/` to wipe file backups only (chat rewinds are unaffected); the plugin rebuilds automatically. For sessions you've left inactive for a long time, a global auto-cleanup (off by default) can remove their snapshot directory in whole, leaving the active session and the chat log untouched. Full security model: [SECURITY.md](SECURITY.md).
|
|
144
159
|
|
|
145
160
|
## Development
|
|
146
161
|
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ dsh plugin --profile web add dsh-rewind-plugin
|
|
|
40
40
|
|
|
41
41
|
## 使用
|
|
42
42
|
|
|
43
|
-
1. 在对话中找到要回退的那条用户消息,或输入 `/rewind
|
|
43
|
+
1. 在对话中找到要回退的那条用户消息,或输入 `/rewind`(或其别名 `/undo`)打开候选列表选择。
|
|
44
44
|
2. **选中它。** 小浮层提供两种模式(「回退对话和代码」仅在目标之后有可还原的变更时显示)。
|
|
45
45
|
3. 回退立即生效:对话回到目标消息当时的样子,被撤回消息的文本自动填入输入框——改完直接重发。
|
|
46
46
|
|
|
@@ -58,9 +58,9 @@ dsh plugin --profile web add dsh-rewind-plugin
|
|
|
58
58
|
|
|
59
59
|
## 存储管理
|
|
60
60
|
|
|
61
|
-
快照(写前备份)存储于 `~/.dsh/rewind-snapshots
|
|
61
|
+
快照(写前备份)存储于 `<dsh home>/rewind-snapshots/`(未设 `$DSH_HOME` 时即 `~/.dsh/rewind-snapshots/`)。插件对**同一会话**的快照做内容去重(内容未变则存为链接)并保留最近 100 组锚点;**手动删除该目录**仅清除文件备份(对话回退不受影响),插件会自动重建。
|
|
62
62
|
|
|
63
|
-
另提供**全局自动清理**(默认关闭):把**长期不活跃**的会话快照整目录移除,不影响活动会话与对话日志。用 `/snapshot-auto-cleanup`
|
|
63
|
+
另提供**全局自动清理**(默认关闭):把**长期不活跃**的会话快照整目录移除,不影响活动会话与对话日志。用 `/snapshot-auto-cleanup` 命令**查看、设置和运行**,配置写入 `<dsh home>/snapshot-cleanup.json`;最近一次自动清扫的时间记录在 `<dsh home>/snapshot-cleanup-last-sweep.json`。详见:[快照自动清理](docs/snapshot-auto-cleanup.zh.md)。
|
|
64
64
|
|
|
65
65
|
## 本插件的优势
|
|
66
66
|
|
package/SECURITY.md
CHANGED
|
@@ -81,9 +81,11 @@ and is repaired offline (see `docs/compat/troubleshooting.md`).
|
|
|
81
81
|
|
|
82
82
|
## Filesystem containment
|
|
83
83
|
|
|
84
|
-
- **Store root**:
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
- **Store root**: `<harness home>/rewind-snapshots/` by default (resolved via
|
|
85
|
+
`resolveDshHome`, so `~/.dsh/...` when `DSH_HOME` is unset); the `snapshotDir`
|
|
86
|
+
config, then `DSH_REWIND_SNAPSHOT_DIR` env, override it. The store never
|
|
87
|
+
overlaps the workspace; deleting it only removes file backups and the store
|
|
88
|
+
rebuilds from scratch.
|
|
87
89
|
- **Path sanitization**: session ids and call ids are scrubbed to
|
|
88
90
|
`[a-zA-Z0-9._-]` (`safeSessionId` / `safeFileId`) before joining the store
|
|
89
91
|
root; `.` and `..` bare values are replaced — hostile ids cannot traverse
|
|
@@ -136,6 +138,10 @@ and is repaired offline (see `docs/compat/troubleshooting.md`).
|
|
|
136
138
|
same operating-system user: state files are created with the process
|
|
137
139
|
default permissions (no special modes are set — a standard umask applies),
|
|
138
140
|
and the host user remains trusted.
|
|
141
|
+
- It does **not** exclude paths from the store: recording and restore mirror the
|
|
142
|
+
paths the model's file tools touched, so a sensitive file (e.g. `.env`) the
|
|
143
|
+
model may read or edit will be backed up and restorable. Keeping one out is
|
|
144
|
+
a **DSH model-permission** concern (a per-path deny), not a rewind feature.
|
|
139
145
|
- It does **not** touch git (no refs, index, or worktree operations), makes
|
|
140
146
|
**no network requests**, and does **not** access credentials.
|
|
141
147
|
- It does **not** roll back whole-log state: telemetry, search, and `/export`
|
package/docs/README.md
CHANGED
|
@@ -11,6 +11,7 @@ index/navigation entry point.
|
|
|
11
11
|
| `architecture.md` | Module layering, rewind/checkpoint pipelines, compatibility strategy, roadmap | maintainers |
|
|
12
12
|
| `format.md` | Durable on-disk format spec (checkpoint entries + restore journals) | maintainers |
|
|
13
13
|
| `harness-reference.md` | DeepSeek Harness interface reference + plugin source layout | maintainers |
|
|
14
|
+
| `snapshot-auto-cleanup.md` | Global snapshot auto-cleanup policy and command (`.zh` mirror) | users / maintainers |
|
|
14
15
|
| `contract/client-contract.md` | Rewind visibility contract for third-party DOM plugins (`.zh` mirror) | integrators |
|
|
15
16
|
| `compat/audit.md` | Compatibility audit: verified surfaces, recorded findings, probe matrix | maintainers |
|
|
16
17
|
| `compat/troubleshooting.md` | Known issues & repair steps (`.zh` mirror) | users / maintainers |
|
package/docs/compat/audit.md
CHANGED
|
@@ -1,137 +1,149 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
>
|
|
1
|
+
# Compatibility audit (compat-audit)
|
|
2
|
+
|
|
3
|
+
> Method: **the tests are the audit**. The probes in `tests/compat-invariants.test.ts`,
|
|
4
|
+
> `tests/compat-interop.test.ts`, `tests/compat-gaps.test.ts` and
|
|
5
|
+
> `scripts/verify-host.mjs` drive the plugin's real execution paths through the DSH
|
|
6
|
+
> subsystems' **real consumer paths** (real `@deepseek-ai/*` packages) and assert
|
|
7
|
+
> compatibility invariants. A probe failure is a finding; it enters the
|
|
8
|
+
> fix/pin/record loop.
|
|
7
9
|
>
|
|
8
|
-
>
|
|
9
|
-
>
|
|
10
|
+
> Targeted version: npm `@deepseek-ai/*@0.1.1-rc.2` (matches `package-lock.json`);
|
|
11
|
+
> source reference: the `oss/deepseek-harness` local fork.
|
|
10
12
|
>
|
|
11
|
-
>
|
|
12
|
-
>
|
|
13
|
-
>
|
|
14
|
-
> `
|
|
15
|
-
>
|
|
13
|
+
> Version alignment: `peerDependencies` use an OR-union (e.g.
|
|
14
|
+
> `^0.1.0-rc.6 || ^0.1.1-rc.2`) covering each published rc tuple series. npm's
|
|
15
|
+
> prerelease matching rules require a candidate to share the range comparator's
|
|
16
|
+
> `[major, minor, patch]` tuple, so each new tuple series (e.g. a future
|
|
17
|
+
> `0.1.2-rc.x`, `0.2.x`) requires appending a union member; rc rolling within a
|
|
18
|
+
> tuple (`0.1.1-rc.2 → rc.3`) is a no-op. Signal: `npm view @deepseek-ai/dsh version`;
|
|
19
|
+
> flow: `scripts/check-dsh-version.mjs`.
|
|
16
20
|
|
|
17
|
-
##
|
|
21
|
+
## Definition of "fully compatible" (invariants)
|
|
18
22
|
|
|
19
|
-
|
|
|
23
|
+
| Invariant | Meaning | Probe location |
|
|
20
24
|
|---|---|---|
|
|
21
|
-
| I1
|
|
22
|
-
| I2 surface
|
|
23
|
-
| I3 step/turn
|
|
24
|
-
| I4
|
|
25
|
-
| I5 compact
|
|
26
|
-
| I6
|
|
27
|
-
| I7
|
|
28
|
-
| I8
|
|
29
|
-
|
|
30
|
-
##
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
| I1 log replayability | A rewound log passes token-meter replay and `Session.create` (the resume-preflight validation) without throwing | `compat-invariants` I1, `verify-host` 12/13 |
|
|
26
|
+
| I2 surface consistency | After a cut, the surface has no duplicate nodes, every node exists in the log, the withdrawn target never returns to the surface, and `deriveMessages()` is legal | `compat-invariants` I2 |
|
|
27
|
+
| I3 step/turn structure | Client turn-tail ordering, unique `step/start`, every `step/end` and `assistant/message` has a paired `step/start`, no ghost turns | `compat-invariants` I3, `helpers.assertTurnTailOrdering` |
|
|
28
|
+
| I4 fold-service safety | stats / title / goal / projection fold a marker-bearing log without throwing, with predictable values | `compat-invariants` I4 |
|
|
29
|
+
| 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 |
|
|
30
|
+
| 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 |
|
|
31
|
+
| I7 client ordering | A log carrying tool turns, rewind markers, and ghost steps satisfies the client builder ordering | `compat-interop` I7 |
|
|
32
|
+
| I8 runtime safety | `rewind`/`compact` combinations never leave a dangling step/turn frame | `verify-host` 15 |
|
|
33
|
+
|
|
34
|
+
## Verified-compatible surfaces (probes pass)
|
|
35
|
+
|
|
36
|
+
- **token-meter replay** (marker + ghost-step frames + multiple rewinds + interleaved real turns + compact stacking).
|
|
37
|
+
- **compaction transactions**: `toolPairingBalancedBefore/After` stays balanced after a marker cut; the real `/compact` command (`command-compact` + `compaction-basic`, stub summarizer) can land `compaction/start…end` on top of a rewind marker and stay replayable; `/compact` is a legal no-op on a small surface.
|
|
38
|
+
- **resume replay**: `Session.create(id, events)` replays a rewind/compact-bearing log.
|
|
39
|
+
- **session-stats**: a ghost-step frame adds one step but no phantom turn (reuses the turn number).
|
|
40
|
+
- **session-title / goal fold**: a marker does not disturb `foldSessionTitle` / `foldGoal`.
|
|
41
|
+
- **client ordering**: turn-tail ordering + `step/start` uniqueness hold for tool turns + marker logs.
|
|
42
|
+
- **rewind across a compact checkpoint**: `RewindError('not-on-surface')` refuses cleanly, no crash.
|
|
43
|
+
- **plan-mode**: `hasOpenTurn` pairs only `turn/start`/`turn/end`; a marker produces no `turn/start`, so a rewind during active plan is unaffected (static review confirmed).
|
|
44
|
+
- **agent-loop cancellation**: `finally` guarantees step/turn closure; the rewind force-stop path leaves no dangling frame.
|
|
45
|
+
|
|
46
|
+
## Known behavior boundaries (deterministic differences, non-crash, documented)
|
|
47
|
+
|
|
48
|
+
- **session-stats / session-telemetry fold the full log**: post-rewind stats do **not** rewind — `turns`/`steps`/`llmMs` still include withdrawn content; telemetry reports the marker and ghost-step frames one-by-one (under the reused old turn). This is the intended "fold the full log" semantics, pinned by probe.
|
|
49
|
+
- **token-meter usage anchor briefly reverts** (G3): after a rewind, the baseline falls back to the heuristic `estimated` until the next real-usage call restores it. Confirmed expected behavior (a marker inherently carries no usage); pinned by `compat-gaps` G3.
|
|
50
|
+
- **Withdrawn content stays searchable/exportable**: session-query full-text and `/export` read the raw log; a rewind cuts only the surface, so withdrawn messages remain (declared in the README).
|
|
51
|
+
- **Session title auto-regeneration**: the title derives from the surface, so an automatically-derived title may change after a rewind.
|
|
52
|
+
- **Files written but uncommitted in a cancelled turn**: a `both` rewind cannot restore them (tool side-effect timing; same as Claude Code).
|
|
53
|
+
- **Attachment files left after a message is shadowed**: attachment storage is not cleaned with the surface (`dsh-attachment-local` not installed, not automatically verified).
|
|
54
|
+
|
|
55
|
+
## Upstream (harness) issues and the plugin's no-compensation stance
|
|
56
|
+
|
|
57
|
+
The plugin treats these as harness-side defects it does not compensate for. Each entry records the harness issue, its current status, and the plugin's stance, so a future maintainer does not "fix the wrong direction."
|
|
58
|
+
|
|
59
|
+
### RU-I18N: host-side locale preference is not reliably readable at command registration (harness-side structural timing defect; plugin reads once, never retries)
|
|
60
|
+
|
|
61
|
+
- **Root cause (harness-side)**: the plugin resolves `activeLocale` in an
|
|
62
|
+
`ctx.inject(['settings'])` callback that reads `settings.get(settingsNamespace('locale')).preference`
|
|
63
|
+
**once, with no retry** (`src/index.ts`). `dsh-client-locale`'s host half
|
|
64
|
+
registers that `locale` settings section **through its own `ctx.inject(['settings'])`**
|
|
65
|
+
(`packages/client/locale/src/index.ts`). Both callbacks wait only on `settings` and are
|
|
66
|
+
independent of each other, so cordis makes **no ordering guarantee** between the
|
|
67
|
+
section registration and the plugin's read.
|
|
68
|
+
- **Observed failure**: a probe confirmed that when the plugin's read runs, `settings.get('locale')`
|
|
69
|
+
returns `undefined` (the section is not yet registered), so `activeLocale` stays at the
|
|
70
|
+
default `'en'` and — because the read is one-shot — is never corrected. The result is that
|
|
71
|
+
**all host-side `t()` output (runtime messages and command descriptions alike) renders in
|
|
72
|
+
English**, regardless of the user's language preference.
|
|
73
|
+
- **Not a user-config problem**: `settings.yaml` correctly carries `locale.preference: zh`;
|
|
74
|
+
reading it directly works. The defect is purely the host-side read racing the section
|
|
75
|
+
registration.
|
|
76
|
+
- **Plugin stance**: no compensation. The plugin does not add a lazy re-read, a retry loop, or a
|
|
77
|
+
post-locale description re-registration as a workaround, because the guarantee belongs to the
|
|
78
|
+
harness (make the locale preference available before plugin registration, or support per-locale
|
|
79
|
+
command descriptions). The plugin's `t()` design is retained; localized host output is treated
|
|
80
|
+
as an upstream capability to be restored when the harness provides it. (Command descriptions
|
|
81
|
+
are English for **every** host command — system plugins also pass raw English `description`
|
|
82
|
+
strings, e.g. `/goal` — so this behavior is consistent with the ecosystem, not a plugin
|
|
83
|
+
deviation.)
|
|
84
|
+
|
|
85
|
+
### R-OPENSTEP: an unclosed `step` in the log lets a rewind break token-meter replay (harness-side; plugin guard attempted and reverted)
|
|
86
|
+
|
|
87
|
+
> **Root cause (harness-side)**: an unclosed step left by a crash makes token-meter replay
|
|
88
|
+
> reject any later step activity. DSH `0.1.1-rc.2` now auto-closes crash-left step/turn/tool
|
|
89
|
+
> boundaries at load via `interruptedTurnClosers` (`dsh-session`, consumed by
|
|
90
|
+
> `session-persistence/src/coordinator.ts`) — **the crash path is fixed**.
|
|
37
91
|
>
|
|
38
|
-
>
|
|
39
|
-
>
|
|
40
|
-
>
|
|
41
|
-
>
|
|
42
|
-
>
|
|
92
|
+
> **Plugin guard (attempted and reverted)**: a `hasOpenStep` + `planRewind` pre-refusal was
|
|
93
|
+
> implemented (`open-step`) but misjudged on **real session logs** (normal rewinds refused, GUI
|
|
94
|
+
> verification broken), and was reverted (`177ec14`). Conclusion: **the plugin sets no guard**,
|
|
95
|
+
> accepting residual risk (an unclosed step produced by a third-party plugin can break
|
|
96
|
+
> `/compact` — that log is already abnormal, and continuing the conversation triggers the same).
|
|
97
|
+
> The fix direction is in the harness (token-meter recovery for unclosed steps), not the plugin.
|
|
43
98
|
|
|
44
|
-
####
|
|
99
|
+
#### Concrete `step/start` trigger paths (source-confirmed)
|
|
45
100
|
|
|
46
|
-
|
|
47
|
-
|
|
101
|
+
The tree has exactly **one** `append('step/start')` producer: `packages/core/agent-loop/src/agent.ts:279`
|
|
102
|
+
(no other producer inside the official packages; `session/end-seed` etc. only truncate torn writes,
|
|
103
|
+
not logically-unclosed steps).
|
|
48
104
|
|
|
49
|
-
| # |
|
|
105
|
+
| # | Trigger path | Plausibility | Basis |
|
|
50
106
|
|---|---|---|---|
|
|
51
|
-
| P1 |
|
|
52
|
-
| P2 |
|
|
53
|
-
| P3 |
|
|
54
|
-
| P4 | **append
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`phase.turn + 1`
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
- **rewind
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- **token-meter 重放**(含 marker + 幽灵 step 帧 + 多次 rewind + 交错真实 turn + compact 叠加)。
|
|
79
|
-
- **compaction 事务**:`toolPairingBalancedBefore/After` 对 marker 切割后的 surface 恒平衡;
|
|
80
|
-
真实 `/compact` 命令(`command-compact` + `compaction-basic`,stub summarizer)可在 rewind
|
|
81
|
-
marker 之上落地 `compaction/start…end` 并保持可重放;小 surface 时 `/compact` 合法 no-op。
|
|
82
|
-
- **resume 重放**:`Session.create(id, events)` 对含 rewind/compact 的日志重放通过。
|
|
83
|
-
- **session-stats**:ghost step 帧只 +1 step、不新增 phantom turn(复用 turn 号)。
|
|
84
|
-
- **session-title / goal fold**:marker 不干扰 `foldSessionTitle` / `foldGoal`。
|
|
85
|
-
- **客户端顺序**:turn-tail ordering + step/start 唯一性对工具 turn + marker 日志成立。
|
|
86
|
-
- **跨 compact checkpoint 的 rewind**:`RewindError('not-on-surface')` 明确拒绝,不崩溃。
|
|
87
|
-
- **plan-mode**:`hasOpenTurn` 只配对 `turn/start`/`turn/end`,marker 不产生 `turn/start`,
|
|
88
|
-
激活 plan 期间 rewind 无影响(静态审查确认)。
|
|
89
|
-
- **agent-loop 取消**:`finally` 保证 step/turn 闭合,rewind 的 force-stop 路径不悬挂帧。
|
|
90
|
-
|
|
91
|
-
## 已知边界(行为差异,非崩溃,文档化)
|
|
92
|
-
|
|
93
|
-
- **session-stats / session-telemetry 折叠完整日志**:rewind 后统计**不回退**——`turns` /
|
|
94
|
-
`steps` / `llmMs` 仍含被撤内容;telemetry 逐条上报 marker 与幽灵 step 帧(归入被复用
|
|
95
|
-
的旧 turn)。这是「折叠完整日志」的预期语义,探针钉住该行为。
|
|
96
|
-
- **token-meter usage 锚点短暂失效**(G3):rewind 后 baseline 退回启发式 `estimated`,
|
|
97
|
-
下一条真实 usage 调用恢复(探针钉住)。
|
|
98
|
-
- **被撤内容仍可搜索、可导出**:session-query 全文搜索与 `/export` 基于原始日志,
|
|
99
|
-
rewind 只切 surface,被撤消息仍在其中(README 已声明)。
|
|
100
|
-
- **session-title 自动重生成**:标题由 surface 派生,rewind 后自动标题可能变化。
|
|
101
|
-
- **取消 turn 中已写盘但未提交快照的文件**:rewind both 无法恢复(工具副作用时序,
|
|
102
|
-
Claude Code 同限制)。
|
|
103
|
-
- **附件消息被遮蔽后文件残留**:attachment 存储不随 surface 清理(`dsh-attachment-local`
|
|
104
|
-
未装,未自动化验证)。
|
|
105
|
-
|
|
106
|
-
## 未覆盖边界(需要额外端到端层,不阻塞)
|
|
107
|
-
|
|
108
|
-
- 真实 LLM 流式与自动标题生成(L2 stub 化)。
|
|
109
|
-
- 真实 SQLite 索引生命周期(`dsh-session-query-sqlite` 未安装、含原生依赖)。
|
|
110
|
-
- 浏览器端实际渲染 replay(客户端契约已由 `client-contract.test.ts` 覆盖逻辑层)。
|
|
111
|
-
- 真实 JSONL 持久化往返(`dsh-session-persistence-jsonl` 依赖 native `koffi`;往返验证的是
|
|
112
|
-
harness 自身 zstd/JSON 编解码,插件不参与,价值/成本不划算,未安装)。
|
|
113
|
-
- session-reference 的 `SessionReferenceResolver.prepare` 需完整 session-query 服务;其数据
|
|
114
|
-
基础(current-surface 投影)已由 G1 探针(`foldSurface` 转换)覆盖。
|
|
115
|
-
- telemetry 管道(`dsh-session-telemetry-otel`)与附件 provider(`dsh-attachment-local`)。
|
|
116
|
-
- 运行中的 workflow/jobs 被 rewind 取消:工具契约要求 observe `exec.signal` 并 settle
|
|
117
|
-
(`packages/core/tools/src/index.ts`),rewind 触发的是 harness 标准取消,非插件特有——
|
|
118
|
-
静态确认,未实测真实 workflow。
|
|
119
|
-
|
|
120
|
-
## 排查矩阵(子系统 × 不变量)
|
|
121
|
-
|
|
122
|
-
| DSH 子系统 | I1 | I2 | I3 | I4 | I5 | I6 | I7 | I8 |
|
|
107
|
+
| P1 | **Abnormal process termination**: `step/start` is batched to disk (write-behind, `maxDelayMs` per batch) → the step is mid-execution (LLM stream/tool, seconds to minutes) → SIGKILL / OOM-kill / power loss / WSL hard-close → `step/end` (in `finally`, only runs while the process is alive) is never persisted | **Most realistic** | `agent.ts:292` finally; write-behind batching; torn-write fix truncates only a half-written line |
|
|
108
|
+
| P2 | **Third-party plugin bug**: only the official agent-loop produces one, but external plugins may `session.append('step/start', …)` and never close it | possible | public `Session.append` |
|
|
109
|
+
| P3 | **Manual session-file editing**: edit `~/.dsh/…/session.jsonl[.zstd]` (zstd needs decompress/recompress; plaintext config edits directly) | possible but laborious | `persistence-jsonl/format.ts` (`JsonlCompression = 'zstd' \| 'none'`) |
|
|
110
|
+
| P4 | **append itself failing**: `append('step/end')` in `finally` throws (payload is plain numbers, nearly impossible) | theoretical | `agent.ts:292` |
|
|
111
|
+
|
|
112
|
+
**Amplifier (rewind is not the only trigger)**: after a crash resume, agent-loop `turn()` opens a
|
|
113
|
+
new turn at `phase.turn + 1` (`agent.ts:251-255`) **without closing the leftover step** — so
|
|
114
|
+
"continue the conversation" (a new `step/start`) trips the same token-meter check. Scope:
|
|
115
|
+
|
|
116
|
+
- **The conversation itself is unaffected** (the request path does not call `tokenMeter.measure`; only compaction-basic does tree-wide).
|
|
117
|
+
- **Manual `/compact` fails permanently** (`compactNow`'s first `measure()` throws the raw error).
|
|
118
|
+
- **Automatic compaction silently stays disabled** (the `agent/pre-step` hook catches and warns; the conversation continues).
|
|
119
|
+
- **rewind's role**: if the user rewinds first (rather than continuing), the ghost `step/start` becomes the first trip-wire, and the plugin has no defensive detection — upgrading a "locally abnormal log" into a "user-visible `/compact` failure."
|
|
120
|
+
|
|
121
|
+
## Uncovered boundaries (need an additional e2e layer; non-blocking)
|
|
122
|
+
|
|
123
|
+
- Real LLM streaming and auto title generation (L2 stubbed).
|
|
124
|
+
- Real SQLite index lifetime (`dsh-session-query-sqlite` not installed; native deps).
|
|
125
|
+
- Actual browser rendering replay (the client contract's logic layer is covered by `client-contract.test.ts`).
|
|
126
|
+
- Real JSONL persistence round-trip (`dsh-session-persistence-jsonl` depends on native `koffi`; the round-trip validates the harness's own zstd/JSON codec, the plugin is not party to it, not worth the cost, not installed).
|
|
127
|
+
- `session-reference`'s `SessionReferenceResolver.prepare` needs a full session-query service; its data base (current-surface projection) is already covered by the G1 probe (`foldSurface`).
|
|
128
|
+
- telemetry pipeline (`dsh-session-telemetry-otel`) and the attachment provider (`dsh-attachment-local`).
|
|
129
|
+
- Running workflow/jobs cancelled by a rewind: the tool contract requires observing `exec.signal` and settling (`packages/core/tools/src/index.ts`); a rewind triggers the harness's standard cancel, not plugin-specific — statically confirmed, real workflows untested.
|
|
130
|
+
|
|
131
|
+
## Audit matrix (subsystem × invariant)
|
|
132
|
+
|
|
133
|
+
| DSH subsystem | I1 | I2 | I3 | I4 | I5 | I6 | I7 | I8 |
|
|
123
134
|
|---|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|
|
124
|
-
|
|
|
135
|
+
| Session core (append/surface/deriveMessages) | ✓ | ✓ | ✓ | — | ✓ | — | ✓ | ✓ |
|
|
125
136
|
| token-meter | ✓ | — | ✓ | — | ✓ | — | — | — |
|
|
126
|
-
| compaction
|
|
137
|
+
| compaction (transaction/command/tool-pairing) | ✓ | — | — | — | ✓ | — | — | ✓ |
|
|
127
138
|
| session-stats / projection | — | — | ✓ | ✓ | — | — | — | — |
|
|
128
139
|
| session-title | — | — | — | ✓ | — | — | — | — |
|
|
129
140
|
| goal | — | — | — | ✓ | — | — | — | — |
|
|
130
|
-
| resume / session-query
|
|
131
|
-
|
|
|
132
|
-
|
|
|
133
|
-
| plan-mode | — | — | — | — | — | — | — |
|
|
134
|
-
|
|
135
|
-
✓ =
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
| resume / session-query replay | ✓ | — | ✓ | — | — | — | ✓ | — |
|
|
142
|
+
| tool pipeline (snapshot/restore) | — | — | — | — | — | ✓ | — | ✓ |
|
|
143
|
+
| client ordering | — | — | ✓ | — | — | — | ✓ | — |
|
|
144
|
+
| plan-mode | — | — | — | — | — | — | — | ✓ (static) |
|
|
145
|
+
|
|
146
|
+
✓ = probe passes; — = not applicable. RU-I18N and R-OPENSTEP are upstream issues the plugin
|
|
147
|
+
does not compensate for (see above); G3 is a confirmed-non-defect behavior pinned in
|
|
148
|
+
`compat-gaps.test.ts` (G1 surface classification via `foldSurface` and G2 projection checkpoint
|
|
149
|
+
both pass).
|
package/docs/format.md
CHANGED
|
@@ -7,10 +7,11 @@ and this spec disagree, the code wins and this spec is a bug.
|
|
|
7
7
|
|
|
8
8
|
## State root
|
|
9
9
|
|
|
10
|
-
The store root defaults to
|
|
11
|
-
directory
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
14
15
|
|
|
15
16
|
```
|
|
16
17
|
<root>/
|
|
@@ -54,7 +54,7 @@ src/index.ts host plugin: /rewind command + checkpoint pipeline (tool
|
|
|
54
54
|
src/rewind.ts pure planning: target resolution, surface range, candidate listing
|
|
55
55
|
src/snapshot.ts checkpoint store (disk before-backups, restore/preview, bounded prune)
|
|
56
56
|
src/session-cwd.ts session-cwd resolution (fs-tools rule)
|
|
57
|
-
src/client/index.ts client plugin: per-message ↶ button
|
|
57
|
+
src/client/index.ts client plugin: /rewind command decoration + per-message ↶ button portals
|
|
58
58
|
src/client/popover.ts mode-selection popover (both-mode impact confirm)
|
|
59
59
|
src/client/hidden.ts withdrawn-span computation (hiddenSeqsOf), pure
|
|
60
60
|
src/client/locales.ts zh / en copy (LocaleNamespaceMap)
|
|
@@ -1,71 +1,90 @@
|
|
|
1
|
-
# Snapshot
|
|
1
|
+
# Snapshot cleanup
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
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.
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
14
26
|
|
|
15
27
|
## Commands
|
|
16
28
|
|
|
17
29
|
```
|
|
18
|
-
/snapshot-auto-cleanup
|
|
19
|
-
/snapshot-auto-cleanup on|off
|
|
20
|
-
/snapshot-auto-cleanup max-age <days>
|
|
21
|
-
/snapshot-auto-cleanup run
|
|
22
|
-
/snapshot-auto-cleanup run --apply
|
|
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
|
|
23
37
|
```
|
|
24
38
|
|
|
25
|
-
`run`
|
|
26
|
-
|
|
39
|
+
`run` always starts as a preview; add `--apply` to make the change. `run` works
|
|
40
|
+
whether or not automatic cleanup is on.
|
|
27
41
|
|
|
28
|
-
##
|
|
42
|
+
## Settings
|
|
29
43
|
|
|
30
|
-
The
|
|
44
|
+
The settings live in `<dsh home>/snapshot-cleanup.json`:
|
|
31
45
|
|
|
32
46
|
```json
|
|
33
47
|
{ "enabled": false, "maxAgeDays": 30 }
|
|
34
48
|
```
|
|
35
49
|
|
|
36
|
-
- `enabled` — whether
|
|
37
|
-
- `maxAgeDays` — how many idle days before a
|
|
38
|
-
|
|
39
|
-
delete everything
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
The file is
|
|
43
|
-
reads as the safe default (off)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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.
|