dsh-rewind-plugin 0.7.5 → 0.9.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/lib/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  // src/index.ts
2
- import { createAssistantMessage } from "@deepseek-ai/dsh-llm";
3
- import { unlink as unlink2 } from "node:fs/promises";
4
- import * as dshSettings from "@deepseek-ai/dsh-settings";
2
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
3
+ import { unlink as unlink3 } from "node:fs/promises";
5
4
  import "@deepseek-ai/schemastery";
6
5
 
7
6
  // src/locales.ts
@@ -51,7 +50,26 @@ var en = {
51
50
  "cleanup.clearActive": "Could not clear session {sessionId}: the session is still running and could not be stopped. Try again once it is idle.",
52
51
  "cleanup.clearCancelled": "Clear cancelled.",
53
52
  "cleanup.clearFailed": "Could not clear session {sessionId}: {detail}.",
54
- "cleanup.usage": "Usage:\n /snapshot-auto-cleanup show status\n /snapshot-auto-cleanup on|off enable/disable auto-cleanup\n /snapshot-auto-cleanup max-age <days> set the idle cutoff\n /snapshot-auto-cleanup run [--apply] dry-run, or execute with --apply\n /snapshot-auto-cleanup run --current [--apply] dry-run/clear this session's snapshots"
53
+ "cleanup.usage": "Usage:\n /snapshot-auto-cleanup show status\n /snapshot-auto-cleanup on|off enable/disable auto-cleanup\n /snapshot-auto-cleanup max-age <days> set the idle cutoff\n /snapshot-auto-cleanup run [--apply] dry-run, or execute with --apply\n /snapshot-auto-cleanup run --current [--apply] dry-run/clear this session's snapshots",
54
+ "rewindfix.description": "Update legacy rewind markers (A/B) in closed sessions to the current form-C shape",
55
+ "rewindfix.inputHint": "no args = dry-run preview; --apply = execute (takes minutes)",
56
+ "rewindfix.dryRun": "=== dry-run: {scan} sessions scanned / {will} will be updated / {skip} skipped / {fail} failed ({secs}s) ===",
57
+ "rewindfix.done": "=== done: {repair} updated / {skip} skipped / {fail} failed ({secs}s) ===",
58
+ "rewindfix.cancelled": "=== cancelled ===",
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
+ "rewindfix.counts": "A={a} B={b} \u2192 C={c}",
61
+ "rewindfix.writeOk": "\u21B3 snapCleared \u2713 written \u2713",
62
+ "rewindfix.skip.loaded": "SKIP (loaded)",
63
+ "rewindfix.skip.noMarkers": "SKIP (no markers)",
64
+ "rewindfix.skip.noArtifact": "SKIP (no artifact)",
65
+ "rewindfix.skip.loadedNow": "SKIP (loaded now)",
66
+ "rewindfix.fail.unreadable": "FAIL (unreadable) {error}",
67
+ "rewindfix.fail.repair": "FAIL (update) {error}",
68
+ "rewindfix.fail.locked": "FAIL (locked) {error}",
69
+ "rewindfix.fail.generic": "FAIL: {error}",
70
+ "rewindfix.persistenceUnavailable": "Session persistence is unavailable; cannot run the rewind-fix command.",
71
+ "rewindfix.sessionStoreUnavailable": "The session store is unavailable; cannot run the rewind-fix command.",
72
+ "rewindfix.locked": "locked by another process"
55
73
  };
