dsh-rewind-plugin 0.9.1 → 0.10.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/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
3
- import { unlink as unlink3 } from "node:fs/promises";
3
+ import { unlink } from "node:fs/promises";
4
4
  import "@deepseek-ai/schemastery";
5
5
 
6
6
  // src/locales.ts
@@ -50,27 +50,7 @@ var en = {
50
50
  "cleanup.clearActive": "Could not clear session {sessionId}: the session is still running and could not be stopped. Try again once it is idle.",
51
51
  "cleanup.clearCancelled": "Clear cancelled.",
52
52
  "cleanup.clearFailed": "Could not clear session {sessionId}: {detail}.",
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 form-C, and fill old empty form-C content with the (empty message) placeholder",
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.args": "\xB7 rewired args target {n}",
62
- "rewindfix.writeOk": "\u21B3 snapCleared \u2713 written \u2713",
63
- "rewindfix.skip.loaded": "SKIP (loaded)",
64
- "rewindfix.skip.noMarkers": "SKIP (no markers)",
65
- "rewindfix.skip.noArtifact": "SKIP (no artifact)",
66
- "rewindfix.skip.loadedNow": "SKIP (loaded now)",
67
- "rewindfix.fail.unreadable": "FAIL (unreadable) {error}",
68
- "rewindfix.fail.repair": "FAIL (update) {error}",
69
- "rewindfix.fail.locked": "FAIL (locked) {error}",
70
- "rewindfix.fail.generic": "FAIL: {error}",
71
- "rewindfix.persistenceUnavailable": "Session persistence is unavailable; cannot run the rewind-fix command.",
72
- "rewindfix.sessionStoreUnavailable": "The session store is unavailable; cannot run the rewind-fix command.",
73
- "rewindfix.locked": "locked by another process"
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"
74
54
  };
