dsh-rewind-plugin 0.14.0-beta.1 → 0.14.0-beta.3

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
@@ -16,7 +16,7 @@ A deliberately focused plugin with one job: **rewind to any user message, no mat
16
16
 
17
17
  ## Preview
18
18
 
19
- Every user message carries a **↶ rewind** button in its action row. Clicking it opens a mode-selection popover — "**rewind conversation only**" or "**rewind conversation and code**", the latter showing the file-change list for confirmation first. You can also rewind conveniently via the **`/rewind` command** or a **keyboard shortcut**.
19
+ Every user message carries a **↶ rewind** button in its action row. Clicking it opens a mode-selection popover — "**rewind conversation only**" or "**rewind conversation and code**", the latter showing the file-change list for confirmation first. You can also pick and rewind conveniently via the **`/rewind` and `/undo` commands**.
20
20
 
21
21
  <table>
22
22
  <tr>
@@ -29,7 +29,7 @@ Every user message carries a **↶ rewind** button in its action row. Clicking i
29
29
  </tr>
30
30
  </table>
31
31
 
32
- ## Install and update
32
+ ## Install
33
33
 
34
34
  Check your local DSH version, then find the matching plugin version in
35
35
  [Releases](https://github.com/SiriLee/dsh-rewind/releases).
@@ -37,17 +37,21 @@ Check your local DSH version, then find the matching plugin version in
37
37
  **Command line**:
38
38
 
39
39
  ```sh
40
- dsh plugin --profile web add dsh-rewind-plugin@<version>
40
+ dsh plugin --profile <name> add dsh-rewind-plugin@<version>
41
41
  ```
42
42
 
43
43
  **Graphical interface**: the **Plugins** page in the sidebar → **Add plugin** → enter `dsh-rewind-plugin@<version>` → install → restart DSH and refresh the browser.
44
44
 
45
- > ⚠️ The npm name `dsh-rewind` belongs to another author's package — install with `dsh-rewind-plugin`.
45
+ <img src="assets/screenshots/add-plugin.png" width="600" alt="Add plugin dialog: enter dsh-rewind-plugin@version in the field"><br>
46
+ <sub>The screenshot is an example only — use the version that matches your DSH version.</sub>
46
47
 
47
48
  **Updating**: the command line overwrites the installed version directly; the
48
49
  graphical interface rejects a re-install, so uninstall the old version first,
49
50
  then install the new one.
50
51
 
52
+ > [!WARNING]
53
+ > The npm name `dsh-rewind` belongs to another author's package — install with `dsh-rewind-plugin`.
54
+
51
55
  ## Usage
52
56
 
53
57
  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.
@@ -87,7 +91,7 @@ auto-cleanup switch and the idle-day cutoff), or use the
87
91
 
88
92
  ```sh
89
93
  # Uninstall the plugin
90
- dsh plugin --profile web remove dsh-rewind-plugin
94
+ dsh plugin --profile <name> remove dsh-rewind-plugin
91
95
 
92
96
  # To also delete local data
93
97
  rm -rf <dsh home>/rewind-snapshots
@@ -127,10 +131,10 @@ The whole design rests on two principles, simple but deliberate: **the conversat
127
131
 
128
132
  The file half follows Claude Code's checkpoint semantics — **partial tracking + before-write backup, plus a re-scan of tracked files at each message**, not a whole-tree snapshot. This trade-off saves space, and it's actually more complete:
129
133
 
130
- - **Before-write backup**: tracks only the write-class tools (`write`, `edit`) — backs up the original content before a write and records/tracks the files it touches; it never backs up the whole workspace, so it's lightweight.
134
+ - **Before-write backup**: tracks only the write-class tools (`write`, `edit`) — backs up the original content before a write and records/tracks the files it touches; it never backs up the whole workspace, so it's lightweight; **a single file that is too large is not backed up**.
131
135
  - **External changes count too**: at every user-message boundary the plugin re-checks all tracked files — external changes such as a command run or a manual edit are recorded as well and restored by a later rewind. "Lightweight" but not "incomplete".
132
136
  - **Unchanged-not-recorded**: an entry is written only when something changed — at the message-boundary re-check, an unchanged file is never backed up (no record); at before-write time, when the new content matches the path's prior record, only a **link to it** (`ref`) is stored instead of a copy.
133
- - **Accurate restore**: backups are the sole standard, checked against the real disk — **only files that actually differ are touched**: modified files restored, newly created files deleted, deleted files recovered. Backups are stored byte for byte, so the restored result matches the backups exactly, with no "ghost impact".
137
+ - **Accurate restore**: backups are the sole standard, checked against the real disk — **only files that actually differ are touched**. Backups are stored byte for byte, so the restored result matches the backups exactly, with no "ghost impact". There is a limit, though: [File-rewind tracking boundary](docs/compat/tracking-boundary.md).
134
138
  - **Safety and integrity**: paths are sanitized so nothing ever escapes the backup root; symlinks / hard links are skipped so one restore can't clobber another name of the same file; a per-file failure never aborts the pass; backups and the restore journal are written atomically and kept across restarts, so a half-applied restore after a crash can be continued or rolled back.
135
139
 
136
140
  > **Design highlight**: this checkpoint's light footprint comes from **recording only what was actually touched and really changed** — before-write backup makes it restorable, unchanged-not-recorded and content-as-link drop the repetition; only the files that differ are touched at restore time.
package/README.md CHANGED
@@ -16,7 +16,7 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
16
16
 
17
17
  ## 效果预览
18
18
 
19
- 每条用户消息的操作行都有一个 **↶ 回退** 按钮。点击后弹出模式选择浮层——「**仅回退对话**」或「**回退对话和代码**」,后者会先展示文件变更清单再确认。还可以通过 **`/rewind` 命令**和**键盘操作**便捷地选择和回退。
19
+ 每条用户消息的操作行都有一个 **↶ 回退** 按钮。点击后弹出模式选择浮层——「**仅回退对话**」或「**回退对话和代码**」,后者会先展示文件变更清单再确认。还可以通过 **`/rewind` 和 `/undo` 命令**便捷地选择和回退。
20
20
 
21
21
  <table>
22
22
  <tr>
@@ -29,22 +29,26 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
29
29
  </tr>
30
30
  </table>
31
31
 
32
- ## 安装与更新
32
+ ## 安装
33
33
 
34
34
  确认本机 DSH 版本后,在 [Release](https://github.com/SiriLee/dsh-rewind/releases) 找到适配的插件版本。
35
35
 
36
36
  **命令行**:
37
37
 
38
38
  ```sh
