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 +3 -3
- package/README.md +3 -3
- package/docs/rewind-fix.md +10 -2
- package/docs/rewind-fix.zh.md +10 -2
- 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-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
|
[](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
|
[](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
|
@@ -9,9 +9,17 @@
|
|
|
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
|
|
package/docs/rewind-fix.zh.md
CHANGED
|
@@ -9,9 +9,17 @@
|
|
|
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
|
|
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.
|
|
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
|
|
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-alpha.
|
|
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",
|