dsh-rewind-plugin 0.7.5 → 0.9.0-alpha.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
@@ -8,6 +8,9 @@ Conversation rewind for DeepSeek Harness: **rewind the conversation to any earli
8
8
 
9
9
  > English | [中文](README.md)
10
10
 
11
+ > [!WARNING]
12
+ > For the upcoming DSH line (v0.1.3), users who want the latest DSH should move to the new plugin (`>=0.9.0-alpha.1`) and update their session logs — see the [update guide](docs/rewind-fix.md).
13
+
11
14
  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
15
 
13
16
  - **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**.
@@ -91,13 +94,13 @@ The whole design rests on two principles, simple but deliberate: **the conversat
91
94
 
92
95
  ### 1. Conversation rewind: a single "mask", not a delete
93
96
 
94
- `append-only` is a hard rule: the session log only grows and is never rewritten — the foundation of auditability and privacy. A rewind never touches history; it makes a single move: append **one empty-content message marker** to the end of the log and use it to "mask out" everything after the target message, so the model and the UI see only the part before it.
97
+ `append-only` is a hard rule: the session log only grows and is never rewritten — the foundation of auditability and privacy. A rewind never touches history; it makes a single move: append **one content-empty marker message** to the end of the log and use it to "mask + replace" everything after the target message, so the model and the UI see only the part before it.
95
98
 
96
- - The marker is **empty** — it never enters the model context and never renders as conversation content; what you and the model see is exactly how the conversation looked at the target. True "in place".
97
- - Because this is **masking, not deleting**, every withdrawn event stays in the log auditable, traceable, and in principle manually recoverable.
98
- - The marker is deeply **aware of dsh internals**: it reuses the **last-started turn** number (never "last turn + 1") and carries its own **ghost step frame**. So the harness's own log replay, `/compact`, and resume preflight all recognize it and never mistake it for a real message.
99
+ - The marker is **canonical** — the plugin **replicates** `/compact`'s "hide + replace": `/compact` compresses a span of history into a summary, while `/rewind` swaps in an empty user message. Because it's canonical, the harness's own log replay, `/compact` compaction, and resume preflight all recognize it and never mistake it for a real message.
100
+ - The replacement is **empty** — the model fully ignores the empty message, with no effect (verified theoretically and empirically). Together with the plugin's UI handling, what you and the model see is exactly how the conversation looked at the target — truly "in place".
101
+ - Because this is **masking, not deleting**, every withdrawn event stays in the log auditable, traceable, viewable, and in principle manually recoverable.
99
102
 
100
- > **Design highlight**: the entire conversation rewind is **a single append**. It's deterministic, auditable, and — because the log was never broken — a "clean" time-travel. Minimal action, complete semantics. The compatibility subtleties with the harness (ghost step frame, reused turn number) are where this plugin is genuinely professional — each is pinned by a dedicated probe test.
103
+ > **Design highlight**: the entire conversation rewind is **a single append**. It's deterministic, auditable, and — because the log was never broken — a "clean" time-travel. Minimal action, complete semantics. The compatibility subtleties with the harness (replicating `/compact`, the empty-message mask) are where this plugin is genuinely professional — each is pinned by a dedicated probe test.
101
104
 
102
105
  ### 2. File restore: lightweight checkpointing, "back up before the change"
103
106
 
@@ -121,7 +124,7 @@ The file half follows Claude Code's checkpoint semantics — **partial tracking
121
124
  | Identical content stored as a link (dedup) | Hundreds of repeated writes cost almost nothing; links are materialized before their group is evicted, never left dangling |
122
125
  | Session-level auto-cleanup | Removes only long-inactive sessions' snapshots; the active session and the chat log are never touched |
123
126
  | Reconcile against the real disk before restoring | Idempotent, zero side effects, no collateral damage |
124
- | Ghost step frame + reused turn number | Deeply compatible with the host, pinned by probe tests |
127
+ | Empty-message mask + replicating `/compact` | Deeply compatible with the host, pinned by probe tests |
125
128
  | Crash safety (atomic writes + restore journal) | Continue or roll back cleanly after a crash |
126
129
  | Pure-function planning + probed store | Fully unit-testable without a host; test-driven |
127
130
 
@@ -154,9 +157,9 @@ withdrew should consume the stable, locale-independent helpers exported from
154
157
 
155
158
  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.
156
159
  2. **Lightweight file rewind has a cost** — in specific cases not all changes can be rewound. Consistent with Claude Code. See: [File-rewind tracking boundary](docs/compat/tracking-boundary.md).