39
- dsh plugin --profile web add dsh-rewind-plugin@<版本>
39
+ dsh plugin --profile <name> add dsh-rewind-plugin@<版本>
40
40
  ```
41
41
 
42
42
  **图形界面**:侧栏「插件」页 →「添加插件」→ 填 `dsh-rewind-plugin@<版本>` → 安装 → 重启 DSH 并刷新浏览器。
43
43
 
44
- > ⚠️ npm 上的 `dsh-rewind` 属于其他作者,请用 `dsh-rewind-plugin` 安装。
44
+ <img src="assets/screenshots/add-plugin.png" width="600" alt="「添加插件」弹窗:在输入框填入 dsh-rewind-plugin@版本号"><br>
45
+ <sub>图片仅为示例,请以实际适配版本号为准。</sub>
45
46
 
46
47
  **更新**:命令行可直接覆盖安装;图形界面会拒绝重复安装,需先卸载旧版本,再装新版本。
47
48
 
49
+ > [!WARNING]
50
+ > npm 上的 `dsh-rewind` 属于其他作者,请用 `dsh-rewind-plugin` 安装。
51
+
48
52
  ## 使用
49
53
 
50
54
  1. 在对话中找到要回退的那条用户消息,或输入 `/rewind`(或其别名 `/undo`)打开候选列表选择。
@@ -75,7 +79,7 @@ dsh plugin --profile web add dsh-rewind-plugin@<版本>
75
79
 
76
80
  ```sh
77
81
  # 卸载插件
78
- dsh plugin --profile web remove dsh-rewind-plugin
82
+ dsh plugin --profile <name> remove dsh-rewind-plugin
79
83
 
80
84
  # 如需同时删除本地数据
81
85
  rm -rf <dsh home>/rewind-snapshots
@@ -114,10 +118,10 @@ rm <dsh home>/snapshot-cleanup-last-sweep.json
114
118
 
115
119
  文件部分对齐 Claude Code 的检查点语义——**局部追踪、写前备份 + 每条消息重扫已跟踪文件**,而不是整树快照。这项取舍既省空间,又更完整:
116
120
 
117
- - **写前备份**:只追踪写类工具(`write`、`edit`),写前**备份原内容**,并**记录、追踪**被处理的文件——从不备份整个工作区,因此轻量。
121
+ - **写前备份**:只追踪写类工具(`write`、`edit`),写前**备份原内容**,并**记录、追踪**被处理的文件——从不备份整个工作区,因此轻量;**单个文件过大时不备份**。
118
122
  - **外部变更也追**:每条用户消息边界,插件重新检查所有已跟踪文件——命令执行、手动修改等外部变更同样被记录,回退时一并还原。这让「轻量」却不「残缺」。
119
123
  - **不变不存**:记录只在有变化时发生——消息边界重扫时无变更的**不备份**(不留记录);写前备份时若与前一条记录一致,只存指向它的链接(`ref`)而非复制内容。
120
- - **还原准确**:以备份为唯一标准,对照真实磁盘,只动真正不一致的文件——被改过的还原、被新建的删除、被删除的恢复;备份逐字节存储。还原结果与备份一致,无“幽灵影响”。
124
+ - **还原准确**:以备份为唯一标准,对照真实磁盘,只动真正不一致的文件;备份逐字节存储。还原结果与备份一致,无“幽灵影响”。但存在限制:[文件回退的追踪边界](docs/compat/tracking-boundary.zh.md)。
121
125
  - **安全与完整性**:路径经安全化处理,绝不越出备份根目录;符号/硬链接跳过,避免透过一次还原误伤同名的另一份文件;单个文件失败绝不中止整轮还原;备份与还原日志均原子写落盘(跨重启仍在),断电或崩溃后的半还原可续做或回滚。
