dsh-rewind-plugin 0.4.0 → 0.4.2

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 ADDED
@@ -0,0 +1,176 @@
1
+ # dsh-rewind
2
+
3
+ Conversation rewind for DeepSeek Harness: **rewind the conversation to any earlier user message in one click, in the same window** — no new branch, no window switch, with optional workspace-file restore (full Claude Code `/rewind` semantics).
4
+
5
+ [![npm version](https://img.shields.io/npm/v/dsh-rewind-plugin.svg)](https://www.npmjs.com/package/dsh-rewind-plugin)
6
+ [![npm license](https://img.shields.io/npm/l/dsh-rewind-plugin.svg)](https://github.com/SiriLee/dsh-rewind/blob/main/LICENSE)
7
+ [![npm downloads](https://img.shields.io/npm/dm/dsh-rewind-plugin.svg)](https://www.npmjs.com/package/dsh-rewind-plugin)
8
+
9
+ > English | [中文](README.md)
10
+
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
+
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.
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.
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
+
18
+ ## Preview
19
+
20
+ 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**.
21
+
22
+ <table>
23
+ <tr>
24
+ <td align="center"><img src="assets/screenshots/rewind-button.png" width="440" alt="Per-message ↶ rewind button"><br><sub>Per-message ↶ rewind button</sub></td>
25
+ <td align="center"><img src="assets/screenshots/mode-popover.png" width="440" alt="Mode-selection popover"><br><sub>Mode-selection popover</sub></td>
26
+ </tr>
27
+ <tr>
28
+ <td align="center"><img src="assets/screenshots/impact-list.png" width="440" alt="Impact list"><br><sub>"Conversation and code" impact list</sub></td>
29
+ <td align="center"><img src="assets/screenshots/rewind-candidates.png" width="440" alt="/rewind candidate picker"><br><sub>/rewind candidate picker</sub></td>
30
+ </tr>
31
+ </table>
32
+
33
+ ## Install
34
+
35
+ ```sh
36
+ dsh plugin --profile web add dsh-rewind-plugin
37
+ ```
38
+
39
+ > ⚠️ The npm name `dsh-rewind` belongs to another author's package — install with `dsh-rewind-plugin`.
40
+
41
+ ## Usage
42
+
43
+ 1. Find the user message you want to rewind to in the conversation, or type `/rewind` to open the candidate picker.
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
+ 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
+
47
+ **Keyboard**: both the candidate picker and the mode popover support ↑↓ to move, Enter to confirm, Esc to cancel/back.
48
+
49
+ <details>
50
+ <summary><b>Edge notes</b></summary>
51
+
52
+ - Rewinds can be repeated — with no limit on stage or count.
53
+ - A rewind itself **cannot be undone**, but the withdrawn content stays in the session log and can be recovered by manually editing it.
54
+ - **Interruptions rewind too** — a `steering` interruption message the model hasn't read yet is also a valid rewind target.
55
+ - **A rewind interrupts the running turn** — to execute the rewind safely.
56
+
57
+ </details>
58
+
59
+ ## Why it stands out
60
+
61
+ Compared with the common approaches, here is the trade-off this plugin makes on "rewind":
62
+
63
+ | Dimension | Common approach | This plugin |
64
+ | --- | --- | --- |
65
+ | Conversation rewind | Fork / branch a new conversation | **In-place rewind** — no new session, no window switch |
66
+ | File restore | No restore feature / git-managed or whole-tree snapshot | **Lightweight before-backups** — auto-captured before writes, one-click restore (aligned with Claude Code) |
67
+ | Dependencies | Often needs a Git repo or a full snapshot engine | **None** — no git required, works on any directory |
68
+ | Storage footprint | Whole-tree snapshots take space | **Lightweight** — only files touched by write tools are stored, persisted on disk |
69
+
70
+ ## How it works
71
+
72
+ The mechanism is the same lineage as Claude Code's checkpointing (Claude Code's file history is also per-file records plus a re-scan of tracked files at each message — not a whole-tree snapshot). This plugin implements the same semantics on dsh:
73
+
74
+ ### 1. Conversation rewind: in place, without losing the log
75
+
76
+ The session log is **append-only** — the plugin never rewrites history. A rewind appends an **empty-content marker** that "shadows out" everything after the target message, so both the model and the UI only see the part before it:
77
+
78
+ - The marker is **empty** — it never enters the model context and never renders as conversation content; what you and the agent see is exactly how the conversation looked at the target;
79
+ - Because this is "shadowing" rather than "deleting", **every withdrawn event stays in the log**, fully auditable;
80
+ - The marker reuses the number of the last-started turn and carries its own step frame — so dsh's own machinery (log replay, `/compact`, resume preflight) recognizes it correctly and never mistakes it for a real message (these compatibility details are pinned by dedicated probe tests, see [Development](#development)).
81
+
82
+ <details>
83
+ <summary><b>Implementation details (for maintainers)</b></summary>
84
+
85
+ The plugin appends an **empty-content marker** `assistant/message` into the session log whose `surfaceOp: { op: 'replace', start, end }` replaces every surface node after the target message with the marker:
86
+
87
+ - The marker carries `sourceEventSeqs` covering every shadowed node, and `Session.append`'s surface rules validate the cut (a contiguous range on the current surface).
88
+ - Because the marker is **empty**, the harness derives it to `null` — it never enters the model context and never renders as conversation content.
89
+ - The marker's **turn number reuses the last started turn** (`markerTurnOf`), never `lastTurn + 1`: the harness numbers its next real turn exactly `last turn/start + 1`, so a `maxTurn + 1` marker would sit *before* that `turn/start` — the client conversation builder rejects the ordering (`…turn-tail… received an update before its start Match`) and history load fails. Reusing an already-consumed turn makes the marker a harmless trailing update on the previous completed turn's tail — it can never collide with a future turn.
90
+ - The marker rides a **ghost step frame** — its own `step/start` … `step/end` with a fresh step number (`markerStepOf`) — because the harness token-meter requires every `assistant/message` to sit inside an open step of the same turn/step; a bare idle-time marker would fail its replay and break `/compact` for the session.
91
+
92
+ </details>
93
+
94
+ A running turn (LLM thinking / streaming) is force-stopped first and the rewind waits for quiescence; if it can't stop, the rewind is aborted with an error.
95
+
96
+ ### 2. File restore: before-backup + external-change tracking + disk reconciliation
97
+
98
+ The plugin tracks the write-class tools — `write`, `edit`, `str_replace_editor`:
99
+
100
+ 1. **Before-backup**: the original content of every file is stored **before** it is written (captured after any approval gate lets the call through — an approval short-circuit cannot skip the backup, and a denied call never records; a read failure only warns, never blocks the write). Backups are grouped by conversation turn and **persist across restarts** (100 most recent groups per session).
101
+ 2. **External changes are tracked too**: at every user-message boundary the plugin re-checks all tracked files — edits or deletions made outside the write tools are recorded as well and restored by a later rewind.
102
+ 3. **Real disk reconciliation before restoring**: at rewind time the plugin reads each file's current content and compares it with the target state — **only files that actually differ are touched**: modified files are written back to their earliest backup, files created after the target are deleted, files already matching are skipped (repeated rewinds have zero side effects). Symlinks / hard links are skipped to avoid collateral damage.
103
+
104
+ <details>
105
+ <summary><b>Implementation details (for maintainers)</b></summary>
106
+
107
+ 1. **Before-capture** at `tools/execute` (the around-dispatch stage): the target file is read; the resolved path + content are held in a pending map. This stage runs only after any pre-execute approval gate let the call through — an `ask` short-circuit (dsh-edit-approval) **cannot skip** the backup, and a denied call never records. If the read fails (e.g. a permission error), the change is simply not backed up — the plugin warns in the log but **does not block the write**.
108
+ 2. **Disk commit** at `tools/post-execute`: the before-backup is written under the turn's anchor message seq (`~/.dsh/rewind-snapshots/<session>/<anchor seq>/<callId>.json`).
109
+ 3. **External-change tracking** (`reconcileTracked`): at each message boundary the plugin re-scans tracked files and records a `recheck-<anchor>-<hash>` entry anchored to the boundary message whenever the on-disk state differs from the last seen state; the first sighting of a path always records (the first boundary after a restart unconditionally records the current state — redundant but correct, mirroring Claude Code's resume-then-re-stat behavior).
110
+ 4. **Restore** (`/rewind @<seq> both`): `planRestore` probes the disk per record — `before === null` (the file did not exist at the target) plans a delete only when the file currently exists (an absent file already matches); `before === 'X'` plans a restore only when the current content differs from X (identical content is a no-op, idempotent); a probe failure is treated conservatively as differing (never silently skipped). Execution: restore = create parent dir + write back content; delete = remove the file (an already-absent file is tolerated as a no-op); symlinks / hard links are skipped (they share an inode with another name; restoring through one would clobber both); failures are recorded per file and never abort the pass.
111
+ 5. A tool body that **throws** skips `tools/post-execute`; a `tools/result` safety net clears the pending capture so nothing leaks in memory. Backups persist across host restarts; `prune` keeps the newest 100 anchor groups per session.
112
+
113
+ </details>
114
+
115
+ ## What it deliberately does NOT do
116
+
117
+ This plugin deliberately stays lightweight and focused on one thing — "conversation rewind". The following are **out of its scope**:
118
+
119
+ - **Whole-tree / Git-level snapshots** — only write-class tool edits plus external changes to already-tracked files are backed up; files never touched by a tool are not restored. For whole-worktree snapshot rollback, use a dedicated snapshot tool (or your git).
120
+ - **Subagent edits** — not tracked (same as Claude Code): a subagent runs its own session, so its backups could never be restored by a rewind of the parent session.
121
+ - **Fork / branch rewind** — the harness already provides this ("branch in new chat"); no need to reinvent it.
122
+
123
+ ## Compatibility
124
+
125
+ - Node.js `^22.19.0 || >=24.0.0`.
126
+ - DeepSeek Harness web profile (`dsh --profile web`); peer `@deepseek-ai/*` packages are resolved by the harness at runtime.
127
+
128
+ > [!WARNING]
129
+ > This project and DeepSeek Harness are both in developer preview. Pin exact
130
+ > versions in reproducible environments and review the behavior notes above.
131
+
132
+ ## Client contract
133
+
134
+ Third-party DOM plugins that need to know which transcript rows a rewind
135
+ withdrew should consume the stable, locale-independent helpers exported from
136
+ `dsh-rewind-plugin/client` — never parse
137
+ `outcome.text`. The `data-dsh-rewind-hidden` attribute marks withdrawn rows
138
+ (observational only). Details: [docs/client-contract.md](docs/client-contract.md).
139
+
140
+ ## Known issues
141
+
142
+ 1. **Exported logs are complete** — a rewind only removes messages from the model context and the view; the exported session log (`/export`) still contains **withdrawn messages**. This plugin cannot alter exports.
143
+ 2. **Rewinds from `≤ v0.2.4`** — sessions rewound with these versions may **fail to load history** after more conversation. Install a v0.3.3-or-earlier release and use its bundled repair tool ([docs/troubleshooting.md](docs/troubleshooting.md)).
144
+ 3. **Rewinds from `≤ v0.3.3`** — compaction (`/compact`) is unavailable for those sessions. Newer versions are compatible; for affected old sessions, start a new session.
145
+
146
+ ## Security
147
+
148
+ 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.
149
+
150
+ ## Development
151
+
152
+ ```sh
153
+ npm install # devDeps from the npm registry
154
+ npm run typecheck # tsc on all three surfaces (host + client + client-test)
155
+ npm test # vitest: all unit and compatibility suites
156
+ npm run build # esbuild: lib/index.js (host ESM) + lib/client.js (loader closure) + .d.ts
157
+ node scripts/verify-host.mjs # end-to-end verification of the built artifact
158
+ ```
159
+
160
+ `prepare` runs the full build, so git installs and `npm pack` / `npm publish` always produce a complete `lib/` and the `LICENSE`.
161
+
162
+ Maintainers: the module map and harness interface reference live in [docs/harness-reference.md](docs/harness-reference.md).
163
+
164
+ ## Release
165
+
166
+ Releases go out through GitHub Actions Trusted Publishing (OIDC, no stored `NPM_TOKEN`): push a `v<version>` tag and CI publishes with Sigstore provenance.
167
+
168
+ ```sh
169
+ npm version patch && git push origin main --tags
170
+ ```
171
+
172
+ One-time npm-side setup and the full workflow details: [docs/release.md](docs/release.md).
173
+
174
+ ## License
175
+
176
+ [MIT](LICENSE)
package/README.md CHANGED
@@ -1,159 +1,173 @@
1
1
  # dsh-rewind
2
2
 
3
- In-place conversation rewind for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): Claude Code's `/rewind` semantics inside the **same session window** — cut the model context back to an earlier user message, and optionally restore workspace files from disk-persisted before-backups.
3
+ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消息**——同窗口内完成,不新建分支、不换窗口,可一并还原工作区文件(完整 Claude Code `/rewind` 语义)。
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/dsh-rewind-plugin.svg)](https://www.npmjs.com/package/dsh-rewind-plugin)
6
6
  [![npm license](https://img.shields.io/npm/l/dsh-rewind-plugin.svg)](https://github.com/SiriLee/dsh-rewind/blob/main/LICENSE)
7
+ [![npm downloads](https://img.shields.io/npm/dm/dsh-rewind-plugin.svg)](https://www.npmjs.com/package/dsh-rewind-plugin)
7
8
 
8
- > English | [中文](README.zh.md)
9
+ > [English](README.en.md) | 中文
9
10
 
10
- A deliberately focused plugin with one job: rewind to any earlier user message, in place.
11
+ 刻意聚焦、保持极简,只做一件事:**就地回退到任意远的用户消息**,还能**顺手还原改过的文件**。
11
12
 
12
- | Mode | Conversation | Workspace files |
13
- | --- | --- | --- |
14
- | **Rewind conversation only** | Cut back to the target message | Untouched |
15
- | **Rewind conversation and code** | Cut back to the target message | Restored to their state before it (modified files written back, later-created files deleted) |
16
-
17
- 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 — no new session, no window switch — and the target's text is offered back in the composer so you can edit and re-send it.
18
-
19
- The plugin never rewrites the append-only session log and never touches your git repository.
13
+ - **回退 = 时间回溯**——目标消息及其之后的全部内容(agent 回复、工具调用)同时从**模型上下文**和**渲染对话**中撤回,不新建会话、不切换窗口;目标消息文本会回填输入框,改完可重发。**在原理上就真正无感、便捷**。
14
+ - **轻量工作区备份**——行为对齐 Claude Code:只跟踪写文件的工具,写前做轻量备份并**落盘持久化**,不依赖、也不触碰 git 仓库。一个轻型插件,即拥有**完备的智能体回退能力**。
15
+ - **信息安全优先**——插件从不删改会话日志(append-only),从不真正删除你的任何对话;文件还原限定在插件自己的备份目录。
16
+ - **完备测试系统**——单元、探针、端到端主机验证,覆盖兼容性探测、日志重放、续接、跨重启等场景;随 harness 升级持续维护,确保功能稳定。
20
17
 
21
- ## Preview
18
+ ## 效果预览
22
19
 
23
- Each user message gains a compact **↶ rewind** action in its action row. Clicking it opens a mode-selection popover; "conversation and code" first shows the exact restore / delete list for confirmation (the option is hidden when there are no tracked changes — like Claude Code's code-restore visibility).
20
+ 每条用户消息的操作行都有一个 **↶ 回退** 按钮。点击后弹出模式选择浮层——「**仅回退对话**」或「**回退对话和代码**」,后者会先展示文件变更清单再确认。还可以通过 **`/rewind` 命令**和**快捷键**便捷地选择和回退。
24
21
 
25
22
  <table>
26
23
  <tr>
27
- <td align="center"><img src="assets/screenshots/rewind-button.png" width="440" alt="Per-messagerewind button"><br><sub>Per-messagerewind button</sub></td>
28
- <td align="center"><img src="assets/screenshots/mode-popover.png" width="440" alt="Mode-selection popover"><br><sub>Mode-selection popover</sub></td>
24
+ <td align="center"><img src="assets/screenshots/rewind-button.png" width="440" alt="用户消息旁的回退按钮"><br><sub>用户消息旁的回退按钮</sub></td>
25
+ <td align="center"><img src="assets/screenshots/mode-popover.png" width="440" alt="模式选择浮层"><br><sub>模式选择浮层</sub></td>
29
26
  </tr>
30
27
  <tr>
31
- <td align="center"><img src="assets/screenshots/impact-list.png" width="440" alt="Impact list"><br><sub>"Conversation and code" impact list</sub></td>
32
- <td align="center"><img src="assets/screenshots/rewind-candidates.png" width="440" alt="/rewind candidate picker"><br><sub>/rewind candidate picker</sub></td>
28
+ <td align="center"><img src="assets/screenshots/impact-list.png" width="440" alt="影响清单"><br><sub>「回退对话和代码」影响清单</sub></td>
29
+ <td align="center"><img src="assets/screenshots/rewind-candidates.png" width="440" alt="/rewind 候选面板"><br><sub>/rewind 候选面板</sub></td>
33
30
  </tr>
34
31
  </table>
35
32
 
36
- ## Install
33
+ ## 安装
37
34
 
38
35
  ```sh
39
36
  dsh plugin --profile web add dsh-rewind-plugin
40
37
  ```
41
38
 
42
- Restart `dsh web` (`--profile web`) after installing.
39
+ > ⚠️ npm 上的 `dsh-rewind` 属于其他作者,请用 `dsh-rewind-plugin` 安装。
43
40
 
44
- > ⚠️ The npm name `dsh-rewind` belongs to another author's package — install with `dsh-rewind-plugin`.
41
+ ## 使用
45
42
 
46
- For contributors: install from a local checkout or a pinned commit — `dsh plugin --profile web add /path/to/dsh-rewind` or `dsh plugin --profile web add github:SiriLee/dsh-rewind#<sha>`. A git install fails on first run until you add an `allowBuilds` key to the profile's `pnpm-workspace.yaml` (pnpm blocks git dependencies from running build scripts); after that it runs the plugin's `prepare` and installs it.
43
+ 1. 在对话中找到要回退的那条用户消息,或输入 `/rewind` 打开候选列表选择。
44
+ 2. **选中它。** 小浮层提供两种模式(「回退对话和代码」仅在目标之后有可还原的变更时显示)。
45
+ 3. 回退立即生效:对话回到目标消息当时的样子,被撤回消息的文本自动填入输入框——改完直接重发。
47
46
 
48
- ## Usage
47
+ **键盘操作**:候选列表与模式浮层均支持 ↑↓ 移动、Enter 确认、Esc 取消/返回。
49
48
 
50
- 1. **Hover** any user message you sent — a **↶ rewind** button appears in its action row.
51
- 2. **Click it.** The target is that message; a small popover offers the two modes ("conversation and code" is hidden when no tracked file changes exist after the target).
52
- 3. The rewind executes as an in-session command; a result message confirms, and the withdrawn message's text is filled back into the composer for editing and re-sending.
49
+ <details>
50
+ <summary><b>边界说明</b></summary>
53
51
 
54
- **Command-line entry**: type a bare `/rewind` and press Enter to open the candidate picker; selecting a target continues the same flow as the button.
52
+ - 回退可以反复进行——没有阶段或次数限制。
53
+ - 回退本身**无法撤销**,但被撤回的内容仍保留在会话日志中,可手动编辑日志恢复。
54
+ - **插话也能回退**——模型尚未读取的 `steering` 插话消息,同样可作为回退目标。
55
+ - **回退会打断当前正在运行的回合**——确保回退安全执行。
55
56
 
56
- Both the candidate picker and the mode popover support the keyboard: ↑↓ to move, Enter to confirm, Esc to cancel/back.
57
+ </details>
57
58
 
58
- Rewinds can be repeated (each appends a marker to the log). A rewind cannot be undone through the plugin, but the withdrawn messages can be recovered by manually editing the session log. The file-restore action is not re-backed up.
59
+ ## 本插件的优势
59
60
 
60
- ## How it works
61
+ 和常见的几种做法相比,本插件在"回退"这件事上的取舍:
61
62
 
62
- ### 1. Conversation rewind (in-place)
63
+ | 维度 | 常见做法 | 本插件 |
64
+ | --- | --- | --- |
65
+ | 对话回退 | Fork 分支新建对话 | **就地回退**——不新建会话、不切窗口,便捷回退 |
66
+ | 文件还原 | 无还原功能 / git 管理或完整快照 | **写前轻量备份**——写文件前自动存原内容,一键还原(对齐 Claude Code) |
67
+ | 依赖 | 常依赖 Git 仓库或完整快照引擎 | **无依赖**——不依赖 git,普通目录即可用 |
68
+ | 存储开销 | 整树快照占空间大 | **轻量**——只存被写工具改动过的文件,落盘持久化 |
63
69
 
64
- The plugin appends an **empty-content marker** `assistant/message` into the session log whose `surfaceOp: { op: 'replace', start, end }` replaces every surface node after the target message with the marker:
70
+ ## 原理
65
71
 
66
- - The marker carries `sourceEventSeqs` covering every shadowed node, and `Session.append`'s surface rules validate the cut (a contiguous range on the current surface).
67
- - Because the marker is **empty**, the harness derives it to `null` — it never enters the model context and never renders as conversation content. Agent and user both see the conversation exactly as it was at the target.
68
- - The marker's **turn number reuses the last started turn** (`markerTurnOf`), never `lastTurn + 1`: the harness numbers its next real turn exactly `last turn/start + 1`, so a `maxTurn + 1` marker would sit *before* that `turn/start` — the client conversation builder rejects the ordering (`…turn-tail… received an update before its start Match`) and history load fails. Reusing an already-consumed turn makes the marker a harmless trailing update on the previous completed turn's tail — it can never collide with a future turn.
69
- - The marker rides a **ghost step frame** — its own `step/start` … `step/end` with a fresh step number (`markerStepOf`) — because the harness token-meter requires every `assistant/message` to sit inside an open step of the same turn/step; a bare idle-time marker would fail its replay and break `/compact` for the session.
70
- - The append-only log is **untouched** — every withdrawn event stays in the audit trail; only the model-visible surface is cut, so the next request derives its context from the target onward.
72
+ 机制与 Claude Code checkpointing 同源(Claude Code 的文件历史也是逐文件记录 + 每条消息重扫已跟踪文件,并非整树快照)。本插件实现的是同一套语义,落在 dsh 上:
71
73
 
72
- A running turn (LLM thinking / streaming) is force-stopped first (`cancel({ kind: 'user' })`) and the rewind waits for quiescence; if it can't stop, the rewind is aborted with an error.
74
+ ### 1. 对话回退:怎么做到"就地"又不丢日志
73
75
 
74
- ### 2. Checkpoint file restore
76
+ 对话日志是**只追加**的——插件绝不改写历史。回退的做法:往日志里追加一条**空内容标记**,用它把目标消息之后的全部对话内容"遮蔽"掉,让模型和界面都只看到目标之前的部分:
75
77
 
76
- The plugin tracks the write-class tools — `write`, `edit`, `str_replace_editor` (mutating commands `create` / `str_replace` / `insert`):
78
+ - 标记本身是**空的**——不进入模型上下文、不渲染成任何对话内容,agent 和你看到的对话就是目标消息当时的样子;
79
+ - 因为只是"遮蔽"而非"删除",**被撤回的每一条事件都完整留在日志里**,审计可追溯;
80
+ - 标记复用"最后一个已开始的回合"的编号,并自带独立的步骤框架——让 dsh 自身的机制(日志重放、`/compact` 压缩、续接检查)都能正确识别它,不会误认为真实对话(这些兼容细节经过专门的探针测试固化,见[开发](#开发)一节)。
77
81
 
78
- 1. **Before-capture** at `tools/execute` (the around-dispatch stage): the target file is read; the resolved path + content are held in a pending map. This stage runs only after any pre-execute approval gate let the call through — an `ask` short-circuit (dsh-edit-approval) **cannot skip** the backup, and a denied call never records. If the read fails (e.g. a permission error), the change is simply not backed up — the plugin warns in the log but **does not block the write**.
79
- 2. **Disk commit** at `tools/post-execute`: the before-backup is written under the turn's anchor message seq (`~/.dsh/rewind-snapshots/<session>/<anchor seq>/<callId>.json`).
80
- 3. **Restore** (`/rewind @<seq> both`): every backup anchored at or after the target applies once reconciled with the current disk — modified files are written back to their **earliest** captured before-state, files created after the target are deleted, files already matching the target state are left untouched (idempotent). Symbolic / hard links are skipped (they share an inode with another name; restoring through one would clobber both). Writes go through plain `node:fs`, independent of the fs service — under sandbox / remote backends, path resolution may be restricted.
81
- 4. A tool body that **throws** skips `tools/post-execute`; a `tools/result` safety net clears the pending capture so nothing leaks in memory.
82
+ <details>
83
+ <summary><b>实现细节(给维护者)</b></summary>
82
84
 
83
- Backups persist across host restarts, bounded to the newest 100 anchor groups per session.
85
+ 插件向会话日志追加一条空内容标记 `assistant/message`,其 `surfaceOp: { op: 'replace', start, end }` 把目标消息之后的全部 surface 节点替换为标记本身:
84
86
 
85
- ## What it deliberately does NOT do
87
+ - 标记携带 `sourceEventSeqs` 覆盖所有被遮蔽节点,`Session.append` surface 规则校验切割合法性(仅限当前 surface 上的连续区间)。
88
+ - 因为标记**内容为空**,harness 会将其派生为 `null`——永不进入模型上下文、也永不渲染成对话内容。
89
+ - 标记的 **turn 号复用最后一个已开始的回合**(`markerTurnOf`),而不是「最后回合 + 1」:harness 恰好用 `最后 turn/start + 1` 编号下一条真实回合。若标记也取这个数,日志里就会出现同一 turn 的 `assistant/message` 先于 `turn/start` 的乱序,客户端 conversation 构建器会以 `conversation Context …:turn-tail… received an update before its start Match` 拒绝重放——历史加载失败、整个对话从界面消失。复用已消费的 turn 号则标记只是上一个已完成回合尾部的一次无害追加,永不与新回合冲突。
90
+ - 标记自带**幽灵步骤框架**——自己的 `step/start` … `step/end`,step 号取该回合未用过的新号(`markerStepOf`):harness 的 token-meter 重放要求每条 `assistant/message` 位于打开的 step 内,裸标记则会让该会话的 `/compact` 失效。
86
91
 
87
- - **Whole-tree / git-first snapshots** — only write-class tool edits plus external changes to already-tracked files are backed up; files never touched by a tool are not restored: the same limitation as Claude Code, which defers such rollbacks to the user's git.
88
- - **Subagent edits** — not tracked (same as Claude Code): a subagent runs its own session, so its backups could never be restored by a rewind of the parent session.
89
- - **Fork / branch rewind and `/compact`** — the harness already provides these ("branch in new chat", compact).
92
+ </details>
90
93
 
91
- ## Comparison with similar projects
94
+ agent 正在运行(LLM 思考/流式输出),会先强制停止并等待安静下来再回退;停不下来则中止并报错。
92
95
 
93
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) also has [Anionex/dsh-turn-rewind](https://github.com/Anionex/dsh-turn-rewind) a rewind plugin with the same user-facing idea (a per-message action that rolls the conversation back and restores workspace files), but a different approach:
96
+ ### 2. 文件还原:写前备份 + 外部变更追踪 + 磁盘比对
94
97
 
95
- | Dimension | dsh-rewind (this plugin) | Anionex dsh-turn-rewind |
96
- | --- | --- | --- |
97
- | Conversation rollback | **In-place, same session/window** — the model-visible surface is cut back to the target; the append-only log is untouched | **Forks a new Session** at the previous `turn/end`; the original session is always retained |
98
- | File-restore engine | **Lightweight before-backups** of write-class tools only, restored via plain `node:fs` | **Change Ledger** — a durable restore-point engine with Git fences, an approval gate, rescue points and crash reconciliation |
99
- | Tracked-change scope | Only write-class tool edits (like Claude Code) | Any Git-managed file (Git worktree required) |
100
- | Public service API | No — a focused single-purpose plugin | Yes — `ctx.changeLedger` service + `/turn-rewind` HTTP endpoint |
98
+ 插件跟踪写文件的工具——`write`、`edit`、`str_replace_editor`:
101
99
 
102
- The essential difference: dsh-turn-rewind keeps the log immutable and therefore forks a new session; this plugin cuts the model-visible surface in place with an empty marker, so the original conversation continues in the same window — the non-trivial part (see [Known issues](#known-issues)) that dsh-turn-rewind sidesteps.
100
+ 1. **写前备份**:每次写文件**之前**先把原内容存下来(在审批门放行之后捕获——审批短路不会漏备份,被拒绝的调用也不会记录;读取失败只警告、不阻塞写操作)。备份按当前对话轮次分组,**跨重启保留**(每会话保留最近 100 组)。
101
+ 2. **外部变更也追踪**:每条用户消息边界会重新检查所有已跟踪文件——外部编辑或删除(写工具从没参与过的改动)同样被记录下来,回退时一并还原。
102
+ 3. **还原前真实磁盘比对**:回退时实时读取文件当前内容,与目标状态比对——**只操作真正不一致的文件**:改过的写回最早备份、目标之后新建的删除、已一致的一律跳过(重复回退零副作用)。符号/硬链接跳过,避免误伤。
103
103
 
104
- ## Compatibility
104
+ <details>
105
+ <summary><b>实现细节(给维护者)</b></summary>
105
106
 
106
- - Node.js `^22.19.0 || >=24.0.0`.
107
- - DeepSeek Harness web profile (`dsh --profile web`); peer `@deepseek-ai/*` packages are resolved by the harness at runtime.
107
+ 1. **写前备份**(`tools/execute`,around-dispatch 阶段):读取目标文件,把解析后的路径与内容放入 pending 表。此阶段只在任何 pre-execute 审批门放行之后运行——审批 `ask` 短路(dsh-edit-approval)**无法跳过**备份,被拒绝的调用也不会记录。若读取失败(如权限错误),该次变更直接不入备份——插件只在日志中警告,**不会阻塞写操作**。
108
+ 2. **落盘提交**(`tools/post-execute`):备份按当前轮**锚点消息 seq** 写入 `~/.dsh/rewind-snapshots/<会话>/<锚点 seq>/<callId>.json`。
109
+ 3. **外部变更追踪**(`reconcileTracked`):消息边界处重扫已跟踪文件,磁盘状态与上次记录不同则记录一条 `recheck-<锚点>-<hash>` 条目(锚定边界消息);首见路径必记(重启后第一次边界无条件记录当前状态——冗余但正确,对应 Claude Code 的 resume 后 restat 行为)。
110
+ 4. **还原**(`/rewind @<seq> both`):`planRestore` 对每条记录实时探测磁盘——`before === null`(目标时不存在)仅当文件当前存在才计划删除(已缺失即已达成);`before === 'X'` 仅当当前内容 ≠ X 才计划还原(一致即 no-op,幂等);探测失败保守视为不一致(绝不静默跳过)。执行时 restore = 建父目录 + 写回内容,delete = 删文件(已不存在容忍为 no-op);符号/硬链接跳过(与另一名字共享 inode,透过一个还原会误伤两个);失败逐文件记录、不中止整轮。
111
+ 5. 工具体**抛异常**会跳过 `tools/post-execute`;`tools/result` 兜底清掉 pending,避免内存泄漏。备份跨 host 重启持久化,`prune` 每会话有界保留最近 100 组锚点。
108
112
 
109
- > [!WARNING]
110
- > This project and DeepSeek Harness are both in developer preview. Pin exact
111
- > versions in reproducible environments and review the behavior notes above.
113
+ </details>
112
114
 
113
- ## Client contract
115
+ ## 明确不做的事
114
116
 
115
- Third-party DOM plugins that need to know which transcript rows a rewind
116
- withdrew should consume the stable, locale-independent helpers exported from
117
- `dsh-rewind-plugin/client` (`hiddenSeqsOf`, `targetSeqOfArgs`) never parse
118
- `outcome.text`. The `data-dsh-rewind-hidden` attribute marks withdrawn rows
119
- (observational only). Details: [docs/client-contract.md](docs/client-contract.md).
117
+ 本插件刻意保持轻量、聚焦"对话回退"这一件事,以下场景**不属于它的职责**:
118
+
119
+ - **整树 / Git 级快照**——只跟踪写类工具编辑 + 已跟踪文件的外部改动,从未被工具碰过的文件不还原。需要 Git 工作树级的完整快照回退时,请交给专门的快照工具(或你的 git)。
120
+ - **子代理的编辑**——不追踪(同 Claude Code):子代理运行在自己的会话里,其备份无法由父会话的回退还原,只会在磁盘上残留。
121
+ - **fork / 分支回退**——harness 已内置「在新对话中分支」,不重复造轮子。
122
+
123
+ ## 兼容性
124
+
125
+ - Node.js `^22.19.0 || >=24.0.0`。
126
+ - DeepSeek Harness web 配置档(`dsh --profile web`);peer `@deepseek-ai/*` 包由 harness 运行时解析。
127
+
128
+ > [!WARNING]
129
+ > 本项目与 DeepSeek Harness 均处于开发者预览阶段。可复现环境请 pin 精确版本,
130
+ > 并阅读上述行为说明。
120
131
 
121
- ## Known issues
132
+ ## 客户端契约
122
133
 
123
- Rewinds created with versions `≤ v0.2.4` could corrupt client replay when followed by more conversation (a marker turn collides with the next `turn/start`). Only pre-upgrade sessions are affected. The offline repair tool (`dsh-rewind-repair`) was shipped before v0.4.0 and is no longer provided from v0.4.0 on — install a pre-v0.4.0 release if you need it ([docs/troubleshooting.md](docs/troubleshooting.md)).
134
+ 需要获知哪些转录行被回退撤回的第三方 DOM 插件,应使用 `dsh-rewind-plugin/client` 导出的稳定、与本地化无关的纯函数,切勿解析 `outcome.text`。`data-dsh-rewind-hidden` 属性标记被撤回的行(仅观测性)。
135
+ 详见:[docs/client-contract.zh.md](docs/client-contract.zh.md)。
124
136
 
125
- Rewinds from `≤ v0.3.3` appended a bare marker (no step frame); the harness token-meter rejects such a log on replay, so `/compact` fails for that session. Newer versions are compatible; affected old sessions have no online repair yet — start a new session.
137
+ ## 已知问题
126
138
 
127
- ## Security
139
+ 1. **导出的日志是完整内容**——回退只是把消息从模型上下文和视图中移除,`/export` 导出的会话日志包含**已撤回的消息**。本插件无法改动导出。
140
+ 2. **v0.2.4 及更早版本**回退过的会话,继续对话后可能加载历史失败。可安装 v0.3.3 及之前版本的随附修复工具处理([完整步骤](docs/troubleshooting.zh.md))。
141
+ 3. **v0.3.3 及更早版本**回退过的会话,压缩对话(compact)不可用。新版本已兼容;受影响的旧会话建议新建会话。
128
142
 
129
- 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.
143
+ ## 安全
130
144
 
131
- > **Note:** a rewind only hides messages from view — the exported session log (`/export`) still contains them, and this plugin cannot alter exports. To remove a conversation completely, delete its session file.
145
+ 本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和代码」时被改写,备份与还原都限定在 `~/.dsh/rewind-snapshots/` 内。不触碰你的 git 仓库,无网络请求,不访问任何凭据。删除 `~/.dsh/rewind-snapshots/` 仅清除文件备份(对话回退不受影响),插件会自动重建。
132
146
 
133
- ## Development
147
+ ## 开发
134
148
 
135
149
  ```sh
136
- npm install # devDeps from the npm registry
137
- npm run typecheck # tsc on all three surfaces (host + client + client-test)
138
- npm test # vitest: rewind / snapshot / hidden / session-cwd / integration
139
- npm run build # esbuild: lib/index.js (host ESM) + lib/client.js (loader closure) + .d.ts
140
- node scripts/verify-host.mjs # boot the BUILT host artifact end-to-end
150
+ npm install # devDeps 来自 npm registry
151
+ npm run typecheck # tsc 三面编译(host + client + client-test
152
+ npm test # vitest:全部单元与兼容性测试套件
153
+ npm run build # esbuildlib/index.jshost ESM)+ lib/client.jsloader 闭包)+ .d.ts
154
+ node scripts/verify-host.mjs # 端到端验证构建产物
141
155
  ```
142
156
 
143
- `prepare` runs the full build, so git installs and `npm pack` / `npm publish` always produce a complete `lib/` and the `LICENSE`.
157
+ `prepare` 执行完整构建,所以 git 安装与 `npm pack` / `npm publish` 总会产出完整的 `lib/` `LICENSE`。
144
158
 
145
- Maintainers: the module map and harness interface reference live in [docs/harness-reference.md](docs/harness-reference.md); publishing steps in [docs/release.md](docs/release.md).
159
+ 维护者:模块地图与 harness 接口参考见 [docs/harness-reference.md](docs/harness-reference.md)
146
160
 
147
- ## Release
161
+ ## 发布
148
162
 
149
- Releases go out through GitHub Actions Trusted Publishing (OIDC, no stored `NPM_TOKEN`): push a `v<version>` tag and CI publishes with Sigstore provenance.
163
+ 通过 GitHub Actions Trusted PublishingOIDC,无存储 `NPM_TOKEN`)发布:推送 `v<版本>` tagCI 即带 Sigstore provenance 发布。
150
164
 
151
165
  ```sh
152
166
  npm version patch && git push origin main --tags
153
167
  ```
154
168
 
155
- One-time npm-side setup and the full workflow details: [docs/release.md](docs/release.md).
169
+ 一次性 npm 侧配置与完整流程:见 [docs/release.md](docs/release.md)
156
170
 
157
- ## License
171
+ ## 许可
158
172
 
159
173
  [MIT](LICENSE)
@@ -0,0 +1,137 @@
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
+ > 进入「修复 / 钉住 / 记录」闭环。
7
+ >
8
+ > 针对版本:npm `@deepseek-ai/*@0.1.1-rc.2`(与 `package-lock.json` 一致);
9
+ > 源码参考:`oss/deepseek-harness` 本地 fork。
10
+ >
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
+ | 不变量 | 含义 | 探针位置 |
20
+ |---|---|---|
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-8、15 |
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 边界——**崩溃路径已根治**。
37
+ >
38
+ > **插件守卫(已尝试并回退)**:曾实现 `hasOpenStep` + `planRewind` 前置拒绝
39
+ > (`open-step`),但在**真实会话日志上产生误判**(正常回退被拒、GUI 验证功能缺失),
40
+ > 已 revert(`177ec14`)。结论:**插件不设守卫**,接受残余风险(运行中第三方插件
41
+ > 产生未闭合 step 时,rewind 可能破坏 /compact——该日志本身已异常,继续对话同样触发)。
42
+ > 根治方向在 harness(token-meter 对未闭合 step 的恢复),不在插件。
43
+
44
+ #### 未闭合 `step/start` 的具体触发情况(源码确认)
45
+
46
+ 全仓**只有一处** append `step/start`:`packages/core/agent-loop/src/agent.ts:279`(官方包内
47
+ 无其他生产者;`session/end-seed` 等修复只做 torn-write 截断,不处理逻辑未闭合)。
48
+
49
+ | # | 触发路径 | 现实性 | 依据 |
50
+ |---|---|---|---|
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` finally;write-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 不是唯一触发者)**:崩溃后 resume,agent-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
+ |---|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
124
+ | 会话内核(append/surface/deriveMessages) | ✓ | ✓ | ✓ | — | ✓ | — | ✓ | ✓ |
125
+ | token-meter | ✓ | — | ✓ | — | ✓ | — | — | — |
126
+ | compaction(事务/命令/tool-pairing) | ✓ | — | — | — | ✓ | — | — | ✓ |
127
+ | session-stats / projection | — | — | ✓ | ✓ | — | — | — | — |
128
+ | session-title | — | — | — | ✓ | — | — | — | — |
129
+ | 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 行为差异。