157
- 3. **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/compat/troubleshooting.md](docs/compat/troubleshooting.md)).
158
- 4. **Rewinds from `≤ v0.3.3`** compaction (`/compact`) is unavailable for those sessions. Newer versions are compatible; for affected old sessions, start a new session.
159
- 5. **The turn-rail shows rewound turns** — the right-side rail added in DSH `v0.1.2-alpha.1` keeps ticks for withdrawn messages: clicking does not jump and hovering shows the withdrawn text. Only a display difference; no functional impact.
160
+ 3. **The turn-rail shows rewound turns** the right-side rail added in DSH `v0.1.2` keeps ticks for withdrawn messages, and hovering shows the withdrawn text. Only a display difference; no functional impact.
161
+ 4. **The system prompt is re-displayed after a rewind** in DSH `v0.1.2`, rewinding and resending a message shows the "System prompt" component again, just like `/compact`. Only a display difference; no functional impact.
162
+ 5. **Old rewind markers are no longer compatible** — DSH `v0.1.3` rejects the rewind markers from the old plugin (≤ 0.8.0). The new version resolves this and provides an in-session update. See the [update guide](docs/rewind-fix.md).
160
163
 
161
164
  > [!NOTE]
162
165
  > Browser diagnostics are available; see [Browser diagnostics](docs/compat/diagnostics.md).
package/README.md CHANGED
@@ -8,6 +8,9 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
8
8
 
9
9
  > [English](README.en.md) | 中文
10
10
 
11
+ > [!WARNING]
12
+ > 为了适配未来版本(DSH v0.1.3),期望使用最新版 DSH 的用户,请尽早使用新版插件(`>=0.9.0-alpha.1`)。并更新你的会话日志,详见:[更新指南](docs/rewind-fix.zh.md)
13
+
11
14
  刻意聚焦、保持极简,只做一件事:**就地回退到任意远的用户消息**,还能**顺手还原改过的文件**。
12
15
 
13
16
  - **回退 = 时间回溯**——目标消息及其之后的全部内容(agent 回复、工具调用)同时从**模型上下文**和**渲染对话**中撤回,不新建会话、不切换窗口;目标消息文本会回填输入框,改完可重发。**在原理上就真正无感、便捷**。
@@ -81,13 +84,13 @@ dsh plugin --profile web add dsh-rewind-plugin
81
84
 
82
85
  ### 1. 对话回退:一次「遮蔽」,而不是「删除」
83
86
 
84
- `append-only` 是铁律:会话日志只追加、从不改写——这是可审计与信息安全的地基。回退从不动历史,它只做一步:往日志末尾追加**一条内容为空的消息标记**,用它把目标消息之后的全部内容「遮蔽」掉,让模型和界面都只看得到目标之前的部分。
87
+ `append-only` 是铁律:会话日志只追加、从不改写——这是可审计与信息安全的地基。回退从不动历史,它只做一步:往日志末尾追加一条**内容为空的标记消息**,把目标消息之后的全部内容「遮蔽 + 替换」掉,让模型和界面都只看得到目标之前的部分。
85
88
 
86
- - 标记本身是**空的**——不进入模型上下文、不渲染成任何对话内容,模型和你看到的对话就是目标消息当时的样子,真正的「就地」;
87
- - 因为是「遮蔽」而非「删除」,**被撤回的每一条事件都完整留在日志里**,可审计、可追溯,原则上也随时能手动恢复;
88
- - 标记非常「懂」dsh——它复用**最后一个已开始的回合**的编号(而不是「最后回合 + 1」),并自带一个独立的**幽灵步骤框架**。于是 harness 自己的日志重放、`/compact` 压缩、续接检查都能正确识别它,绝不会把它误认为真实对话。
89
+ - **标记是规范的**——插件复刻 `/compact` 标准的「隐藏 + 替换」:`/compact` 把一段历史压缩成摘要,`/rewind` 则换成一条空用户消息。由于其规范性,harness 的日志重放、`/compact` 压缩、续接检查都能正确识别它,绝不会把它误认为真实对话;
90
+ - **替换内容是空的**——模型对空消息完全忽略、无感(理论 + 实测验证)。配合插件对界面显示的处理,模型和你看到的对话就是目标消息当时的样子,真正的「就地」;
91
+ - 因为是「遮蔽」而非「删除」,**被撤回的每一条事件都完整留在日志里**,可审计、可追溯、可查看,原则上也随时能手动恢复。
89
92
 
90
- > **设计点睛**:整个对话回退就是**一条**追加。它确定、可审计,且因为日志从未被破坏,回溯是「干净的」——用最小的动作,实现最完整的语义。那些与 harness 内部的兼容细节(幽灵步骤框架、复用回合号)正是插件的专业所在,每一条都由专门的探针测试固化。
93
+ > **设计点睛**:整个对话回退就是**一条**追加。它确定、可审计,且因为日志从未被破坏,回溯是「干净的」——用最小的动作,实现最完整的语义。那些与 harness 内部的兼容细节(对 `/compact` 的复刻、空消息的遮蔽)正是插件的专业所在,每一条都由专门的探针测试固化。
91
94
 
92
95
  ### 2. 文件还原:轻量检查点,「改前备份」
93
96
 
@@ -111,7 +114,7 @@ dsh plugin --profile web add dsh-rewind-plugin
111
114
  | 同内容存为链接(去重) | 上百次重复写入几乎不占空间;淘汰组前先落地链接,绝不悬空 |