56
74
  var zh = {
57
75
  "usage.title": "\u7528\u6CD5\uFF1A",
@@ -99,7 +117,26 @@ var zh = {
99
117
  "cleanup.clearActive": "\u65E0\u6CD5\u6E05\u9664\u4F1A\u8BDD {sessionId}\uFF1A\u4F1A\u8BDD\u4ECD\u5728\u8FD0\u884C\u4E14\u672A\u80FD\u505C\u6B62\uFF0C\u8BF7\u5F85\u5176\u7A7A\u95F2\u540E\u91CD\u8BD5\u3002",
100
118
  "cleanup.clearCancelled": "\u6E05\u7A7A\u5DF2\u53D6\u6D88\u3002",
101
119
  "cleanup.clearFailed": "\u65E0\u6CD5\u6E05\u9664\u4F1A\u8BDD {sessionId}\uFF1A{detail}\u3002",
102
- "cleanup.usage": "\u7528\u6CD5\uFF1A\n /snapshot-auto-cleanup \u67E5\u770B\u72B6\u6001\n /snapshot-auto-cleanup on|off \u5F00\u542F/\u5173\u95ED\u81EA\u52A8\u6E05\u7406\n /snapshot-auto-cleanup max-age <\u5929\u6570> \u8BBE\u7F6E\u5931\u6D3B\u9608\u503C\uFF08\u5929\uFF09\n /snapshot-auto-cleanup run [--apply] \u9884\u6F14\uFF0C\u6216\u52A0 --apply \u6267\u884C\n /snapshot-auto-cleanup run --current [--apply] \u9884\u6F14/\u6E05\u9664\u672C\u4F1A\u8BDD\u5FEB\u7167"
120
+ "cleanup.usage": "\u7528\u6CD5\uFF1A\n /snapshot-auto-cleanup \u67E5\u770B\u72B6\u6001\n /snapshot-auto-cleanup on|off \u5F00\u542F/\u5173\u95ED\u81EA\u52A8\u6E05\u7406\n /snapshot-auto-cleanup max-age <\u5929\u6570> \u8BBE\u7F6E\u5931\u6D3B\u9608\u503C\uFF08\u5929\uFF09\n /snapshot-auto-cleanup run [--apply] \u9884\u6F14\uFF0C\u6216\u52A0 --apply \u6267\u884C\n /snapshot-auto-cleanup run --current [--apply] \u9884\u6F14/\u6E05\u9664\u672C\u4F1A\u8BDD\u5FEB\u7167",
121
+ "rewindfix.description": "\u5C06\u5DF2\u5173\u95ED\u4F1A\u8BDD\u4E2D\u7684\u65E7\u7248 rewind \u6807\u8BB0\uFF08A/B\uFF09\u66F4\u65B0\u4E3A\u5F53\u524D form-C \u5F62\u6001",
122
+ "rewindfix.inputHint": "\u65E0\u53C2 = dry-run \u9884\u89C8\uFF1B--apply = \u6267\u884C\uFF08\u9700\u6570\u5206\u949F\uFF09",
123
+ "rewindfix.dryRun": "=== \u9884\u6F14\uFF1A\u626B\u63CF {scan} \u4E2A\u4F1A\u8BDD / \u5C06\u66F4\u65B0 {will} \u4E2A / \u8DF3\u8FC7 {skip} \u4E2A / \u5931\u8D25 {fail} \u4E2A\uFF08{secs}s\uFF09===",
124
+ "rewindfix.done": "=== \u5B8C\u6210\uFF1A\u66F4\u65B0 {repair} \u4E2A / \u8DF3\u8FC7 {skip} \u4E2A / \u5931\u8D25 {fail} \u4E2A\uFF08{secs}s\uFF09===",
125
+ "rewindfix.cancelled": "=== \u5DF2\u53D6\u6D88 ===",
126
+ "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
+ "rewindfix.counts": "A={a} B={b} \u2192 C={c}",
128
+ "rewindfix.writeOk": "\u21B3 \u5FEB\u7167\u5DF2\u6E05 \u2713 \u5DF2\u5199\u56DE \u2713",
129
+ "rewindfix.skip.loaded": "\u8DF3\u8FC7\uFF08\u5DF2\u52A0\u8F7D\uFF09",
130
+ "rewindfix.skip.noMarkers": "\u8DF3\u8FC7\uFF08\u65E0\u6807\u8BB0\uFF09",
131
+ "rewindfix.skip.noArtifact": "\u8DF3\u8FC7\uFF08\u65E0\u4EA7\u7269\uFF09",
132
+ "rewindfix.skip.loadedNow": "\u8DF3\u8FC7\uFF08\u73B0\u5DF2\u52A0\u8F7D\uFF09",
133
+ "rewindfix.fail.unreadable": "\u5931\u8D25\uFF08\u65E0\u6CD5\u8BFB\u53D6\uFF09{error}",
134
+ "rewindfix.fail.repair": "\u5931\u8D25\uFF08\u66F4\u65B0\uFF09{error}",
135
+ "rewindfix.fail.locked": "\u5931\u8D25\uFF08\u88AB\u9501\u5B9A\uFF09{error}",
136
+ "rewindfix.fail.generic": "\u5931\u8D25\uFF1A{error}",
137
+ "rewindfix.persistenceUnavailable": "\u4F1A\u8BDD\u6301\u4E45\u5C42\u4E0D\u53EF\u7528\uFF0C\u65E0\u6CD5\u6267\u884C rewind-fix \u547D\u4EE4\u3002",
138
+ "rewindfix.sessionStoreUnavailable": "\u4F1A\u8BDD\u5B58\u50A8\u4E0D\u53EF\u7528\uFF0C\u65E0\u6CD5\u6267\u884C rewind-fix \u547D\u4EE4\u3002",
139
+ "rewindfix.locked": "\u5DF2\u88AB\u53E6\u4E00\u8FDB\u7A0B\u9501\u5B9A"
103
140
  };
104
141
  var HOST_DICTS = { en, zh };
105
142
  function translate(lang, key, params = {}) {
@@ -111,20 +148,6 @@ function translate(lang, key, params = {}) {
111
148
  return text;
112
149
  }
113
150
 
114
- // src/session-events.ts
115
- function eventsOf(session) {
116
- const s = session;
117
- if (s.snapshotEvents !== void 0) return s.snapshotEvents();
118
- if (s.events !== void 0) return s.events;
119
- return [];
120
- }
121
-
122
- // src/settings-locale.ts
123
- function readSettingsSection(provider, ns, brand) {
124
- const key = brand?.(ns) ?? ns;
125
- return provider.get(key);
126
- }
127
-
128
151
  // src/rewind.ts
129
152
  var RewindError = class extends Error {
130
153
  constructor(code, message) {
@@ -136,24 +159,6 @@ var RewindError = class extends Error {
136
159
  };
137
160
  var CANDIDATE_PREVIEW_CHARS = 80;
138
161
  var DEFAULT_CANDIDATE_LIMIT = 100;
139
- function markerTurnOf(events) {
140
- let lastStarted = 0;
141
- for (const event of events) {
142
- if (event.type === "turn/start" && event.data.turn > lastStarted) {
143
- lastStarted = event.data.turn;
144
- }
145
- }
146
- return lastStarted;
147
- }
148
- function markerStepOf(events, turn) {
149
- let lastStarted = 0;
150
- for (const event of events) {
151
- if (event.type === "step/start" && event.data.turn === turn && event.data.step > lastStarted) {
152
- lastStarted = event.data.step;
153
- }
154
- }
155
- return lastStarted + 1;
156
- }
157
162
  function isUserMessageEvent(event) {
158
163
  return event.type === "user/message";
159
164
  }
@@ -1335,8 +1340,467 @@ async function reconcileTracked(store, sessionId, anchorSeq, tracked, probe = de
1335
1340
  return recorded;
1336
1341
  }
1337
1342
 
1343
+ // src/rewind-marker-repair.ts
1344
+ var REWIND_MARKER_SOURCE = { kind: "plugin", plugin: "dsh-rewind" };
1345
+ var REWIND_MARKER_CONTENT = [];
1346
+ function buildRewindMarkerData(id) {
1347
+ return {
1348
+ role: "user",
1349
+ content: REWIND_MARKER_CONTENT,
1350
+ source: { ...REWIND_MARKER_SOURCE },
1351
+ ...id !== void 0 ? { id } : {}
1352
+ };
1353
+ }
1354
+ function isReplaceSurfaceOp(value) {
1355
+ return typeof value === "object" && value !== null && value.op === "replace" && typeof value.start === "number" && typeof value.end === "number";
1356
+ }
1357
+ function isLegacyRewindMarker(event) {
1358
+ return isLegacyRewindMarkerRow(asRow(event));
1359
+ }
1360
+ function repairRewindMarkers(input) {
1361
+ const rows = input.map(toRow);
1362
+ const stats = { a: 0, b: 0, c: 0, removedGhosts: 0 };
1363
+ for (const row of rows) {
1364
+ if (isFormCMarkerRow(row)) stats.c += 1;
1365
+ }
1366
+ const removed = /* @__PURE__ */ new Set();
1367
+ for (let i = 0; i < rows.length; i += 1) {
1368
+ const row = rows[i];
1369
+ if (!isLegacyRewindMarkerRow(row)) continue;
1370
+ const prev = rows[i - 1];
1371
+ const next = rows[i + 1];
1372
+ const hasBefore = prev !== void 0 && prev.type === "step/start";
1373
+ const hasAfter = next !== void 0 && next.type === "step/end";
1374
+ if (hasBefore && hasAfter) {
1375
+ removed.add(i - 1);
1376
+ removed.add(i + 1);
1377
+ stats.b += 1;
1378
+ stats.removedGhosts += 2;
1379
+ } else {
1380
+ stats.a += 1;
1381
+ }
1382
+ }
1383
+ const survivors = [];
1384
+ const oldToNew = /* @__PURE__ */ new Map();
1385
+ for (let i = 0; i < rows.length; i += 1) {
1386
+ if (removed.has(i)) continue;
1387
+ const original = rows[i];
1388
+ const newSeq = survivors.length;
1389
+ oldToNew.set(original.seq, newSeq);
1390
+ survivors.push(isLegacyRewindMarkerRow(original) ? toEvent(retargetToC(original, newSeq)) : toEvent({ ...original, seq: newSeq }));
1391
+ }
1392
+ const mapSeq = (old) => {
1393
+ const mapped = oldToNew.get(old);
1394
+ if (mapped === void 0) {
1395
+ throw new Error(`rewind-fix: reference targets consumed seq ${old} (not a surviving event)`);
1396
+ }
1397
+ return mapped;
1398
+ };
1399
+ const events = survivors.map((event) => toEvent(remapReferences(toRow(event), mapSeq)));
1400
+ return { events, mapSeq: oldToNew, stats };
1401
+ }
1402
+ function isLegacyRewindMarkerRow(row) {
1403
+ if (row.type !== "assistant/message") return false;
1404
+ const message = row.data["message"];
1405
+ const source = message?.["source"];
1406
+ if (source?.["kind"] !== "model") return false;
1407
+ if (source?.["provider"] !== "dsh-rewind" || source?.["model"] !== "rewind-marker") return false;
1408
+ return isReplaceSurfaceOp(row.surfaceOp);
1409
+ }
1410
+ function isFormCMarkerRow(row) {
1411
+ if (row.type !== "user/message") return false;
1412
+ const source = row.data["source"];
1413
+ return source?.["kind"] === "plugin" && source?.["plugin"] === "dsh-rewind";
1414
+ }
1415
+ function asRow(event) {
1416
+ const e = event;
1417
+ return {
1418
+ type: e.type,
1419
+ seq: e.seq,
1420
+ time: e.time,
1421
+ data: { ...e.data },
1422
+ ..."surfaceOp" in e ? { surfaceOp: e.surfaceOp } : {},
1423
+ ...Array.isArray(e.sourceEventSeqs) ? { sourceEventSeqs: [...e.sourceEventSeqs] } : {},
1424
+ ..."ignorable" in e ? { ignorable: e.ignorable } : {}
1425
+ };
1426
+ }
1427
+ function toRow(event) {
1428
+ return asRow(event);
1429
+ }
1430
+ function toEvent(row) {
1431
+ const e = {
1432
+ type: row.type,
1433
+ seq: row.seq,
1434
+ time: row.time,
1435
+ data: row.data
1436
+ };
1437
+ if (row.surfaceOp !== void 0) e["surfaceOp"] = row.surfaceOp;
1438
+ if (row.sourceEventSeqs !== void 0) e["sourceEventSeqs"] = row.sourceEventSeqs;
1439
+ if (row.ignorable !== void 0) e["ignorable"] = row.ignorable;
1440
+ return e;
1441
+ }
1442
+ function retargetToC(row, seq) {
1443
+ const message = row.data["message"];
1444
+ const id = typeof message?.["id"] === "string" ? message["id"] : void 0;
1445
+ return {
1446
+ type: "user/message",
1447
+ seq,
1448
+ time: row.time,
1449
+ data: buildRewindMarkerData(id),
1450
+ surfaceOp: row.surfaceOp,
1451
+ sourceEventSeqs: row.sourceEventSeqs
1452
+ };
1453
+ }
1454
+ function remapReferences(row, mapSeq) {
1455
+ const map = (value) => mapSeq(value);
1456
+ if (row.sourceEventSeqs !== void 0) {
1457
+ row.sourceEventSeqs = row.sourceEventSeqs.map(map);
1458
+ }
1459
+ if (isReplaceSurfaceOp(row.surfaceOp)) {
1460
+ row.surfaceOp = { op: "replace", start: map(row.surfaceOp.start), end: map(row.surfaceOp.end) };
1461
+ }
1462
+ const data = row.data;
1463
+ const range = data["shadowedRange"];
1464
+ if (range !== void 0 && typeof range === "object") {
1465
+ data["shadowedRange"] = { start: map(range.start), end: map(range.end) };
1466
+ }
1467
+ for (const key of ["shadowedSeqs", "messageSeqs"]) {
1468
+ const seqs = data[key];
1469
+ if (Array.isArray(seqs)) data[key] = seqs.map(map);
1470
+ }
1471
+ const singleSeq = data["sourceEventSeq"];
1472
+ if (typeof singleSeq === "number") data["sourceEventSeq"] = map(singleSeq);
1473
+ return row;
1474
+ }
1475
+
1476
+ // src/rewind-fix.ts
1477
+ import { copyFile, rename as rename2, unlink, writeFile as writeFile2, readFile as readFile2, open } from "node:fs/promises";
1478
+ import { isDeepStrictEqual } from "node:util";
1479
+
1480
+ // src/session-log-io.ts
1481
+ import { constants, zstdCompress, zstdDecompress } from "node:zlib";
1482
+ import { promisify } from "node:util";
1483
+ import {
1484
+ decodeSeqRanges,
1485
+ decodeStorageRecord,
1486
+ encodeSeqRanges,
1487
+ packChunkRuns
1488
+ } from "@deepseek-ai/dsh-session";
1489
+ var zstdCompressAsync = promisify(zstdCompress);
1490
+ var zstdDecompressAsync = promisify(zstdDecompress);
1491
+ async function compressZstdFrame(input) {
1492
+ return zstdCompressAsync(Buffer.from(input), { params: { [constants.ZSTD_c_checksumFlag]: 1 } });
1493
+ }
1494
+ function splitSession(plaintext) {
1495
+ const nl = plaintext.indexOf("\n");
1496
+ if (nl === -1) throw new Error("session plaintext has no header line");
1497
+ return { headerLine: plaintext.slice(0, nl), body: plaintext.slice(nl + 1) };
1498
+ }
1499
+ function expandProvenanceFromStorage(parsed) {
1500
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1501
+ throw new TypeError("stored session records must be objects");
1502
+ }
1503
+ const record = parsed;
1504
+ if (record.sourceEventSeqs === void 0) return parsed;
1505
+ if (!Number.isSafeInteger(record.seq) || record.seq < 0) {
1506
+ throw new TypeError("stored session event seq must be a non-negative safe integer");
1507
+ }
1508
+ return { ...record, sourceEventSeqs: decodeSeqRanges(record.sourceEventSeqs, record.seq) };
1509
+ }
1510
+ function decodeEventBody(body) {
1511
+ const events = [];
1512
+ for (const line of body.split("\n")) {
1513
+ if (line.length === 0) continue;
1514
+ const record = JSON.parse(line);
1515
+ const expanded = expandProvenanceFromStorage(record);
1516
+ events.push(...decodeStorageRecord(expanded));
1517
+ }
1518
+ return events;
1519
+ }
1520
+ function eventLines(events, packChunks) {
1521
+ const records = packChunks ? packChunkRuns(events) : events;
1522
+ return records.map((record) => {
1523
+ const provenance = record.sourceEventSeqs;
1524
+ if (provenance !== void 0) {
1525
+ return JSON.stringify({ ...record, sourceEventSeqs: encodeSeqRanges(provenance) });
1526
+ }
1527
+ return JSON.stringify(record);
1528
+ }).join("\n");
1529
+ }
1530
+ async function encodeSessionLog(headerLine, events, options = {}) {
1531
+ const packChunks = options.packChunks ?? true;
1532
+ const headerOut = headerLine + "\n";
1533
+ const bodyOut = eventLines(events, packChunks) + "\n";
1534
+ const [headerFrame, eventFrame] = await Promise.all([compressZstdFrame(headerOut), compressZstdFrame(bodyOut)]);
1535
+ return Buffer.concat([headerFrame, eventFrame]);
1536
+ }
1537
+
1538
+ // src/rewind-fix.ts
1539
+ function isApplyInput(raw) {
1540
+ return raw.trim() === "--apply";
1541
+ }
1542
+ function registerRewindFix(ctx, store, render) {
1543
+ let persistence;
1544
+ ctx.inject(["sessionPersistence"], (scope) => {
1545
+ persistence = scope.sessionPersistence;
1546
+ });
1547
+ ctx.effect(function* () {
1548
+ yield ctx.commands.register({
1549
+ name: "dsh-rewind-fix",
1550
+ description: render("rewindfix.description"),
1551
+ input: { hint: render("rewindfix.inputHint") },
1552
+ handler: (invocation) => handleRewindFix(ctx, store, persistence, invocation, render)
1553
+ });
1554
+ }, "dsh-rewind-fix command");
1555
+ }
1556
+ async function handleRewindFix(ctx, store, persistence, invocation, render) {
1557
+ if (persistence === void 0) {
1558
+ return { kind: "error", text: render("rewindfix.persistenceUnavailable") };
1559
+ }
1560
+ const sessionStore = ctx.get("sessions");
1561
+ if (sessionStore === void 0) {
1562
+ return { kind: "error", text: render("rewindfix.sessionStoreUnavailable") };
1563
+ }
1564
+ const launcher = invocation.agent?.session;
1565
+ const deps = {
1566
+ listSnapshots: (signal) => persistence.listSnapshots(signal),
1567
+ readRaw: (id, signal) => persistence.readRaw(id, signal),
1568
+ locate: (header) => persistence.locate(header),
1569
+ isSessionLoaded: (id) => sessionStore.get(id) !== void 0,
1570
+ clearSession: (id) => store.clearSession(id)
1571
+ };
1572
+ const result = await runRewindFix(deps, {
1573
+ apply: isApplyInput(invocation.rawInput),
1574
+ launcherHasMarkers: (launcher?.snapshotEvents() ?? []).some(isLegacyRewindMarker),
1575
+ signal: invocation.signal
1576
+ });
1577
+ return { kind: "success", text: renderRewindFixReport(result, render) };
1578
+ }
1579
+ async function runRewindFix(deps, opts) {
1580
+ const snapshots = await deps.listSnapshots(opts.signal);
1581
+ const total = snapshots.length;
1582
+ const started = Date.now();
1583
+ let cancelled = false;
1584
+ const concurrency = Math.max(1, Math.min(opts.concurrency ?? 4, total));
1585
+ const results = new Array(total);
1586
+ let cursor = 0;
1587
+ const worker = async () => {
1588
+ for (; ; ) {
1589
+ if (opts.signal?.aborted) {
1590
+ cancelled = true;
1591
+ return;
1592
+ }
1593
+ const i = cursor++;
1594
+ if (i >= total) return;
1595
+ results[i] = await processOneSession(deps, opts, snapshots[i].header);
1596
+ }
1597
+ };
1598
+ await Promise.all(Array.from({ length: concurrency }, () => worker()));
1599
+ const sessions = results.filter((o) => o !== void 0);
1600
+ return {
1601
+ launcherHasMarkers: opts.launcherHasMarkers,
1602
+ cancelled,
1603
+ scanned: total,
1604
+ durationMs: Date.now() - started,
1605
+ apply: opts.apply,
1606
+ sessions
1607
+ };
1608
+ }
1609
+ async function processOneSession(deps, opts, header) {
1610
+ const id = header.id;
1611
+ if (deps.isSessionLoaded(header.id)) {
1612
+ return { id, status: "skipped", a: 0, b: 0, c: 0, reason: "loaded" };
1613
+ }
1614
+ let decoded;
1615
+ try {
1616
+ decoded = await readEvents(deps, header.id, opts.signal);
1617
+ } catch (error) {
1618
+ return { id, status: "failed", a: 0, b: 0, c: 0, reason: "unreadable", error: textOf(error) };
1619
+ }
1620
+ if (decoded === void 0) {
1621
+ return { id, status: "skipped", a: 0, b: 0, c: 0, reason: "no-artifact" };
1622
+ }
1623
+ let repair;
1624
+ try {
1625
+ repair = repairRewindMarkers(decoded.events);
1626
+ } catch (error) {
1627
+ return { id, status: "failed", a: 0, b: 0, c: 0, reason: "repair", error: textOf(error) };
1628
+ }
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" };
1632
+ }
1633
+ 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 };
1635
+ }
1636
+ return executeSession(deps, header, decoded.headerLine, repair);
1637
+ }
1638
+ async function executeSession(deps, header, headerLine, repair) {
1639
+ const base = {
1640
+ id: header.id,
1641
+ a: repair.stats.a,
1642
+ b: repair.stats.b,
1643
+ c: repair.stats.a + repair.stats.b + repair.stats.c
1644
+ };
1645
+ if (deps.isSessionLoaded(header.id)) {
1646
+ return { ...base, status: "skipped", reason: "loaded-between", error: "loaded between scan and write" };
1647
+ }
1648
+ const location = deps.locate(header);
1649
+ if (location === void 0) return { ...base, status: "failed", error: "locate() returned no path" };
1650
+ const path = location.path;
1651
+ const lockPath = `${path}.rewind-fix.lock`;
1652
+ const tmpPath = `${path}.rewind-fix.tmp`;
1653
+ const bakPath = `${path}.rewind-fix.bak`;
1654
+ const lock = await acquireSessionLock(lockPath);
1655
+ if (lock === void 0) {
1656
+ return { ...base, status: "failed", reason: "locked", error: "another process holds the session lock" };
1657
+ }
1658
+ try {
1659
+ await copyFile(path, bakPath);
1660
+ const buffer = await encodeSessionLog(headerLine, repair.events);
1661
+ await writeFile2(tmpPath, buffer);
1662
+ await rename2(tmpPath, path);
1663
+ const verification = await verifyWritten(deps, header.id);
1664
+ if (verification === void 0) throw new Error("written artifact could not be re-read");
1665
+ if (!isDeepStrictEqual(normalizeForVerify(verification), normalizeForVerify(repair.events))) {
1666
+ throw new Error("round-trip mismatch after write");
1667
+ }
1668
+ await deps.clearSession(idOf(header));
1669
+ return { ...base, status: "repaired" };
1670
+ } catch (error) {
1671
+ await unlink(tmpPath).catch(() => {
1672
+ });
1673
+ await rename2(bakPath, path).catch(() => {
1674
+ });
1675
+ return { ...base, status: "failed", error: textOf(error) };
1676
+ } finally {
1677
+ await lock.close().catch(() => {
1678
+ });
1679
+ await unlink(lockPath).catch(() => {
1680
+ });
1681
+ await unlink(bakPath).catch(() => {
1682
+ });
1683
+ }
1684
+ }
1685
+ async function readEvents(deps, id, signal) {
1686
+ const raw = await deps.readRaw(id, signal);
1687
+ if (raw === void 0) return void 0;
1688
+ const { headerLine, body } = splitSession(raw.content);
1689
+ return { events: decodeEventBody(body), headerLine };
1690
+ }
1691
+ async function verifyWritten(deps, id) {
1692
+ const raw = await deps.readRaw(id);
1693
+ if (raw === void 0) return void 0;
1694
+ return decodeEventBody(splitSession(raw.content).body);
1695
+ }
1696
+ function normalizeForVerify(events) {
1697
+ return events.map((e) => ({ type: e.type, seq: e.seq, data: e.data }));
1698
+ }
1699
+ function textOf(error) {
1700
+ return error instanceof Error ? error.message : String(error);
1701
+ }
1702
+ function idOf(header) {
1703
+ return header.id;
1704
+ }
1705
+ async function acquireSessionLock(lockPath) {
1706
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1707
+ try {
1708
+ const handle = await open(lockPath, "wx");
1709
+ await handle.writeFile(String(process.pid));
1710
+ return handle;
1711
+ } catch (error) {
1712
+ const code = error.code;
1713
+ if (code !== "EEXIST") throw error;
1714
+ if (attempt === 1) return void 0;
1715
+ if (await removeStaleLock(lockPath)) continue;
1716
+ return void 0;
1717
+ }
1718
+ }
1719
+ return void 0;
1720
+ }
1721
+ async function removeStaleLock(lockPath) {
1722
+ let pid = 0;
1723
+ try {
1724
+ pid = Number((await readFile2(lockPath, "utf8")).trim());
1725
+ } catch {
1726
+ return false;
1727
+ }
1728
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
1729
+ if (pidAlive(pid)) return false;
1730
+ await unlink(lockPath).catch(() => {
1731
+ });
1732
+ return true;
1733
+ }
1734
+ function pidAlive(pid) {
1735
+ try {
1736
+ process.kill(pid, 0);
1737
+ return true;
1738
+ } catch (error) {
1739
+ return error.code === "EPERM";
1740
+ }
1741
+ }
1742
+ function renderRewindFixReport(result, render) {
1743
+ const parts = [];
1744
+ if (result.launcherHasMarkers) parts.push(render("rewindfix.launcherGuard"));
1745
+ parts.push(headline(result, render));
1746
+ result.sessions.forEach((outcome, i) => parts.push(...sessionLines(outcome, i + 1, result.scanned, result.apply, render)));
1747
+ return parts.join("\n");
1748
+ }
1749
+ function headline(result, render) {
1750
+ if (result.cancelled) return render("rewindfix.cancelled");
1751
+ const repaired = result.sessions.filter((o) => o.status === "repaired").length;
1752
+ const skipped = result.sessions.filter((o) => o.status === "skipped").length;
1753
+ const failed = result.sessions.filter((o) => o.status === "failed").length;
1754
+ const secs = (result.durationMs / 1e3).toFixed(1);
1755
+ if (!result.apply) {
1756
+ return render("rewindfix.dryRun", { scan: result.scanned, will: repaired, skip: skipped, fail: failed, secs });
1757
+ }
1758
+ return render("rewindfix.done", { repair: repaired, skip: skipped, fail: failed, secs });
1759
+ }
1760
+ function sessionLines(outcome, index, total, apply2, render) {
1761
+ const head = `[${index}/${total}] ${outcome.id}`;
1762
+ if (outcome.status === "repaired") {
1763
+ 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")}`];
1766
+ }
1767
+ if (outcome.status === "skipped") {
1768
+ return [`${head} ${skipLabel(outcome, render)}`];
1769
+ }
1770
+ return [`${head} ${failLabel(outcome, render)}`];
1771
+ }
1772
+ function skipLabel(outcome, render) {
1773
+ switch (outcome.reason) {
1774
+ case "no-markers":
1775
+ return render("rewindfix.skip.noMarkers");
1776
+ case "no-artifact":
1777
+ return render("rewindfix.skip.noArtifact");
1778
+ case "loaded-between": {
1779
+ const label = render("rewindfix.skip.loadedNow");
1780
+ return outcome.error ? `${label} ${outcome.error}` : label;
1781
+ }
1782
+ case "loaded":
1783
+ default:
1784
+ return render("rewindfix.skip.loaded");
1785
+ }
1786
+ }
1787
+ function failLabel(outcome, render) {
1788
+ switch (outcome.reason) {
1789
+ case "unreadable":
1790
+ return render("rewindfix.fail.unreadable", { error: outcome.error ?? "" });
1791
+ case "repair":
1792
+ return render("rewindfix.fail.repair", { error: outcome.error ?? "" });
1793
+ case "locked":
1794
+ return render("rewindfix.fail.locked", {
1795
+ error: `${render("rewindfix.locked")}${outcome.error ? ` (${outcome.error})` : ""}`
1796
+ });
1797
+ default:
1798
+ return render("rewindfix.fail.generic", { error: outcome.error ?? "" });
1799
+ }
1800
+ }
1801
+
1338
1802
  // src/snapshot-cleanup.ts
