dsh-rewind-plugin 0.2.4 → 0.2.5
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.md +28 -1
- package/README.zh.md +22 -1
- package/lib/client.js +17 -1
- package/lib/index.js +10 -10
- package/lib/types/rewind.d.ts +23 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
In-place conversation rewind for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): the Claude Code `/rewind` semantics inside the **same session window** — cut the model context back to an earlier user message, and optionally restore workspace files from **disk-persisted before-backups**.
|
|
6
6
|
|
|
7
|
-
> **Status:** published to npm (`dsh-rewind-plugin`, v0.2.
|
|
7
|
+
> **Status:** published to npm (`dsh-rewind-plugin`, v0.2.5) via GitHub Actions Trusted Publishing + Sigstore provenance. Targets the web profile (`dsh --profile web`). Interaction mirrors Claude Code's rewind, adapted to dsh's real web UI.
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/dsh-rewind-plugin)
|
|
10
10
|
[](https://github.com/SiriLee/dsh-rewind/blob/main/LICENSE)
|
|
@@ -61,6 +61,7 @@ The plugin appends an **empty-content marker** `assistant/message` into the sess
|
|
|
61
61
|
|
|
62
62
|
- The marker carries `sourceEventSeqs` covering every shadowed node, and the `Session.append` surface rules validate the cut (only a contiguous range on the current surface).
|
|
63
63
|
- Because the marker is **empty**, the harness derives it to `null` — it never enters the model context and never renders as conversation content. The agent and the user both see the conversation exactly as it was at the target.
|
|
64
|
+
- The marker's **turn number reuses the LAST STARTED turn** (`markerTurnOf`), never `lastTurn + 1`: the harness's agent loop numbers its next real turn exactly `last turn/start + 1`, so a marker numbered the same way would leave an `assistant/message` BEFORE the `turn/start` of the same turn — the client conversation-context builder rejects that ordering with `conversation Context …:turn-tail… received an update before its start Match`, history load fails, and the whole conversation disappears from the UI (the real defect in ≤ 0.2.4, fixed in 0.2.5). Reusing an already-consumed turn makes the marker a harmless trailing update on the previous completed turn's tail — it can never collide with a future turn.
|
|
64
65
|
- The append-only log is **untouched** — the audit trail keeps every withdrawn event; only the model-visible surface is cut, so the next request derives its context from the target onward.
|
|
65
66
|
|
|
66
67
|
A running turn (LLM thinking / streaming) is force-stopped first (`cancel({ kind: 'user' })`) and the rewind waits for quiescence; if it can't stop, the rewind is aborted with an error.
|
|
@@ -76,6 +77,32 @@ The plugin tracks the write-class tools — `write`, `edit`, `str_replace_editor
|
|
|
76
77
|
|
|
77
78
|
Backups persist across host restarts, bounded to the newest 100 anchor groups per session.
|
|
78
79
|
|
|
80
|
+
## 🔧 Troubleshooting: history load failure (`…turn-tail… received an update before its start Match`)
|
|
81
|
+
|
|
82
|
+
Versions ≤ 0.2.4 corrupted client replay when a rewind was **followed by further
|
|
83
|
+
conversation**: the marker's turn number collided with the next real turn's
|
|
84
|
+
`turn/start`, so reopening the session showed
|
|
85
|
+
`Failed to load history: conversation Context …:turn-tail… received an update before its start Match (internal)`
|
|
86
|
+
and the history vanished. Rewinds created from 0.2.5 on no longer produce the
|
|
87
|
+
collision, but **already-corrupted sessions need an offline repair** (the log is
|
|
88
|
+
append-only — it cannot be rewritten in memory):
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
# 1. Fully quit dsh web / host first (while a session is resident in memory,
|
|
92
|
+
# a disk repair is overwritten by the next checkpoint)
|
|
93
|
+
# 2. Run the offline repair script (scans every session under ~/.dsh/sessions,
|
|
94
|
+
# rewriting each marker's turn back to the last started turn)
|
|
95
|
+
node scripts/repair-markers.mjs # default: scan ~/.dsh/sessions
|
|
96
|
+
node scripts/repair-markers.mjs --dry-run # report only, no writes
|
|
97
|
+
node scripts/repair-markers.mjs --dir <sessions root> # custom data dir
|
|
98
|
+
# 3. Restart dsh web — the repaired sessions load their history again
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The script only rewrites the `data.turn` of `dsh-rewind` empty-marker events
|
|
102
|
+
(keeping seqs, order, and the zstd frame structure intact), backs up the original
|
|
103
|
+
file to `session.jsonl.zstd.bak-<timestamp>` before writing, and never touches
|
|
104
|
+
any other event — safe to run repeatedly.
|
|
105
|
+
|
|
79
106
|
## 📦 Install
|
|
80
107
|
|
|
81
108
|
Published to npm — the registry path is the recommended one. **Restart dsh web (`--profile web`) after installing.**
|
package/README.zh.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件:**同一会话窗口的 in-place 对话回退**(Claude Code `/rewind` 语义)——把模型上下文剪回更早的一条用户消息,并可基于**落盘的写前备份**还原工作区文件。
|
|
6
6
|
|
|
7
|
-
> **状态**:已发布 npm(`dsh-rewind-plugin`,v0.2.
|
|
7
|
+
> **状态**:已发布 npm(`dsh-rewind-plugin`,v0.2.5),经 GitHub Actions Trusted Publishing + Sigstore provenance 构建发布。目标为 web 配置档(`dsh --profile web`)。交互以 Claude Code 的 rewind 为参考,并贴合 dsh Web 实际 UI。
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/dsh-rewind-plugin)
|
|
10
10
|
[](https://github.com/SiriLee/dsh-rewind/blob/main/LICENSE)
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
|
|
62
62
|
- 标记携带 `sourceEventSeqs` 覆盖所有被遮蔽节点,`Session.append` 的 surface 规则校验切割合法性(仅限当前 surface 上的连续区间)。
|
|
63
63
|
- 因为标记**内容为空**,harness 会将其派生为 `null`——永不进入模型上下文、也永不渲染成对话内容。agent 与用户看到的对话都回到目标消息当时的样子。
|
|
64
|
+
- 标记的 **turn 号复用最后一个已开始的回合**(`markerTurnOf`),而不是「最后回合 + 1」:harness 的 agent loop 恰好用 `最后 turn/start + 1` 编号下一条真实回合。若标记也取这个数,日志里就会出现同一 turn 的 `assistant/message` 先于 `turn/start` 的乱序,客户端 conversation 构建器会以 `conversation Context …:turn-tail… received an update before its start Match` 拒绝重放——历史加载失败、整个对话从界面消失(0.2.4 及之前的真实缺陷,已在 0.2.5 修复)。复用已消费的 turn 号则标记只是上一个已完成回合尾部的一次无害追加,永不与新回合冲突。
|
|
64
65
|
- append-only 日志**不被改写**——审计轨迹完整保留每条被撤回的事件,只有模型可见的 surface 被剪掉,下一条请求从目标消息起派生上下文。
|
|
65
66
|
|
|
66
67
|
若 agent 正在运行(LLM 思考/流式输出),会先强制停止(`cancel({ kind: 'user' })`)并等待 quiescence 再回退;停不下来则中止并报错。
|
|
@@ -76,6 +77,26 @@
|
|
|
76
77
|
|
|
77
78
|
备份跨 host 重启持久化,每会话有界保留最近 100 组锚点。
|
|
78
79
|
|
|
80
|
+
## 🔧 故障修复:历史加载失败(`…turn-tail… received an update before its start Match`)
|
|
81
|
+
|
|
82
|
+
0.2.4 及之前版本在回退**之后继续对话**的场景下会损坏会话的客户端重放:标记的 turn 号
|
|
83
|
+
与下一条真实回合的 `turn/start` 编号冲突,重新打开会话时界面报
|
|
84
|
+
`历史加载失败:conversation Context …:turn-tail… received an update before its start Match(internal)`,
|
|
85
|
+
历史整段消失。0.2.5 起新的回退不再产生该冲突;但**已损坏的会话需要离线修复**(日志是
|
|
86
|
+
append-only 的,不能在内存中改写):
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
# 1. 先完全退出 dsh web / host(会话处于驻留内存时,磁盘修复会被下次 checkpoint 覆盖)
|
|
90
|
+
# 2. 运行离线修复脚本(扫描 ~/.dsh/sessions 下所有会话,把标记 turn 改回最后一个已开始的回合)
|
|
91
|
+
node scripts/repair-markers.mjs # 默认扫描 ~/.dsh/sessions
|
|
92
|
+
node scripts/repair-markers.mjs --dry-run # 只报告不写盘
|
|
93
|
+
node scripts/repair-markers.mjs --dir <sessions 根目录> # 指定数据目录
|
|
94
|
+
# 3. 重启 dsh web,损坏的会话即可正常加载历史
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
脚本只改写 `dsh-rewind` 空标记事件的 `data.turn` 字段(保持 seq / 顺序 / 帧结构不变),
|
|
98
|
+
改前自动备份原文件为 `session.jsonl.zstd.bak-<时间戳>`;不改动任何其它事件,可安全重复运行。
|
|
99
|
+
|
|
79
100
|
## 📦 安装
|
|
80
101
|
|
|
81
102
|
已发布 npm——推荐走 registry 路径。**装完重启 dsh web(`--profile web`)。**
|
package/lib/client.js
CHANGED
|
@@ -586,7 +586,20 @@ function apply(ctx) {
|
|
|
586
586
|
hidden.delete(seat);
|
|
587
587
|
}
|
|
588
588
|
}
|
|
589
|
-
if (session !== void 0)
|
|
589
|
+
if (session !== void 0) {
|
|
590
|
+
const snap = session.getSnapshot();
|
|
591
|
+
if (!fillBaselineTaken && snap.chat.order.length > 0) {
|
|
592
|
+
fillBaselineTaken = true;
|
|
593
|
+
for (const key of snap.chat.order) {
|
|
594
|
+
const node = snap.chat.nodes.get(key);
|
|
595
|
+
if (node?.kind === "command") {
|
|
596
|
+
const command = node.data;
|
|
597
|
+
if (command.seq > fillBaselineSeq) fillBaselineSeq = command.seq;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
fillComposerForRewind(session, filledTargets);
|
|
602
|
+
}
|
|
590
603
|
if (hiddenSeqs.size > 0 || hiddenCount > 0) {
|
|
591
604
|
console.info(
|
|
592
605
|
`[dsh-rewind] hiding: ${hiddenCount} rows, seqs [${[...hiddenSeqs].slice(0, 20).join(", ")}${hiddenSeqs.size > 20 ? "\u2026" : ""}]`
|
|
@@ -597,6 +610,8 @@ function apply(ctx) {
|
|
|
597
610
|
}
|
|
598
611
|
};
|
|
599
612
|
const filledTargets = /* @__PURE__ */ new Set();
|
|
613
|
+
let fillBaselineSeq = -1;
|
|
614
|
+
let fillBaselineTaken = false;
|
|
600
615
|
const fillComposerForRewind = (session, filled) => {
|
|
601
616
|
const snap = session.getSnapshot();
|
|
602
617
|
for (const key of snap.chat.order) {
|
|
@@ -604,6 +619,7 @@ function apply(ctx) {
|
|
|
604
619
|
if (node === void 0 || node.kind !== "command") continue;
|
|
605
620
|
const command = node.data;
|
|
606
621
|
if (command.name !== "rewind" || command.outcome?.kind !== "success") continue;
|
|
622
|
+
if (command.seq <= fillBaselineSeq) continue;
|
|
607
623
|
if (command.outcome.sourceEventSeq === void 0) continue;
|
|
608
624
|
const target = targetOfOutcome(command.outcome.text);
|
|
609
625
|
if (target === void 0 || filled.has(target)) continue;
|
package/lib/index.js
CHANGED
|
@@ -12,6 +12,15 @@ var RewindError = class extends Error {
|
|
|
12
12
|
code;
|
|
13
13
|
};
|
|
14
14
|
var CANDIDATE_PREVIEW_CHARS = 80;
|
|
15
|
+
function markerTurnOf(events) {
|
|
16
|
+
let lastStarted = 0;
|
|
17
|
+
for (const event of events) {
|
|
18
|
+
if (event.type === "turn/start" && event.data.turn > lastStarted) {
|
|
19
|
+
lastStarted = event.data.turn;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return lastStarted;
|
|
23
|
+
}
|
|
15
24
|
function isUserMessageEvent(event) {
|
|
16
25
|
return event.type === "user/message";
|
|
17
26
|
}
|
|
@@ -364,15 +373,6 @@ function buildMarker() {
|
|
|
364
373
|
source: { provider: "dsh-rewind", model: "rewind-marker" }
|
|
365
374
|
});
|
|
366
375
|
}
|
|
367
|
-
function nextTurnOf(session) {
|
|
368
|
-
let max = -1;
|
|
369
|
-
for (const event of session.events) {
|
|
370
|
-
if (event.type === "turn/start" || event.type === "turn/end" || event.type === "assistant/message") {
|
|
371
|
-
if (event.data.turn > max) max = event.data.turn;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
return max + 1;
|
|
375
|
-
}
|
|
376
376
|
function describeTarget(target) {
|
|
377
377
|
return target.kind === "seq" ? `seq ${target.seq}` : `\u7B2C ${target.index} \u6761\u6D88\u606F`;
|
|
378
378
|
}
|
|
@@ -427,7 +427,7 @@ async function executeRewind(ctx, store, invocation, rawTarget, mode) {
|
|
|
427
427
|
const marker = buildMarker();
|
|
428
428
|
let event;
|
|
429
429
|
try {
|
|
430
|
-
event = agent.session.append("assistant/message", { turn:
|
|
430
|
+
event = agent.session.append("assistant/message", { turn: markerTurnOf(agent.session.events), step: 0, message: marker }, {
|
|
431
431
|
surfaceOp: { op: "replace", start: plan.surfaceStart, end: plan.surfaceEnd },
|
|
432
432
|
sourceEventSeqs: [...plan.shadowedSeqs]
|
|
433
433
|
});
|
package/lib/types/rewind.d.ts
CHANGED
|
@@ -60,6 +60,29 @@ export interface RewindPlan {
|
|
|
60
60
|
}
|
|
61
61
|
/** Preview length cap for candidate listings. */
|
|
62
62
|
export declare const CANDIDATE_PREVIEW_CHARS = 80;
|
|
63
|
+
/**
|
|
64
|
+
* Turn number for the rewind marker.
|
|
65
|
+
*
|
|
66
|
+
* The marker MUST NOT reuse the harness's next-turn number. The agent loop
|
|
67
|
+
* numbers its next real turn `lastTurn/start + 1` (dsh-agent-loop), so a
|
|
68
|
+
* marker numbered `maxTurn + 1` collides: the log then holds an
|
|
69
|
+
* `assistant/message` (the marker) BEFORE the `turn/start` of the same turn,
|
|
70
|
+
* and the client conversation-context builder rejects that ordering with
|
|
71
|
+
* `conversation Context …:turn-tail… received an update before its start
|
|
72
|
+
* Match` — history load fails and the whole conversation disappears from the
|
|
73
|
+
* UI (reproduced across real sessions).
|
|
74
|
+
*
|
|
75
|
+
* The marker therefore reuses the LAST STARTED turn's number: the harness has
|
|
76
|
+
* already consumed it (its next turn is strictly larger), so it can never be
|
|
77
|
+
* reused by a future `turn/start`, and the marker lands as a harmless
|
|
78
|
+
* trailing update on that turn's already-closed tail context (its `turn/end`
|
|
79
|
+
* is already matched) — no new context, no reordering, nothing rendered, and
|
|
80
|
+
* the empty content still derives to `null` in the model context.
|
|
81
|
+
*
|
|
82
|
+
* @param events - the full session event log.
|
|
83
|
+
* @returns a turn number the harness can never reuse for a future `turn/start`.
|
|
84
|
+
*/
|
|
85
|
+
export declare function markerTurnOf(events: readonly SessionEvent[]): number;
|
|
63
86
|
/** Narrow an event to a user message. */
|
|
64
87
|
export declare function isUserMessageEvent(event: SessionEvent): event is SessionEvent<'user/message'>;
|
|
65
88
|
/** Join the text blocks of a message into one plain string. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rewind-plugin",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "DeepSeek Harness plugin: in-place conversation rewind in the same session window (Claude Code /rewind semantics) with optional workspace file restore",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|