75
55
  var zh = {
76
56
  "usage.title": "\u7528\u6CD5\uFF1A",
@@ -118,27 +98,7 @@ var zh = {
118
98
  "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",
119
99
  "cleanup.clearCancelled": "\u6E05\u7A7A\u5DF2\u53D6\u6D88\u3002",
120
100
  "cleanup.clearFailed": "\u65E0\u6CD5\u6E05\u9664\u4F1A\u8BDD {sessionId}\uFF1A{detail}\u3002",
121
- "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",
122
- "rewindfix.description": "\u5C06\u5DF2\u5173\u95ED\u4F1A\u8BDD\u4E2D\u7684\u65E7\u7248 rewind \u6807\u8BB0\uFF08A/B\uFF09\u66F4\u65B0\u4E3A form-C\uFF0C\u5E76\u628A\u65E7\u7684\u7A7A form-C \u5185\u5BB9\u8865\u6210 (empty message) \u5360\u4F4D",
123
- "rewindfix.inputHint": "\u65E0\u53C2 = dry-run \u9884\u89C8\uFF1B--apply = \u6267\u884C\uFF08\u9700\u6570\u5206\u949F\uFF09",
124
- "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===",
125
- "rewindfix.done": "=== \u5B8C\u6210\uFF1A\u66F4\u65B0 {repair} \u4E2A / \u8DF3\u8FC7 {skip} \u4E2A / \u5931\u8D25 {fail} \u4E2A\uFF08{secs}s\uFF09===",
126
- "rewindfix.cancelled": "=== \u5DF2\u53D6\u6D88 ===",
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",
128
- "rewindfix.counts": "A={a} B={b} \u2192 C={c}",
129
- "rewindfix.args": "\xB7 \u4FEE\u6B63 args \u76EE\u6807 {n}",
130
- "rewindfix.writeOk": "\u21B3 \u5FEB\u7167\u5DF2\u6E05 \u2713 \u5DF2\u5199\u56DE \u2713",
131
- "rewindfix.skip.loaded": "\u8DF3\u8FC7\uFF08\u5DF2\u52A0\u8F7D\uFF09",
132
- "rewindfix.skip.noMarkers": "\u8DF3\u8FC7\uFF08\u65E0\u6807\u8BB0\uFF09",
133
- "rewindfix.skip.noArtifact": "\u8DF3\u8FC7\uFF08\u65E0\u4EA7\u7269\uFF09",
134
- "rewindfix.skip.loadedNow": "\u8DF3\u8FC7\uFF08\u73B0\u5DF2\u52A0\u8F7D\uFF09",
135
- "rewindfix.fail.unreadable": "\u5931\u8D25\uFF08\u65E0\u6CD5\u8BFB\u53D6\uFF09{error}",
136
- "rewindfix.fail.repair": "\u5931\u8D25\uFF08\u66F4\u65B0\uFF09{error}",
137
- "rewindfix.fail.locked": "\u5931\u8D25\uFF08\u88AB\u9501\u5B9A\uFF09{error}",
138
- "rewindfix.fail.generic": "\u5931\u8D25\uFF1A{error}",
139
- "rewindfix.persistenceUnavailable": "\u4F1A\u8BDD\u6301\u4E45\u5C42\u4E0D\u53EF\u7528\uFF0C\u65E0\u6CD5\u6267\u884C rewind-fix \u547D\u4EE4\u3002",
140
- "rewindfix.sessionStoreUnavailable": "\u4F1A\u8BDD\u5B58\u50A8\u4E0D\u53EF\u7528\uFF0C\u65E0\u6CD5\u6267\u884C rewind-fix \u547D\u4EE4\u3002",
141
- "rewindfix.locked": "\u5DF2\u88AB\u53E6\u4E00\u8FDB\u7A0B\u9501\u5B9A"
101
+ "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"
142
102
  };
143
103
  var HOST_DICTS = { en, zh };
144
104
  function translate(lang, key, params = {}) {
@@ -1342,553 +1302,11 @@ async function reconcileTracked(store, sessionId, anchorSeq, tracked, probe = de
1342
1302
  return recorded;
1343
1303
  }
1344
1304
 
1345
- // src/rewind-marker-repair.ts
1346
- var REWIND_MARKER_SOURCE = { kind: "plugin", plugin: "dsh-rewind" };
1347
- var REWIND_MARKER_CONTENT = [{ type: "text", text: "(empty message)" }];
1348
- function isCanonicalMarkerContent(content) {
1349
- return Array.isArray(content) && content.length === 1 && content[0]?.type === "text" && content[0]?.text === "(empty message)";
1350
- }
1351
- function buildRewindMarkerData(id) {
1352
- return {
1353
- role: "user",
1354
- content: REWIND_MARKER_CONTENT,
1355
- source: { ...REWIND_MARKER_SOURCE },
1356
- ...id !== void 0 ? { id } : {}
1357
- };
1358
- }
1359
- function isReplaceSurfaceOp(value) {
1360
- return typeof value === "object" && value !== null && value.op === "replace" && typeof value.start === "number" && typeof value.end === "number";
1361
- }
1362
- function isLegacyRewindMarker(event) {
1363
- return isLegacyRewindMarkerRow(asRow(event));
1364
- }
1365
- function isFormCMarker(event) {
1366
- return isFormCMarkerRow(asRow(event));
1367
- }
1368
- function repairRewindMarkers(input) {
1369
- const rows = input.map(toRow);
1370
- const stats = { a: 0, b: 0, c: 0, removedGhosts: 0 };
1371
- let contentUpgrades = 0;
1372
- for (const row of rows) {
1373
- if (isFormCMarkerRow(row)) stats.c += 1;
1374
- }
1375
- const removed = /* @__PURE__ */ new Set();
1376
- for (let i = 0; i < rows.length; i += 1) {
1377
- const row = rows[i];
1378
- if (!isLegacyRewindMarkerRow(row)) continue;
1379
- const prev = rows[i - 1];
1380
- const next = rows[i + 1];
1381
- const hasBefore = prev !== void 0 && prev.type === "step/start";
1382
- const hasAfter = next !== void 0 && next.type === "step/end";
1383
- if (hasBefore && hasAfter) {
1384
- removed.add(i - 1);
1385
- removed.add(i + 1);
1386
- stats.b += 1;
1387
- stats.removedGhosts += 2;
1388
- } else {
1389
- stats.a += 1;
1390
- }
1391
- }
1392
- const survivors = [];
1393
- const oldToNew = /* @__PURE__ */ new Map();
1394
- for (let i = 0; i < rows.length; i += 1) {
1395
- if (removed.has(i)) continue;
1396
- const original = rows[i];
1397
- const newSeq = survivors.length;
1398
- oldToNew.set(original.seq, newSeq);
1399
- if (!isRewindMarkerRow(original) || isCanonicalCMarker(original)) {
1400
- survivors.push(toEvent({ ...original, seq: newSeq }));
1401
- } else {
1402
- if (isFormCMarkerRow(original)) contentUpgrades += 1;
1403
- survivors.push(toEvent(retargetToC(original, newSeq)));
1404
- }
1405
- }
1406
- const mapSeq = (old) => {
1407
- const mapped = oldToNew.get(old);
1408
- if (mapped === void 0) {
1409
- throw new Error(`rewind-fix: reference targets consumed seq ${old} (not a surviving event)`);
1410
- }
1411
- return mapped;
1412
- };
1413
- const events = survivors.map((event) => toEvent(remapReferences(toRow(event), mapSeq)));
1414
- return { events, mapSeq: oldToNew, stats, contentUpgrades };
1415
- }
1416
- function rowSource(row) {
1417
- const message = row.data["message"];
1418
- return message?.["source"] ?? row.data["source"];
1419
- }
1420
- function isLegacyRewindMarkerRow(row) {
1421
- if (row.type !== "assistant/message") return false;
1422
- const source = rowSource(row);
1423
- if (source?.["kind"] !== "model") return false;
1424
- if (source?.["provider"] !== "dsh-rewind" || source?.["model"] !== "rewind-marker") return false;
1425
- return isReplaceSurfaceOp(row.surfaceOp);
1426
- }
1427
- function isFormCMarkerRow(row) {
1428
- if (row.type !== "user/message" && row.type !== "assistant/message") return false;
1429
- if (!isReplaceSurfaceOp(row.surfaceOp)) return false;
1430
- const source = rowSource(row);
1431
- return source?.["kind"] === "plugin" && source?.["plugin"] === "dsh-rewind";
1432
- }
1433
- function isRewindMarkerRow(row) {
1434
- return isLegacyRewindMarkerRow(row) || isFormCMarkerRow(row);
1435
- }
1436
- function isCanonicalCMarker(row) {
1437
- return row.type === "user/message" && isFormCMarkerRow(row) && isCanonicalMarkerContent(row.data["content"]);
1438
- }
1439
- function asRow(event) {
1440
- const e = event;
1441
- return {
1442
- type: e.type,
1443
- seq: e.seq,
1444
- time: e.time,
1445
- data: { ...e.data },
1446
- ..."surfaceOp" in e ? { surfaceOp: e.surfaceOp } : {},
1447
- ...Array.isArray(e.sourceEventSeqs) ? { sourceEventSeqs: [...e.sourceEventSeqs] } : {},
1448
- ..."ignorable" in e ? { ignorable: e.ignorable } : {}
1449
- };
1450
- }
1451
- function toRow(event) {
1452
- return asRow(event);
1453
- }
1454
- function toEvent(row) {
1455
- const e = {
1456
- type: row.type,
1457
- seq: row.seq,
1458
- time: row.time,
1459
- data: row.data
1460
- };
1461
- if (row.surfaceOp !== void 0) e["surfaceOp"] = row.surfaceOp;
1462
- if (row.sourceEventSeqs !== void 0) e["sourceEventSeqs"] = row.sourceEventSeqs;
1463
- if (row.ignorable !== void 0) e["ignorable"] = row.ignorable;
1464
- return e;
1465
- }
1466
- function retargetToC(row, seq) {
1467
- const message = row.data["message"];
1468
- const id = typeof message?.["id"] === "string" ? message["id"] : typeof row.data["id"] === "string" ? row.data["id"] : void 0;
1469
- return {
1470
- type: "user/message",
1471
- seq,
1472
- time: row.time,
1473
- data: buildRewindMarkerData(id),
1474
- surfaceOp: row.surfaceOp,
1475
- sourceEventSeqs: row.sourceEventSeqs
1476
- };
1477
- }
1478
- function remapReferences(row, mapSeq) {
1479
- const map = (value) => mapSeq(value);
1480
- if (row.sourceEventSeqs !== void 0) {
1481
- row.sourceEventSeqs = row.sourceEventSeqs.map(map);
1482
- }
1483
- if (isReplaceSurfaceOp(row.surfaceOp)) {
1484
- row.surfaceOp = { op: "replace", start: map(row.surfaceOp.start), end: map(row.surfaceOp.end) };
1485
- }
1486
- const data = row.data;
1487
- const range = data["shadowedRange"];
1488
- if (range !== void 0 && typeof range === "object") {
1489
- data["shadowedRange"] = { start: map(range.start), end: map(range.end) };
1490
- }
1491
- for (const key of ["shadowedSeqs", "messageSeqs"]) {
1492
- const seqs = data[key];
1493
- if (Array.isArray(seqs)) data[key] = seqs.map(map);
1494
- }
1495
- const singleSeq = data["sourceEventSeq"];
1496
- if (typeof singleSeq === "number") data["sourceEventSeq"] = map(singleSeq);
1497
- if (row.type === "command/run" && data["name"] === "rewind") {
1498
- const args = data["args"];
1499
- if (typeof args === "string") {
1500
- data["args"] = args.replace(/@(\d+)/g, (match, seqText) => {
1501
- const seq = Number(seqText);
1502
- return Number.isSafeInteger(seq) ? `@${mapSeq(seq)}` : match;
1503
- });
1504
- }
1505
- }
1506
- return row;
1507
- }
1508
- function rewindTargetSeqOfArgs(args) {
1509
- if (typeof args !== "string") return void 0;
1510
- const match = args.match(/@(\d+)/);
1511
- return match !== null ? Number(match[1]) : void 0;
1512
- }
1513
- function repairStaleArgs(input) {
1514
- const markerBySeq = /* @__PURE__ */ new Map();
1515
- for (const event of input) {
1516
- if (isFormCMarker(event)) markerBySeq.set(event.seq, event);
1517
- }
1518
- const markerSeqByCommandId = /* @__PURE__ */ new Map();
1519
- for (const event of input) {
1520
- if (event.type !== "command/done") continue;
1521
- const data = event.data;
1522
- if (typeof data.commandId === "string" && typeof data.sourceEventSeq === "number") {
1523
- markerSeqByCommandId.set(data.commandId, data.sourceEventSeq);
1524
- }
1525
- }
1526
- let fixed = 0;
1527
- const events = input.map((event) => {
1528
- if (event.type !== "command/run") return event;
1529
- const data = event.data;
1530
- if (data.name !== "rewind" || typeof data.commandId !== "string" || typeof data.args !== "string") {
1531
- return event;
1532
- }
1533
- const markerSeq = markerSeqByCommandId.get(data.commandId);
1534
- const marker = markerSeq === void 0 ? void 0 : markerBySeq.get(markerSeq);
1535
- const start = marker?.surfaceOp?.start;
1536
- const target = rewindTargetSeqOfArgs(data.args);
1537
- if (start === void 0 || target === void 0 || target === start) return event;
1538
- const newArgs = data.args.replace(new RegExp(`@${target}(?!\\d)`), `@${start}`);
1539
- if (newArgs === data.args) return event;
1540
- fixed += 1;
1541
- return { ...event, data: { ...data, args: newArgs } };
1542
- });
1543
- return { events, fixed };
1544
- }
1545
-
1546
- // src/rewind-fix.ts
1547
- import { copyFile, rename as rename2, unlink, writeFile as writeFile2, readFile as readFile2, open } from "node:fs/promises";
1548
- import { isDeepStrictEqual } from "node:util";
1549
-
1550
- // src/session-log-io.ts
1551
- import { constants, zstdCompress, zstdDecompress } from "node:zlib";
1552
- import { promisify } from "node:util";
1553
- import {
1554
- decodeSeqRanges,
1555
- decodeStorageRecord,
1556
- encodeSeqRanges,
1557
- packChunkRuns
1558
- } from "@deepseek-ai/dsh-session";
1559
- var zstdCompressAsync = promisify(zstdCompress);
1560
- var zstdDecompressAsync = promisify(zstdDecompress);
1561
- async function compressZstdFrame(input) {
1562
- return zstdCompressAsync(Buffer.from(input), { params: { [constants.ZSTD_c_checksumFlag]: 1 } });
1563
- }
1564
- function splitSession(plaintext) {
1565
- const nl = plaintext.indexOf("\n");
1566
- if (nl === -1) throw new Error("session plaintext has no header line");
1567
- return { headerLine: plaintext.slice(0, nl), body: plaintext.slice(nl + 1) };
1568
- }
1569
- function expandProvenanceFromStorage(parsed) {
1570
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1571
- throw new TypeError("stored session records must be objects");
1572
- }
1573
- const record = parsed;
1574
- if (record.sourceEventSeqs === void 0) return parsed;
1575
- if (!Number.isSafeInteger(record.seq) || record.seq < 0) {
1576
- throw new TypeError("stored session event seq must be a non-negative safe integer");
1577
- }
1578
- return { ...record, sourceEventSeqs: decodeSeqRanges(record.sourceEventSeqs, record.seq) };
1579
- }
1580
- function decodeEventBody(body) {
1581
- const events = [];
1582
- for (const line of body.split("\n")) {
1583
- if (line.length === 0) continue;
1584
- const record = JSON.parse(line);
1585
- const expanded = expandProvenanceFromStorage(record);
1586
- events.push(...decodeStorageRecord(expanded));
1587
- }
1588
- return events;
1589
- }
1590
- function eventLines(events, packChunks) {
1591
- const records = packChunks ? packChunkRuns(events) : events;
1592
- return records.map((record) => {
1593
- const provenance = record.sourceEventSeqs;
1594
- if (provenance !== void 0) {
1595
- return JSON.stringify({ ...record, sourceEventSeqs: encodeSeqRanges(provenance) });
1596
- }
1597
- return JSON.stringify(record);
1598
- }).join("\n");
1599
- }
1600
- async function encodeSessionLog(headerLine, events, options = {}) {
1601
- const packChunks = options.packChunks ?? true;
1602
- const headerOut = headerLine + "\n";
1603
- const bodyOut = eventLines(events, packChunks) + "\n";
1604
- const [headerFrame, eventFrame] = await Promise.all([compressZstdFrame(headerOut), compressZstdFrame(bodyOut)]);
1605
- return Buffer.concat([headerFrame, eventFrame]);
1606
- }
1607
-
1608
- // src/rewind-fix.ts
1609
- function isApplyInput(raw) {
1610
- return raw.trim() === "--apply";
1611
- }
1612
- function registerRewindFix(ctx, store, render) {
1613
- let persistence;
1614
- ctx.inject(["sessionPersistence"], (scope) => {
1615
- persistence = scope.sessionPersistence;
1616
- });
1617
- ctx.effect(function* () {
1618
- yield ctx.commands.register({
1619
- name: "dsh-rewind-fix",
1620
- description: render("rewindfix.description"),
1621
- input: { hint: render("rewindfix.inputHint") },
1622
- handler: (invocation) => handleRewindFix(ctx, store, persistence, invocation, render)
1623
- });
1624
- }, "dsh-rewind-fix command");
1625
- }
1626
- async function handleRewindFix(ctx, store, persistence, invocation, render) {
1627
- if (persistence === void 0) {
1628
- return { kind: "error", text: render("rewindfix.persistenceUnavailable") };
1629
- }
1630
- const sessionStore = ctx.get("sessions");
1631
- if (sessionStore === void 0) {
1632
- return { kind: "error", text: render("rewindfix.sessionStoreUnavailable") };
1633
- }
1634
- const launcher = invocation.agent?.session;
1635
- const deps = {
1636
- listSnapshots: (signal) => persistence.listSnapshots(signal),
1637
- readRaw: (id, signal) => persistence.readRaw(id, signal),
1638
- locate: (header) => persistence.locate(header),
1639
- isSessionLoaded: (id) => sessionStore.get(id) !== void 0,
1640
- clearSession: (id) => store.clearSession(id)
1641
- };
1642
- const result = await runRewindFix(deps, {
1643
- apply: isApplyInput(invocation.rawInput),
1644
- launcherHasMarkers: (launcher?.snapshotEvents() ?? []).some(isLegacyRewindMarker),
1645
- signal: invocation.signal
1646
- });
1647
- return { kind: "success", text: renderRewindFixReport(result, render) };
1648
- }
1649
- async function runRewindFix(deps, opts) {
1650
- const snapshots = await deps.listSnapshots(opts.signal);
1651
- const total = snapshots.length;
1652
- const started = Date.now();
1653
- let cancelled = false;
1654
- const concurrency = Math.max(1, Math.min(opts.concurrency ?? 4, total));
1655
- const results = new Array(total);
1656
- let cursor = 0;
1657
- const worker = async () => {
1658
- for (; ; ) {
1659
- if (opts.signal?.aborted) {
1660
- cancelled = true;
1661
- return;
1662
- }
1663
- const i = cursor++;
1664
- if (i >= total) return;
1665
- results[i] = await processOneSession(deps, opts, snapshots[i].header);
1666
- }
1667
- };
1668
- await Promise.all(Array.from({ length: concurrency }, () => worker()));
1669
- const sessions = results.filter((o) => o !== void 0);
1670
- return {
1671
- launcherHasMarkers: opts.launcherHasMarkers,
1672
- cancelled,
1673
- scanned: total,
1674
- durationMs: Date.now() - started,
1675
- apply: opts.apply,
1676
- sessions
1677
- };
1678
- }
1679
- async function processOneSession(deps, opts, header) {
1680
- const id = header.id;
1681
- if (deps.isSessionLoaded(header.id)) {
1682
- return { id, status: "skipped", a: 0, b: 0, c: 0, staleArgs: 0, reason: "loaded" };
1683
- }
1684
- let decoded;
1685
- try {
1686
- decoded = await readEvents(deps, header.id, opts.signal);
1687
- } catch (error) {
1688
- return { id, status: "failed", a: 0, b: 0, c: 0, staleArgs: 0, reason: "unreadable", error: textOf(error) };
1689
- }
1690
- if (decoded === void 0) {
1691
- return { id, status: "skipped", a: 0, b: 0, c: 0, staleArgs: 0, reason: "no-artifact" };
1692
- }
1693
- let converted;
1694
- try {
1695
- converted = repairRewindMarkers(decoded.events);
1696
- } catch (error) {
1697
- return { id, status: "failed", a: 0, b: 0, c: 0, staleArgs: 0, reason: "repair", error: textOf(error) };
1698
- }
1699
- let stale;
1700
- try {
1701
- stale = repairStaleArgs(converted.events);
1702
- } catch (error) {
1703
- return { id, status: "failed", a: 0, b: 0, c: 0, staleArgs: 0, reason: "repair", error: textOf(error) };
1704
- }
1705
- const a = converted.stats.a;
1706
- const b = converted.stats.b;
1707
- const c = converted.stats.a + converted.stats.b + converted.stats.c;
1708
- const staleArgs = stale.fixed;
1709
- const contentUpgrades = converted.contentUpgrades;
1710
- const needsRepair = a + b > 0 || staleArgs > 0 || contentUpgrades > 0;
1711
- if (!needsRepair) {
1712
- return { id, status: "skipped", a, b, c, staleArgs, reason: "no-markers" };
1713
- }
1714
- if (!opts.apply) {
1715
- return { id, status: "repaired", a, b, c, staleArgs };
1716
- }
1717
- const repair = { events: stale.events, a, b, c, staleArgs };
1718
- return executeSession(deps, header, decoded.headerLine, repair);
1719
- }
1720
- async function executeSession(deps, header, headerLine, repair) {
1721
- const base = {
1722
- id: header.id,
1723
- a: repair.a,
1724
- b: repair.b,
1725
- c: repair.c,
1726
- staleArgs: repair.staleArgs
1727
- };
1728
- if (deps.isSessionLoaded(header.id)) {
1729
- return { ...base, status: "skipped", reason: "loaded-between", error: "loaded between scan and write" };
1730
- }
1731
- const location = deps.locate(header);
1732
- if (location === void 0) return { ...base, status: "failed", error: "locate() returned no path" };
1733
- const path = location.path;
1734
- const lockPath = `${path}.rewind-fix.lock`;
1735
- const tmpPath = `${path}.rewind-fix.tmp`;
1736
- const bakPath = `${path}.rewind-fix.bak`;
1737
- const lock = await acquireSessionLock(lockPath);
1738
- if (lock === void 0) {
1739
- return { ...base, status: "failed", reason: "locked", error: "another process holds the session lock" };
1740
- }
1741
- try {
1742
- await copyFile(path, bakPath);
1743
- const buffer = await encodeSessionLog(headerLine, repair.events);
1744
- await writeFile2(tmpPath, buffer);
1745
- await rename2(tmpPath, path);
1746
- const verification = await verifyWritten(deps, header.id);
1747
- if (verification === void 0) throw new Error("written artifact could not be re-read");
1748
- if (!isDeepStrictEqual(normalizeForVerify(verification), normalizeForVerify(repair.events))) {
1749
- throw new Error("round-trip mismatch after write");
1750
- }
1751
- await deps.clearSession(idOf(header));
1752
- return { ...base, status: "repaired" };
1753
- } catch (error) {
1754
- await unlink(tmpPath).catch(() => {
1755
- });
1756
- await rename2(bakPath, path).catch(() => {
1757
- });
1758
- return { ...base, status: "failed", error: textOf(error) };
1759
- } finally {
1760
- await lock.close().catch(() => {
1761
- });
1762
- await unlink(lockPath).catch(() => {
1763
- });
1764
- await unlink(bakPath).catch(() => {
1765
- });
1766
- }
1767
- }
1768
- async function readEvents(deps, id, signal) {
1769
- const raw = await deps.readRaw(id, signal);
1770
- if (raw === void 0) return void 0;
1771
- const { headerLine, body } = splitSession(raw.content);
1772
- return { events: decodeEventBody(body), headerLine };
1773
- }
1774
- async function verifyWritten(deps, id) {
1775
- const raw = await deps.readRaw(id);
1776
- if (raw === void 0) return void 0;
1777
- return decodeEventBody(splitSession(raw.content).body);
1778
- }
1779
- function normalizeForVerify(events) {
1780
- return events.map((e) => ({ type: e.type, seq: e.seq, data: e.data }));
1781
- }
1782
- function textOf(error) {
1783
- return error instanceof Error ? error.message : String(error);
1784
- }
1785
- function idOf(header) {
1786
- return header.id;
1787
- }
1788
- async function acquireSessionLock(lockPath) {
1789
- for (let attempt = 0; attempt < 2; attempt += 1) {
1790
- try {
1791
- const handle = await open(lockPath, "wx");
1792
- await handle.writeFile(String(process.pid));
1793
- return handle;
1794
- } catch (error) {
1795
- const code = error.code;
1796
- if (code !== "EEXIST") throw error;
1797
- if (attempt === 1) return void 0;
1798
- if (await removeStaleLock(lockPath)) continue;
1799
- return void 0;
1800
- }
1801
- }
1802
- return void 0;
1803
- }
1804
- async function removeStaleLock(lockPath) {
1805
- let pid = 0;
1806
- try {
1807
- pid = Number((await readFile2(lockPath, "utf8")).trim());
1808
- } catch {
1809
- return false;
1810
- }
1811
- if (!Number.isSafeInteger(pid) || pid <= 0) return false;
1812
- if (pidAlive(pid)) return false;
1813
- await unlink(lockPath).catch(() => {
1814
- });
1815
- return true;
1816
- }
1817
- function pidAlive(pid) {
1818
- try {
1819
- process.kill(pid, 0);
1820
- return true;
1821
- } catch (error) {
1822
- return error.code === "EPERM";
1823
- }
1824
- }
1825
- function renderRewindFixReport(result, render) {
1826
- const parts = [];
1827
- if (result.launcherHasMarkers) parts.push(render("rewindfix.launcherGuard"));
1828
- parts.push(headline(result, render));
1829
- result.sessions.forEach((outcome, i) => parts.push(...sessionLines(outcome, i + 1, result.scanned, result.apply, render)));
1830
- return parts.join("\n");
1831
- }
1832
- function headline(result, render) {
1833
- if (result.cancelled) return render("rewindfix.cancelled");
1834
- const repaired = result.sessions.filter((o) => o.status === "repaired").length;
1835
- const skipped = result.sessions.filter((o) => o.status === "skipped").length;
1836
- const failed = result.sessions.filter((o) => o.status === "failed").length;
1837
- const secs = (result.durationMs / 1e3).toFixed(1);
1838
- if (!result.apply) {
1839
- return render("rewindfix.dryRun", { scan: result.scanned, will: repaired, skip: skipped, fail: failed, secs });
1840
- }
1841
- return render("rewindfix.done", { repair: repaired, skip: skipped, fail: failed, secs });
1842
- }
1843
- function sessionLines(outcome, index, total, apply2, render) {
1844
- const head = `[${index}/${total}] ${outcome.id}`;
1845
- if (outcome.status === "repaired") {
1846
- const counts = render("rewindfix.counts", { a: outcome.a, b: outcome.b, c: outcome.c });
1847
- const argsNote = outcome.staleArgs > 0 ? " " + render("rewindfix.args", { n: outcome.staleArgs }) : "";
1848
- if (!apply2) return [`${head} ${counts}${argsNote}`];
1849
- return [`${head} ${counts}${argsNote}`, ` ${render("rewindfix.writeOk")}`];
1850
- }
1851
- if (outcome.status === "skipped") {
1852
- return [`${head} ${skipLabel(outcome, render)}`];
1853
- }
1854
- return [`${head} ${failLabel(outcome, render)}`];
1855
- }
1856
- function skipLabel(outcome, render) {
1857
- switch (outcome.reason) {
1858
- case "no-markers":
1859
- return render("rewindfix.skip.noMarkers");
1860
- case "no-artifact":
1861
- return render("rewindfix.skip.noArtifact");
1862
- case "loaded-between": {
1863
- const label = render("rewindfix.skip.loadedNow");
1864
- return outcome.error ? `${label} ${outcome.error}` : label;
1865
- }
1866
- case "loaded":
1867
- default:
1868
- return render("rewindfix.skip.loaded");
1869
- }
1870
- }
1871
- function failLabel(outcome, render) {
1872
- switch (outcome.reason) {
1873
- case "unreadable":
1874
- return render("rewindfix.fail.unreadable", { error: outcome.error ?? "" });
1875
- case "repair":
1876
- return render("rewindfix.fail.repair", { error: outcome.error ?? "" });
1877
- case "locked":
1878
- return render("rewindfix.fail.locked", {
1879
- error: `${render("rewindfix.locked")}${outcome.error ? ` (${outcome.error})` : ""}`
1880
- });
1881
- default:
1882
- return render("rewindfix.fail.generic", { error: outcome.error ?? "" });
1883
- }
1884
- }
1885
-
1886
1305
  // src/snapshot-cleanup.ts
1887
- import { mkdir as mkdir2, readFile as readFile3, rename as rename3, unlink as unlink2, writeFile as writeFile3 } from "node:fs/promises";
1306
+ import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
1888
1307
  import { dirname as dirname2, join as join2 } from "node:path";
1889
1308
  import { resolveDshHome as resolveDshHome2 } from "@deepseek-ai/dsh-home-paths";
1890
1309
  import z from "@deepseek-ai/schemastery";
1891
- var CLEANUP_CONFIG_FILENAME = "snapshot-cleanup.json";
1892
1310
  var DEFAULT_MAX_AGE_DAYS = 30;
1893
1311
  var DEFAULT_CLEANUP_CONFIG = { enabled: false, maxAgeDays: DEFAULT_MAX_AGE_DAYS };
1894
1312
  var CLEANUP_SETTINGS_NAMESPACE = "dsh-rewind-snapshot-cleanup";
@@ -1906,35 +1324,14 @@ function settingsCleanupStore(scope) {
1906
1324
  }
1907
1325
  };