1339
- import { mkdir as mkdir2, readFile as readFile2, rename as rename2, unlink, writeFile as writeFile2 } from "node:fs/promises";
1803
+ import { mkdir as mkdir2, readFile as readFile3, rename as rename3, unlink as unlink2, writeFile as writeFile3 } from "node:fs/promises";
1340
1804
  import { dirname as dirname2, join as join2 } from "node:path";
1341
1805
  import { resolveDshHome as resolveDshHome2 } from "@deepseek-ai/dsh-home-paths";
1342
1806
  import z from "@deepseek-ai/schemastery";
@@ -1363,16 +1827,16 @@ async function migrateLegacyCleanupConfig(legacyPath, scope, log) {
1363
1827
  if (!loaded.ok) {
1364
1828
  log(`[dsh-rewind] legacy snapshot-cleanup config invalid, migrating defaults and removing: ${loaded.error}`);
1365
1829
  await scope.update({ enabled: false, maxAgeDays: DEFAULT_MAX_AGE_DAYS });
1366
- await unlink(legacyPath).catch(() => void 0);
1830
+ await unlink2(legacyPath).catch(() => void 0);
1367
1831
  return true;
1368
1832
  }
1369
1833
  if (!loaded.fromFile) return false;
1370
1834
  if (loaded.config.enabled === DEFAULT_CLEANUP_CONFIG.enabled && loaded.config.maxAgeDays === DEFAULT_CLEANUP_CONFIG.maxAgeDays) {
1371
- await unlink(legacyPath).catch(() => void 0);
1835
+ await unlink2(legacyPath).catch(() => void 0);
1372
1836
  return true;
1373
1837
  }
1374
1838
  await scope.update({ enabled: loaded.config.enabled, maxAgeDays: loaded.config.maxAgeDays });
1375
- await unlink(legacyPath).catch(() => void 0);
1839
+ await unlink2(legacyPath).catch(() => void 0);
1376
1840
  log(`[dsh-rewind] migrated legacy snapshot-cleanup config (enabled=${String(loaded.config.enabled)}, maxAgeDays=${String(loaded.config.maxAgeDays)})`);
1377
1841
  return true;
1378
1842
  }