112
115
  | 会话级自动清理 | 只移除长期不活跃会话的快照,活动会话与对话日志永不触及 |
113
116
  | 对照真实磁盘再还原 | 幂等、零副作用、不误伤 |
114
- | 幽灵步骤框架 + 复用回合号 | 与宿主深度兼容,且被探针测试固化 |
117
+ | 空消息遮蔽 + `/compact` 的复刻 | 与宿主深度兼容,且被探针测试固化 |
115
118
  | 崩溃安全(原子写 + 还原日志) | 断电/崩溃后仍可续做或回滚 |
116
119
  | 纯函数规划 + 注入探针的存储 | 无需宿主即可单测,测试驱动 |
117
120
 
@@ -141,9 +144,9 @@ dsh plugin --profile web add dsh-rewind-plugin
141
144
 
142
145
  1. **导出的日志是完整内容**——回退只是把消息从模型上下文和视图中移除,`/export` 导出的会话日志包含**已撤回的消息**。本插件无法改动导出。
143
146
  2. **轻量文件回退存在代价**——特定情况可能无法回退所有修改。行为与 Claude Code 一致。详见:[文件回退的追踪边界](docs/compat/tracking-boundary.zh.md)。
144
- 3. **v0.2.4 及更早版本**回退过的会话,继续对话后可能加载历史失败。可安装 v0.3.3 及之前版本的随附修复工具处理([完整步骤](docs/compat/troubleshooting.zh.md))。
145
- 4. **v0.3.3 及更早版本**回退过的会话,压缩对话(compact)不可用。新版本已兼容;受影响的旧会话建议新建会话。
146
- 5. **导轨显示已回退轮次**——DSH `v0.1.2-alpha.1` 新增的右侧导轨,会为已撤回消息保留刻度:点击不跳转、悬浮显示已撤回正文。仅显示差异,无功能影响。
147
+ 3. **导轨显示已回退轮次**——DSH `v0.1.2` 新增右侧导轨,为已撤回消息保留刻度,悬浮显示已撤回正文。仅显示差异,无功能影响。
148
+ 4. **回退重显系统提示词**——DSH `v0.1.2` 回退重发消息时,与 `/compact` 一样重显“系统提示词”组件。仅显示差异,无功能影响。
149
+ 5. **旧回退标记不再兼容**——DSH `v0.1.3` 拒绝旧版插件(≤ 0.8.0)的回退标记。新版本已解决,并提供会话更新功能。详见:[更新指南](docs/rewind-fix.zh.md)。
147
150
 
148
151
  > [!NOTE]
149
152
  > 本插件提供浏览器端诊断输出;详见 [浏览器诊断](docs/compat/diagnostics.zh.md)。
package/SECURITY.md CHANGED
@@ -70,14 +70,13 @@ runs.
70
70
  ## Conversation integrity
71
71
 
72
72
  The session log is **append-only** — the plugin never deletes or rewrites
73
- recorded history. A rewind appends a single marker event: an **empty**
74
- `assistant/message` whose `surfaceOp` replaces every surface node after the
75
- target. The raw log (audit trail, search, `/export`) is untouched — only the
73
+ recorded history. A rewind appends a single marker event (an **empty**
74
+ `user/message`) whose `surfaceOp` replaces every surface node after the target.
75
+ The raw log (audit trail, search, `/export`) is untouched — only the
76
76
  model-visible surface is cut, so the next request derives its context from the
77
- target onward. The marker sits inside a ghost `step/start step/end` frame so
78
- the harness token-meter replay keeps accepting the log; a malformed marker
79
- (duplicated turn/step, dangling open step) is what earlier versions produced
80
- and is repaired offline (see `docs/compat/troubleshooting.md`).
77
+ target onward. The marker is empty, so it carries no untrusted text into the
78
+ model context; the client hides the `[target, marker]` span from the rendered
79
+ transcript.
81
80
 
82
81
  ## Filesystem containment
83
82
 
package/docs/README.md CHANGED
@@ -12,9 +12,10 @@ index/navigation entry point.
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
14
  | `snapshot-auto-cleanup.md` | Global snapshot auto-cleanup policy and command (`.zh` mirror) | users / maintainers |
15
+ | `rewind-fix.md` | Migrate old rewind markers (A/B→C) with `/dsh-rewind-fix` (`.zh` mirror) | users / maintainers |
15
16
  | `contract/client-contract.md` | Rewind visibility contract for third-party DOM plugins (`.zh` mirror) | integrators |
16
17
  | `compat/audit.md` | Compatibility audit: verified surfaces, recorded findings, probe matrix | maintainers |
17
- | `compat/troubleshooting.md` | Known issues & repair steps (`.zh` mirror) | users / maintainers |
18
+ | `compat/tracking-boundary.md` | Which files a rewind restores: the tracking boundary (`.zh` mirror) | users / maintainers |
18
19
  | `compat/diagnostics.md` | Browser diagnostics (anomaly alerts; verbose switch gates the startup identity line) (`.zh` mirror) | users / maintainers |