1908
1326
  }
1909
- async function migrateLegacyCleanupConfig(legacyPath, scope, log) {
1910
- const loaded = await loadCleanupConfig(legacyPath);
1911
- if (!loaded.ok) {
1912
- log(`[dsh-rewind] legacy snapshot-cleanup config invalid, migrating defaults and removing: ${loaded.error}`);
1913
- await scope.update({ enabled: false, maxAgeDays: DEFAULT_MAX_AGE_DAYS });
1914
- await unlink2(legacyPath).catch(() => void 0);
1915
- return true;
1916
- }
1917
- if (!loaded.fromFile) return false;
1918
- if (loaded.config.enabled === DEFAULT_CLEANUP_CONFIG.enabled && loaded.config.maxAgeDays === DEFAULT_CLEANUP_CONFIG.maxAgeDays) {
1919
- await unlink2(legacyPath).catch(() => void 0);
1920
- return true;
1921
- }
1922
- await scope.update({ enabled: loaded.config.enabled, maxAgeDays: loaded.config.maxAgeDays });
1923
- await unlink2(legacyPath).catch(() => void 0);
1924
- log(`[dsh-rewind] migrated legacy snapshot-cleanup config (enabled=${String(loaded.config.enabled)}, maxAgeDays=${String(loaded.config.maxAgeDays)})`);
1925
- return true;
1926
- }
1927
1327
  var AUTO_SWEEP_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1928