@@ -1386,7 +1850,7 @@ function resolveCleanupStatePath(dshHome) {
1386
1850
  }
1387
1851
  async function loadLastSweepAt(path) {
1388
1852
  try {
1389
- const raw = JSON.parse(await readFile2(path, "utf8"));
1853
+ const raw = JSON.parse(await readFile3(path, "utf8"));
1390
1854
  const value = raw["lastSweepAt"];
1391
1855
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
1392
1856
  } catch {
@@ -1396,8 +1860,8 @@ async function loadLastSweepAt(path) {
1396
1860
  async function saveLastSweepAt(path, ms) {
1397
1861
  const tmp = `${path}.tmp`;
1398
1862
  await mkdir2(dirname2(path), { recursive: true });
1399
- await writeFile2(tmp, JSON.stringify({ lastSweepAt: ms }), "utf8");
1400
- await rename2(tmp, path);
1863
+ await writeFile3(tmp, JSON.stringify({ lastSweepAt: ms }), "utf8");
1864
+ await rename3(tmp, path);
1401
1865
  }
1402
1866
  async function runAutoCleanupCheck(deps, sessionId) {
1403
1867
  try {
@@ -1437,7 +1901,7 @@ function parseCleanupConfig(raw) {
1437
1901
  async function loadCleanupConfig(path) {
1438
1902
  let text;
1439
1903
  try {
1440
- text = await readFile2(path, "utf8");
1904
+ text = await readFile3(path, "utf8");
1441
1905
  } catch (error) {
1442
1906
  if (error.code === "ENOENT") {
1443
1907
  return { ok: true, config: { ...DEFAULT_CLEANUP_CONFIG }, fromFile: false };
@@ -1524,7 +1988,7 @@ function mutationPathOf(exec) {
1524
1988
  return void 0;
1525
1989
  }
1526
1990
  function anchorSeqOf(session, cache) {
1527
- const events = eventsOf(session);
1991
+ const events = session.snapshotEvents();
1528
1992
  const cached = cache.get(session);
1529
1993
  if (cached !== void 0 && cached.eventsLength === events.length) return cached.anchor;
1530
1994
  let anchor = cached?.anchor;
@@ -1591,10 +2055,11 @@ async function commitEntry(store, pending, anchorCache, trackedBySession, exec,
1591
2055
  }
1592
2056
  tracked.add(capture.path);
1593
2057
  }
2058
+ var REWIND_MARKER_CONTENT2 = [];
1594
2059
  function buildMarker() {
1595
- return createAssistantMessage({
1596
- content: [],
1597
- source: { provider: "dsh-rewind", model: "rewind-marker" }
2060
+ return createUserMessage({
2061
+ content: REWIND_MARKER_CONTENT2,
2062
+ source: REWIND_MARKER_SOURCE
1598
2063
  });
1599
2064
  }
1600
2065
  function describeTarget(target) {
@@ -1702,26 +2167,17 @@ async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflig
1702
2167
  }
1703
2168
  let plan;
1704
2169
  try {
1705
- plan = resolveOrError(eventsOf(agent.session), agent.session.surface.nodes, rawTarget);
2170
+ plan = resolveOrError(agent.session.snapshotEvents(), agent.session.surface.nodes, rawTarget);
1706
2171
  } catch (error) {
1707
2172
  return rewindErrorResult(error);
1708
2173
  }
1709
2174
  const marker = buildMarker();
1710
2175
  let event;
1711
2176
  try {
1712
- const turn = markerTurnOf(eventsOf(agent.session));
1713
- const step = markerStepOf(eventsOf(agent.session), turn);
1714
- agent.session.append("step/start", { turn, step });
1715
- try {
1716
- event = agent.session.append("assistant/message", { turn, step, message: marker }, {
1717
- surfaceOp: { op: "replace", start: plan.surfaceStart, end: plan.surfaceEnd },
1718
- sourceEventSeqs: [...plan.shadowedSeqs]
1719
- });
1720
- } catch (error) {
1721
- agent.session.append("step/end", { turn, step });
1722
- throw error;
1723
- }
1724
- agent.session.append("step/end", { turn, step });
2177
+ event = agent.session.append("user/message", marker, {
2178
+ surfaceOp: { op: "replace", start: plan.surfaceStart, end: plan.surfaceEnd },
2179
+ sourceEventSeqs: [...plan.shadowedSeqs]
2180
+ });
1725
2181
  } catch (error) {
1726
2182
  return {
1727
2183
  kind: "error",
@@ -1730,7 +2186,7 @@ async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflig
1730
2186
  }
1731
2187
  let restore = "";
1732
2188
  if (mode === "both") {
1733
- const outcome = await store.restoreAfter(agent.session.id, plan.targetSeq, (path) => unlink2(path));
2189
+ const outcome = await store.restoreAfter(agent.session.id, plan.targetSeq, (path) => unlink3(path));
1734
2190
  await syncRestoreObservations(ctx, fs, agent, outcome);
1735
2191
  const parts = [];
1736
2192
  if (outcome.restored.length > 0) parts.push(t("restore.count", { count: outcome.restored.length }));
@@ -1764,7 +2220,7 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
1764
2220
  const session = invocation.agent.session;
1765
2221
  const input = invocation.rawInput.trim();
1766
2222
  if (input === "") {
1767
- const candidates = listRewindCandidates(eventsOf(session), session.surface.nodes, 1);
2223
+ const candidates = listRewindCandidates(session.snapshotEvents(), session.surface.nodes, 1);
1768
2224
  if (candidates.length === 0) {
1769
2225
  return { kind: "error", text: t("noUserMessages") };
1770
2226
  }
@@ -1776,7 +2232,7 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
1776
2232
  if (target2 === void 0) return { kind: "error", text: usage() };
1777
2233
  let plan;
1778
2234
  try {
1779
- plan = resolveOrError(eventsOf(session), session.surface.nodes, target2);
2235
+ plan = resolveOrError(session.snapshotEvents(), session.surface.nodes, target2);
1780
2236
  } catch (error) {
1781
2237
  return rewindErrorResult(error);
1782
2238
  }
@@ -1784,7 +2240,7 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
1784
2240
  return { kind: "success", text: formatPlan(plan, impacts) };
1785
2241
  }
1786
2242
  if (parts[0] === "__candidates") {
1787
- const candidates = listRewindCandidates(eventsOf(session), session.surface.nodes);
2243
+ const candidates = listRewindCandidates(session.snapshotEvents(), session.surface.nodes);
1788
2244
  return { kind: "success", text: formatCandidateList(candidates) };
1789
2245
  }
1790
2246
  const target = parts[0];
@@ -1930,15 +2386,16 @@ function apply(ctx, config) {
1930
2386
  const trackedBySession = /* @__PURE__ */ new Map();
1931
2387
  let fsService;
1932
2388
  ctx.inject(["settings"], (settingsCtx) => {
1933
- const section = readSettingsSection(
1934
- settingsCtx.settings,
1935
- "locale",
1936
- dshSettings.settingsNamespace
1937
- );
2389
+ const settings = settingsCtx;
2390
+ const section = settings.settings.get("locale");
1938
2391
  if (section?.preference === "zh" || section?.preference === "en") {
1939
2392
  activeLocale = section.preference;
1940
2393
  }
1941
- const cleanupScope = settingsCtx.settings.register(CLEANUP_SETTINGS_NAMESPACE, CleanupConfigSchema, { base: DEFAULT_CLEANUP_CONFIG });
2394
+ const cleanupScope = settings.settings.register(
2395
+ CLEANUP_SETTINGS_NAMESPACE,
2396
+ CleanupConfigSchema,
2397
+ { base: DEFAULT_CLEANUP_CONFIG }
2398
+ );
1942
2399
  cleanupStore = settingsCleanupStore(cleanupScope);
1943
2400
  void migrateLegacyCleanupConfig(
1944
2401
  resolveCleanupConfigPath(dshHome),
@@ -1967,6 +2424,7 @@ function apply(ctx, config) {
1967
2424
  handler: (invocation) => handleSnapshotCleanup(store, invocation, dshHome, trackedBySession)
1968
2425
  });
1969
2426
  }, "dsh-rewind command");
2427
+ registerRewindFix(ctx, store, (key, params) => t(key, params));
1970
2428
  ctx.on("session/event", (session, event) => {
1971
2429
  if (event.type !== "user/message") return;
1972
2430
  const header = session.header;