19
20
  | `release/release.md` | Release workflow & DSH peer-version alignment (`.zh` mirror) | maintainers |
20
21
 
@@ -17,15 +17,20 @@ credentials access.
17
17
 
18
18
  ```
19
19
  src/
20
- ├── index.ts host plugin: /rewind command + checkpoint pipeline
21
- ├── rewind.ts pure planning: target parsing, surface-range plan,
22
- │ candidate listing, marker turn/step math
20
+ ├── index.ts host plugin: /rewind, /undo, /snapshot-auto-cleanup,
21
+ │ /dsh-rewind-fix commands + checkpoint pipeline
22
+ ├── rewind.ts pure planning: target parsing, surface-range plan, candidate listing
23
+ ├── rewind-fix.ts /dsh-rewind-fix orchestration (repair pipeline, locks, rollback)
24
+ ├── rewind-marker-repair.ts pure legacy-marker transform (A/B → C)
25
+ ├── session-log-io.ts session-log zstd codec + lossless re-encoder (rewind-fix write-back)
23
26
  ├── snapshot.ts checkpoint store: disk before-backups, journaled restore,
24
27
  │ reconcile / continue / rollback, bounded prune
28
+ ├── snapshot-cleanup.ts cleanup policy + dsh-settings persistence + auto-sweep throttle
25
29
  ├── session-cwd.ts session working-directory resolution (fs-tools rule)
30
+ ├── locales.ts host i18n (t() renderer, HostKey)
26
31
  └── client/ browser half: per-message ↶ button (portal bridge),
27
32
  mode popover, hidden-span computation, candidate parsing,
28
- locales, styles
33
+ pending interaction, locales, styles
29
34
  ```
30
35
 
31
36
  Two dependency rules keep the design testable:
