dsh-rewind-plugin 0.9.0-alpha.1 → 0.9.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md 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-alpha.1`) 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
  [![npm version](https://img.shields.io/npm/v/dsh-rewind-plugin.svg)](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-alpha.1`),并运行 `/dsh-rewind-fix` 更新旧回退标记**([更新指南](docs/rewind-fix.zh.md))。
5
+
3
6
  DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消息**——同窗口内完成,不新建分支、不换窗口,可一并还原工作区文件(完整 Claude Code `/rewind` 语义)。
4
7
 
5
8
  [![npm version](https://img.shields.io/npm/v/dsh-rewind-plugin.svg)](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 回复、工具调用)同时从**模型上下文**和**渲染对话**中撤回,不新建会话、不切换窗口;目标消息文本会回填输入框,改完可重发。**在原理上就真正无感、便捷**。
@@ -9,9 +9,17 @@
9
9
 
10
10
  ## Background
11
11
 
12
- `/rewind` appends a **marker** to the session log, telling DSH "everything from this screen onward is withdrawn". Aged plugin versions used a "ghost step frame" for this; the current version uses a more canonical, functionally-equivalent shape a content-empty `user/message` that replaces the content after the target (see the [README](../README.en.md), the "How it works" section).
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
- The next DSH line (`v0.1.3`) will reject the old shape, so **sessions rewound by an older plugin may fail to open after an upgrade**. That's why the plugin ships `/dsh-rewind-fix`: it translates those old markers into the new shape and makes the sessions usable again. The new shape is also fully compatible with older DSH versions.
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
 
@@ -9,9 +9,17 @@
9
9
 
10
10
  ## 背景
11
11
 
12
- `/rewind` 回退会往会话日志追加一条**标记**,告诉 DSH 从某一屏往后都算「已撤回」。旧版插件用的是「幽灵步骤框架」写法;新版本已换成更规范且功能一致的形态——一条内容为空的 `user/message`,把目标之后的内容整体替换掉(原理见 [README](../README.md) 的「原理」一节)。
12
+ `dsh-rewind` 的回退是**同一会话**的消息回退。它从不新建分支,同时遵循保守策略:**不删除任何历史**(会话日志是 append-only 审计轨迹),只在日志里**追加一条标记**,表示“模型可见的对话从这条消息继续,其后内容回卷”。旧版插件的 **rewind 标记**用的是「幽灵步骤框架」写法,在 DSH v0.1.2-rc.1 及之前版本长期验证有效。
13
13
 
14
- 新一代 DSH(`v0.1.3`)会拒绝旧写法,所以**用旧版插件回退过的会话,升级后可能打不开**。因此本插件提供了 `/dsh-rewind-fix` 便捷命令,把这些旧标记「翻译」成新格式,让会话重新可用。新格式同样对旧版本 DSH 完全兼容。
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
 
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-alpha.1" : "dev";
1336
+ var PLUGIN_VERSION = true ? "0.9.0-alpha.2" : "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 repair;
1680
+ let stale;
1624
1681
  try {
1625
- repair = repairRewindMarkers(decoded.events);
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 hasMarkers = repair.stats.a + repair.stats.b > 0;
1630
- if (!hasMarkers) {
1631
- return { id, status: "skipped", a: 0, b: 0, c: 0, reason: "no-markers" };
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: repair.stats.a, b: repair.stats.b, c: repair.stats.a + repair.stats.b + repair.stats.c };
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.stats.a,
1642
- b: repair.stats.b,
1643
- c: repair.stats.a + repair.stats.b + repair.stats.c
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
- if (!apply2) return [`${head} ${counts}`];
1765
- return [`${head} ${counts}`, ` ${render("rewindfix.writeOk")}`];
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)}`];
@@ -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-alpha.1",
3
+ "version": "0.9.0-alpha.2",
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",