122
126
 
123
127
  > **设计点睛**:这套检查点的「轻」,来自**只记录被工具动过、且确实变化的文件**——写前备份保证可还原,不变不存与存链接压掉重复;还原时再对照真实磁盘,只动不一致的文件。
package/SECURITY.md CHANGED
@@ -175,7 +175,7 @@ transcript.
175
175
  distinct cases follow:
176
176
  1. **Sensitive / personal-information files** (e.g. a `.env` file, which may
177
177
  sit inside the workspace) the model may read or edit are backed up and
178
- restorable.
178
+ restorable, unless the file is over the size cap.
179
179
  2. **Files outside the session workspace** the model is allowed to touch are
180
180
  likewise backed up and restored.
181
181
  In both cases the plugin introduces no authority of its own — DSH already
package/assets/icon.svg CHANGED
@@ -1,7 +1,7 @@
1
- <svg width="32" height="32" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
1
+ <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
2
2
  <!-- Layout follows the terminal card glyph (packages/client/ui-sidebar-terminal):
3
- a rounded tile inset from the canvas (28/36 here, 22/28 there) with a
4
- white round-capped prompt. The fill follows the Subagent plugin artwork
3
+ a rounded tile inset from the canvas with a white round-capped prompt. The
4
+ fill follows the Subagent plugin artwork
5
5
  (packages/client/ui-primitives/src/plugin-artwork.tsx), whose blue square
6
6
  carries the same vertical light-to-dark gradient: #69B9FF at the top,
7
7
  #324DE2 at the bottom.
@@ -10,13 +10,17 @@
10
10
  (1.75,1.75)-(14.25,12.75) on that grid, so the translate lands its centre
11
11
  on the tile centre (18,18), and the ink spans 62.5% of the tile width —
12
12
  the terminal glyph's own ratio.
13
- The drawing is 36 viewBox units but renders at 32 px, i.e. a uniform
14
- shrink inside the card's and row's fixed frames; no inner coordinate
15
- changes, so the mark cannot shift. -->
16
- <rect x="4" y="4" width="28" height="28" rx="8" fill="url(#rewind_blue)" />
17
- <g transform="translate(6.8 7.85) scale(1.4)" stroke="#FFFFFF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
18
- <path d="M6.5 2.5 2.5 6.5l4 4" />
19
- <path d="M2.5 6.5h7a4 4 0 0 1 4 4v1.5" />
13
+ The canvas stays 36x36, the size the harness and the official artwork use,
14
+ so the consumer's `size` attribute keeps the authority over display size;
15
+ the inner group shrinks the finished drawing to 26/28 of the canvas around
16
+ its centre — one uniform scale over coordinates that are already correct,
17
+ so the tile, its radius, the mark and the stroke ratio stay in proportion. -->
18
+ <g transform="translate(18 18) scale(0.9286) translate(-18 -18)">
19
+ <rect x="4" y="4" width="28" height="28" rx="8" fill="url(#rewind_blue)" />
20
+ <g transform="translate(6.8 7.85) scale(1.4)" stroke="#FFFFFF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
21
+ <path d="M6.5 2.5 2.5 6.5l4 4" />
22
+ <path d="M2.5 6.5h7a4 4 0 0 1 4 4v1.5" />
23
+ </g>
20
24
  </g>
21
25
  <defs>
22
26
  <linearGradient id="rewind_blue" x1="18" y1="4" x2="18" y2="32" gradientUnits="userSpaceOnUse">
@@ -1,31 +1,24 @@
1
1
  # Compatibility audit (compat-audit)
2
2
 
3
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
4
+ > `tests/compat-interop.test.ts`, `tests/compat-gaps.test.ts`,
5
+ > `tests/compat-tool-updates.test.ts` and
5
6
  > `scripts/verify-host.mjs` drive the plugin's real execution paths through the DSH
6
7
  > subsystems' **real consumer paths** (real `@deepseek-ai/*` packages) and assert
7
8
  > compatibility invariants. A probe failure is a finding; it enters the
8
9
  > fix/pin/record loop.
9
10
  >
10
- > Targeted version: npm `@deepseek-ai/*@0.1.7-rc.1`, declared by the peers as `^0.1.7-rc.1`.
11
+ > Targeted version: npm `@deepseek-ai/*@0.1.7-rc.2`, declared by the peers as `^0.1.7-rc.2`.
11
12
  > Source reference: the upstream [github.com/deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness).
12
13
  >