- function resolveCleanupConfigPath(dshHome) {
1929
- return join2(resolveDshHome2(dshHome), CLEANUP_CONFIG_FILENAME);
1930
- }
1931
1328
  var STATE_FILENAME = "snapshot-cleanup-last-sweep.json";
1932
1329
  function resolveCleanupStatePath(dshHome) {
1933
- return join2(dirname2(resolveCleanupConfigPath(dshHome)), STATE_FILENAME);
1330
+ return join2(resolveDshHome2(dshHome), STATE_FILENAME);
1934
1331
  }
1935
1332
  async function loadLastSweepAt(path) {
1936
1333
  try {
1937
- const raw = JSON.parse(await readFile3(path, "utf8"));
1334
+ const raw = JSON.parse(await readFile2(path, "utf8"));
1938
1335
  const value = raw["lastSweepAt"];
1939
1336
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
1940
1337
  } catch {
@@ -1944,8 +1341,8 @@ async function loadLastSweepAt(path) {
1944
1341
  async function saveLastSweepAt(path, ms) {
1945
1342
  const tmp = `${path}.tmp`;
1946
1343
  await mkdir2(dirname2(path), { recursive: true });
1947
- await writeFile3(tmp, JSON.stringify({ lastSweepAt: ms }), "utf8");
1948
- await rename3(tmp, path);
1344
+ await writeFile2(tmp, JSON.stringify({ lastSweepAt: ms }), "utf8");
1345
+ await rename2(tmp, path);
1949
1346
  }
1950
1347
  async function runAutoCleanupCheck(deps, sessionId) {
1951
1348
  try {
@@ -1982,26 +1379,6 @@ function parseCleanupConfig(raw) {
1982
1379
  }
1983
1380
  return { ok: true, config: { enabled, maxAgeDays } };
1984
1381
  }
1985
- async function loadCleanupConfig(path) {
1986
- let text;
1987
- try {
1988
- text = await readFile3(path, "utf8");
1989
- } catch (error) {
1990
- if (error.code === "ENOENT") {
1991
- return { ok: true, config: { ...DEFAULT_CLEANUP_CONFIG }, fromFile: false };
1992
- }
1993
- return { ok: false, error: `config file unreadable: ${error instanceof Error ? error.message : String(error)}` };
1994
- }
1995
- let raw;
1996
- try {
1997
- raw = JSON.parse(text);
1998
- } catch (error) {
1999
- return { ok: false, error: `config file is not valid JSON: ${error instanceof Error ? error.message : String(error)}` };
2000
- }
2001
- const parsed = parseCleanupConfig(raw);
2002
- if (!parsed.ok) return { ok: false, error: parsed.error };
2003
- return { ok: true, config: parsed.config, fromFile: true };
2004
- }
2005
1382
  function parseCleanupCommand(rawInput) {
2006
1383
  const parts = rawInput.trim().split(/\s+/).filter(Boolean);
2007
1384
  if (parts.length === 0) return { action: "status" };
@@ -2045,8 +1422,7 @@ function shouldRunAutoSweep(lastAtMs, nowMs) {
2045
1422
  // src/index.ts
2046
1423
  var name = "dsh-rewind";
2047
1424
  var inject = ["commands", "tools"];
2048
- var TRACKED_TOOLS = /* @__PURE__ */ new Set(["write", "edit", "str_replace_editor"]);
2049
- var MUTATING_EDITOR_COMMANDS = /* @__PURE__ */ new Set(["create", "str_replace", "insert"]);
1425
+ var TRACKED_TOOLS = /* @__PURE__ */ new Set(["write", "edit"]);
2050
1426
  var activeLocale = "en";
2051
1427
  var cleanupStore;
2052
1428
  function t(key, params) {
@@ -2065,10 +1441,6 @@ function mutationPathOf(exec) {
2065
1441
  if (exec.name === "write" || exec.name === "edit") {
2066
1442
  return typeof args.file_path === "string" ? args.file_path : void 0;
2067
1443
  }
2068
- if (exec.name === "str_replace_editor") {
2069
- if (typeof args.command !== "string" || !MUTATING_EDITOR_COMMANDS.has(args.command)) return void 0;
2070
- return typeof args.path === "string" ? args.path : void 0;
2071
- }
2072
1444
  return void 0;
2073
1445
  }
2074
1446
  function anchorSeqOf(session, cache) {
@@ -2139,6 +1511,8 @@ async function commitEntry(store, pending, anchorCache, trackedBySession, exec,
2139
1511
  }
2140
1512
  tracked.add(capture.path);
2141
1513
  }
1514
+ var REWIND_MARKER_SOURCE = { kind: "plugin", plugin: "dsh-rewind" };
1515
+ var REWIND_MARKER_CONTENT = [{ type: "text", text: "(empty message)" }];
2142
1516
  function buildMarker() {
2143
1517
  return createUserMessage({
2144
1518
  content: REWIND_MARKER_CONTENT,
@@ -2269,7 +1643,7 @@ async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflig
2269
1643
  }
2270
1644
  let restore = "";
2271
1645
  if (mode === "both") {
2272
- const outcome = await store.restoreAfter(agent.session.id, plan.targetSeq, (path) => unlink3(path));
1646
+ const outcome = await store.restoreAfter(agent.session.id, plan.targetSeq, (path) => unlink(path));
2273
1647
  await syncRestoreObservations(ctx, fs, agent, outcome);
2274
1648
  const parts = [];
2275
1649
  if (outcome.restored.length > 0) parts.push(t("restore.count", { count: outcome.restored.length }));
@@ -2480,13 +1854,6 @@ function apply(ctx, config) {
2480
1854
  { base: DEFAULT_CLEANUP_CONFIG }
2481
1855
  );
2482
1856
  cleanupStore = settingsCleanupStore(cleanupScope);
2483
- void migrateLegacyCleanupConfig(
2484
- resolveCleanupConfigPath(dshHome),
2485
- cleanupScope,
2486
- (msg) => ctx.logger.warn(msg)
2487
- ).catch((error) => {
2488
- ctx.logger.warn(`[dsh-rewind] snapshot cleanup migration failed: ${error instanceof Error ? error.message : String(error)}`);
2489
- });
2490
1857
  });
2491
1858
  ctx.effect(function* () {
2492
1859
  const rewindHandler = (invocation) => handleRewind(ctx, store, fsService, invocation, inflight);
@@ -2507,7 +1874,6 @@ function apply(ctx, config) {
2507
1874
  handler: (invocation) => handleSnapshotCleanup(store, invocation, dshHome, trackedBySession)
2508
1875
  });
2509
1876
  }, "dsh-rewind command");
2510
- registerRewindFix(ctx, store, (key, params) => t(key, params));
2511
1877
  ctx.on("session/event", (session, event) => {
2512
1878
  if (event.type !== "user/message") return;
2513
1879
  const header = session.header;