dsh-rewind-plugin 0.5.0 → 0.6.1

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 CHANGED
@@ -11,7 +11,7 @@ Conversation rewind for DeepSeek Harness: **rewind the conversation to any earli
11
11
  A deliberately focused plugin with one job: **rewind to any user message, no matter how far back, in place** — and conveniently **restore the files it changed** along the way.
12
12
 
13
13
  - **Rewinding is time-travel** — the target message and everything after it (agent replies, tool calls) are withdrawn from the model context *and* the rendered transcript at once, with no new session and no window switch; the target's text is offered back in the composer so you can edit and re-send it — **truly seamless and convenient by design**.
14
- - **Lightweight workspace backup** — Claude Code-aligned behavior: only file-writing tools are tracked, a lightweight before-backup is **persisted on disk**, and your git repository is never touched or relied on. One lightweight plugin gives you a **complete** agentic rewind capability.
14
+ - **Lightweight workspace backup** — Claude Code-aligned behavior: tracks files edited by the write-class tools, and external changes to **already-tracked** files are restorable too. Selective tracking, before-write backup, store only on change. One lightweight plugin gives you a **complete** agentic rewind capability.
15
15
  - **Privacy-first** — the plugin never deletes or rewrites the session log (append-only) and never actually deletes any of your conversation; file restores stay inside the plugin's own backup directory. Full security model: [SECURITY.md](SECURITY.md).
16
16
  - **A complete test system** — unit, probe, and end-to-end host verification, covering compatibility probing, log replay, resume, cross-restart and other scenarios; maintained continuously as the harness evolves to ensure feature stability.
17
17
 
@@ -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 `~/.dsh/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. Full security model: [SECURITY.md](SECURITY.md).
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
@@ -11,7 +11,7 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
11
11
  刻意聚焦、保持极简,只做一件事:**就地回退到任意远的用户消息**,还能**顺手还原改过的文件**。
12
12
 
13
13
  - **回退 = 时间回溯**——目标消息及其之后的全部内容(agent 回复、工具调用)同时从**模型上下文**和**渲染对话**中撤回,不新建会话、不切换窗口;目标消息文本会回填输入框,改完可重发。**在原理上就真正无感、便捷**。
14
- - **轻量工作区备份**——行为对齐 Claude Code:只跟踪写文件的工具,写前做轻量备份并**落盘持久化**,不依赖、也不触碰 git 仓库。一个轻型插件,即拥有**完备的智能体回退能力**。
14
+ - **轻量工作区备份**——对齐 Claude Code:追踪写类工具编辑过的文件,**已跟踪文件的外部变更也能还原**。局部追踪、写前备份、不变不存。一个轻型插件,即拥有**完备的智能体回退能力**。
15
15
  - **信息安全优先**——插件从不删改会话日志(append-only),从不真正删除你的任何对话;文件还原限定在插件自己的备份目录。完整安全模型:[SECURITY.md](SECURITY.md)。
16
16
  - **完备测试系统**——单元、探针、端到端主机验证,覆盖兼容性探测、日志重放、续接、跨重启等场景;随 harness 升级持续维护,确保功能稳定。
17
17
 
@@ -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/`。插件对**同一会话**的快照做内容去重(内容未变则存为链接)并保留最近 100 组锚点;**手动删除该目录**仅清除文件备份(对话回退不受影响),插件会自动重建。
61
+ 快照(写前备份)存储于 `<dsh home>/rewind-snapshots/`(未设 `$DSH_HOME` 时即 `~/.dsh/rewind-snapshots/`)。插件对**同一会话**的快照做内容去重(内容未变则存为链接)并保留最近 100 组锚点;**手动删除该目录**仅清除文件备份(对话回退不受影响),插件会自动重建。
62
62
 