13
- > Version alignment: `peerDependencies` use one tuple per DSH line
14
- > (e.g. `^0.1.2-rc.1`). npm's prerelease matching rules require a candidate to
15
- > share the range comparator's `[major, minor, patch]` tuple, so a new DSH tuple
16
- > replaces the peer tuple (single-line model). The range is conservative — it
17
- > declares only what was verified, so a release already inside it changes
18
- > nothing — unless it changes interfaces inside the tuple: `0.1.6-alpha.2`
19
- > removed the client session-ownership and queue-mirror APIs, and
20
- > `0.1.7-alpha.1` moved the session writer to format v4 (producer-owned message
21
- > sources, tool-role results, `developer/message`) and replaced the settings
22
- > namespace registry with each entry's volatile `Config`, so the peer floor and
23
- > both plugin seams moved with them. Signal: `npm view @deepseek-ai/dsh dist-tags`;
24
- > flow: `scripts/check-dsh-version.mjs` (it reads the `latest` dist-tag only; a
25
- > prerelease published under another tag is a manual pre-release check).
26
- >
27
- > The plugin targets a single DSH version line; compatibility with earlier
28
- > lines is not kept.
14
+ > Version alignment: one peer tuple per DSH line, declaring only what was verified — the
15
+ > model, the update steps, and the `check-dsh-version.mjs` signal live in
16
+ > `docs/release/release.md`. The floor moved when a release changed interfaces inside the
17
+ > tuple: `0.1.6-alpha.2` removed the client session-ownership and queue-mirror APIs, and
18
+ > `0.1.7-alpha.1` moved the session writer to format v4 (producer-owned message sources,
19
+ > tool-role results, `developer/message`) and replaced the settings namespace registry with
20
+ > each entry's volatile `Config`, so the peer floor and both plugin seams moved with them.
21
+ > The plugin targets a single DSH version line; compatibility with earlier lines is not kept.
29
22
 
30
23
  ### Single channel
31
24
 
@@ -53,29 +46,25 @@ legacy branch).
53
46
  | I6 tool pipeline | before-snapshot capture/commit/restore is correct (existing `snapshot.test.ts` + `verify-host` 4–8) for the tracked tools `write` / `edit` — `str_replace_editor` is an optional DSH package that stopped being a default tool in DSH 0.1.3, so it is not tracked; cancellation timing never hangs | `verify-host` 4–9 |
54
47
  | I7 client ordering | A log carrying tool turns and rewind markers (a single `user/message` replace) satisfies the client builder ordering | `compat-interop` I7 |
55
48
  | I8 runtime safety | `rewind`/`compact` combinations never leave a dangling step/turn frame | `verify-host` 13 |
49
+ | I9 tool-update projection | rc.2 folds mid-conversation tool additions from the whole log while a rewind cuts only the surface: after a withdrawal no `tool-addition`/`tool-removal` block reaches the provider and the current declarations stay complete | `compat-tool-updates` I9 |
56
50
 
57
51
  ## Verified-compatible surfaces (probes pass)
58
52
 
59
- - **token-meter replay** (the `user/message` marker + multiple rewinds + interleaved real turns + compact stacking).
60
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.
61
- - **resume replay**: `Session.create(id, events)` replays a rewind/compact-bearing log.
54
+ - **tool-update history (`0.1.7-rc.2`)**: `Session.toolHistory()` folds the full append-only log, so a withdrawn `tool-addition` stays recorded after a rewind; `projectToolUpdates` drops every update whose message left the request and falls back to the complete current declarations. Pin: `tests/compat-tool-updates.test.ts` (on-surface projection, then the post-rewind fallback).
62
55
  - **session-stats**: the `user/message` marker adds no step (the step count stays at the real turns' steps), no phantom turn.
63
56
  - **session-title / goal fold**: a marker does not disturb `foldSessionTitle` / `foldGoal`.
64
- - **client ordering**: turn-tail ordering + `step/start` uniqueness hold for tool turns + marker logs.
65
- - **rewind across a compact checkpoint**: `RewindError('not-on-surface')` refuses cleanly, no crash.
66
57
  - **plan-mode**: the marker is a turn-less `user/message` (no phantom turn); a rewind never touches the log-only `plan/mode` state (plan mode stays active; the user leaves it with `/plan off`) and the log stays replayable (`compat-invariants` I1/I3 marker + `plan/mode` probe, `verify-host`).
67
58
  - **agent-loop cancellation**: `finally` guarantees step/turn closure; the rewind force-stop path leaves no dangling frame.
68
- - **settings-card registration (`0.1.7-alpha.1`)**: `settingsScope` is gone with the namespace registry. The card declares `configForms` as a module-level inject (the web profile always composes `ui-settings`), stages through the harness's own `SettingsFormModel` + `SettingsForm`, and registers into `plugins.bundle.config` keyed by the bundle package name; it reads the entry's form snapshot and writes through its revision-fenced `mutate`, and disposes the model's subscription with the fiber.
59
+ - **settings card (`0.1.7-alpha.1`)**: the card declares `configForms` as a module-level inject (the web profile always composes `ui-settings`), stages through the harness's own `SettingsFormModel` + `SettingsForm`, and registers into `plugins.bundle.config` keyed by the bundle package name; it reads that shared per-entry form's snapshot and writes through its revision-fenced `mutate`, disposing the model's subscription with the fiber. The host half declares the bundle's page policy (`configure({ auto: false }, ctx.fiber)` inside a `ctx.inject(['settings'])` child, per the dsh-settings README), opting the entry out of a client-built schema page. Pins: `verify:host` (`the bundle declares its own config page`), `tests/client-settings-card.test.ts` (staged plan, refusal, subscription release), `tests/client-lifecycle.test.ts`.
69
60
  - **client session ownership (`0.1.6-alpha.2`)**: `SessionListState.current` / `currentAddress` are gone; main-view ownership is read from the row's `retainedBy.mainView` label — what `ui-workspace` retains the open session with. Pin: `tests/client-refill.test.ts`.
70
- - **client pending input (`0.1.6-alpha.2`)**: `SessionSnapshot.queue` and the host `queue-mirror` are gone; pending steering rows come from the session's own `inbox` projection, and only USER-sourced `next-step` rows are retractable (the removed mapping was `source.kind === 'user' ? 'steering' : 'context'`). Pins: `tests/pending.test.ts`, `tests/client-retract.test.ts`.
61
+ - **client pending input (`0.1.6-alpha.2`)**: `SessionSnapshot.queue` and the host `queue-mirror` are gone; pending steering rows come from the session's own `inbox` projection, and only USER-sourced `next-step` rows are retractable (the removed mapping was `source.kind === 'user' ? 'steering' : 'context'`). The rendered bubble also stays the client's own submission echo until the durable node arrives (`ChatView.pendingInputs` merges the Host occurrence into that echo by `rpcId`; `QueueDock` mirrors only `next-turn`), so the occurrence lands with NO DOM mutation — the portal refresh therefore follows the `inbox` face's own `subscribe` beside the DOM observer. Pins: `tests/pending.test.ts`, `tests/client-retract.test.ts`, `tests/client-portal-refresh.test.ts`.
71
62
  - **client settings slot (`0.1.6-alpha.2`)**: `settings.plugin.item` is gone; the configuration form registers into `plugins.bundle.config`, keyed by the bundle package name. Pin: `tests/client-contract.test.ts`.
72
- - **client settings form (`0.1.7-alpha.1`)**: the same release retired the `settingsScope` bind (namespace registry) in favour of the shared per-entry form; the card now reads that form's snapshot and stages edits through `mutate`. Pin: `tests/client-settings-card.test.ts` (staged plan, refusal, and the fiber releasing the model's subscription), `tests/client-lifecycle.test.ts`.
73
-
74
63
  - **`agent/created` lifecycle guard**: the session-format reconcile runs on the alpha line's `agent/created` (fire-and-forget); pin: `verify-host` 4e dispatches it.
