dsh-rewind-plugin 0.10.0-alpha.1 → 0.11.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 +3 -3
- package/README.md +3 -3
- package/SECURITY.md +10 -7
- package/docs/architecture.md +13 -9
- package/docs/compat/audit.md +13 -3
- package/docs/release/release.md +7 -0
- package/docs/release/release.zh.md +5 -0
- package/docs/rewind-fix.md +9 -8
- package/docs/rewind-fix.zh.md +9 -8
- package/lib/client.js +11 -5
- package/lib/index.js +105 -6
- package/lib/types/client/hidden.d.ts +1 -1
- package/lib/types/client/portals.d.ts +14 -0
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/index.d.ts +6 -6
- package/lib/types/locales.d.ts +5 -0
- package/lib/types/rewind.d.ts +22 -7
- package/lib/types/snapshot.d.ts +51 -0
- package/package.json +47 -44
package/README.en.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# dsh-rewind
|
|
2
2
|
|
|
3
3
|
> [!WARNING]
|
|
4
|
-
> **
|
|
4
|
+
> **Install a `v0.9.x` release on DSH `0.1.2-rc.1` as early as possible, and run `/dsh-rewind-fix` to update old sessions' rewind markers** ([update guide](docs/rewind-fix.md)).
|
|
5
5
|
|
|
6
6
|
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).
|
|
7
7
|
|
|
@@ -96,8 +96,8 @@ The whole design rests on two principles, simple but deliberate: **the conversat
|
|
|
96
96
|
|
|
97
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.
|
|
98
98
|
|
|
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
|
|
100
|
-
- The replacement is **
|
|
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 marker 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 **imperceptible** — the model ignores the empty marker, with no effect (verified 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
101
|
- Because this is **masking, not deleting**, every withdrawn event stays in the log — auditable, traceable, viewable, and in principle manually recoverable.
|
|
102
102
|
|
|
103
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.
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# dsh-rewind
|
|
2
2
|
|
|
3
3
|
> [!WARNING]
|
|
4
|
-
>
|
|
4
|
+
> **请尽早在 DSH `0.1.2-rc.1` 安装 `v0.9.x` 版本,运行 `/dsh-rewind-fix` 更新旧会话的回退标记**([更新指南](docs/rewind-fix.zh.md))。
|
|
5
5
|
|
|
6
6
|
DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消息**——同窗口内完成,不新建分支、不换窗口,可一并还原工作区文件(完整 Claude Code `/rewind` 语义)。
|
|
7
7
|
|
|
@@ -86,8 +86,8 @@ dsh plugin --profile web add dsh-rewind-plugin
|
|
|
86
86
|
|
|
87
87
|
`append-only` 是铁律:会话日志只追加、从不改写——这是可审计与信息安全的地基。回退从不动历史,它只做一步:往日志末尾追加一条**内容为空的标记消息**,把目标消息之后的全部内容「遮蔽 + 替换」掉,让模型和界面都只看得到目标之前的部分。
|
|
88
88
|
|
|
89
|
-
- **标记是规范的**——插件复刻 `/compact` 标准的「隐藏 + 替换」:`/compact` 把一段历史压缩成摘要,`/rewind`
|
|
90
|
-
-
|
|
89
|
+
- **标记是规范的**——插件复刻 `/compact` 标准的「隐藏 + 替换」:`/compact` 把一段历史压缩成摘要,`/rewind` 则换成一条空标记消息。由于其规范性,harness 的日志重放、`/compact` 压缩、续接检查都能正确识别它,绝不会把它误认为真实对话;
|
|
90
|
+
- **替换内容无感**——模型对空标记忽略、无感(实测验证)。配合插件对界面显示的处理,模型和你看到的对话就是目标消息当时的样子,真正的「就地」;
|
|
91
91
|
- 因为是「遮蔽」而非「删除」,**被撤回的每一条事件都完整留在日志里**,可审计、可追溯、可查看,原则上也随时能手动恢复。
|
|
92
92
|
|
|
93
93
|
> **设计点睛**:整个对话回退就是**一条**追加。它确定、可审计,且因为日志从未被破坏,回溯是「干净的」——用最小的动作,实现最完整的语义。那些与 harness 内部的兼容细节(对 `/compact` 的复刻、空消息的遮蔽)正是插件的专业所在,每一条都由专门的探针测试固化。
|
package/SECURITY.md
CHANGED
|
@@ -59,13 +59,16 @@ than hidden.
|
|
|
59
59
|
A failed gate fails closed: an invalid target, a missing store, an absent
|
|
60
60
|
backup, or a cancelled invocation aborts the rewind with an error.
|
|
61
61
|
|
|
62
|
-
**Automatic store deletion
|
|
63
|
-
`snapshot-auto-cleanup`
|
|
64
|
-
**long-inactive** sessions
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
**Automatic store deletion**: two things delete session snapshots. The opt-in
|
|
63
|
+
`snapshot-auto-cleanup` sweep (default off) removes the whole directories of
|
|
64
|
+
**long-inactive** sessions, unrelated to restores. Separately, a session's
|
|
65
|
+
snapshots are cleared automatically when the conversation log's session format
|
|
66
|
+
has changed (a DSH upgrade), because those backups are anchored to message
|
|
67
|
+
positions that no longer line up. Both stay confined to the store root, use
|
|
68
|
+
`lstat` (so they never follow a symlink out of the root), the sweep never targets
|
|
69
|
+
the active session, and neither touches the conversation log. When auto-cleanup
|
|
70
|
+
is disabled (the default), only the format-change clear runs, and only for an
|
|
71
|
+
affected session.
|
|
69
72
|
|
|
70
73
|
## Conversation integrity
|
|
71
74
|
|
package/docs/architecture.md
CHANGED
|
@@ -55,8 +55,8 @@ machine channels (see [Compatibility strategy](#compatibility-strategy)).
|
|
|
55
55
|
user/message currently on the surface)
|
|
56
56
|
→ agent.cancel({ keepInbox: true }) if running; waitForAgentIdle
|
|
57
57
|
→ dropPendingSteering (next-step inbox only; queued messages untouched)
|
|
58
|
-
→ append the rewind marker =
|
|
59
|
-
{ op: 'replace',
|
|
58
|
+
→ append the rewind marker = a user/message with surfaceOp
|
|
59
|
+
{ op: 'replace', startSeq, endSeq } over every surface node after the
|
|
60
60
|
target (+ sourceEventSeqs = shadowed seqs) [a single event]
|
|
61
61
|
→ if mode 'both': store.restoreAfter(targetSeq) + syncRestoreObservations
|
|
62
62
|
→ result text carries machine tokens (impact=<n>, restore:/delete: lines)
|
|
@@ -68,11 +68,14 @@ Key invariants:
|
|
|
68
68
|
|
|
69
69
|
- **The log is append-only.** The marker is the *only* mutation: it cuts the
|
|
70
70
|
model-visible surface, never the raw history (search/export still see it).
|
|
71
|
-
- **The marker is
|
|
72
|
-
source
|
|
73
|
-
shadowed seqs (`sourceEventSeqs`) — `assistant/message` can
|
|
74
|
-
them (v2). It derives to itself, so it stays as a present
|
|
75
|
-
turn at the surface tail
|
|
71
|
+
- **The marker content is a constant `(empty message)` placeholder.** It carries
|
|
72
|
+
the dsh-rewind plugin source and is a `user/message`, the only surface type
|
|
73
|
+
that can cite the shadowed seqs (`sourceEventSeqs`) — `assistant/message` can
|
|
74
|
+
no longer carry them (v2). It derives to itself, so it stays as a present
|
|
75
|
+
user turn at the surface tail. The content is provider-independent and never
|
|
76
|
+
empty: the session log is immutable but the model serving a session may
|
|
77
|
+
change later, and a strict OpenAI-compatible gateway rejects an empty user
|
|
78
|
+
message (HTTP 400, Issue #21).
|
|
76
79
|
- **No ghost step frame is needed**: the token-meter step machine ignores
|
|
77
80
|
`user/message`, and the session invariant imposes no open-turn requirement
|
|
78
81
|
on it, so the marker is appended while idle, outside any turn, as one event.
|
|
@@ -82,8 +85,9 @@ Key invariants:
|
|
|
82
85
|
|
|
83
86
|
### Marker format history
|
|
84
87
|
|
|
85
|
-
The rewind marker is written as form C (
|
|
86
|
-
`surfaceOp.replace` over the shadowed range
|
|
88
|
+
The rewind marker is written as form C (a `user/message` with a
|
|
89
|
+
`surfaceOp.replace` over the shadowed range, `sourceEventSeqs`, and the
|
|
90
|
+
constant `(empty message)` content). Earlier
|
|
87
91
|
plugin versions wrote shapes a newer harness no longer accepts:
|
|
88
92
|
|
|
89
93
|
- **form A** — a bare `assistant/message(turn=N, step=0)` with no frame.
|
package/docs/compat/audit.md
CHANGED
|
@@ -50,7 +50,7 @@ legacy branch).
|
|
|
50
50
|
|
|
51
51
|
## Verified-compatible surfaces (probes pass)
|
|
52
52
|
|
|
53
|
-
- **token-meter replay** (the
|
|
53
|
+
- **token-meter replay** (the `user/message` marker + multiple rewinds + interleaved real turns + compact stacking).
|
|
54
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.
|
|
55
55
|
- **resume replay**: `Session.create(id, events)` replays a rewind/compact-bearing log.
|
|
56
56
|
- **session-stats**: the `user/message` marker adds no step (the step count stays at the real turns' steps), no phantom turn.
|
|
@@ -63,8 +63,10 @@ legacy branch).
|
|
|
63
63
|
|
|
64
64
|
## Known behavior boundaries (deterministic differences, non-crash, documented)
|
|
65
65
|
|
|
66
|
-
- **session-stats / session-telemetry fold the full log**: post-rewind stats do **not** rewind — `turns`/`steps`/`llmMs` still include withdrawn content; the
|
|
67
|
-
- **token-meter usage anchor stays stable** (G3): the
|
|
66
|
+
- **session-stats / session-telemetry fold the full log**: post-rewind stats do **not** rewind — `turns`/`steps`/`llmMs` still include withdrawn content; the `user/message` marker is folded as a present 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 `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.
|
|
68
|
+
- **marker content is the constant `(empty message)` placeholder**: never empty, because the session log is immutable but the model serving a session may change later — a strict OpenAI-compatible gateway rejects an empty user message (HTTP 400, Issue #21). The marker is a small visible user turn in derived history. Pin: `verify-host` (`marker is a user/message with the dsh-rewind plugin source and the (empty message) placeholder`).
|
|
69
|
+
- **marker shape (v3)**: the replace `surfaceOp` is `{ op: 'replace', startSeq, endSeq }` (renamed from `start`/`end` in session-format v3), and the dsh-rewind source now carries `targetSeq`; the `user/message` + `sourceEventSeqs` shape and the `(empty message)` content are unchanged.
|
|
68
70
|
- **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).
|
|
69
71
|
- **Session title auto-regeneration**: the title derives from the surface, so an automatically-derived title may change after a rewind.
|
|
70
72
|
- **Files written but uncommitted in a cancelled turn**: a `both` rewind cannot restore them (tool side-effect timing; same as Claude Code).
|
|
@@ -100,6 +102,14 @@ The plugin treats these as harness-side defects it does not compensate for. Each
|
|
|
100
102
|
are English for **every** host command — system plugins also pass raw English `description`
|
|
101
103
|
strings, e.g. `/goal` — so this behavior is consistent with the ecosystem, not a plugin
|
|
102
104
|
deviation.)
|
|
105
|
+
- **Client-side command-description i18n is first-party-only**: DSH also localizes host command
|
|
106
|
+
descriptions through the client `locale` binding (`ui-commands`), but the description keys come
|
|
107
|
+
from a **closed allowlist** (`HOST_DESCRIPTION_KEYS`: compact, export, feedback, goal,
|
|
108
|
+
permission, plan). A command outside that set — every third-party plugin — is passed through
|
|
109
|
+
verbatim, never translated (`hostDescription` only rewrites a description that equals the
|
|
110
|
+
first-party English copy). So the plugin's own `/rewind` command description can never ride this
|
|
111
|
+
channel either; it is authored in the host's tongue (English by default), same as the host
|
|
112
|
+
runtime copy above.
|
|
103
113
|
|
|
104
114
|
### R-OPENSTEP (rewind part resolved): an unclosed `step` in the log breaks token-meter replay; the rewind no longer compounds it
|
|
105
115
|
|
package/docs/release/release.md
CHANGED
|
@@ -91,6 +91,13 @@ appended) when DSH releases a new tuple.
|
|
|
91
91
|
- **After DSH goes final**: final releases are not bound by the prerelease
|
|
92
92
|
tuple rule, so the peers can converge to a single stable range (e.g.
|
|
93
93
|
`^0.1.x`); this section can then be deleted.
|
|
94
|
+
- **Declared minimum (`dsh.engines.dsh`)**: alongside the peer tuple, each
|
|
95
|
+
release declares the DSH runtime floor under `dsh.engines.dsh` (e.g.
|
|
96
|
+
`>=0.1.2-rc.1`), consumed by the plugin-manager update guard. Bump it in
|
|
97
|
+
the same release that raises the peer tuple; never leave code raised while
|
|
98
|
+
the declared floor stays behind. Only the `>=X.Y.Z[-pre]` form is
|
|
99
|
+
supported (`^`/`~`/multi-range are treated as "cannot verify" and
|
|
100
|
+
fail closed).
|
|
94
101
|
|
|
95
102
|
## Versioned-line release model
|
|
96
103
|
|
|
@@ -79,6 +79,11 @@ DSH 仍在 rc 阶段,npm 的 prerelease 匹配规则要求 peer 范围与宿
|
|
|
79
79
|
→ devDependencies 同步升到最新 → `npm install` → `npm run check` → 发版。
|
|
80
80
|
- **正式版后收敛**:DSH 发布 final 版本后,正式版不受 prerelease 元组规则
|
|
81
81
|
限制,peer 可收敛为稳定的 `^0.1.x` 单范围,此节即可删除。
|
|
82
|
+
- **声明的最低运行时(`dsh.engines.dsh`)**:与 peer 元组一起,每个发布在
|
|
83
|
+
`dsh.engines.dsh` 声明 DSH 运行时下限(如 `>=0.1.2-rc.1`),供插件管理器
|
|
84
|
+
更新守卫读取。**与 peer 元组持平的同一发布里一并 bump**;不可只升代码、
|
|
85
|
+
声明下限停留在旧值。仅支持 `>=X.Y.Z[-pre]` 形式(`^`/`~`/多范围会被视为
|
|
86
|
+
「无法校验」而 fail-closed)。
|
|
82
87
|
|
|
83
88
|
## 发布版本线模型
|
|
84
89
|
|
package/docs/rewind-fix.md
CHANGED
|
@@ -4,18 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
## Applies to
|
|
6
6
|
|
|
7
|
-
- **DSH**: `0.1.2-rc.1
|
|
8
|
-
- **
|
|
7
|
+
- **DSH version**: `0.1.2-rc.1`.
|
|
8
|
+
- **Plugin version**: `v0.9.0-alpha.1`, `v0.9.0-alpha.2`, `v0.9.0`, `v0.9.1` (recommended).
|
|
9
|
+
- **Audience**: users planning to use a future DSH version who used an early plugin (≤ 0.8.0).
|
|
9
10
|
|
|
10
11
|
## Background
|
|
11
12
|
|
|
12
|
-
`dsh-rewind` rewinds **within the same session** — it never forks a branch
|
|
13
|
+
`dsh-rewind` rewinds **within the same session** — it never forks a branch, and the session log stays the single source. The plugin follows a conservative policy: it deletes no history, and only **appends a rewind marker** telling DSH "the model-visible conversation continues from this message; everything after it is rolled back". Older plugin versions wrote that **rewind marker** as a "ghost step frame", which worked reliably on DSH `v0.1.2-rc.1` and earlier.
|
|
13
14
|
|
|
14
|
-
But DSH v0.1.3
|
|
15
|
+
But starting with DSH `v0.1.3-alpha.1`, a stricter **session-format validation** is introduced. We discovered ahead of time that the **rewind markers** written by older plugin versions **cannot be validated** under those stricter checks, so **a session that was rewound may fail to open**.
|
|
15
16
|
|
|
16
17
|
The plugin is prepared in two parts, both shipped in the new version:
|
|
17
18
|
|
|
18
|
-
1. **New rewind markers use the new shape** (forward) — this is a **correct, low-risk**, long-term change (see the [README](../README.en.md), the "How it works" section), and the new shape is fully compatible with **both old and new** DSH.
|
|
19
|
+
1. **New rewind markers use the new shape** (forward) — this is a **correct, low-risk**, long-term change aligned with the official `/compact` design (see the [README](../README.en.md), the "How it works" section), and the new shape is fully compatible with **both old and new** DSH.
|
|
19
20
|
|
|
20
21
|
2. **The `/dsh-rewind-fix` command** (backward) — for **already-existing** old sessions, the plugin ships a convenient update command that translates those old markers into the new shape so the sessions are usable again.
|
|
21
22
|
|
|
@@ -54,7 +55,7 @@ Then run the command **in this new session**, and it will update the old session
|
|
|
54
55
|
|
|
55
56
|
### Step 4 · Preview the update scope
|
|
56
57
|
|
|
57
|
-
Type `/dsh-rewind-fix` in the composer and send it. It only scans, doesn't write. The editor locks while it runs, which is normal. **Don't switch session windows midway
|
|
58
|
+
Type `/dsh-rewind-fix` in the composer and send it. It only scans, doesn't write. The editor locks while it runs, which is normal. **Don't switch session windows midway**. When it finishes, it reports how many sessions were scanned, how many will be updated, how many skipped, and how many failed. Confirm those are what you expect before moving on.
|
|
58
59
|
```
|
|
59
60
|
/dsh-rewind-fix
|
|
60
61
|
```
|
|
@@ -73,7 +74,7 @@ After the run, restart DSH, then **preview once more**:
|
|
|
73
74
|
```
|
|
74
75
|
/dsh-rewind-fix
|
|
75
76
|
```
|
|
76
|
-
If it says there are no more sessions to update, the markers are all current and the old sessions will open normally.
|
|
77
|
+
If it says there are no more sessions to update, the markers are all current and the old sessions will open normally. If there are still sessions to update, they may have been loaded during the run — restart in a temporary session window and retry.
|
|
77
78
|
|
|
78
79
|
### Step 7 · Delete the backup after confirming (optional)
|
|
79
80
|
|
|
@@ -84,5 +85,5 @@ rm -rf ~/.dsh/sessions.backup ~/.dsh/rewind-snapshots.backup
|
|
|
84
85
|
|
|
85
86
|
## Notes
|
|
86
87
|
|
|
87
|
-
- The session logs are still v0 format; when DSH's later `v0 → v1 → v2` migration
|
|
88
|
+
- The session logs are still v0 format; when DSH's later `v0 → v1 → v2 → v3` migration arrives, if you've completed the `rewind` marker update, the markers themselves won't block it.
|
|
88
89
|
- Some sessions may still be blocked by an **unclosed turn** (a turn that was interrupted/cancelled and never wrote `turn/end`). Per the empirical analysis these are widespread and **unrelated to rewind** — they're a DSH-side issue and this tool doesn't handle them. **So updating the markers doesn't guarantee you can upgrade to the next DSH line.**
|
package/docs/rewind-fix.zh.md
CHANGED
|
@@ -4,18 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
## 适配
|
|
6
6
|
|
|
7
|
-
- **DSH
|
|
8
|
-
-
|
|
7
|
+
- **DSH 版本**:`v0.1.2-rc.1`。
|
|
8
|
+
- **插件版本**:`v0.9.0-alpha.1`,`v0.9.0-alpha.2`,`v0.9.0`,`v0.9.1`(推荐)。
|
|
9
|
+
- **适用人群**:计划使用未来版本 DSH,且使用过早期版本插件(≤ 0.8.0)的用户。
|
|
9
10
|
|
|
10
11
|
## 背景
|
|
11
12
|
|
|
12
|
-
`dsh-rewind`
|
|
13
|
+
`dsh-rewind` 的回退是**同一会话**的消息回退。它从不新建分支,会话日志保持同一份。插件遵循保守策略:不删除任何历史,只在会话日志里**追加一条 rewind 标记**,表示“模型可见的对话从这条消息继续,其后内容回卷”。旧版插件的 **rewind 标记**用的是「幽灵步骤框架」写法,在 DSH `v0.1.2-rc.1` 及之前的版本长期验证有效。
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
但从 `v0.1.3-alpha.1` 开始,DSH 引入了更严格的**会话格式校验**。我们提前发现:旧版本往会话日志里写入的 **rewind 标记**,在这种严格校验下**无法自证合法**,届时会导致**回退过的旧会话打不开**。
|
|
15
16
|
|
|
16
17
|
插件做了两阶段准备,已在新版本中包含:
|
|
17
18
|
|
|
18
|
-
1. **新产生的 rewind
|
|
19
|
+
1. **新产生的 rewind 标记换成新格式**(向前)——这一步是**正确、低风险**的长期改动(对标官方 `/compact` 设计,见 [README](../README.md)「原理」一节),新格式对**新旧版本** DSH 均完全兼容。
|
|
19
20
|
|
|
20
21
|
2. **提供 `/dsh-rewind-fix` 命令**(向后)——对于**已经存在**的旧会话,本插件提供便捷更新命令,把旧标记「翻译」成新格式,让会话重新可用。
|
|
21
22
|
|
|
@@ -54,7 +55,7 @@ cp -r ~/.dsh/rewind-snapshots ~/.dsh/rewind-snapshots.backup
|
|
|
54
55
|
|
|
55
56
|
### 第四步 · 预览更新范围
|
|
56
57
|
|
|
57
|
-
在编辑区域输入 `/dsh-rewind-fix`
|
|
58
|
+
在编辑区域输入 `/dsh-rewind-fix` 并发送。它只扫描、不写盘。执行时编辑区会正常锁定。**不要中途切换会话窗口**。完成后会告诉你:扫描了几个会话、其中几个将被更新、几个跳过、几个失败。确认是你预期的那几项,再进下一步。
|
|
58
59
|
```
|
|
59
60
|
/dsh-rewind-fix
|
|
60
61
|
```
|
|
@@ -73,7 +74,7 @@ cp -r ~/.dsh/rewind-snapshots ~/.dsh/rewind-snapshots.backup
|
|
|
73
74
|
```
|
|
74
75
|
/dsh-rewind-fix
|
|
75
76
|
```
|
|
76
|
-
|
|
77
|
+
如果提示已经没有待更新的会话,说明标记已全部更新到位,旧会话就能正常打开了。如果还有未更新的会话,可能是执行时被加载过,在临时会话窗口重启并重试即可。
|
|
77
78
|
|
|
78
79
|
### 第七步 · 确认正常后删除备份(可选)
|
|
79
80
|
|
|
@@ -84,5 +85,5 @@ rm -rf ~/.dsh/sessions.backup ~/.dsh/rewind-snapshots.backup
|
|
|
84
85
|
|
|
85
86
|
## 补充说明
|
|
86
87
|
|
|
87
|
-
- 当前会话日志仍为 v0 格式;DSH
|
|
88
|
+
- 当前会话日志仍为 v0 格式;DSH 后续的 `v0 → v1 → v2 → v3` 迁移到来时,如果你完成了 `rewind` 标记更新,标记本身就不会阻断它。
|
|
88
89
|
- 个别会话还可能因**未闭合的 turn**(某次回合被打断/取消、没写 `turn/end`)等问题而卡住迁移——根据实测分析,这些问题普遍存在,**与 rewind 无关**,属 DSH 侧自身问题,本工具不处理。**所以更新完标记并不保证一定能升到下一 DSH 线。**
|
package/lib/client.js
CHANGED
|
@@ -399,7 +399,7 @@ var STYLE = `
|
|
|
399
399
|
.dsh-rewind-cleanup-body {
|
|
400
400
|
border-top: 0.5px solid var(--dsw-alias-border-l2);
|
|
401
401
|
margin: 0 16px;
|
|
402
|
-
padding:
|
|
402
|
+
padding: 0 0 8px;
|
|
403
403
|
}
|
|
404
404
|
.dsh-rewind-cleanup-readonly {
|
|
405
405
|
margin: 12px 0 0;
|
|
@@ -421,6 +421,12 @@ var STYLE = `
|
|
|
421
421
|
.dsh-rewind-cleanup-field + .dsh-rewind-cleanup-field {
|
|
422
422
|
border-top: 0.5px solid var(--dsw-alias-border-l2);
|
|
423
423
|
}
|
|
424
|
+
/* Auto-cleanup (permission) block and the max-age field are consecutive control
|
|
425
|
+
* sections of the open card \u2014 give them the same 0.5px divider the harness gives
|
|
426
|
+
* every pair of config fields (fields.module.css field + field). */
|
|
427
|
+
.dsh-rewind-cleanup-permission + .dsh-rewind-cleanup-field {
|
|
428
|
+
border-top: 0.5px solid var(--dsw-alias-border-l2);
|
|
429
|
+
}
|
|
424
430
|
.dsh-rewind-cleanup-head {
|
|
425
431
|
display: flex;
|
|
426
432
|
align-items: center;
|
|
@@ -505,7 +511,7 @@ var STYLE = `
|
|
|
505
511
|
box-sizing: border-box;
|
|
506
512
|
height: 34px;
|
|
507
513
|
padding: 0 12px;
|
|
508
|
-
border:
|
|
514
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
509
515
|
border-radius: 8px;
|
|
510
516
|
background: var(--dsw-alias-bg-layer-3);
|
|
511
517
|
font: inherit;
|
|
@@ -530,7 +536,7 @@ var STYLE = `
|
|
|
530
536
|
justify-content: flex-end;
|
|
531
537
|
gap: 8px;
|
|
532
538
|
padding: 12px 0 4px;
|
|
533
|
-
border-top:
|
|
539
|
+
border-top: 0.5px solid var(--dsw-alias-border-l2);
|
|
534
540
|
}
|
|
535
541
|
.dsh-rewind-cleanup-failed {
|
|
536
542
|
flex: 1;
|
|
@@ -1339,8 +1345,8 @@ function createRewindBridge(deps) {
|
|
|
1339
1345
|
}
|
|
1340
1346
|
|
|
1341
1347
|
// src/client/build-info.ts
|
|
1342
|
-
var PLUGIN_VERSION = true ? "0.
|
|
1343
|
-
var BUILD_HASH = true ? "
|
|
1348
|
+
var PLUGIN_VERSION = true ? "0.11.0-alpha.1" : "dev";
|
|
1349
|
+
var BUILD_HASH = true ? "d770983e" : "dev";
|
|
1344
1350
|
|
|
1345
1351
|
// src/client/locales.ts
|
|
1346
1352
|
var zh = {
|
package/lib/index.js
CHANGED
|
@@ -119,6 +119,10 @@ var RewindError = class extends Error {
|
|
|
119
119
|
}
|
|
120
120
|
code;
|
|
121
121
|
};
|
|
122
|
+
var REWIND_MARKER_BRAND = Object.freeze({ kind: "plugin", plugin: "dsh-rewind" });
|
|
123
|
+
function rewindMarkerSource(targetSeq) {
|
|
124
|
+
return Object.freeze({ ...REWIND_MARKER_BRAND, targetSeq });
|
|
125
|
+
}
|
|
122
126
|
var CANDIDATE_PREVIEW_CHARS = 80;
|
|
123
127
|
var DEFAULT_CANDIDATE_LIMIT = 100;
|
|
124
128
|
function isUserMessageEvent(event) {
|
|
@@ -356,6 +360,13 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
356
360
|
lastEntry = /* @__PURE__ */ new Map();
|
|
357
361
|
/** Sessions whose dedup state has been seeded from disk this process. */
|
|
358
362
|
seededSessions = /* @__PURE__ */ new Set();
|
|
363
|
+
/**
|
|
364
|
+
* Session-format version snapshots are anchored under, stamped into each
|
|
365
|
+
* session's `format` marker when an entry is recorded. `null` until the host
|
|
366
|
+
* sets it (from `agent/session-start`), so a session that never records is
|
|
367
|
+
* never materialized and a marker is only written where snapshots exist.
|
|
368
|
+
*/
|
|
369
|
+
formatVersion = null;
|
|
359
370
|
constructor(root, opts) {
|
|
360
371
|
this.dedup = opts?.dedup ?? true;
|
|
361
372
|
this.root = root ?? process.env[SNAPSHOT_ROOT_ENV] ?? join(resolveDshHome(opts?.dshHome), SNAPSHOT_DIR_NAME);
|
|
@@ -435,6 +446,9 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
435
446
|
await writeJsonAtomic(file, committed, () => opts?.crash?.("after-temp-write"));
|
|
436
447
|
this.lastEntry.set(key, { content: entry.before, ref: selfRef });
|
|
437
448
|
}
|
|
449
|
+
if (this.formatVersion !== null) {
|
|
450
|
+
await this.markFormatVersion(sessionId, this.formatVersion);
|
|
451
|
+
}
|
|
438
452
|
const now = Date.now();
|
|
439
453
|
if (now - this.lastPruneAt >= _SnapshotStore.PRUNE_INTERVAL_MS) {
|
|
440
454
|
this.lastPruneAt = now;
|
|
@@ -631,6 +645,8 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
631
645
|
}
|
|
632
646
|
/** Prefix of one restore-op journal file inside the session dir. */
|
|
633
647
|
static JOURNAL_PREFIX = "restore-journal-";
|
|
648
|
+
/** Session-format-version marker file inside the session dir. Non-`.json`, so it never counts as a checkpoint entry. */
|
|
649
|
+
static FORMAT_FILE = "format";
|
|
634
650
|
/** Absolute path of one restore-op journal file. */
|
|
635
651
|
journalPath(sessionId, opId) {
|
|
636
652
|
return join(this.sessionDir(sessionId), `${_SnapshotStore.JOURNAL_PREFIX}${safeFileId(opId)}.json`);
|
|
@@ -1275,6 +1291,75 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
1275
1291
|
}
|
|
1276
1292
|
return { sessionId, ...stats, dryRun };
|
|
1277
1293
|
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Read the session-format version marker recorded for a session, or `null`
|
|
1296
|
+
* when there is no marker — a pre-marker, legacy snapshot dir, or a session
|
|
1297
|
+
* that never materialized a dir.
|
|
1298
|
+
*/
|
|
1299
|
+
async readFormatVersion(sessionId) {
|
|
1300
|
+
try {
|
|
1301
|
+
const raw = await readFile(join(this.sessionDir(sessionId), _SnapshotStore.FORMAT_FILE), "utf8");
|
|
1302
|
+
const parsed = Number(raw.trim());
|
|
1303
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1304
|
+
} catch {
|
|
1305
|
+
return null;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Record the session-format version a session's snapshots are anchored under
|
|
1310
|
+
* (`session.header.version`: 2 for the v2 format, 3 for the v3 format). The
|
|
1311
|
+
* marker is a tiny non-`.json` file, so it never counts as a checkpoint
|
|
1312
|
+
* entry in `sessionStats`/`clearSession`. Written atomically (temp + rename)
|
|
1313
|
+
* like every other persisted marker, so a crash mid-write can only leave an
|
|
1314
|
+
* inert `format.tmp` — never a partial marker that a later reconcile could
|
|
1315
|
+
* misread as a version mismatch and wrongly clear.
|
|
1316
|
+
*/
|
|
1317
|
+
async markFormatVersion(sessionId, sessionVersion) {
|
|
1318
|
+
const dir = this.sessionDir(sessionId);
|
|
1319
|
+
await mkdir(dir, { recursive: true });
|
|
1320
|
+
const file = join(dir, _SnapshotStore.FORMAT_FILE);
|
|
1321
|
+
const tmp = `${file}.tmp`;
|
|
1322
|
+
await writeFile(tmp, `${sessionVersion}`, "utf8");
|
|
1323
|
+
await rename(tmp, file);
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Set the session-format version the store stamps onto every snapshot it
|
|
1327
|
+
* records. The host sets this once per process from `agent/session-start`
|
|
1328
|
+
* (`agent.session.header.version`), so a marker is only materialized for a
|
|
1329
|
+
* session that actually records a snapshot.
|
|
1330
|
+
*/
|
|
1331
|
+
setFormatVersion(sessionVersion) {
|
|
1332
|
+
this.formatVersion = sessionVersion;
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Session-format-version guard: clear a session's snapshot dir when the
|
|
1336
|
+
* format its snapshots were anchored under differs from the current session
|
|
1337
|
+
* format, so seq-anchored references can never survive a format migration
|
|
1338
|
+
* mis-mapped. Runs at `agent/session-start` — after DSH has migrated/loaded
|
|
1339
|
+
* the session, so `sessionVersion` is the post-migration value.
|
|
1340
|
+
*
|
|
1341
|
+
* Conservative rule (per the "delete stale snapshots" policy): a session
|
|
1342
|
+
* with no recorded marker but with snapshot content is treated as legacy and
|
|
1343
|
+
* cleared; a session whose marker differs from `sessionVersion` is cleared.
|
|
1344
|
+
* A matching version — or an untouched session with nothing to protect — is
|
|
1345
|
+
* left alone. The marker is re-stamped to the current version afterward so a
|
|
1346
|
+
* FUTURE format change is detected on the next start.
|
|
1347
|
+
*
|
|
1348
|
+
* @returns whether a session snapshot dir was cleared.
|
|
1349
|
+
*/
|
|
1350
|
+
async reconcileFormatVersion(sessionId, sessionVersion) {
|
|
1351
|
+
const stored = await this.readFormatVersion(sessionId);
|
|
1352
|
+
if (stored === sessionVersion) return { cleared: false };
|
|
1353
|
+
const stats = await this.sessionStats(sessionId);
|
|
1354
|
+
const hasContent = stats.anchorGroups > 0 || stats.journals > 0;
|
|
1355
|
+
if (hasContent) {
|
|
1356
|
+
await this.clearSession(sessionId);
|
|
1357
|
+
}
|
|
1358
|
+
if (hasContent || stored !== null) {
|
|
1359
|
+
await this.markFormatVersion(sessionId, sessionVersion);
|
|
1360
|
+
}
|
|
1361
|
+
return { cleared: hasContent };
|
|
1362
|
+
}
|
|
1278
1363
|
};
|
|
1279
1364
|
function hashPath(path) {
|
|
1280
1365
|
return createHash("sha256").update(path).digest("hex").slice(0, 8);
|
|
@@ -1511,12 +1596,11 @@ async function commitEntry(store, pending, anchorCache, trackedBySession, exec,
|
|
|
1511
1596
|
}
|
|
1512
1597
|
tracked.add(capture.path);
|
|
1513
1598
|
}
|
|
1514
|
-
var
|
|
1515
|
-
|
|
1516
|
-
function buildMarker() {
|
|
1599
|
+
var REWIND_MARKER_CONTENT = [{ type: "text", text: "(empty message)" }];
|
|
1600
|
+
function buildMarker(targetSeq) {
|
|
1517
1601
|
return createUserMessage({
|
|
1518
1602
|
content: REWIND_MARKER_CONTENT,
|
|
1519
|
-
source:
|
|
1603
|
+
source: rewindMarkerSource(targetSeq)
|
|
1520
1604
|
});
|
|
1521
1605
|
}
|
|
1522
1606
|
function describeTarget(target) {
|
|
@@ -1628,11 +1712,11 @@ async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflig
|
|
|
1628
1712
|
} catch (error) {
|
|
1629
1713
|
return rewindErrorResult(error);
|
|
1630
1714
|
}
|
|
1631
|
-
const marker = buildMarker();
|
|
1715
|
+
const marker = buildMarker(plan.targetSeq);
|
|
1632
1716
|
let event;
|
|
1633
1717
|
try {
|
|
1634
1718
|
event = agent.session.append("user/message", marker, {
|
|
1635
|
-
surfaceOp: { op: "replace",
|
|
1719
|
+
surfaceOp: { op: "replace", startSeq: plan.surfaceStart, endSeq: plan.surfaceEnd },
|
|
1636
1720
|
sourceEventSeqs: [...plan.shadowedSeqs]
|
|
1637
1721
|
});
|
|
1638
1722
|
} catch (error) {
|
|
@@ -1874,6 +1958,21 @@ function apply(ctx, config) {
|
|
|
1874
1958
|
handler: (invocation) => handleSnapshotCleanup(store, invocation, dshHome, trackedBySession)
|
|
1875
1959
|
});
|
|
1876
1960
|
}, "dsh-rewind command");
|
|
1961
|
+
ctx.on("agent/session-start", ({ agent }) => {
|
|
1962
|
+
const session = agent.session;
|
|
1963
|
+
if (session.header.origin === "subagent" || (session.header.delegationDepth ?? 0) > 0) return;
|
|
1964
|
+
void (async () => {
|
|
1965
|
+
try {
|
|
1966
|
+
store.setFormatVersion(session.header.version);
|
|
1967
|
+
const result = await store.reconcileFormatVersion(session.id, session.header.version);
|
|
1968
|
+
if (result.cleared) {
|
|
1969
|
+
ctx.logger.warn(`[dsh-rewind] cleared snapshots for ${session.id}: session format changed (v${session.header.version})`);
|
|
1970
|
+
}
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
ctx.logger.warn(`[dsh-rewind] session-format reconcile failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1973
|
+
}
|
|
1974
|
+
})();
|
|
1975
|
+
}, { global: true });
|
|
1877
1976
|
ctx.on("session/event", (session, event) => {
|
|
1878
1977
|
if (event.type !== "user/message") return;
|
|
1879
1978
|
const header = session.header;
|
|
@@ -104,7 +104,7 @@ export declare function isCandidateCommand(command: CommandNode): boolean;
|
|
|
104
104
|
* (pending, succeeded, or errored — it only exists to feed the popover) and
|
|
105
105
|
* every SUCCESSFUL executed `/rewind` command row, plus every message
|
|
106
106
|
* withdrawn by a rewind — the target message itself, everything after it, and
|
|
107
|
-
* the (
|
|
107
|
+
* the (unrendered) marker.
|
|
108
108
|
*
|
|
109
109
|
* Each executed rewind cuts ONE span `[target, marker]`: the target message
|
|
110
110
|
* and everything after it, up to the marker appended at rewind time. Spans are
|
|
@@ -20,6 +20,20 @@
|
|
|
20
20
|
* typed structurally (see `SlotsLike`) so the plugin never imports the
|
|
21
21
|
* conversation UI package's types and survives its version drift.
|
|
22
22
|
*
|
|
23
|
+
* COUPLING NOTE — HIGH. There is no harness interface for a per-user-message
|
|
24
|
+
* action (see docs/compat/audit.md): `MessageIconActions.extraActions` and the
|
|
25
|
+
* `conversation.chat.assistant-actions` slot are wired for assistant messages
|
|
26
|
+
* only, and no per-user-message action slot exists. So this portal targets
|
|
27
|
+
* undocumented internal structure — the `data-chat-flow-kind`,
|
|
28
|
+
* `data-chat-anchor-key`, `data-composer-input`, `data-composer-card`,
|
|
29
|
+
* `data-pending-steering` and `data-time-hover-root` attributes plus the
|
|
30
|
+
* `anchorSeq` field read in `client/hidden.ts`. Those are harness-internal and
|
|
31
|
+
* may change with the UI; this module (and `hidden.ts`) must be re-adapted to
|
|
32
|
+
* follow, and is the migration target when a first-class user-action slot or
|
|
33
|
+
* an official renderer hook surface appears. The coupling is accepted
|
|
34
|
+
* deliberately because a standards-conformant alternative does not exist
|
|
35
|
+
* today; it is not a defect to be removed while the DOM-portal approach stands.
|
|
36
|
+
*
|
|
23
37
|
* @module dsh-rewind/client/portals
|
|
24
38
|
*/
|
|
25
39
|
import { type ReactNode } from 'react';
|
|
@@ -23,4 +23,4 @@ export declare const CLASS: {
|
|
|
23
23
|
/** The ↶ glyph, drawn inline so the bundle stays dependency-free. */
|
|
24
24
|
export declare const REWIND_ICON_SVG: string;
|
|
25
25
|
/** One injected stylesheet (scoped under `.dsh-rewind-*`). */
|
|
26
|
-
export declare const STYLE = "\n.dsh-rewind-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 6px;\n border: none;\n border-radius: 28px;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.dsh-rewind-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.dsh-rewind-popover {\n position: fixed;\n z-index: 1000;\n width: 288px;\n padding: 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 14px;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-popover-title {\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n.dsh-rewind-popover-target {\n margin: 4px 0 10px;\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-all;\n}\n.dsh-rewind-popover-option {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n margin: 0 0 6px;\n padding: 8px 10px;\n border: 1px solid transparent;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.dsh-rewind-popover-option:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.dsh-rewind-popover-option:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-option-label {\n font-weight: 500;\n}\n.dsh-rewind-popover-option-hint {\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-popover-impact {\n margin: 4px 0 10px;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover);\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-wrap;\n max-height: 160px;\n overflow: auto;\n}\n.dsh-rewind-popover-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.dsh-rewind-popover-primary,\n.dsh-rewind-popover-ghost {\n padding: 5px 12px;\n border: none;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n}\n.dsh-rewind-popover-primary {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dsh-rewind-popover-primary:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover);\n}\n.dsh-rewind-popover-primary:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-ghost {\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-popover-ghost:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-rewind-guard-hint {\n position: fixed;\n z-index: 1000;\n max-width: min(440px, calc(100vw - 24px));\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n\n/* ---- Snapshot-cleanup settings card (mirrors the harness PluginCard look) ---- */\n/* Standardized on the 0.1.2 harness card look (border-radius 16px + 0.5px\n border-l4, 0.5px internal separators). */\n.dsh-rewind-cleanup-card {\n list-style: none;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 16px;\n background: var(--dsw-alias-bg-layer-3);\n transition: border-color .16s, background .16s;\n}\n.dsh-rewind-cleanup-card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-card-open {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-header {\n width: 100%;\n appearance: none;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-radius: 12px;\n}\n.dsh-rewind-cleanup-header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.dsh-rewind-cleanup-head-text {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.dsh-rewind-cleanup-name {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-desc {\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-chevron {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n transition: transform .16s;\n}\n.dsh-rewind-cleanup-chevron-open {\n transform: rotate(180deg);\n}\n.dsh-rewind-cleanup-pending {\n flex: none;\n border-radius: 999px;\n padding: 1px 8px;\n font-size: 11px;\n line-height: 17px;\n font-weight: 500;\n white-space: nowrap;\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-body {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n margin: 0 16px;\n padding: 4px 0 8px;\n}\n.dsh-rewind-cleanup-readonly {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-permission {\n display: grid;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field + .dsh-rewind-cleanup-field {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.dsh-rewind-cleanup-label {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-error {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n/* Switch row: label left, role=switch button right, hint below (Subagent module).\n * The track mirrors the harness ui-primitives Switch (0.1.3-alpha.2 line): the\n * on/off appearance keys off aria-checked rather than a parallel class, so the\n * visual state cannot disagree with the state assistive technology reads, and\n * corner-shape: round opts the capsule track out of the global superellipse\n * (which would square the capsule ends off against the round thumb inside). */\n.dsh-rewind-cleanup-toggle-row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-toggle-label {\n flex: 1;\n min-width: 0;\n}\n.dsh-rewind-cleanup-switch {\n box-sizing: border-box;\n position: relative;\n flex: 0 0 auto;\n width: 36px;\n height: 20px;\n padding: 2px;\n border: 0;\n border-radius: 10px;\n corner-shape: round;\n background: var(--dsw-alias-border-l3);\n cursor: pointer;\n}\n.dsh-rewind-cleanup-switch[aria-checked='true'] {\n background: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-switch:disabled {\n cursor: default;\n opacity: 0.5;\n}\n.dsh-rewind-cleanup-switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n}\n.dsh-rewind-cleanup-thumb {\n display: block;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n corner-shape: round;\n background: var(--dsw-alias-label-primary-foreground);\n transition: transform 120ms ease;\n}\n.dsh-rewind-cleanup-switch[aria-checked='true'] .dsh-rewind-cleanup-thumb {\n transform: translateX(16px);\n}\n.dsh-rewind-cleanup-input {\n box-sizing: border-box;\n height: 34px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-input:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.dsh-rewind-cleanup-input-invalid {\n border-color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 0 4px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-failed {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-discard,\n.dsh-rewind-cleanup-save {\n appearance: none;\n border: 1px solid transparent;\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n}\n.dsh-rewind-cleanup-discard {\n border-color: var(--dsw-alias-border-l2);\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-discard:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-save {\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n.dsh-rewind-cleanup-discard:disabled,\n.dsh-rewind-cleanup-save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.dsh-rewind-cleanup-discard:focus-visible,\n.dsh-rewind-cleanup-save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n";
|
|
26
|
+
export declare const STYLE = "\n.dsh-rewind-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 6px;\n border: none;\n border-radius: 28px;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.dsh-rewind-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.dsh-rewind-popover {\n position: fixed;\n z-index: 1000;\n width: 288px;\n padding: 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 14px;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-popover-title {\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n.dsh-rewind-popover-target {\n margin: 4px 0 10px;\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-all;\n}\n.dsh-rewind-popover-option {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n margin: 0 0 6px;\n padding: 8px 10px;\n border: 1px solid transparent;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.dsh-rewind-popover-option:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.dsh-rewind-popover-option:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-option-label {\n font-weight: 500;\n}\n.dsh-rewind-popover-option-hint {\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-popover-impact {\n margin: 4px 0 10px;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover);\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-wrap;\n max-height: 160px;\n overflow: auto;\n}\n.dsh-rewind-popover-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.dsh-rewind-popover-primary,\n.dsh-rewind-popover-ghost {\n padding: 5px 12px;\n border: none;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n}\n.dsh-rewind-popover-primary {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dsh-rewind-popover-primary:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover);\n}\n.dsh-rewind-popover-primary:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-ghost {\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-popover-ghost:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-rewind-guard-hint {\n position: fixed;\n z-index: 1000;\n max-width: min(440px, calc(100vw - 24px));\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n\n/* ---- Snapshot-cleanup settings card (mirrors the harness PluginCard look) ---- */\n/* Standardized on the 0.1.2 harness card look (border-radius 16px + 0.5px\n border-l4, 0.5px internal separators). */\n.dsh-rewind-cleanup-card {\n list-style: none;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 16px;\n background: var(--dsw-alias-bg-layer-3);\n transition: border-color .16s, background .16s;\n}\n.dsh-rewind-cleanup-card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-card-open {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-header {\n width: 100%;\n appearance: none;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-radius: 12px;\n}\n.dsh-rewind-cleanup-header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.dsh-rewind-cleanup-head-text {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.dsh-rewind-cleanup-name {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-desc {\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-chevron {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n transition: transform .16s;\n}\n.dsh-rewind-cleanup-chevron-open {\n transform: rotate(180deg);\n}\n.dsh-rewind-cleanup-pending {\n flex: none;\n border-radius: 999px;\n padding: 1px 8px;\n font-size: 11px;\n line-height: 17px;\n font-weight: 500;\n white-space: nowrap;\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-body {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n margin: 0 16px;\n padding: 0 0 8px;\n}\n.dsh-rewind-cleanup-readonly {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-permission {\n display: grid;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field + .dsh-rewind-cleanup-field {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n/* Auto-cleanup (permission) block and the max-age field are consecutive control\n * sections of the open card \u2014 give them the same 0.5px divider the harness gives\n * every pair of config fields (fields.module.css field + field). */\n.dsh-rewind-cleanup-permission + .dsh-rewind-cleanup-field {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.dsh-rewind-cleanup-label {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-error {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n/* Switch row: label left, role=switch button right, hint below (Subagent module).\n * The track mirrors the harness ui-primitives Switch (0.1.3-alpha.2 line): the\n * on/off appearance keys off aria-checked rather than a parallel class, so the\n * visual state cannot disagree with the state assistive technology reads, and\n * corner-shape: round opts the capsule track out of the global superellipse\n * (which would square the capsule ends off against the round thumb inside). */\n.dsh-rewind-cleanup-toggle-row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-toggle-label {\n flex: 1;\n min-width: 0;\n}\n.dsh-rewind-cleanup-switch {\n box-sizing: border-box;\n position: relative;\n flex: 0 0 auto;\n width: 36px;\n height: 20px;\n padding: 2px;\n border: 0;\n border-radius: 10px;\n corner-shape: round;\n background: var(--dsw-alias-border-l3);\n cursor: pointer;\n}\n.dsh-rewind-cleanup-switch[aria-checked='true'] {\n background: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-switch:disabled {\n cursor: default;\n opacity: 0.5;\n}\n.dsh-rewind-cleanup-switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n}\n.dsh-rewind-cleanup-thumb {\n display: block;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n corner-shape: round;\n background: var(--dsw-alias-label-primary-foreground);\n transition: transform 120ms ease;\n}\n.dsh-rewind-cleanup-switch[aria-checked='true'] .dsh-rewind-cleanup-thumb {\n transform: translateX(16px);\n}\n.dsh-rewind-cleanup-input {\n box-sizing: border-box;\n height: 34px;\n padding: 0 12px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-input:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.dsh-rewind-cleanup-input-invalid {\n border-color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 0 4px;\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-failed {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-discard,\n.dsh-rewind-cleanup-save {\n appearance: none;\n border: 1px solid transparent;\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n}\n.dsh-rewind-cleanup-discard {\n border-color: var(--dsw-alias-border-l2);\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-discard:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-save {\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n.dsh-rewind-cleanup-discard:disabled,\n.dsh-rewind-cleanup-save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.dsh-rewind-cleanup-discard:focus-visible,\n.dsh-rewind-cleanup-save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n";
|
package/lib/types/index.d.ts
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* surface node after the target message with the marker. The append-only log
|
|
9
9
|
* (and the rendered transcript) is untouched — only the model-visible surface
|
|
10
10
|
* is cut, so the next request derives its context from the target onward.
|
|
11
|
-
* The marker is
|
|
12
|
-
*
|
|
13
|
-
* `assistant/message` can no longer carry those — so
|
|
14
|
-
* a `user/message`, exactly as /compact's checkpoint
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* The marker is a `user/message` carrying the shadowed-seq citations
|
|
12
|
+
* (`sourceEventSeqs`): v2 reserves surface `replace` to a node that cites
|
|
13
|
+
* every shadowed seq, and `assistant/message` can no longer carry those — so
|
|
14
|
+
* the replacement node is a `user/message`, exactly as /compact's checkpoint
|
|
15
|
+
* is. It derives to itself (a present user turn), so the marker stays as the
|
|
16
|
+
* surface-tail cut point rather than vanishing.
|
|
17
17
|
*
|
|
18
18
|
*
|
|
19
19
|
* File restore (mode `both`) follows Claude Code's checkpointing: the plugin
|
package/lib/types/locales.d.ts
CHANGED
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* renders through machine tokens (`impact=<n>`, `args` @seq), never by parsing
|
|
16
16
|
* host prose.
|
|
17
17
|
*
|
|
18
|
+
* DSH localizes only its OWN command descriptions via the client locale — a
|
|
19
|
+
* closed `HOST_DESCRIPTION_KEYS` allowlist; third-party plugin descriptions are
|
|
20
|
+
* not in that channel (see docs/compat/audit.md, RU-I18N), so this plugin's host
|
|
21
|
+
* command copy stays in the host's tongue (English by default).
|
|
22
|
+
*
|
|
18
23
|
* English is the key-set source of truth; zh is checked complete against it.
|
|
19
24
|
*
|
|
20
25
|
* @module dsh-rewind/locales
|
package/lib/types/rewind.d.ts
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* transcript) is untouched; only the model-visible surface is cut, so the
|
|
10
10
|
* next request derives its context from the target message onward.
|
|
11
11
|
*
|
|
12
|
-
* Marker shape (v0.1.
|
|
13
|
-
*
|
|
12
|
+
* Marker shape (v0.1.5/v3): the marker is a `user/message` carrying a replace
|
|
13
|
+
* `surfaceOp` — a single event:
|
|
14
14
|
*
|
|
15
|
-
* user/message (marker
|
|
15
|
+
* user/message (marker content) → { surfaceOp {replace, startSeq, endSeq} }
|
|
16
16
|
*
|
|
17
17
|
* v2 reserves surface `replace` to a node that cites every shadowed seq via
|
|
18
18
|
* `sourceEventSeqs`, and `assistant/message` can no longer carry
|
|
@@ -21,13 +21,12 @@
|
|
|
21
21
|
* is. No ghost `step/start`…`step/end` frame is needed: the token-meter's
|
|
22
22
|
* step state machine ignores `user/message`, and the session invariant
|
|
23
23
|
* (`invariant.ts`) imposes no open-turn requirement on it, so the marker is
|
|
24
|
-
* appended while idle, outside any turn.
|
|
25
|
-
*
|
|
26
|
-
* "cut point" (an empty `user/message` derives to itself, so it remains a
|
|
27
|
-
* present-but-empty user turn in derived history).
|
|
24
|
+
* appended while idle, outside any turn. It sits at the surface tail as the
|
|
25
|
+
* model-visible "cut point" — a present user turn in derived history.
|
|
28
26
|
*
|
|
29
27
|
* @module dsh-rewind/rewind
|
|
30
28
|
*/
|
|
29
|
+
import type { MessageSource } from '@deepseek-ai/dsh-llm/message';
|
|
31
30
|
import type { SessionEvent, UserMessage } from '@deepseek-ai/dsh-session';
|
|
32
31
|
/** Which of the two rewind modes a rewind executes. */
|
|
33
32
|
export type RewindMode = 'chat' | 'both';
|
|
@@ -75,6 +74,22 @@ export interface RewindPlan {
|
|
|
75
74
|
/** Last surface node — the replace range end (inclusive). */
|
|
76
75
|
readonly surfaceEnd: number;
|
|
77
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Create rewind-marker provenance correlated with one rewind target.
|
|
79
|
+
* @param targetSeq - absolute log seq of the user message rewound to.
|
|
80
|
+
* @returns immutable rewind-marker source.
|
|
81
|
+
*/
|
|
82
|
+
export declare function rewindMarkerSource(targetSeq: number): Readonly<{
|
|
83
|
+
kind: 'plugin';
|
|
84
|
+
plugin: 'dsh-rewind';
|
|
85
|
+
targetSeq: number;
|
|
86
|
+
}>;
|
|
87
|
+
/**
|
|
88
|
+
* Test whether a persisted message source identifies a rewind marker.
|
|
89
|
+
* @param source - source restored from a surface user message.
|
|
90
|
+
* @returns whether the source carries the backend-independent rewind brand.
|
|
91
|
+
*/
|
|
92
|
+
export declare function isRewindMarker(source: MessageSource): boolean;
|
|
78
93
|
/** Preview length cap for candidate listings. */
|
|
79
94
|
export declare const CANDIDATE_PREVIEW_CHARS = 80;
|
|
80
95
|
/**
|
package/lib/types/snapshot.d.ts
CHANGED
|
@@ -317,6 +317,13 @@ export declare class SnapshotStore {
|
|
|
317
317
|
private readonly lastEntry;
|
|
318
318
|
/** Sessions whose dedup state has been seeded from disk this process. */
|
|
319
319
|
private readonly seededSessions;
|
|
320
|
+
/**
|
|
321
|
+
* Session-format version snapshots are anchored under, stamped into each
|
|
322
|
+
* session's `format` marker when an entry is recorded. `null` until the host
|
|
323
|
+
* sets it (from `agent/session-start`), so a session that never records is
|
|
324
|
+
* never materialized and a marker is only written where snapshots exist.
|
|
325
|
+
*/
|
|
326
|
+
private formatVersion;
|
|
320
327
|
constructor(root?: string, opts?: {
|
|
321
328
|
readonly dedup?: boolean;
|
|
322
329
|
readonly dshHome?: string;
|
|
@@ -423,6 +430,8 @@ export declare class SnapshotStore {
|
|
|
423
430
|
restoreAfter(sessionId: string, targetSeq: number, deleteFile: DeleteFile, probe?: DiskProbe, opts?: RestoreRunOptions): Promise<RestoreOutcome>;
|
|
424
431
|
/** Prefix of one restore-op journal file inside the session dir. */
|
|
425
432
|
private static readonly JOURNAL_PREFIX;
|
|
433
|
+
/** Session-format-version marker file inside the session dir. Non-`.json`, so it never counts as a checkpoint entry. */
|
|
434
|
+
private static readonly FORMAT_FILE;
|
|
426
435
|
/** Absolute path of one restore-op journal file. */
|
|
427
436
|
private journalPath;
|
|
428
437
|
/**
|
|
@@ -622,6 +631,48 @@ export declare class SnapshotStore {
|
|
|
622
631
|
clearSession(sessionId: string, opts?: {
|
|
623
632
|
readonly dryRun?: boolean;
|
|
624
633
|
}): Promise<ClearSessionReport>;
|
|
634
|
+
/**
|
|
635
|
+
* Read the session-format version marker recorded for a session, or `null`
|
|
636
|
+
* when there is no marker — a pre-marker, legacy snapshot dir, or a session
|
|
637
|
+
* that never materialized a dir.
|
|
638
|
+
*/
|
|
639
|
+
private readFormatVersion;
|
|
640
|
+
/**
|
|
641
|
+
* Record the session-format version a session's snapshots are anchored under
|
|
642
|
+
* (`session.header.version`: 2 for the v2 format, 3 for the v3 format). The
|
|
643
|
+
* marker is a tiny non-`.json` file, so it never counts as a checkpoint
|
|
644
|
+
* entry in `sessionStats`/`clearSession`. Written atomically (temp + rename)
|
|
645
|
+
* like every other persisted marker, so a crash mid-write can only leave an
|
|
646
|
+
* inert `format.tmp` — never a partial marker that a later reconcile could
|
|
647
|
+
* misread as a version mismatch and wrongly clear.
|
|
648
|
+
*/
|
|
649
|
+
markFormatVersion(sessionId: string, sessionVersion: number): Promise<void>;
|
|
650
|
+
/**
|
|
651
|
+
* Set the session-format version the store stamps onto every snapshot it
|
|
652
|
+
* records. The host sets this once per process from `agent/session-start`
|
|
653
|
+
* (`agent.session.header.version`), so a marker is only materialized for a
|
|
654
|
+
* session that actually records a snapshot.
|
|
655
|
+
*/
|
|
656
|
+
setFormatVersion(sessionVersion: number): void;
|
|
657
|
+
/**
|
|
658
|
+
* Session-format-version guard: clear a session's snapshot dir when the
|
|
659
|
+
* format its snapshots were anchored under differs from the current session
|
|
660
|
+
* format, so seq-anchored references can never survive a format migration
|
|
661
|
+
* mis-mapped. Runs at `agent/session-start` — after DSH has migrated/loaded
|
|
662
|
+
* the session, so `sessionVersion` is the post-migration value.
|
|
663
|
+
*
|
|
664
|
+
* Conservative rule (per the "delete stale snapshots" policy): a session
|
|
665
|
+
* with no recorded marker but with snapshot content is treated as legacy and
|
|
666
|
+
* cleared; a session whose marker differs from `sessionVersion` is cleared.
|
|
667
|
+
* A matching version — or an untouched session with nothing to protect — is
|
|
668
|
+
* left alone. The marker is re-stamped to the current version afterward so a
|
|
669
|
+
* FUTURE format change is detected on the next start.
|
|
670
|
+
*
|
|
671
|
+
* @returns whether a session snapshot dir was cleared.
|
|
672
|
+
*/
|
|
673
|
+
reconcileFormatVersion(sessionId: string, sessionVersion: number): Promise<{
|
|
674
|
+
cleared: boolean;
|
|
675
|
+
}>;
|
|
625
676
|
}
|
|
626
677
|
/**
|
|
627
678
|
* Re-check every tracked file at a user-message boundary and record the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rewind-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0-alpha.1",
|
|
4
4
|
"description": "DSH 插件:真正便捷无感的同窗口内对话回退,从不新建分支;自带轻量工作区备份,可一并还原文件(完整 Claude Code /rewind 语义)。 · DSH plugin: genuinely effortless in-window conversation rewind — never forking a new session; ships a lightweight workspace backup that restores files together with the rewind (full Claude Code /rewind semantics).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -49,6 +49,9 @@
|
|
|
49
49
|
"bundle": {
|
|
50
50
|
"patch": "./cordis.patch.yml"
|
|
51
51
|
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"dsh": ">=0.1.5-alpha.1"
|
|
54
|
+
},
|
|
52
55
|
"client": {
|
|
53
56
|
"inject": [
|
|
54
57
|
"@deepseek-ai/dsh-client-locale",
|
|
@@ -73,18 +76,18 @@
|
|
|
73
76
|
"peerDependencies": {
|
|
74
77
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
75
78
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
76
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-commands": "^0.1.
|
|
78
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.
|
|
79
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.
|
|
80
|
-
"@deepseek-ai/dsh-commands": "^0.1.
|
|
81
|
-
"@deepseek-ai/dsh-fs": "^0.1.
|
|
82
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.
|
|
83
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
84
|
-
"@deepseek-ai/dsh-sandbox": "^0.1.
|
|
85
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
86
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
87
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
79
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.5-alpha.1",
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-commands": "^0.1.5-alpha.1",
|
|
81
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-alpha.1",
|
|
82
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-alpha.1",
|
|
83
|
+
"@deepseek-ai/dsh-commands": "^0.1.5-alpha.1",
|
|
84
|
+
"@deepseek-ai/dsh-fs": "^0.1.5-alpha.1",
|
|
85
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.5-alpha.1",
|
|
86
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-alpha.1",
|
|
87
|
+
"@deepseek-ai/dsh-sandbox": "^0.1.5-alpha.1",
|
|
88
|
+
"@deepseek-ai/dsh-session": "^0.1.5-alpha.1",
|
|
89
|
+
"@deepseek-ai/dsh-settings": "^0.1.5-alpha.1",
|
|
90
|
+
"@deepseek-ai/dsh-tools": "^0.1.5-alpha.1"
|
|
88
91
|
},
|
|
89
92
|
"peerDependenciesMeta": {
|
|
90
93
|
"@deepseek-ai/cordis": {
|
|
@@ -133,37 +136,37 @@
|
|
|
133
136
|
"devDependencies": {
|
|
134
137
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
135
138
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
136
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
137
|
-
"@deepseek-ai/dsh-attachment": "^0.1.
|
|
138
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
139
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.
|
|
140
|
-
"@deepseek-ai/dsh-client-store": "^0.1.
|
|
141
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.
|
|
142
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.1.
|
|
143
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.
|
|
144
|
-
"@deepseek-ai/dsh-scope": "^0.1.
|
|
145
|
-
"@deepseek-ai/dsh-typert-protocol": "^0.1.
|
|
146
|
-
"@deepseek-ai/dsh-user-questions": "^0.1.
|
|
147
|
-
"@deepseek-ai/dsh-client-ui-commands": "^0.1.
|
|
148
|
-
"@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.
|
|
149
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.
|
|
150
|
-
"@deepseek-ai/dsh-command-compact": "^0.1.
|
|
151
|
-
"@deepseek-ai/dsh-commands": "^0.1.
|
|
152
|
-
"@deepseek-ai/dsh-compaction": "^0.1.
|
|
153
|
-
"@deepseek-ai/dsh-compaction-basic": "^0.1.
|
|
154
|
-
"@deepseek-ai/dsh-fs": "^0.1.
|
|
155
|
-
"@deepseek-ai/dsh-goal": "^0.1.
|
|
156
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.
|
|
157
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
158
|
-
"@deepseek-ai/dsh-plan-mode": "^0.1.
|
|
159
|
-
"@deepseek-ai/dsh-sandbox": "^0.1.
|
|
160
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
161
|
-
"@deepseek-ai/dsh-session-projection": "^0.1.
|
|
162
|
-
"@deepseek-ai/dsh-session-stats": "^0.1.
|
|
163
|
-
"@deepseek-ai/dsh-session-title": "^0.1.
|
|
164
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
165
|
-
"@deepseek-ai/dsh-token-meter": "^0.1.
|
|
166
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
139
|
+
"@deepseek-ai/dsh-agent": "^0.1.5-alpha.1",
|
|
140
|
+
"@deepseek-ai/dsh-attachment": "^0.1.5-alpha.1",
|
|
141
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.5-alpha.1",
|
|
142
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.5-alpha.1",
|
|
143
|
+
"@deepseek-ai/dsh-client-store": "^0.1.5-alpha.1",
|
|
144
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-alpha.1",
|
|
145
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.1.5-alpha.1",
|
|
146
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-alpha.1",
|
|
147
|
+
"@deepseek-ai/dsh-scope": "^0.1.5-alpha.1",
|
|
148
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.5-alpha.1",
|
|
149
|
+
"@deepseek-ai/dsh-user-questions": "^0.1.5-alpha.1",
|
|
150
|
+
"@deepseek-ai/dsh-client-ui-commands": "^0.1.5-alpha.1",
|
|
151
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.5-alpha.1",
|
|
152
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-alpha.1",
|
|
153
|
+
"@deepseek-ai/dsh-command-compact": "^0.1.5-alpha.1",
|
|
154
|
+
"@deepseek-ai/dsh-commands": "^0.1.5-alpha.1",
|
|
155
|
+
"@deepseek-ai/dsh-compaction": "^0.1.5-alpha.1",
|
|
156
|
+
"@deepseek-ai/dsh-compaction-basic": "^0.1.5-alpha.1",
|
|
157
|
+
"@deepseek-ai/dsh-fs": "^0.1.5-alpha.1",
|
|
158
|
+
"@deepseek-ai/dsh-goal": "^0.1.5-alpha.1",
|
|
159
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.5-alpha.1",
|
|
160
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-alpha.1",
|
|
161
|
+
"@deepseek-ai/dsh-plan-mode": "^0.1.5-alpha.1",
|
|
162
|
+
"@deepseek-ai/dsh-sandbox": "^0.1.5-alpha.1",
|
|
163
|
+
"@deepseek-ai/dsh-session": "^0.1.5-alpha.1",
|
|
164
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.5-alpha.1",
|
|
165
|
+
"@deepseek-ai/dsh-session-stats": "^0.1.5-alpha.1",
|
|
166
|
+
"@deepseek-ai/dsh-session-title": "^0.1.5-alpha.1",
|
|
167
|
+
"@deepseek-ai/dsh-settings": "^0.1.5-alpha.1",
|
|
168
|
+
"@deepseek-ai/dsh-token-meter": "^0.1.5-alpha.1",
|
|
169
|
+
"@deepseek-ai/dsh-tools": "^0.1.5-alpha.1",
|
|
167
170
|
"@types/node": "^24.0.0",
|
|
168
171
|
"@types/react": "^18.3.31",
|
|
169
172
|
"@types/react-dom": "^18.3.7",
|