@@ -48,11 +53,9 @@ machine channels (see [Compatibility strategy](#compatibility-strategy)).
48
53
  user/message currently on the surface)
49
54
  → agent.cancel({ keepInbox: true }) if running; waitForAgentIdle
50
55
  → dropPendingSteering (next-step inbox only; queued messages untouched)
51
- → append ghost step frame: step/start (turn = last started turn,
52
- step = that turn's next unused number) marker step/end
53
- marker = empty assistant/message with
54
- surfaceOp { op: 'replace', start, end } over every surface node
55
- after the target (+ sourceEventSeqs = shadowed seqs)
56
+ → append the rewind marker = an empty user/message with surfaceOp
57
+ { op: 'replace', start, end } over every surface node after the
58
+ target (+ sourceEventSeqs = shadowed seqs) [a single event]
56
59
  → if mode 'both': store.restoreAfter(targetSeq) + syncRestoreObservations
57
60
  → result text carries machine tokens (impact=<n>, restore:/delete: lines)
58
61
  → client: hides withdrawn rows (data-dsh-rewind-hidden), refills composer
@@ -63,18 +66,32 @@ Key invariants:
63
66
 
64
67
  - **The log is append-only.** The marker is the *only* mutation: it cuts the
65
68
  model-visible surface, never the raw history (search/export still see it).
66
- - **The marker is empty** (`content: []`, derives to `null` in the model
67
- context) so it renders nothing and enters no model context.
68
- - **The ghost step frame exists for the token-meter**: replay requires every
69
- `assistant/message` inside an open step of the same `(turn, step)`; a bare
70
- marker appended while idle would throw on the next `measure()` and silently
71
- break `/compact`. The turn/step numbers are chosen so they can never collide
72
- with a future real turn (`markerTurnOf` / `markerStepOf` — see
73
- `src/rewind.ts`).
69
+ - **The marker is empty** (`content: []`) and carries the dsh-rewind plugin
70
+ source. It is a `user/message`, the only surface type that can cite the
71
+ shadowed seqs (`sourceEventSeqs`) `assistant/message` can no longer carry
72
+ them (v2). It derives to itself, so it stays as a present-but-empty user
73
+ turn at the surface tail rather than entering model context as content.
74
+ - **No ghost step frame is needed**: the token-meter step machine ignores
75
+ `user/message`, and the session invariant imposes no open-turn requirement
76
+ on it, so the marker is appended while idle, outside any turn, as one event.
74
77
  - **Restore is reconciled against the live disk** (`planRestore`), so repeated
75
78
  rewinds are idempotent and a rewind whose target state already matches is a
76
79
  no-op.
77
80
 
81
+ ### Marker format history
82
+
83
+ The rewind marker is written as form C (an empty `user/message` with a
84
+ `surfaceOp.replace` over the shadowed range and `sourceEventSeqs`). Earlier
85
+ plugin versions wrote shapes a newer harness no longer accepts:
86
+
87
+ - **form A** — a bare `assistant/message(turn=N, step=0)` with no frame.
88
+ - **form B** — a ghost turn frame: `[step/start][assistant/message][step/end]`
89
+ inside a closed turn.
90
+
91
+ A/B became unreadable once v2 reserved surface `replace` to a node that cites
92
+ `sourceEventSeqs` (`assistant/message` can no longer carry them). `/dsh-rewind-fix`
93
+ rewrites form A/B in closed sessions to form C so a newer harness accepts the log.
94
+
78
95
  ## Checkpoint pipeline (Claude Code before-backup model)
79
96
 
80
97
  ```
@@ -98,10 +115,11 @@ pruneStale cross-session auto-cleanup (default off): whole
98
115
 
99
116
  ## Compatibility strategy
100
117
 
101
- - **Peer ranges as OR-union of DSH version tuples** (`^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2`):
102
- npm's prerelease rules require the peer range to share the host's
103
- `[major, minor, patch]` tuple; `scripts/check-dsh-version.mjs` detects when
104
- a new tuple requires appending. See `docs/release/release.md`.
118
+ - **Peer ranges as one tuple per DSH line** (`^0.1.2-rc.1`): npm's prerelease
119
+ rules require the peer range to share the host's `[major, minor, patch]`
120
+ tuple, so a new DSH tuple replaces the peer tuple (the single-line model);
121
+ `scripts/check-dsh-version.mjs` flags when a new tuple arrives. See
122
+ `docs/release/release.md`.
105
123
  - **Test-driven investigation**: `tests/compat-invariants.ts` /
106
124
  `compat-interop` / `compat-gaps` probe harness behavior and pin findings in
107
125
  `docs/compat/audit.md`; `scripts/verify-host.mjs` runs a real end-to-end
@@ -7,35 +7,33 @@
7
7
  > compatibility invariants. A probe failure is a finding; it enters the
8
8
  > fix/pin/record loop.
9
9
  >
10
- > Targeted version: npm `@deepseek-ai/*@0.1.1-rc.2` (matches `package-lock.json`).
10
+ > Targeted version: npm `@deepseek-ai/*@0.1.2-rc.1` (matches `package-lock.json`).
11
11
  > Source reference: the upstream [github.com/deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness).
12
12
  >
13
- > Version alignment: `peerDependencies` use an OR-union (e.g.
14
- > `^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2`) covering each published tuple series. npm's
15
- > prerelease matching rules require a candidate to share the range comparator's
16
- > `[major, minor, patch]` tuple, so appending is needed only for a genuinely new
17
- > tuple (e.g. a future `0.2.x`); any prerelease within the SAME tuple (alpha → rc)
18
- > is a no-op. Signal: `npm view @deepseek-ai/dsh version`;
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); any prerelease within the SAME
17
+ > tuple is a no-op. Signal: `npm view @deepseek-ai/dsh version`;
19
18
  > flow: `scripts/check-dsh-version.mjs` (it reads the `latest` dist-tag only; a
20
- > `-alpha` prerelease published under another tag, e.g. `0.1.2-alpha.2` under
21
- > `alpha`, is a manual pre-release check).
19
+ > prerelease published under another tag is a manual pre-release check).
22
20
  >
23
- > `0.1.2-alpha.1` was never published to npm, so the peer OR-union is declared only from `0.1.2-alpha.2`; the plugin code still supports the `alpha.1`+ client.
24
- >
25
- > `0.1.2-alpha.2` … `0.1.2-alpha.5` and `0.1.2-rc.1` verified-compatible; `rc.1` is
26
- > source-identical to `alpha.5`. They share the `0.1.2` tuple, so the single
27
- > `^0.1.2-alpha.2` peer member covers all six. `rc.2` (`latest`) stays the primary
28
- > baseline; verified: rc.2, alpha.2, alpha.3, alpha.4, alpha.5, rc.1.
21
+ > The plugin targets a single DSH version line (`0.1.2-rc.1`); compatibility
22
+ > with earlier lines is not kept.
23
+
24
+ ### Single channel (`0.1.2-rc.1`)
29
25
 
30
- ### Channeled seams (version × channel)
26
+ The plugin targets one DSH channel. Each seam below reads the 0.1.2-rc.1 shape
27
+ only (no `Session.events` / `[data-time-hover-root]` / `<textarea>` / face-`chat`
28
+ legacy branch).
31
29
 
32
- | Seam | rc.2 (`0.1.1-rc.2`) | alpha.2 / alpha.3 | alpha.4 / alpha.5 / rc.1 |
33
- | --- | --- | --- | --- |
34
- | Host session log (`eventsOf`) | `Session.events` | `Session.events` | `session.snapshotEvents()` |
35
- | Client chat snapshot (`chatSnapshotOf`) | session-face `chat` field | `uiConversation` `chat` view | `uiConversation` `chat` view |
36
- | Client composer refill (`writeComposer`) | `<textarea>` DOM write | `conversation.input.setDraft` | `conversation.input.setDraft` |
37
- | Client settings card | nested `ctx.inject(['settingsScope'])` | nested `ctx.inject(['settingsScope'])` | nested `ctx.inject(['settingsScope'])` |
38
- | Client seat-button DOM (`actionsContainerOf`) | `[data-time-hover-root]` | `[data-actions-reveal]` | structural locate of the copy-`<button>` container |
30
+ | Seam | 0.1.2-rc.1 implementation |
31
+ | --- | --- |
32
+ | Host session log | `session.snapshotEvents()` |
33
+ | Client chat snapshot | `uiConversation` `chat` view (`chatSnapshotOf`) |
34
+ | Client composer refill | `conversation.input.setDraft` facade |
35
+ | Client settings card | nested `ctx.inject(['settingsScope'])` |
36
+ | Client seat-button DOM | structural locate of the copy-`<button>` container (`actionsContainerOf`) |
39
37
 
40
38
  ## Definition of "fully compatible" (invariants)
41
39
 
@@ -43,30 +41,30 @@
43
41
  |---|---|---|
44
42
  | 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 |
45
43
  | 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 |
46
- | 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
+ | I3 step/turn structure | Client turn-tail ordering, unique `step/start` (real turns), no ghost turns; the `user/message` rewind marker adds no step frame | `compat-invariants` I3, `helpers.assertTurnTailOrdering` |
47
45
  | I4 fold-service safety | stats / title / goal / projection fold a marker-bearing log without throwing, with predictable values | `compat-invariants` I4 |
48
46
  | 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 |
49
47
  | I6 tool pipeline | before-snapshot capture/commit/restore is correct (existing `snapshot.test.ts` + `verify-host` 4–8); cancellation timing never hangs | `verify-host` 4–8, 15 |
50
- | I7 client ordering | A log carrying tool turns, rewind markers, and ghost steps satisfies the client builder ordering | `compat-interop` I7 |
48
+ | I7 client ordering | A log carrying tool turns and rewind markers (a single `user/message` replace) satisfies the client builder ordering | `compat-interop` I7 |
51
49
  | I8 runtime safety | `rewind`/`compact` combinations never leave a dangling step/turn frame | `verify-host` 15 |
52
50
 
53
51
  ## Verified-compatible surfaces (probes pass)
54
52
 
55
- - **token-meter replay** (marker + ghost-step frames + multiple rewinds + interleaved real turns + compact stacking).
53
+ - **token-meter replay** (the empty `user/message` marker + multiple rewinds + interleaved real turns + compact stacking).
56
54
  - **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.
57
55
  - **resume replay**: `Session.create(id, events)` replays a rewind/compact-bearing log.
58
- - **session-stats**: a ghost-step frame adds one step but no phantom turn (reuses the turn number).
56
+ - **session-stats**: the `user/message` marker adds no step (the step count stays at the real turns' steps), no phantom turn.
59
57
  - **session-title / goal fold**: a marker does not disturb `foldSessionTitle` / `foldGoal`.
60
58
  - **client ordering**: turn-tail ordering + `step/start` uniqueness hold for tool turns + marker logs.
61
59
  - **rewind across a compact checkpoint**: `RewindError('not-on-surface')` refuses cleanly, no crash.
62
- - **plan-mode**: a marker reuses the last started turn (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`).
60
+ - **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`).
63
61
  - **agent-loop cancellation**: `finally` guarantees step/turn closure; the rewind force-stop path leaves no dangling frame.
64
- - **settings-card cross-version reach**: the Snapshot cleanup card must be registered through a **nested** `ctx.inject(['settingsScope'], …)` — naming `settingsScope` in the module-level inject unmounts the whole client plugin on rc.2 (card and rewind button disappear). It uses only the rc.2↔alpha-common scope subset (`getSnapshot().value` + `set`), never the alpha-only `mutate`.
62
+ - **settings-card registration**: the Snapshot cleanup card must be registered through a **nested** `ctx.inject(['settingsScope'], …)` — naming `settingsScope` in the module-level inject would keep the whole client plugin unmounted on a host without that service (card and rewind button would disappear). It reads `getSnapshot().value` + `set`, never the `mutate` write API.
65
63
 
66
64
  ## Known behavior boundaries (deterministic differences, non-crash, documented)
67
65
 
68
- - **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.
69
- - **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
+ - **session-stats / session-telemetry fold the full log**: post-rewind stats do **not** rewind — `turns`/`steps`/`llmMs` still include withdrawn content; the empty `user/message` marker is folded as a present-but-empty user turn (it adds no step). This is the intended "fold the full log" semantics, pinned by probe.
67
+ - **token-meter usage anchor stays stable** (G3): the empty `user/message` marker carries no usage, but because it is not an `assistant/message`, the baseline anchor does not drop to a heuristic estimate it stays `usage` across a rewind. Pinned by `compat-gaps` G3.
70
68
  - **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).
71
69
  - **Session title auto-regeneration**: the title derives from the surface, so an automatically-derived title may change after a rewind.
72
70
  - **Files written but uncommitted in a cancelled turn**: a `both` rewind cannot restore them (tool side-effect timing; same as Claude Code).
@@ -80,7 +78,7 @@ The plugin treats these as harness-side defects it does not compensate for. Each
80
78
  ### RU-I18N: host-side locale preference is not reliably readable at command registration (harness-side structural timing defect; plugin reads once, never retries)
81
79
 
82
80
  - **Root cause (harness-side)**: the plugin resolves `activeLocale` in an
83
- `ctx.inject(['settings'])` callback that reads `settings.get(settingsNamespace('locale')).preference`
81
+ `ctx.inject(['settings'])` callback that reads `settings.get('locale').preference`
84
82
  **once, with no retry** (`src/index.ts`). `dsh-client-locale`'s host half
85
83
  registers that `locale` settings section **through its own `ctx.inject(['settings'])`**
86
84
  (`packages/client/locale/src/index.ts`). Both callbacks wait only on `settings` and are
@@ -103,7 +101,7 @@ The plugin treats these as harness-side defects it does not compensate for. Each
103
101
  strings, e.g. `/goal` — so this behavior is consistent with the ecosystem, not a plugin
104
102
  deviation.)
105
103
 
106
- ### R-OPENSTEP: an unclosed `step` in the log lets a rewind break token-meter replay (harness-side; plugin guard attempted and reverted)
104
+ ### R-OPENSTEP (rewind part resolved): an unclosed `step` in the log breaks token-meter replay; the rewind no longer compounds it
107
105
 
108
106
  > **Root cause (harness-side)**: an unclosed step left by a crash makes token-meter replay
109
107
  > reject any later step activity. DSH `0.1.1-rc.2` now auto-closes crash-left step/turn/tool
@@ -137,7 +135,7 @@ new turn at `phase.turn + 1` (`agent.ts:251-255`) **without closing the leftover
137
135
  - **The conversation itself is unaffected** (the request path does not call `tokenMeter.measure`; only compaction-basic does tree-wide).
138
136
  - **Manual `/compact` fails permanently** (`compactNow`'s first `measure()` throws the raw error).
139
137
  - **Automatic compaction silently stays disabled** (the `agent/pre-step` hook catches and warns; the conversation continues).
140
- - **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."
138
+ - **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.
141
139
 
142
140
  ## Uncovered boundaries (need an additional e2e layer; non-blocking)
143
141
 
@@ -21,7 +21,7 @@ the plugin itself uses (it never reads the DOM or host copy):
21
21
  ```ts
22
22
  import { hiddenSeqsOf, type HiddenChat } from 'dsh-rewind-plugin/client'
23
23
 
24
- const chat = session.getSnapshot().chat // rc.2 source; 0.1.2-rc.1 uses the uiConversation "chat" view
24
+ const chat = uiConversation.binding(sessionId).target('chat')?.getSnapshot() // the 0.1.2-rc.1 "chat" view
25
25
  const hidden = hiddenSeqsOf(chat as HiddenChat) // Set<number> of anchor seqs
26
26
  ```
27
27
 
@@ -20,7 +20,7 @@
20
20
  ```ts
21
21
  import { hiddenSeqsOf, type HiddenChat } from 'dsh-rewind-plugin/client'
22
22
 
23
- const chat = session.getSnapshot().chat // rc.2 来源;0.1.2-rc.1 uiConversation "chat" 视图
23
+ const chat = uiConversation.binding(sessionId).target('chat')?.getSnapshot() // 0.1.2-rc.1 "chat" 视图
24
24
  const hidden = hiddenSeqsOf(chat as HiddenChat) // 被隐藏的 anchor seq 集合
25
25
  ```
26
26
 
package/docs/format.md CHANGED
@@ -103,9 +103,11 @@ interface RestoreJournalAction {
103
103
  }
104
104
  ```
105
105
 
106
- States: `running` and `rollback-running` are non-terminal; a host restart turns
107
- them into `interrupted` (or `recovery-required` when the journal is corrupt or
108
- a rollback could not complete). `completed` / `rolled-back` are terminal.
106
+ States: `running` and `rollback-running` are non-terminal; `completed` /
107
+ `rolled-back` are terminal. A restart never rewrites the journal a
108
+ reconciliation *reports* a still non-terminal op as `interrupted` (or
109
+ `recovery-required` when the journal is corrupt or a rollback could not
110
+ complete), while the journal itself stays `running` / `rollback-running`.
109
111
 
110
112
  ## Write guarantees
111
113
 
@@ -144,11 +146,12 @@ a rollback could not complete). `completed` / `rolled-back` are terminal.
144
146
 
145
147
  ## Versioning policy
146
148
 
147
- The journal schema is `version: 1`. Checkpoint entries currently carry no
148
- version field. A future incompatible format must either bump the journal
149
- `version` (readers reject unknown values there is no best-effort fallback or
150
- legacy coercion) or move the state root (e.g. `rewind-snapshots/v2`) and ship
151
- an explicit migration tool. Old-format data is never silently re-interpreted.
149
+ The journal schema is `version: 1` (currently descriptive `isRestoreJournal`
150
+ validates shape, not version). Checkpoint entries carry no version field. A
151
+ future incompatible format should either make readers reject an unknown
152
+ `version` (there is no best-effort fallback or legacy coercion) or move the
153
+ state root (e.g. `rewind-snapshots/v2`) and ship an explicit migration tool.
154
+ Old-format data is never silently re-interpreted.
152
155
 
153
156
  ## Cleanup policy persistence
154
157
 
@@ -39,8 +39,8 @@ Also under `docs/` at the repo root: `persistence-catalog.md` (full
39
39
  | `tools/pre-execute` / `execute` / `post-execute` | [packages/core/tools/src/index.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/core/tools/src/index.ts) |
40
40
  | Client DOM anchors (`data-chat-flow-kind` / `data-chat-anchor-key`) | [packages/client/ui-conversation/src/client/chat/ChatNodeSeat.tsx](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/client/ui-conversation/src/client/chat/ChatNodeSeat.tsx) |
41
41
  | User bubble rendering | [packages/client/ui-conversation/src/client/chat/MessageItem.tsx](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/client/ui-conversation/src/client/chat/MessageItem.tsx) |
42
- | Client `SessionFace` (`command` / `cancel`) | [packages/client/runtime/src/client/contract/session.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/client/runtime/src/client/contract/session.ts) |
43
- | Client `PendingWait` (`respond`) | [packages/client/runtime/src/client/sessions/pending.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/client/runtime/src/client/sessions/pending.ts) |
42
+ | Client `SessionFace` (`command` / `cancel`) | [packages/api/session-controller/src/client/contract/session.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/api/session-controller/src/client/contract/session.ts) |
43
+ | Client pending interaction (SessionPendingInteraction) | [packages/client/ui-session/src/client/index.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/client/ui-session/src/client/index.ts) |
44
44
 
45
45
  ## Plugin source layout
46
46
 
@@ -49,16 +49,29 @@ the canonical source; this block only adds the finer-grained client-side files
49
49
  and packaging entries.
50
50
 
51
51
  ```
52
- src/index.ts host plugin: /rewind command + checkpoint pipeline (tools/execute|post-execute)
52
+ src/index.ts host plugin: /rewind|/undo|/snapshot-auto-cleanup|/dsh-rewind-fix
53
+ + checkpoint pipeline (tools/execute|post-execute)
53
54
  src/rewind.ts pure planning: target resolution, surface range, candidate listing
55
+ src/rewind-fix.ts /dsh-rewind-fix orchestration (repair pipeline, locks, rollback)
56
+ src/rewind-marker-repair.ts pure legacy-marker transform (A/B → C)
57
+ src/session-log-io.ts session-log zstd codec + lossless re-encoder (rewind-fix write-back)
54
58
  src/snapshot.ts checkpoint store (disk before-backups, restore/preview, bounded prune)
59
+ src/snapshot-cleanup.ts cleanup policy + dsh-settings persistence + auto-sweep throttle
55
60
  src/session-cwd.ts session-cwd resolution (fs-tools rule)
56
- src/client/index.ts client plugin: /rewind command decoration + per-message ↶ button portals
61
+ src/locales.ts host i18n (t() renderer, HostKey)
62
+ src/client/index.ts client plugin: /rewind command decoration + per-message ↶ button portals;
63
+ re-exports the client contract (hiddenSeqsOf / targetSeqOfArgs / HiddenChat)
57
64
  src/client/popover.ts mode-selection popover (both-mode impact confirm)
58
65
  src/client/hidden.ts withdrawn-span computation (hiddenSeqsOf), pure
66
+ src/client/candidates.ts rewind candidate listing (rewindCandidatesOf), pure
67
+ src/client/pending.ts pending-steering bubble ↔ queue-mirror matching, pure
59
68
  src/client/locales.ts zh / en copy (LocaleNamespaceMap)
60
69
  src/client/styles.ts injected styles (dsh design tokens)
70
+ src/client/build-info.ts client build identity (__DSH_REWIND_VERSION__ / __DSH_REWIND_BUILD__)
71
+ src/client/log.ts client logger (namespaced, dsh-rewind.debug-gated)
61
72
  scripts/build.mjs esbuild: lib/index.js (host ESM) + lib/client.js (loader closure) + .d.ts
73
+ scripts/check-dsh-version.mjs DSH peer-tuple check (latest dist-tag vs peers)
74
+ scripts/update-badge.mjs regenerate the tests badge (CI only)
62
75
  scripts/verify-host.mjs end-to-end host verification (full check suite)
63
76
  tests/ vitest suites (rewind / snapshot / hidden / session-cwd / integration)
64
77
  docs/ maintainer docs: contract/, compat/, release/ subdirectories