75
64
  - **marker vs `/compact`**: the checkpoint shape is unchanged on this line — a `user/message` replace (`surfaceOp {replace, startSeq, endSeq}` + `sourceEventSeqs`); `assistant/message` still cannot carry `sourceEventSeqs`.
76
65
  - **`image/offload` projection**: the alpha line's first `SessionMessageProjection` changes derived content without touching surface node membership; candidate listing and target resolution tolerate it. Pin: `tests/image-offload-projection.test.ts`.
77
66
  - **session-cwd**: the fs tools no longer canonicalize a parent-traversing cwd, so `src/session-cwd.ts` returns `header.cwd` verbatim. Pin: `tests/session-cwd.test.ts`.
78
- - **startup admission (`0.1.7-rc.1`)**: the new gate reads the manifest's own `@deepseek-ai/dsh-*` peers and requires each to satisfy the runtime with `includePrerelease`; it runs for a profile bundle and again for every row its patch inserts, and a denial skips the bundle or disables the row. This plugin's declarations all satisfy `0.1.7-rc.1`, so it is admitted with no exemption — a skipped bundle on a later line is a peer mismatch, not a load failure.
67
+ - **startup admission (`0.1.7-rc.1`)**: the gate reads the manifest's own `@deepseek-ai/dsh-*` peers and requires each to satisfy the runtime with `includePrerelease`; it runs for a profile bundle and again for every row its patch inserts, and a denial skips the bundle or disables the row. This plugin's declarations all satisfy `0.1.7-rc.2`, so it is admitted with no exemption — a skipped bundle on a later line is a peer mismatch, not a load failure.
79
68
 
80
69
  ## Known behavior boundaries (deterministic differences, non-crash, documented)
81
70
 
@@ -89,107 +78,89 @@ legacy branch).
89
78
  - **Attachment files left after a message is shadowed**: attachment storage is not cleaned with the surface (`dsh-attachment-local` not installed, not automatically verified).
90
79
  - **Rewind leaves plan mode untouched**: `/plan text` is two independent actions (enter plan mode + steer the message). Rewinding the message undoes only the message — the log-only `plan/mode` state stays active, and the user leaves plan mode with `/plan off`, which still commits after a rewind (the marker creates no open turn). Pin: `verify-host` plan checks (`plan rewind leaves plan mode active`, `/plan off after rewind turns plan mode off`), `tests/hidden.test.ts` `messageTextAt`.
91
80
  - **Bundle card metadata is declared by the plugin, not by the manager (`0.1.7-alpha.1`)**: the manager still titles a bundle with `shortName(pkg.name)` and its raw `package.json` description when nothing else is declared, but it now reads plugin-owned resources first — `locale/<lang>.json` (`meta.title`/`meta.description`) through the package exports map, and an `icon` file relative to the manifest. This plugin declares both, so its card shows an icon and copy that follows the active language; only the fallback description remains mixed-language. Pin: `tests/package-layout.test.ts` (icon path/media/size and both dictionaries).