63
- 另提供**全局自动清理**(默认关闭):把**长期不活跃**的会话快照整目录移除,不影响活动会话与对话日志。用 `/snapshot-auto-cleanup` 命令**查看与设置**,配置写入 `~/.dsh/snapshot-cleanup.json`。详见:[快照自动清理](docs/snapshot-auto-cleanup.zh.md)。
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**: `~/.dsh/rewind-snapshots/` by default (`DSH_REWIND_SNAPSHOT_DIR`
85
- overrides it). The store never overlaps the workspace; deleting it only
86
- removes file backups and the store rebuilds from scratch.
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 |
@@ -1,137 +1,165 @@
1
- # 兼容性排查记录(compat-audit
2
-
3
- > 排查方式:**测试即排查**。`tests/compat-invariants.test.ts`、`tests/compat-interop.test.ts`
4
- > `scripts/verify-host.mjs` 中的探针,把插件的真实执行路径插入 DSH 各子系统的
5
- > **真实消费路径**(真实 `@deepseek-ai/*` 包),断言兼容性不变量。探针失败即发现,
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
- > 针对版本:npm `@deepseek-ai/*@0.1.1-rc.2`(与 `package-lock.json` 一致);
9
- > 源码参考:`oss/deepseek-harness` 本地 fork
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
- > 版本适配说明:peerDependencies 采用 OR 并集(如 `^0.1.0-rc.6 || ^0.1.1-rc.2`),
12
- > 覆盖 DSH 已发布的 rc 元组系列。npm prerelease 匹配规则要求候选与范围内比较器
13
- > **同 `[major, minor, patch]` 元组**,因此 DSH 每次发布新元组(如未来的 `0.1.2-rc.x`、
14
- > `0.2.x`)时需追加并集项;同元组内 rc 滚动(如 `0.1.1-rc.2 rc.3`)无需动作。
15
- > 判断信号:`npm view @deepseek-ai/dsh version`;流程见 `scripts/check-dsh-version.mjs`。
16
-
17
- ## 「完全适配」的可执行定义(不变量)
18
-
19
- | 不变量 | 含义 | 探针位置 |
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`.
20
+ >
21
+ > Client channel (harness `0.1.2-alpha.1`, a **bundled pre-release — NOT published
22
+ > to npm**): the plugin also reads the session chat when a DSH Desktop build ships
23
+ > that version (`latest` on the registry is still `0.1.1-rc.2`; the alpha is only
24
+ > in the bundled `app.asar.unpacked/node_modules/@deepseek-ai/*`). There
25
+ > `uiConversation` exposes the chat as a per-session named `"chat"` view
26
+ > (contributed by `dsh-client-ui-chat` through the `uiSession` slot hook) instead
27
+ > of the session-face `chat` field. `src/client/hidden.ts` (`chatSnapshotOf`) reads
28
+ > the session-face snapshot first (rc.2 path), then the `uiConversation` `"chat"`
29
+ > view (alpha.1 path); both missing degrades to `undefined` (no targets, never a
30
+ > crash). Peers are intentionally **NOT** extended for `0.1.2-alpha.1` because it is
31
+ > not a registry version (see above); the OR-union stays
32
+ > `^0.1.0-rc.6 || ^0.1.1-rc.2` and will gain the `0.1.2` tuple when the harness
33
+ > publishes it. Pinned by `tests/chat-channel.test.ts` (channel precedence + the
34
+ > alpha.1 snapshot shape) and `tests/client-dom.test.ts` (the button-target
35
+ > pairing that consumes the chat).
36
+
37
+ ## Definition of "fully compatible" (invariants)
38
+
39
+ | Invariant | Meaning | Probe location |
20
40
  |---|---|---|
21
- | I1 日志可重放 | rewind 后日志过 token-meter 重放、`Session.create`(resume preflight 同款校验)均不抛 | `compat-invariants` I1、`verify-host` 12/13 |
22
- | I2 surface 一致 | 切割后 surface 无重复节点、节点存在于日志、被撤目标不再回到 surface、`deriveMessages()` 合法 | `compat-invariants` I2 |
23
- | I3 step/turn 结构合法 | 客户端 turn-tail 顺序、step/start 唯一、step/end assistant/message 均有配对 step/start、无幽灵 turn | `compat-invariants` I3、`helpers.assertTurnTailOrdering` |
24
- | I4 折叠服务安全 | stats / title / goal / projection 对含 marker 日志折叠不抛且值可预测 | `compat-invariants` I4 |
25
- | I5 compact 互操作 | 取消 turn 遗留的 tool-call rewind 遮蔽后配对恢复平衡;跨 checkpoint rewind 明确拒绝;rewind compact 事务合法 | `compat-interop` I5、`verify-host` 12/14 |
26
- | I6 工具管线正确 | before-快照捕获/提交/恢复正确(既有 `snapshot.test.ts` + `verify-host` 4-8);取消时序不悬挂 | `verify-host` 4-815 |
27
- | I7 客户端顺序合法 | 含工具 turn、rewind marker、幽灵 step 的日志满足客户端 builder 顺序 | `compat-interop` I7 |
28
- | I8 运行时安全 | rewind/compact 组合不留下悬空 step/turn | `verify-host` 15 |
29
-
30
- ## 发现的兼容性问题
31
-
32
- ### R-OPENSTEP:日志存在未闭合 step 时,rewind 会破坏 token-meter 重放(**harness 侧问题;插件守卫已尝试并回退**)
33
-
34
- > **根因(harness 侧)**:崩溃遗留的未闭合 step 会让 token-meter 重放拒绝任何后续
35
- > step 活动。DSH `0.1.1-rc.2` 已在加载时用 `interruptedTurnClosers`
36
- > (`dsh-session/repair`)自动闭合崩溃遗留的 step/turn/tool 边界——**崩溃路径已根治**。
41
+ | 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 |
42
+ | 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 |
43
+ | 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` |
44
+ | I4 fold-service safety | stats / title / goal / projection fold a marker-bearing log without throwing, with predictable values | `compat-invariants` I4 |
45
+ | 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 |
46
+ | I6 tool pipeline | before-snapshot capture/commit/restore is correct (existing `snapshot.test.ts` + `verify-host` 48); cancellation timing never hangs | `verify-host` 48, 15 |
47
+ | I7 client ordering | A log carrying tool turns, rewind markers, and ghost steps satisfies the client builder ordering | `compat-interop` I7 |
48
+ | I8 runtime safety | `rewind`/`compact` combinations never leave a dangling step/turn frame | `verify-host` 15 |
49
+
50
+ ## Verified-compatible surfaces (probes pass)
51
+
52
+ - **token-meter replay** (marker + ghost-step frames + multiple rewinds + interleaved real turns + compact stacking).
53
+ - **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.
54
+ - **resume replay**: `Session.create(id, events)` replays a rewind/compact-bearing log.
55
+ - **session-stats**: a ghost-step frame adds one step but no phantom turn (reuses the turn number).
56
+ - **session-title / goal fold**: a marker does not disturb `foldSessionTitle` / `foldGoal`.
57
+ - **client ordering**: turn-tail ordering + `step/start` uniqueness hold for tool turns + marker logs.
58
+ - **rewind across a compact checkpoint**: `RewindError('not-on-surface')` refuses cleanly, no crash.
59
+ - **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).
60
+ - **agent-loop cancellation**: `finally` guarantees step/turn closure; the rewind force-stop path leaves no dangling frame.
61
+
62
+ ## Known behavior boundaries (deterministic differences, non-crash, documented)
63
+
64
+ - **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.
65
+ - **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.
66
+ - **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).
67
+ - **Session title auto-regeneration**: the title derives from the surface, so an automatically-derived title may change after a rewind.
68
+ - **Files written but uncommitted in a cancelled turn**: a `both` rewind cannot restore them (tool side-effect timing; same as Claude Code).
69
+ - **Attachment files left after a message is shadowed**: attachment storage is not cleaned with the surface (`dsh-attachment-local` not installed, not automatically verified).
70
+
71
+ ## Upstream (harness) issues and the plugin's no-compensation stance
72
+
73
+ 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."
74
+
75
+ ### RU-I18N: host-side locale preference is not reliably readable at command registration (harness-side structural timing defect; plugin reads once, never retries)
76
+
77
+ - **Root cause (harness-side)**: the plugin resolves `activeLocale` in an
78
+ `ctx.inject(['settings'])` callback that reads `settings.get(settingsNamespace('locale')).preference`
79
+ **once, with no retry** (`src/index.ts`). `dsh-client-locale`'s host half
80
+ registers that `locale` settings section **through its own `ctx.inject(['settings'])`**
81
+ (`packages/client/locale/src/index.ts`). Both callbacks wait only on `settings` and are
82
+ independent of each other, so cordis makes **no ordering guarantee** between the
83
+ section registration and the plugin's read.
84
+ - **Observed failure**: a probe confirmed that when the plugin's read runs, `settings.get('locale')`
85
+ returns `undefined` (the section is not yet registered), so `activeLocale` stays at the
86
+ default `'en'` and — because the read is one-shot — is never corrected. The result is that
87
+ **all host-side `t()` output (runtime messages and command descriptions alike) renders in
88
+ English**, regardless of the user's language preference.
89
+ - **Not a user-config problem**: `settings.yaml` correctly carries `locale.preference: zh`;
90
+ reading it directly works. The defect is purely the host-side read racing the section
91
+ registration.
92
+ - **Plugin stance**: no compensation. The plugin does not add a lazy re-read, a retry loop, or a
93
+ post-locale description re-registration as a workaround, because the guarantee belongs to the
94
+ harness (make the locale preference available before plugin registration, or support per-locale
95
+ command descriptions). The plugin's `t()` design is retained; localized host output is treated
96
+ as an upstream capability to be restored when the harness provides it. (Command descriptions
97
+ are English for **every** host command — system plugins also pass raw English `description`
98
+ strings, e.g. `/goal` — so this behavior is consistent with the ecosystem, not a plugin
99
+ deviation.)
100
+
101
+ ### R-OPENSTEP: an unclosed `step` in the log lets a rewind break token-meter replay (harness-side; plugin guard attempted and reverted)
102
+
103
+ > **Root cause (harness-side)**: an unclosed step left by a crash makes token-meter replay
104
+ > reject any later step activity. DSH `0.1.1-rc.2` now auto-closes crash-left step/turn/tool
105
+ > boundaries at load via `interruptedTurnClosers` (`dsh-session`, consumed by
106
+ > `session-persistence/src/coordinator.ts`) — **the crash path is fixed**.
37
107
  >
38
- > **插件守卫(已尝试并回退)**:曾实现 `hasOpenStep` + `planRewind` 前置拒绝
39
- > (`open-step`),但在**真实会话日志上产生误判**(正常回退被拒、GUI 验证功能缺失),
40
- > revert(`177ec14`)。结论:**插件不设守卫**,接受残余风险(运行中第三方插件
41
- > 产生未闭合 step 时,rewind 可能破坏 /compact——该日志本身已异常,继续对话同样触发)。
42
- > 根治方向在 harness(token-meter 对未闭合 step 的恢复),不在插件。
108
+ > **Plugin guard (attempted and reverted)**: a `hasOpenStep` + `planRewind` pre-refusal was
109
+ > implemented (`open-step`) but misjudged on **real session logs** (normal rewinds refused, GUI
110
+ > verification broken), and was reverted (`177ec14`). Conclusion: **the plugin sets no guard**,
111
+ > accepting residual risk (an unclosed step produced by a third-party plugin can break
112
+ > `/compact` that log is already abnormal, and continuing the conversation triggers the same).
113
+ > The fix direction is in the harness (token-meter recovery for unclosed steps), not the plugin.
43
114
 
44
- #### 未闭合 `step/start` 的具体触发情况(源码确认)
115
+ #### Concrete `step/start` trigger paths (source-confirmed)
45
116
 
46
- 全仓**只有一处** append `step/start`:`packages/core/agent-loop/src/agent.ts:279`(官方包内
47
- 无其他生产者;`session/end-seed` 等修复只做 torn-write 截断,不处理逻辑未闭合)。
117
+ The tree has exactly **one** `append('step/start')` producer: `packages/core/agent-loop/src/agent.ts:279`
118
+ (no other producer inside the official packages; `session/end-seed` etc. only truncate torn writes,
119
+ not logically-unclosed steps).
48
120
 
49
- | # | 触发路径 | 现实性 | 依据 |
121
+ | # | Trigger path | Plausibility | Basis |
50
122
  |---|---|---|---|
51
- | P1 | **进程非优雅终止**:`step/start` write-behind 批量落盘(`session-persistence/src/write-behind.ts`,`maxDelayMs` 后写一批)→ step 执行中(LLM 流式/工具,秒级到分钟级)→ SIGKILL / OOM-kill / 断电 / WSL 强关 → `step/end`(在 `finally`,进程活着才执行)未落盘 | **最现实** | `agent.ts:292` finallywrite-behind 批量;torn-write 修复只截断写一半的行 |
52
- | P2 | **第三方插件 bug**:官方包只有 agent-loop 一个生产者,但外部插件可任意 `session.append('step/start', …)` 不闭合 | 可能 | 公开 `Session.append` |
53
- | P3 | **手工编辑会话文件**:改 `~/.dsh/…/session.jsonl[.zstd]`(zstd 需解压/重压;plaintext 配置可直接改) | 可能但费事 | `persistence-jsonl/format.ts`(`JsonlCompression = 'zstd' \| 'none'`) |
54
- | P4 | **append 自身故障**:`finally` `append('step/end')` 抛错(payload 是纯数字,几乎不可能) | 理论 | `agent.ts:292` |
55
-
56
- **放大机制(rewind 不是唯一触发者)**:崩溃后 resumeagent-loop `turn()` 直接
57
- `phase.turn + 1` 开新 turn(`agent.ts:251-255`),**不闭合遗留 step**——所以「继续对话」
58
- (新 `step/start`)同样踩中 token-meter 校验。影响范围精确界定:
59
-
60
- - **对话本身不受影响**(请求路径不调用 `tokenMeter.measure`,全仓仅 compaction-basic 调用);
61
- - **手动 `/compact` 永久报错**(`compactNow` 首步 `measure()` 抛原始错误);
62
- - **自动压缩永久静默失效**(`agent/pre-step` 钩子 catch 后仅 warn,对话继续);
63
- - **rewind 的角色**:若用户先 rewind(而非继续对话),幽灵 step/start 成为第一个踩中者,
64
- 且插件无防御性检测——把「局部异常日志」升级为「用户可感知的 /compact 失效」。
65
-
66
- ### G3(已确认:合理行为,非缺陷):rewind 让 token-meter 的 usage 锚点短暂失效
67
-
68
- - **机制**:rewind marker 是日志最后一条 `assistant/message` 且无 `usage`,token-meter
69
- `_sync` 重放以它收尾,把 `MeasurementAnchor.baseline` provider 实测 `usage` 覆盖为
70
- 启发式 `estimated`——直到下一条带 usage 的真实消息才恢复(探针验证了完整链条:
71
- `usage rewind estimated 真实 turn usage`)。
72
- - **定性(已确认)**:marker 语义上就是空消息,不携带 usage 是正确的;锚点短暂退回估算
73
- 是该语义的自然结果,且短暂、自愈、不破坏功能。**属于预期行为,不修复**。
74
- - **探针**:`tests/compat-gaps.test.ts` → `G3`(钉住该行为,防止未来 harness 变更改变它)。
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 |
123
+ | 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 |
124
+ | 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` |
125
+ | 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'`) |
126
+ | P4 | **append itself failing**: `append('step/end')` in `finally` throws (payload is plain numbers, nearly impossible) | theoretical | `agent.ts:292` |
127
+
128
+ **Amplifier (rewind is not the only trigger)**: after a crash resume, agent-loop `turn()` opens a
129
+ new turn at `phase.turn + 1` (`agent.ts:251-255`) **without closing the leftover step** — so
130
+ "continue the conversation" (a new `step/start`) trips the same token-meter check. Scope:
131
+
132
+ - **The conversation itself is unaffected** (the request path does not call `tokenMeter.measure`; only compaction-basic does tree-wide).
133
+ - **Manual `/compact` fails permanently** (`compactNow`'s first `measure()` throws the raw error).
134
+ - **Automatic compaction silently stays disabled** (the `agent/pre-step` hook catches and warns; the conversation continues).
135
+ - **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."
136
+
137
+ ## Uncovered boundaries (need an additional e2e layer; non-blocking)
138
+
139
+ - Real LLM streaming and auto title generation (L2 stubbed).
140
+ - Real SQLite index lifetime (`dsh-session-query-sqlite` not installed; native deps).
141
+ - Actual browser rendering replay (the client contract's logic layer is covered by `client-contract.test.ts`).
142
+ - 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).
143
+ - `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`).
144
+ - telemetry pipeline (`dsh-session-telemetry-otel`) and the attachment provider (`dsh-attachment-local`).
145
+ - 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.
146
+
147
+ ## Audit matrix (subsystem × invariant)
148
+
149
+ | DSH subsystem | I1 | I2 | I3 | I4 | I5 | I6 | I7 | I8 |
123
150
  |---|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
124
- | 会话内核(append/surface/deriveMessages | ✓ | ✓ | ✓ | — | ✓ | — | ✓ | ✓ |
151
+ | Session core (append/surface/deriveMessages) | ✓ | ✓ | ✓ | — | ✓ | — | ✓ | ✓ |
125
152
  | token-meter | ✓ | — | ✓ | — | ✓ | — | — | — |
126
- | compaction(事务/命令/tool-pairing | ✓ | — | — | — | ✓ | — | — | ✓ |
153
+ | compaction (transaction/command/tool-pairing) | ✓ | — | — | — | ✓ | — | — | ✓ |
127
154
  | session-stats / projection | — | — | ✓ | ✓ | — | — | — | — |
128
155
  | session-title | — | — | — | ✓ | — | — | — | — |
129
156
  | goal | — | — | — | ✓ | — | — | — | — |
130
- | resume / session-query 重放 | ✓ | — | ✓ | — | — | — | ✓ | — |
131
- | 工具管线(快照/恢复) | — | — | — | — | — | ✓ | — | ✓ |
132
- | 客户端顺序 | — | — | ✓ | — | — | — | ✓ | — |
133
- | plan-mode | — | — | — | — | — | — | — | ✓(静态) |
134
-
135
- ✓ = 探针通过;— = 不适用。R-OPENSTEP 见上(harness 已修、插件不设守卫)。G3 见「已知边界」。
136
- 缺口探针 `tests/compat-gaps.test.ts`:G1 surface 分类(`foldSurface` current/shadowed/log-only)
137
- G2 projection checkpoint 均验证通过;G3 钉住 token-meter baseline 行为差异。
157
+ | resume / session-query replay | ✓ | — | ✓ | — | — | — | ✓ | — |
158
+ | tool pipeline (snapshot/restore) | — | — | — | — | — | ✓ | — | ✓ |
159
+ | client ordering | — | — | ✓ | — | — | — | ✓ | — |
160
+ | plan-mode | — | — | — | — | — | — | — | (static) |
161
+
162
+ ✓ = probe passes; — = not applicable. RU-I18N and R-OPENSTEP are upstream issues the plugin
163
+ does not compensate for (see above); G3 is a confirmed-non-defect behavior pinned in
164
+ `compat-gaps.test.ts` (G1 surface classification via `foldSurface` and G2 projection checkpoint
165
+ 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 `~/.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.
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 + manual /rewind guard
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)
@@ -38,6 +38,9 @@ npm version patch
38
38
  git push origin main --tags # triggers .github/workflows/publish.yml
39
39
  ```
40
40
 
41
+ - **Before bumping, manually confirm there is no newer DSH version the plugin
42
+ has not been verified against** (a pre-release can ship in DSH Desktop
43
+ without being on npm, e.g. `0.1.2-alpha.1`; see docs/compat/audit.md).
41
44
  - The workflow verifies the tag matches `package.json`, runs typecheck + tests +
42
45
  a full build + artifact verification, publishes with `--provenance`
43
46
  (Sigstore), and creates a GitHub Release. It is **idempotent** — an already
@@ -58,9 +61,11 @@ uses an OR-union covering every published rc tuple series
58
61
  (`0.1.1 → 0.1.2 → 0.2.x`); rc rolling within a tuple (`0.1.1-rc.2 → rc.3`)
59
62
  needs nothing. All `@deepseek-ai/*` packages release together;
60
63
  `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
+ - **Published-tuple check (optional)**: `node scripts/check-dsh-version.mjs`
65
+ compares the latest npm version against the tuples the peers cover (exit 0 =
66
+ nothing to do, exit 1 = update). It reads npm published versions only; a
67
+ newer-but-unpublished pre-release is a manual pre-release check — see the
68
+ "Before bumping" step above.
64
69
  - **Update steps**: append `|| ^<new-tuple>-rc.<n>` to every
65
70
  `@deepseek-ai/dsh-*` peer → bump devDependencies to the latest → `npm
66
71
  install` → `npm run check` → release.
@@ -37,6 +37,7 @@ npm version patch
37
37
  git push origin main --tags # 触发 .github/workflows/publish.yml
38
38
  ```
39
39
 
40
+ - **升版前手动确认**:确认无插件未针对其验证过的更新 DSH 版本(pre-release 可能只随 Desktop 捆绑、而不发到 npm,如 `0.1.2-alpha.1`;见 docs/compat/audit.md)。
40
41
  - workflow 校验 tag 与 `package.json` 版本一致,跑 typecheck + 测试 + 完整
41
42
  构建 + 产物验证,以 `--provenance`(Sigstore)发布并创建 GitHub Release。
42
43
  **幂等**——已发布的版本会跳过。
@@ -54,8 +55,9 @@ OR 并集覆盖 DSH 已发布的每个 rc 元组系列(如 `^0.1.0-rc.6 || ^0.
54
55
  - **何时需要更新**:仅当 DSH 发布新元组(`0.1.1 → 0.1.2 → 0.2.x`)时;
55
56
  同元组内 rc 滚动(`0.1.1-rc.2 → rc.3`)无需动作。DSH 所有包同版本发布,
56
57
  `npm view @deepseek-ai/dsh version` 即权威信号。
57
- - **自动检测**:`node scripts/check-dsh-version.mjs` 对比 npm 最新版本与
58
- peer 覆盖的元组,输出是否需追加(exit 0 无需动作,exit 1 需要)。
58
+ - **已发布元组检查(可选)**:`node scripts/check-dsh-version.mjs` npm 最新版本
59
+ 对比 peer 覆盖的元组(exit 0 无需动作,exit 1 需要)。它**只读 npm 已发布版本**;
60
+ 更新的未发布 pre-release 走**手动发布前检查**——见上文"升版前手动确认"。
59
61
  - **更新步骤**:给每个 `@deepseek-ai/dsh-*` peer 追加 `|| ^<新元组>-rc.<n>`
60
62
  → devDependencies 同步升到最新 → `npm install` → `npm run check` → 发版。
61
63
  - **正式版后收敛**:DSH 发布 final 版本后,正式版不受 prerelease 元组规则