dsh-rewind-plugin 0.9.0-alpha.1 → 0.9.0
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/docs/rewind-fix.md +15 -7
- package/docs/rewind-fix.zh.md +16 -8
- package/lib/client.js +1 -1
- package/lib/index.js +79 -15
- package/lib/types/locales.d.ts +1 -0
- package/lib/types/rewind-fix.d.ts +2 -0
- package/lib/types/rewind-marker-repair.d.ts +27 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# dsh-rewind
|
|
2
2
|
|
|
3
|
+
> [!WARNING]
|
|
4
|
+
> **Planning to use DSH `0.1.3`? Upgrade to the latest plugin (`>= 0.9.0`) as soon as possible and run `/dsh-rewind-fix` to update old rewind markers** ([update guide](docs/rewind-fix.md)).
|
|
5
|
+
|
|
3
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).
|
|
4
7
|
|
|
5
8
|
[](https://www.npmjs.com/package/dsh-rewind-plugin)
|
|
@@ -8,9 +11,6 @@ Conversation rewind for DeepSeek Harness: **rewind the conversation to any earli
|
|
|
8
11
|
|
|
9
12
|
> English | [中文](README.md)
|
|
10
13
|
|
|
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
|
-
|
|
14
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.
|
|
15
15
|
|
|
16
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**.
|
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# dsh-rewind
|
|
2
2
|
|
|
3
|
+
> [!WARNING]
|
|
4
|
+
> **计划使用 DSH `0.1.3` 的用户:请尽早升级到最新插件(`>= 0.9.0`),并运行 `/dsh-rewind-fix` 更新旧回退标记**([更新指南](docs/rewind-fix.zh.md))。
|
|
5
|
+
|
|
3
6
|
DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消息**——同窗口内完成,不新建分支、不换窗口,可一并还原工作区文件(完整 Claude Code `/rewind` 语义)。
|
|
4
7
|
|
|
5
8
|
[](https://www.npmjs.com/package/dsh-rewind-plugin)
|
|
@@ -8,9 +11,6 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
|
|
|
8
11
|
|
|
9
12
|
> [English](README.en.md) | 中文
|
|
10
13
|
|
|
11
|
-
> [!WARNING]
|
|
12
|
-
> 为了适配未来版本(DSH v0.1.3),期望使用最新版 DSH 的用户,请尽早使用新版插件(`>=0.9.0-alpha.1`)。并更新你的会话日志,详见:[更新指南](docs/rewind-fix.zh.md)
|
|
13
|
-
|
|
14
14
|
刻意聚焦、保持极简,只做一件事:**就地回退到任意远的用户消息**,还能**顺手还原改过的文件**。
|
|
15
15
|
|
|
16
16
|
- **回退 = 时间回溯**——目标消息及其之后的全部内容(agent 回复、工具调用)同时从**模型上下文**和**渲染对话**中撤回,不新建会话、不切换窗口;目标消息文本会回填输入框,改完可重发。**在原理上就真正无感、便捷**。
|
package/docs/rewind-fix.md
CHANGED
|
@@ -5,13 +5,21 @@
|
|
|
5
5
|
## Applies to
|
|
6
6
|
|
|
7
7
|
- **DSH**: `0.1.2-rc.1`
|
|
8
|
-
- **dsh-rewind**: `0.9.0-alpha.1`
|
|
8
|
+
- **dsh-rewind**: `0.9.0-alpha.1`, `0.9.0-alpha.2`, `0.9.0`
|
|
9
9
|
|
|
10
10
|
## Background
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
`dsh-rewind` rewinds **within the same session** — it never forks a branch. It also follows the conservative policy of **deleting no history** (the session log is an append-only audit trail): it only **appends a 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
13
|
|
|
14
|
-
|
|
14
|
+
But DSH v0.1.3 (alpha) introduces a stricter **session-format validation**. We discovered ahead of time that the **rewind markers** written by older plugin versions **cannot be validated** under those stricter checks, so **a session rewound with `/rewind` may fail to open** when that line ships.
|
|
15
|
+
|
|
16
|
+
The plugin is prepared in two parts, both shipped in the new version:
|
|
17
|
+
|
|
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
|
+
|
|
20
|
+
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
|
+
This document covers mainly the **update command**.
|
|
15
23
|
|
|
16
24
|
## Warnings
|
|
17
25
|
|
|
@@ -32,7 +40,7 @@ The command rewrites session files and clears some snapshots, so a backup is a g
|
|
|
32
40
|
cp -r ~/.dsh/sessions ~/.dsh/sessions.backup
|
|
33
41
|
cp -r ~/.dsh/rewind-snapshots ~/.dsh/rewind-snapshots.backup
|
|
34
42
|
```
|
|
35
|
-
Or copy them anywhere convenient.
|
|
43
|
+
Or copy them anywhere convenient. You can also let the AI run the backup for you.
|
|
36
44
|
|
|
37
45
|
### Step 3 · Start a new session, or pick one that was never rewound
|
|
38
46
|
|
|
@@ -42,11 +50,11 @@ The command only handles **closed** sessions, and it can't run on itself. So:
|
|
|
42
50
|
|
|
43
51
|
Then run the command **in this new session**, and it will update the old sessions that need it.
|
|
44
52
|
|
|
45
|
-
>
|
|
53
|
+
> Don't run it in a session that itself needs updating and is currently open — the command skips this session, and any other currently-open/in-use session.
|
|
46
54
|
|
|
47
55
|
### Step 4 · Preview the update scope
|
|
48
56
|
|
|
49
|
-
Type `/dsh-rewind-fix` in the composer and send it. It only scans, doesn't write
|
|
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.** 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.
|
|
50
58
|
```
|
|
51
59
|
/dsh-rewind-fix
|
|
52
60
|
```
|
|
@@ -59,7 +67,7 @@ Once you're sure, re-enter the command with `--apply` to actually run it:
|
|
|
59
67
|
```
|
|
60
68
|
This really rewrites the session logs and updates the old markers, and **may take a few minutes**. Please let it finish — **don't switch to another session window or close DSH** while it runs: a session you switch to is **safely skipped** (no corruption, but it won't be updated this pass and you'll need to re-run). A session that fails stays as it was, unharmed, and can be updated again later. Sessions that are updated also get their matching **snapshot backups cleared**, so file restore to earlier messages is no longer available for them.
|
|
61
69
|
|
|
62
|
-
### Step 6 · Restart DSH and preview again
|
|
70
|
+
### Step 6 · Restart DSH and preview again
|
|
63
71
|
|
|
64
72
|
After the run, restart DSH, then **preview once more**:
|
|
65
73
|
```
|
package/docs/rewind-fix.zh.md
CHANGED
|
@@ -4,14 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
## 适配
|
|
6
6
|
|
|
7
|
-
- **DSH
|
|
8
|
-
- **dsh-rewind
|
|
7
|
+
- **DSH**: `0.1.2-rc.1`
|
|
8
|
+
- **dsh-rewind**: `0.9.0-alpha.1`, `0.9.0-alpha.2`, `0.9.0`
|
|
9
9
|
|
|
10
10
|
## 背景
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
`dsh-rewind` 的回退是**同一会话**的消息回退。它从不新建分支,同时遵循保守策略:**不删除任何历史**(会话日志是 append-only 审计轨迹),只在日志里**追加一条标记**,表示“模型可见的对话从这条消息继续,其后内容回卷”。旧版插件的 **rewind 标记**用的是「幽灵步骤框架」写法,在 DSH v0.1.2-rc.1 及之前版本长期验证有效。
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
但 DSH v0.1.3(alpha)引入了更严格的**会话格式校验**。我们提前发现:旧版本往会话日志里写入的 **rewind 标记**,在这种严格校验下**无法自证合法**,届时会导致**碰过 `/rewind` 的旧会话打不开**。
|
|
15
|
+
|
|
16
|
+
插件做了两阶段准备,已在新版本中包含:
|
|
17
|
+
|
|
18
|
+
1. **新产生的 rewind 标记换成新格式**(向前)——这一步是**正确、低风险**的长期改动(见 [README](../README.md) 的「原理」一节),新格式对**新旧版本** DSH 均完全兼容。
|
|
19
|
+
|
|
20
|
+
2. **提供 `/dsh-rewind-fix` 命令**(向后)——对于**已经存在**的旧会话,本插件提供便捷更新命令,把旧标记「翻译」成新格式,让会话重新可用。
|
|
21
|
+
|
|
22
|
+
本文档主要讲解**更新命令**的使用方法。
|
|
15
23
|
|
|
16
24
|
## 警告
|
|
17
25
|
|
|
@@ -32,7 +40,7 @@
|
|
|
32
40
|
cp -r ~/.dsh/sessions ~/.dsh/sessions.backup
|
|
33
41
|
cp -r ~/.dsh/rewind-snapshots ~/.dsh/rewind-snapshots.backup
|
|
34
42
|
```
|
|
35
|
-
|
|
43
|
+
或者复制到任意合适的位置。你也可以直接让 AI 帮你完成数据的备份。
|
|
36
44
|
|
|
37
45
|
### 第三步 · 新建会话,或选一个从未回退过的会话
|
|
38
46
|
|
|
@@ -42,11 +50,11 @@ cp -r ~/.dsh/rewind-snapshots ~/.dsh/rewind-snapshots.backup
|
|
|
42
50
|
|
|
43
51
|
然后在**这个新会话窗口里**运行命令,去更新那些需要处理的旧会话。
|
|
44
52
|
|
|
45
|
-
>
|
|
53
|
+
> 别在「本身就需要更新、且正开着」的会话里运行——命令会跳过本会话,以及任何正被打开/使用的会话。
|
|
46
54
|
|
|
47
55
|
### 第四步 · 预览更新范围
|
|
48
56
|
|
|
49
|
-
在编辑区域输入 `/dsh-rewind-fix`
|
|
57
|
+
在编辑区域输入 `/dsh-rewind-fix` 并发送。它只扫描、不写盘。执行时编辑区会正常锁定。**不要中途切换会话窗口。**完成后会告诉你:扫描了几个会话、其中几个将被更新、几个跳过、几个失败。确认是你预期的那几项,再进下一步。
|
|
50
58
|
```
|
|
51
59
|
/dsh-rewind-fix
|
|
52
60
|
```
|
|
@@ -59,7 +67,7 @@ cp -r ~/.dsh/rewind-snapshots ~/.dsh/rewind-snapshots.backup
|
|
|
59
67
|
```
|
|
60
68
|
这一步会真的改动会话日志、把旧标记更新成新格式,**可能需要几分钟**。请耐心等它跑完,**不要中途切换会话窗口**或关闭 DSH——被切到的会话会被**安全地跳过**(不损坏,但这次不会更新,需重新执行)。更新失败的会话保持原状态不变、无破坏,可重新进行更新。被更新的会话会同时**清掉对应的文件快照备份**,更早消息回退时文件还原不可用。
|
|
61
69
|
|
|
62
|
-
### 第六步 · 重启 DSH
|
|
70
|
+
### 第六步 · 重启 DSH,再预览确认
|
|
63
71
|
|
|
64
72
|
执行完后重启一下 DSH,然后**再跑一次预览**:
|
|
65
73
|
```
|
package/lib/client.js
CHANGED
|
@@ -1333,7 +1333,7 @@ function createRewindBridge(deps) {
|
|
|
1333
1333
|
}
|
|
1334
1334
|
|
|
1335
1335
|
// src/client/build-info.ts
|
|
1336
|
-
var PLUGIN_VERSION = true ? "0.9.0
|
|
1336
|
+
var PLUGIN_VERSION = true ? "0.9.0" : "dev";
|
|
1337
1337
|
var BUILD_HASH = true ? "ca24fc4e" : "dev";
|
|
1338
1338
|
|
|
1339
1339
|
// src/client/locales.ts
|
package/lib/index.js
CHANGED
|
@@ -58,6 +58,7 @@ var en = {
|
|
|
58
58
|
"rewindfix.cancelled": "=== cancelled ===",
|
|
59
59
|
"rewindfix.launcherGuard": "! This session itself needs updating; it is open and cannot be modified in place. Start a NEW session (or close it) \u2014 a closed session is updated when no window holds it.",
|
|
60
60
|
"rewindfix.counts": "A={a} B={b} \u2192 C={c}",
|
|
61
|
+
"rewindfix.args": "\xB7 rewired args target {n}",
|
|
61
62
|
"rewindfix.writeOk": "\u21B3 snapCleared \u2713 written \u2713",
|
|
62
63
|
"rewindfix.skip.loaded": "SKIP (loaded)",
|
|
63
64
|
"rewindfix.skip.noMarkers": "SKIP (no markers)",
|
|
@@ -125,6 +126,7 @@ var zh = {
|
|
|
125
126
|
"rewindfix.cancelled": "=== \u5DF2\u53D6\u6D88 ===",
|
|
126
127
|
"rewindfix.launcherGuard": "! \u4F60\u6B63\u5728\u4E00\u4E2A\u672C\u8EAB\u9700\u8981\u66F4\u65B0\u7684\u4F1A\u8BDD\u91CC\u8FD0\u884C\uFF1B\u5B83\u5904\u4E8E\u6253\u5F00\u72B6\u6001\uFF0C\u65E0\u6CD5\u539F\u5730\u4FEE\u6539\u3002\u8BF7\u65B0\u5EFA\u4E00\u4E2A\u4F1A\u8BDD\uFF08\u6216\u5173\u95ED\u5B83\uFF09\u2014\u2014\u5173\u95ED\u7684\u4F1A\u8BDD\u4F1A\u5728\u6CA1\u6709\u7A97\u53E3\u6301\u6709\u5B83\u65F6\u88AB\u66F4\u65B0\u3002",
|
|
127
128
|
"rewindfix.counts": "A={a} B={b} \u2192 C={c}",
|
|
129
|
+
"rewindfix.args": "\xB7 \u4FEE\u6B63 args \u76EE\u6807 {n}",
|
|
128
130
|
"rewindfix.writeOk": "\u21B3 \u5FEB\u7167\u5DF2\u6E05 \u2713 \u5DF2\u5199\u56DE \u2713",
|
|
129
131
|
"rewindfix.skip.loaded": "\u8DF3\u8FC7\uFF08\u5DF2\u52A0\u8F7D\uFF09",
|
|
130
132
|
"rewindfix.skip.noMarkers": "\u8DF3\u8FC7\uFF08\u65E0\u6807\u8BB0\uFF09",
|
|
@@ -1357,6 +1359,9 @@ function isReplaceSurfaceOp(value) {
|
|
|
1357
1359
|
function isLegacyRewindMarker(event) {
|
|
1358
1360
|
return isLegacyRewindMarkerRow(asRow(event));
|
|
1359
1361
|
}
|
|
1362
|
+
function isFormCMarker(event) {
|
|
1363
|
+
return isFormCMarkerRow(asRow(event));
|
|
1364
|
+
}
|
|
1360
1365
|
function repairRewindMarkers(input) {
|
|
1361
1366
|
const rows = input.map(toRow);
|
|
1362
1367
|
const stats = { a: 0, b: 0, c: 0, removedGhosts: 0 };
|
|
@@ -1470,8 +1475,54 @@ function remapReferences(row, mapSeq) {
|
|
|
1470
1475
|
}
|
|
1471
1476
|
const singleSeq = data["sourceEventSeq"];
|
|
1472
1477
|
if (typeof singleSeq === "number") data["sourceEventSeq"] = map(singleSeq);
|
|
1478
|
+
if (row.type === "command/run" && data["name"] === "rewind") {
|
|
1479
|
+
const args = data["args"];
|
|
1480
|
+
if (typeof args === "string") {
|
|
1481
|
+
data["args"] = args.replace(/@(\d+)/g, (match, seqText) => {
|
|
1482
|
+
const seq = Number(seqText);
|
|
1483
|
+
return Number.isSafeInteger(seq) ? `@${mapSeq(seq)}` : match;
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1473
1487
|
return row;
|
|
1474
1488
|
}
|
|
1489
|
+
function rewindTargetSeqOfArgs(args) {
|
|
1490
|
+
if (typeof args !== "string") return void 0;
|
|
1491
|
+
const match = args.match(/@(\d+)/);
|
|
1492
|
+
return match !== null ? Number(match[1]) : void 0;
|
|
1493
|
+
}
|
|
1494
|
+
function repairStaleArgs(input) {
|
|
1495
|
+
const markerBySeq = /* @__PURE__ */ new Map();
|
|
1496
|
+
for (const event of input) {
|
|
1497
|
+
if (isFormCMarker(event)) markerBySeq.set(event.seq, event);
|
|
1498
|
+
}
|
|
1499
|
+
const markerSeqByCommandId = /* @__PURE__ */ new Map();
|
|
1500
|
+
for (const event of input) {
|
|
1501
|
+
if (event.type !== "command/done") continue;
|
|
1502
|
+
const data = event.data;
|
|
1503
|
+
if (typeof data.commandId === "string" && typeof data.sourceEventSeq === "number") {
|
|
1504
|
+
markerSeqByCommandId.set(data.commandId, data.sourceEventSeq);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
let fixed = 0;
|
|
1508
|
+
const events = input.map((event) => {
|
|
1509
|
+
if (event.type !== "command/run") return event;
|
|
1510
|
+
const data = event.data;
|
|
1511
|
+
if (data.name !== "rewind" || typeof data.commandId !== "string" || typeof data.args !== "string") {
|
|
1512
|
+
return event;
|
|
1513
|
+
}
|
|
1514
|
+
const markerSeq = markerSeqByCommandId.get(data.commandId);
|
|
1515
|
+
const marker = markerSeq === void 0 ? void 0 : markerBySeq.get(markerSeq);
|
|
1516
|
+
const start = marker?.surfaceOp?.start;
|
|
1517
|
+
const target = rewindTargetSeqOfArgs(data.args);
|
|
1518
|
+
if (start === void 0 || target === void 0 || target === start) return event;
|
|
1519
|
+
const newArgs = data.args.replace(new RegExp(`@${target}(?!\\d)`), `@${start}`);
|
|
1520
|
+
if (newArgs === data.args) return event;
|
|
1521
|
+
fixed += 1;
|
|
1522
|
+
return { ...event, data: { ...data, args: newArgs } };
|
|
1523
|
+
});
|
|
1524
|
+
return { events, fixed };
|
|
1525
|
+
}
|
|
1475
1526
|
|
|
1476
1527
|
// src/rewind-fix.ts
|
|
1477
1528
|
import { copyFile, rename as rename2, unlink, writeFile as writeFile2, readFile as readFile2, open } from "node:fs/promises";
|
|
@@ -1609,38 +1660,50 @@ async function runRewindFix(deps, opts) {
|
|
|
1609
1660
|
async function processOneSession(deps, opts, header) {
|
|
1610
1661
|
const id = header.id;
|
|
1611
1662
|
if (deps.isSessionLoaded(header.id)) {
|
|
1612
|
-
return { id, status: "skipped", a: 0, b: 0, c: 0, reason: "loaded" };
|
|
1663
|
+
return { id, status: "skipped", a: 0, b: 0, c: 0, staleArgs: 0, reason: "loaded" };
|
|
1613
1664
|
}
|
|
1614
1665
|
let decoded;
|
|
1615
1666
|
try {
|
|
1616
1667
|
decoded = await readEvents(deps, header.id, opts.signal);
|
|
1617
1668
|
} catch (error) {
|
|
1618
|
-
return { id, status: "failed", a: 0, b: 0, c: 0, reason: "unreadable", error: textOf(error) };
|
|
1669
|
+
return { id, status: "failed", a: 0, b: 0, c: 0, staleArgs: 0, reason: "unreadable", error: textOf(error) };
|
|
1619
1670
|
}
|
|
1620
1671
|
if (decoded === void 0) {
|
|
1621
|
-
return { id, status: "skipped", a: 0, b: 0, c: 0, reason: "no-artifact" };
|
|
1672
|
+
return { id, status: "skipped", a: 0, b: 0, c: 0, staleArgs: 0, reason: "no-artifact" };
|
|
1673
|
+
}
|
|
1674
|
+
let converted;
|
|
1675
|
+
try {
|
|
1676
|
+
converted = repairRewindMarkers(decoded.events);
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
return { id, status: "failed", a: 0, b: 0, c: 0, staleArgs: 0, reason: "repair", error: textOf(error) };
|
|
1622
1679
|
}
|
|
1623
|
-
let
|
|
1680
|
+
let stale;
|
|
1624
1681
|
try {
|
|
1625
|
-
|
|
1682
|
+
stale = repairStaleArgs(converted.events);
|
|
1626
1683
|
} catch (error) {
|
|
1627
|
-
return { id, status: "failed", a: 0, b: 0, c: 0, reason: "repair", error: textOf(error) };
|
|
1684
|
+
return { id, status: "failed", a: 0, b: 0, c: 0, staleArgs: 0, reason: "repair", error: textOf(error) };
|
|
1628
1685
|
}
|
|
1629
|
-
const
|
|
1630
|
-
|
|
1631
|
-
|
|
1686
|
+
const a = converted.stats.a;
|
|
1687
|
+
const b = converted.stats.b;
|
|
1688
|
+
const c = converted.stats.a + converted.stats.b + converted.stats.c;
|
|
1689
|
+
const staleArgs = stale.fixed;
|
|
1690
|
+
const needsRepair = a + b > 0 || staleArgs > 0;
|
|
1691
|
+
if (!needsRepair) {
|
|
1692
|
+
return { id, status: "skipped", a, b, c, staleArgs, reason: "no-markers" };
|
|
1632
1693
|
}
|
|
1633
1694
|
if (!opts.apply) {
|
|
1634
|
-
return { id, status: "repaired", a
|
|
1695
|
+
return { id, status: "repaired", a, b, c, staleArgs };
|
|
1635
1696
|
}
|
|
1697
|
+
const repair = { events: stale.events, a, b, c, staleArgs };
|
|
1636
1698
|
return executeSession(deps, header, decoded.headerLine, repair);
|
|
1637
1699
|
}
|
|
1638
1700
|
async function executeSession(deps, header, headerLine, repair) {
|
|
1639
1701
|
const base = {
|
|
1640
1702
|
id: header.id,
|
|
1641
|
-
a: repair.
|
|
1642
|
-
b: repair.
|
|
1643
|
-
c: repair.
|
|
1703
|
+
a: repair.a,
|
|
1704
|
+
b: repair.b,
|
|
1705
|
+
c: repair.c,
|
|
1706
|
+
staleArgs: repair.staleArgs
|
|
1644
1707
|
};
|
|
1645
1708
|
if (deps.isSessionLoaded(header.id)) {
|
|
1646
1709
|
return { ...base, status: "skipped", reason: "loaded-between", error: "loaded between scan and write" };
|
|
@@ -1761,8 +1824,9 @@ function sessionLines(outcome, index, total, apply2, render) {
|
|
|
1761
1824
|
const head = `[${index}/${total}] ${outcome.id}`;
|
|
1762
1825
|
if (outcome.status === "repaired") {
|
|
1763
1826
|
const counts = render("rewindfix.counts", { a: outcome.a, b: outcome.b, c: outcome.c });
|
|
1764
|
-
|
|
1765
|
-
return [`${head} ${counts}
|
|
1827
|
+
const argsNote = outcome.staleArgs > 0 ? " " + render("rewindfix.args", { n: outcome.staleArgs }) : "";
|
|
1828
|
+
if (!apply2) return [`${head} ${counts}${argsNote}`];
|
|
1829
|
+
return [`${head} ${counts}${argsNote}`, ` ${render("rewindfix.writeOk")}`];
|
|
1766
1830
|
}
|
|
1767
1831
|
if (outcome.status === "skipped") {
|
|
1768
1832
|
return [`${head} ${skipLabel(outcome, render)}`];
|
package/lib/types/locales.d.ts
CHANGED
|
@@ -76,6 +76,7 @@ export declare const en: {
|
|
|
76
76
|
'rewindfix.cancelled': string;
|
|
77
77
|
'rewindfix.launcherGuard': string;
|
|
78
78
|
'rewindfix.counts': string;
|
|
79
|
+
'rewindfix.args': string;
|
|
79
80
|
'rewindfix.writeOk': string;
|
|
80
81
|
'rewindfix.skip.loaded': string;
|
|
81
82
|
'rewindfix.skip.noMarkers': string;
|
|
@@ -74,6 +74,8 @@ export interface SessionOutcome {
|
|
|
74
74
|
readonly a: number;
|
|
75
75
|
readonly b: number;
|
|
76
76
|
readonly c: number;
|
|
77
|
+
/** Number of `/rewind` `args` targets rewired (C→C stale-args repair). */
|
|
78
|
+
readonly staleArgs: number;
|
|
77
79
|
readonly reason?: OutcomeReason;
|
|
78
80
|
readonly error?: string;
|
|
79
81
|
}
|
|
@@ -81,3 +81,30 @@ export interface RepairOutput {
|
|
|
81
81
|
* (a removed ghost frame); failing closed avoids writing a corrupt log.
|
|
82
82
|
*/
|
|
83
83
|
export declare function repairRewindMarkers(input: ReadonlyArray<SessionEvent | unknown>): RepairOutput;
|
|
84
|
+
/** Result of {@link repairStaleArgs}. */
|
|
85
|
+
export interface StaleArgsRepair {
|
|
86
|
+
/** The coherence-repaired events, in log order. */
|
|
87
|
+
readonly events: SessionEvent[];
|
|
88
|
+
/** Number of `/rewind` command `args` targets rewritten. */
|
|
89
|
+
readonly fixed: number;
|
|
90
|
+
}
|
|
91
|
+
/** The rewind-target seq parsed from a `/rewind` command's `args` string. */
|
|
92
|
+
export declare function rewindTargetSeqOfArgs(args: unknown): number | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* C→C coherence repair: after a legacy→C migration renumbered a session's
|
|
95
|
+
* seqs, a `/rewind` command's `args @<seq>` target can remain in the OLD
|
|
96
|
+
* numbering while its (form-C) marker carries the NEW `surfaceOp.start`. The
|
|
97
|
+
* client `hiddenSeqsOf` derives the USER-side hide span from
|
|
98
|
+
* `args @<target>` + `outcome.sourceEventSeq`; when the two diverge it hides
|
|
99
|
+
* the wrong set (the AGENT-side hide reads the marker's `sourceEventSeqs`,
|
|
100
|
+
* which IS correctly remapped). This rewrites each such `@<seq>` to the
|
|
101
|
+
* marker's `surfaceOp.start` — the authoritative post-migration target.
|
|
102
|
+
*
|
|
103
|
+
* Matching follows the same channel `hiddenSeqsOf` uses: a command/run is
|
|
104
|
+
* joined to its marker via the command/done with the same `commandId` (whose
|
|
105
|
+
* `sourceEventSeq` cites the marker seq). It is a no-op for a coherent session
|
|
106
|
+
* (a fresh live rewind, or one already repaired), so it is idempotent and safe
|
|
107
|
+
* to run on every closed session. It never mutates its input: a rewritten
|
|
108
|
+
* command/run is rebuilt onto a fresh event object.
|
|
109
|
+
*/
|
|
110
|
+
export declare function repairStaleArgs(input: readonly SessionEvent[]): StaleArgsRepair;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rewind-plugin",
|
|
3
|
-
"version": "0.9.0
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|