92
-
93
- - **Synchronous Session history reads are deprecated** on this line (`snapshotEvents` / `eventAt` / `ownEvents`): existing calls may remain, new calls are prohibited. The plugin keeps its existing reads (turn anchor + candidate listing); the migration path is a `ctx.sessionProjections` projection unit or the async paged history read this line has not shipped yet — not a plugin-side index. Not migrated in this release.
81
+ - **Synchronous Session history reads are deprecated** on this line (`snapshotEvents` / `eventAt` / `ownEvents`): existing calls may remain, new calls are prohibited. The plugin keeps its existing reads (turn anchor + candidate listing); the migration path is a `ctx.sessionProjections` projection unit or the async paged history read this line has not shipped yet — not a plugin-side index.
82
+ - **A peer version range is never enforced at runtime**, and a physical copy nearer than the host still wins (upstream profile-resolution lookup order: "a peer version range is not an additional resolver filter"; "a nearer physical candidate wins"). So the `@deepseek-ai/schemastery` peer floor *is* the availability guarantee for `.volatile()` (#40): a plugin directory or linked checkout left holding an older copy (e.g. the pre-3.18.2 devDependency) shadows the host's 3.18.4 and fails the top-level `Config` evaluation before the plugin loads. Floor pinned at `^3.18.4`.
94
83
 
95
84
  ## Upstream (harness) issues and the plugin's no-compensation stance
96
85
 
97
- 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."
86
+ The plugin treats these as harness-side defects it does not compensate for. Each entry records
87
+ the harness issue, its current status, and the plugin's stance, so a future maintainer does not
88
+ "fix the wrong direction."
98
89
 
99
90
  ### RU-I18N (resolved on the 0.1.7 line): host command copy follows the user's language
100
91
 
101
- - **History (0.1.2–0.1.6 lines)**: the plugin resolved `activeLocale` in an
102
- `ctx.inject(['settings'])` callback that read `settings.get('locale').preference`
103
- **once, with no retry** (`src/index.ts`), while `dsh-client-locale`'s host half registered that
104
- `locale` settings section through its own `ctx.inject(['settings'])`
105
- (`packages/client/locale/src/index.ts`). Both callbacks wait only on `settings` and are
106
- independent of each other, so cordis made **no ordering guarantee** between the section
107
- registration and the plugin's read. The plugin's own command descriptors were registered
108
- **synchronously** in the same `apply`, i.e. before that microtask ran, so their `description`
109
- strings were baked in the neutral English default — the defect reported as #28 — while the
110
- runtime output, evaluated later at command execution from the then-updated `activeLocale`, did
111
- follow the stored preference. A preference changed *after* the mount reached the output only on
112
- the next mount, because the read was one-shot.
113
- - **Resolution (0.1.7)**: the section is gone — the locale host half now only calls
114
- `settings.configure({ auto: false })`, and the preference lives in the `locale` entry's own
115
- Config. The plugin reads that same value through the settings service's descriptor read
116
- (`readHostLocale` in `src/index.ts` calls `settings.describe({ redactSecrets: true })`, the route
117
- the settings UI and the client's own locale runtime take): at mount, which is what the
118
- once-registered command descriptions carry, and again before each command renders, so the output
119
- follows a language switch without a remount (the descriptors keep the mount-time language until
120
- the plugin remounts). The read is best-effort: a missing method, a missing entry, or a
121
- throwing read stays on the neutral English default.
122
- - **Client-side command-description i18n is still first-party-only**: DSH localizes host command
123
- descriptions through the client `locale` binding (`ui-commands`), but the description keys come
124
- from a **closed allowlist** (`HOST_FACES`: compact, export, feedback, goal,
125
- permission, plan). A command outside that set — every third-party plugin — is passed through
126
- verbatim, never translated (`builtinRowFace` only rewrites a description that equals the
127
- first-party English copy), and no host command *output* is localized by the harness for anyone.
128
- This plugin's host dictionary is what carries both, in the language read above.
129
-
130
- ### R-OPENSTEP (rewind part resolved): an unclosed `step` in the log breaks token-meter replay; the rewind no longer compounds it
131
-
132
- > **Root cause (harness-side)**: an unclosed step left by a crash makes token-meter replay
133
- > reject any later step activity. DSH `0.1.1-rc.2` now auto-closes crash-left step/turn/tool
134
- > boundaries at load via `interruptedTurnClosers` (`dsh-session`, consumed by
135
- > `packages/core/agent-loop/src/index.ts:907`) — **the crash path is fixed**.
92
+ > **Status**: the `locale` settings section is gone — the locale host half only calls
93
+ > `settings.configure({ auto: false })`, and the preference lives in the `locale` entry's own
94
+ > Config. The plugin reads that value through the settings service's descriptor read
95
+ > (`readHostLocale` in `src/index.ts` calls `settings.describe({ redactSecrets: true })`, the
96
+ > route the settings UI and the client's own locale runtime take) at mount — which is what the
97
+ > once-registered command descriptions carry — and again before each command renders, so the
98
+ > output follows a language switch without a remount. The read is best-effort: a missing
99
+ > method, a missing entry, or a throwing read stays on the neutral English default.
100
+ >
101
+ > **Client-side command-description i18n is still first-party-only**: DSH localizes host
102
+ > command descriptions through the client `locale` binding (`ui-commands`), but the
103
+ > description keys come from a **closed allowlist** (`HOST_FACES`: compact, export, feedback,
104
+ > goal, permission, plan). A command outside that set — every third-party plugin — is passed
105
+ > through verbatim, and no host command *output* is localized by the harness for anyone. This
106
+ > plugin's host dictionary is what carries both, in the language read above.
136
107
  >
137
- > **Plugin guard (attempted and reverted)**: a `hasOpenStep` + `planRewind` pre-refusal was
138
- > implemented (`open-step`) but misjudged on **real session logs** (normal rewinds refused, GUI
139
- > verification broken), and was reverted (`177ec14`). Conclusion: **the plugin sets no guard**,
140
- > accepting residual risk (an unclosed step produced by a third-party plugin can break
141
- > `/compact` — that log is already abnormal, and continuing the conversation triggers the same).
142
- > The fix direction is in the harness (token-meter recovery for unclosed steps), not the plugin.
143
-
144
- #### Concrete `step/start` trigger paths (source-confirmed)
145
-
146
- The tree has exactly **one** `append('step/start')` producer: `packages/core/agent-loop/src/agent.ts:303`
147
- (no other producer inside the official packages; `session/end-seed` etc. only truncate torn writes,
148
- not logically-unclosed steps).
149
-
150
- | # | Trigger path | Plausibility | Basis |
151
- |---|---|---|---|
152
- | 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:313` finally; write-behind batching; torn-write fix truncates only a half-written line |
153
- | 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` |
154
- | 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'`) |
155
- | P4 | **append itself failing**: `append('step/end')` in `finally` throws (payload is plain numbers, nearly impossible) | theoretical | `agent.ts:313` |
156
-
157
- **Amplifier (rewind is not the only trigger)**: after a crash resume, agent-loop `turn()` opens a
158
- new turn at `phase.turn + 1` (`agent.ts:277-283`) **without closing the leftover step** — so
159
- "continue the conversation" (a new `step/start`) trips the same token-meter check. Scope:
160
-
161
- - **The conversation itself is unaffected** (the request path does not call `tokenMeter.measure`; only compaction-basic does tree-wide).
162
- - **Manual `/compact` fails permanently** (`compactNow`'s first `measure()` throws the raw error).
163
- - **Automatic compaction silently stays disabled** (the `agent/pre-step` hook catches and warns; the conversation continues).
164
- - **rewind's role (resolved)**: the v2 `user/message` rewind marker appends no `step/start`, so a rewind no longer introduces the first trip-wire and the R-OPENSTEP rewind amplifier is closed. The harness-side root cause (an unclosed step after a crash, tripped by continuing the conversation) remains.
108
+ > **Plugin stance**: no compensation; the seam is the descriptor read above. The wrong direction
109
+ > is the earlier one — reading `settings.get('locale').preference` once inside an
110
+ > `ctx.inject(['settings'])` callback and assuming cordis ordered the locale section before it
111
+ > (it does not; the descriptors were baked in the English default, defect #28).
112
+
113
+ ### R-OPENSTEP (rewind part resolved): an unclosed `step` in the log breaks token-meter replay
114
+
115
+ > **Situation**: once a step is left unclosed, replay rejects any later step activity, so
116
+ > manual `/compact` fails permanently and automatic compaction silently stays disabled (the
117
+ > `agent/pre-step` hook catches and warns). The conversation itself is unaffected — only
118
+ > compaction-basic measures tree-wide. DSH now auto-closes crash-left step/turn/tool
119
+ > boundaries at load (`interruptedTurnClosers`, consumed by
120
+ > `packages/core/agent-loop/src/index.ts:907`), so **the crash path is fixed**. The tree has
121
+ > exactly one `append('step/start')` producer, `agent-loop/src/agent.ts:303`; an unclosed step
122
+ > otherwise comes from a third-party plugin appending `step/start` through the public
123
+ > `Session.append`, or from a hand-edited log. After a crash resume, `turn()` opens a new turn
124
+ > (`agent.ts:277-283`) without closing the leftover step, so continuing the conversation trips
125
+ > the same check — a rewind is **not** the only trigger.
126
+ >
127
+ > **Rewind's part**: the marker is a `user/message` and appends no `step/start`, so a rewind
128
+ > no longer introduces the first trip-wire and the R-OPENSTEP rewind amplifier is closed.
129
+ >
130
+ > **Plugin stance**: no guard. A `hasOpenStep` + `planRewind` pre-refusal was implemented and
131
+ > misjudged on **real session logs** (normal rewinds refused, GUI verification broken), so it
132
+ > was reverted — do not re-attempt a plugin-side pre-refusal. The fix direction is upstream
133
+ > (token-meter recovery for unclosed steps), and the residual risk is accepted: that log is
134
+ > already abnormal.
165
135
 
166
136
  ### R-PROMPTCARD (resolved on the 0.1.7 line): the repeated `System prompt` card is filtered out of chat
167
137
 
168
- > **History (0.1.2–0.1.6 lines)**: Chat rendered a collapsed `System prompt`
169
- > card at every request-series start. Any surface mutation bumps `contentGeneration` (a positional
170
- > `replace` — rewind, `/compact` — or a `SessionMessageProjection`, e.g. image offload), so the next
171
- > request logs `request/header { reason: 'series' }`; a process restart logs `'resume'`. The client
172
- > shows the card for every reason except `'change'`. The effective prompt is unchanged — only the
173
- > card repeats. Triggers beyond rewind: `/compact`, restart/resume, projections, explicit
174
- > `startsSeries`.
175
- >
176
- > **Upstream decision (deliberate, twice)**: `6ad01cbd8d` suppressed the repeat on `resume` (note
177
- > `2026-09-03-resume-headers-do-not-repeat-system-prompts.md`, rejecting the card as a "lifecycle
178
- > marker" that "incorrectly implies another prompt injection"); three days later `77b6b10c21`
179
- > restored it — a resume/series header starts a visible request series. Locked by the `ui-chat` test
180
- > `repeats an unchanged system prompt after a surface rewrite…`.
138
+ > **Status**: `isVisibleChatNode`
139
+ > (`packages/client/ui-chat/src/client/contract/chat-visibility.ts`) drops `system-prompt`
140
+ > nodes — and the `permission` command — from the visible chat rows before the row is built,
141
+ > so a rewind on this line adds no prompt row at all; the durable events and the trajectory
142
+ > inspection keep them. #22, #31 and README known issue #4 no longer reproduce.
181
143
  >
182
- > **Resolution (0.1.7)**: upstream stopped rendering the card in chat at all. `isVisibleChatNode`
183
- > (`packages/client/ui-chat/src/client/contract/chat-visibility.ts`, added by `b6f99fad55`
184
- > "feat(ui-chat): filter system prompts and permission commands") drops `system-prompt` nodes — and
185
- > the `permission` command — from the visible chat rows, while the durable events and the trajectory
186
- > inspection keep them. The `showsPrompt` computation is unchanged and no longer matters: the node is
187
- > filtered before the row is built, so a rewind on this line adds no prompt row at all. #22, #31 and
188
- > README known issue #4 no longer reproduce.
144
+ > **Plugin stance**: still no compensation. The DOM row-hiding seam (`hiddenSeqsOf`) covers
145
+ > withdrawn messages, where hiding is essential, not cosmetic.
146
+
147
+ ### R-PANELKEYS: the panel keyboard stays the plugin's own
148
+
149
+ > **Situation**: on Web, Escape, Tab, the four arrows, and Enter without Alt are
150
+ > platform-reserved — inline in the shortcuts `bindingIssue` list, with no exported form — so
151
+ > neither a plugin nor a user may bind them and there is nothing to read and follow. `Menu`
152
+ > is the dropdown primitive, while the harness's command-option panel (`ui-commands`'
153
+ > popupSelect) renders `MenuSurface` plus its own controller with `role="listbox"`/
154
+ > `role="option"`. `ui-commands`' `settle()` also calls `focusComposer()` unconditionally
155
+ > after `await onSelect(...)`; its only early return requires replacing the popup binding,
156
+ > which needs the internal `segment` that business code cannot reach.
189
157
  >
190
- > **Plugin stance**: still no compensation. The DOM row-hiding seam (`hiddenSeqsOf`) covers withdrawn
191
- > messages, where hiding is essential, not cosmetic; nothing was added for the card, and the
192
- > display-layer workup above stays valid history.
158
+ > **Decision**: the mode-selection and retract panels keep their own capture-phase
159
+ > Escape/↑/↓ handling — focus-independent, the shape the official command panel uses — and
160
+ > keep `role="dialog"`. The plugin neither renders `Menu` nor compensates the composer focus
161
+ > restore. Capture ownership is pinned by `tests/client-popover.test.ts`; the ghost-row walk
162
+ > exclusion is **not** pinned, and on the typed `/rewind` path Enter still needs a row to hold
163
+ > focus after the shell's focus restore.
193
164
 
194
165
  ## Uncovered boundaries (need an additional e2e layer; non-blocking)
195
166
 
@@ -216,8 +187,6 @@ new turn at `phase.turn + 1` (`agent.ts:277-283`) **without closing the leftover
216
187
  | client ordering | — | — | ✓ | — | — | — | ✓ | — |
217
188
  | plan-mode | ✓ | — | ✓ | — | — | — | — | ✓ (static) |
218
189
 
219
- ✓ = probe passes; — = not applicable. RU-I18N and R-PROMPTCARD are resolved on the 0.1.7 line (see
220
- above); R-OPENSTEP is an upstream issue the
221
- plugin does not compensate for (see above); G3 is a confirmed-non-defect behavior pinned in
222
- `compat-gaps.test.ts` (G1 surface classification via `foldSurface` and G2 projection checkpoint
223
- both pass).
190
+ ✓ = probe passes; — = not applicable. Statuses and stances for the named findings live in the
191
+ sections above; G1 (surface classification via `foldSurface`), G2 (projection checkpoint) and
192
+ G3 (the confirmed-non-defect usage anchor on token-meter) all pass in `compat-gaps.test.ts`.