dsh-rewind-plugin 0.9.0 → 0.10.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,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 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.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\u5F53\u524D form-C \u5F62\u6001",
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,533 +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 = [];
1348
- function buildRewindMarkerData(id) {
1349
- return {
1350
- role: "user",
1351
- content: REWIND_MARKER_CONTENT,
1352
- source: { ...REWIND_MARKER_SOURCE },
1353
- ...id !== void 0 ? { id } : {}
1354
- };
1355
- }
1356
- function isReplaceSurfaceOp(value) {
1357
- return typeof value === "object" && value !== null && value.op === "replace" && typeof value.start === "number" && typeof value.end === "number";
1358
- }
1359
- function isLegacyRewindMarker(event) {
1360
- return isLegacyRewindMarkerRow(asRow(event));
1361
- }
1362
- function isFormCMarker(event) {
1363
- return isFormCMarkerRow(asRow(event));
1364
- }
1365
- function repairRewindMarkers(input) {
1366
- const rows = input.map(toRow);
1367
- const stats = { a: 0, b: 0, c: 0, removedGhosts: 0 };
1368
- for (const row of rows) {
1369
- if (isFormCMarkerRow(row)) stats.c += 1;
1370
- }
1371
- const removed = /* @__PURE__ */ new Set();
1372
- for (let i = 0; i < rows.length; i += 1) {
1373
- const row = rows[i];
1374
- if (!isLegacyRewindMarkerRow(row)) continue;
1375
- const prev = rows[i - 1];
1376
- const next = rows[i + 1];
1377
- const hasBefore = prev !== void 0 && prev.type === "step/start";
1378
- const hasAfter = next !== void 0 && next.type === "step/end";
1379
- if (hasBefore && hasAfter) {
1380
- removed.add(i - 1);
1381
- removed.add(i + 1);
1382
- stats.b += 1;
1383
- stats.removedGhosts += 2;
1384
- } else {
1385
- stats.a += 1;
1386
- }
1387
- }
1388
- const survivors = [];
1389
- const oldToNew = /* @__PURE__ */ new Map();
1390
- for (let i = 0; i < rows.length; i += 1) {
1391
- if (removed.has(i)) continue;
1392
- const original = rows[i];
1393
- const newSeq = survivors.length;
1394
- oldToNew.set(original.seq, newSeq);
1395
- survivors.push(isLegacyRewindMarkerRow(original) ? toEvent(retargetToC(original, newSeq)) : toEvent({ ...original, seq: newSeq }));
1396
- }
1397
- const mapSeq = (old) => {
1398
- const mapped = oldToNew.get(old);
1399
- if (mapped === void 0) {
1400
- throw new Error(`rewind-fix: reference targets consumed seq ${old} (not a surviving event)`);
1401
- }
1402
- return mapped;
1403
- };
1404
- const events = survivors.map((event) => toEvent(remapReferences(toRow(event), mapSeq)));
1405
- return { events, mapSeq: oldToNew, stats };
1406
- }
1407
- function isLegacyRewindMarkerRow(row) {
1408
- if (row.type !== "assistant/message") return false;
1409
- const message = row.data["message"];
1410
- const source = message?.["source"];
1411
- if (source?.["kind"] !== "model") return false;
1412
- if (source?.["provider"] !== "dsh-rewind" || source?.["model"] !== "rewind-marker") return false;
1413
- return isReplaceSurfaceOp(row.surfaceOp);
1414
- }
1415
- function isFormCMarkerRow(row) {
1416
- if (row.type !== "user/message") return false;
1417
- const source = row.data["source"];
1418
- return source?.["kind"] === "plugin" && source?.["plugin"] === "dsh-rewind";
1419
- }
1420
- function asRow(event) {
1421
- const e = event;
1422
- return {
1423
- type: e.type,
1424
- seq: e.seq,
1425
- time: e.time,
1426
- data: { ...e.data },
1427
- ..."surfaceOp" in e ? { surfaceOp: e.surfaceOp } : {},
1428
- ...Array.isArray(e.sourceEventSeqs) ? { sourceEventSeqs: [...e.sourceEventSeqs] } : {},
1429
- ..."ignorable" in e ? { ignorable: e.ignorable } : {}
1430
- };
1431
- }
1432
- function toRow(event) {
1433
- return asRow(event);
1434
- }
1435
- function toEvent(row) {
1436
- const e = {
1437
- type: row.type,
1438
- seq: row.seq,
1439
- time: row.time,
1440
- data: row.data
1441
- };
1442
- if (row.surfaceOp !== void 0) e["surfaceOp"] = row.surfaceOp;
1443
- if (row.sourceEventSeqs !== void 0) e["sourceEventSeqs"] = row.sourceEventSeqs;
1444
- if (row.ignorable !== void 0) e["ignorable"] = row.ignorable;
1445
- return e;
1446
- }
1447
- function retargetToC(row, seq) {
1448
- const message = row.data["message"];
1449
- const id = typeof message?.["id"] === "string" ? message["id"] : void 0;
1450
- return {
1451
- type: "user/message",
1452
- seq,
1453
- time: row.time,
1454
- data: buildRewindMarkerData(id),
1455
- surfaceOp: row.surfaceOp,
1456
- sourceEventSeqs: row.sourceEventSeqs
1457
- };
1458
- }
1459
- function remapReferences(row, mapSeq) {
1460
- const map = (value) => mapSeq(value);
1461
- if (row.sourceEventSeqs !== void 0) {
1462
- row.sourceEventSeqs = row.sourceEventSeqs.map(map);
1463
- }
1464
- if (isReplaceSurfaceOp(row.surfaceOp)) {
1465
- row.surfaceOp = { op: "replace", start: map(row.surfaceOp.start), end: map(row.surfaceOp.end) };
1466
- }
1467
- const data = row.data;
1468
- const range = data["shadowedRange"];
1469
- if (range !== void 0 && typeof range === "object") {
1470
- data["shadowedRange"] = { start: map(range.start), end: map(range.end) };
1471
- }
1472
- for (const key of ["shadowedSeqs", "messageSeqs"]) {
1473
- const seqs = data[key];
1474
- if (Array.isArray(seqs)) data[key] = seqs.map(map);
1475
- }
1476
- const singleSeq = data["sourceEventSeq"];
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
- }
1487
- return row;
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
- }
1526
-
1527
- // src/rewind-fix.ts
1528
- import { copyFile, rename as rename2, unlink, writeFile as writeFile2, readFile as readFile2, open } from "node:fs/promises";
1529
- import { isDeepStrictEqual } from "node:util";
1530
-
1531
- // src/session-log-io.ts
1532
- import { constants, zstdCompress, zstdDecompress } from "node:zlib";
1533
- import { promisify } from "node:util";
1534
- import {
1535
- decodeSeqRanges,
1536
- decodeStorageRecord,
1537
- encodeSeqRanges,
1538
- packChunkRuns
1539
- } from "@deepseek-ai/dsh-session";
1540
- var zstdCompressAsync = promisify(zstdCompress);
1541
- var zstdDecompressAsync = promisify(zstdDecompress);
1542
- async function compressZstdFrame(input) {
1543
- return zstdCompressAsync(Buffer.from(input), { params: { [constants.ZSTD_c_checksumFlag]: 1 } });
1544
- }
1545
- function splitSession(plaintext) {
1546
- const nl = plaintext.indexOf("\n");
1547
- if (nl === -1) throw new Error("session plaintext has no header line");
1548
- return { headerLine: plaintext.slice(0, nl), body: plaintext.slice(nl + 1) };
1549
- }
1550
- function expandProvenanceFromStorage(parsed) {
1551
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1552
- throw new TypeError("stored session records must be objects");
1553
- }
1554
- const record = parsed;
1555
- if (record.sourceEventSeqs === void 0) return parsed;
1556
- if (!Number.isSafeInteger(record.seq) || record.seq < 0) {
1557
- throw new TypeError("stored session event seq must be a non-negative safe integer");
1558
- }
1559
- return { ...record, sourceEventSeqs: decodeSeqRanges(record.sourceEventSeqs, record.seq) };
1560
- }
1561
- function decodeEventBody(body) {
1562
- const events = [];
1563
- for (const line of body.split("\n")) {
1564
- if (line.length === 0) continue;
1565
- const record = JSON.parse(line);
1566
- const expanded = expandProvenanceFromStorage(record);
1567
- events.push(...decodeStorageRecord(expanded));
1568
- }
1569
- return events;
1570
- }
1571
- function eventLines(events, packChunks) {
1572
- const records = packChunks ? packChunkRuns(events) : events;
1573
- return records.map((record) => {
1574
- const provenance = record.sourceEventSeqs;
1575
- if (provenance !== void 0) {
1576
- return JSON.stringify({ ...record, sourceEventSeqs: encodeSeqRanges(provenance) });
1577
- }
1578
- return JSON.stringify(record);
1579
- }).join("\n");
1580
- }
1581
- async function encodeSessionLog(headerLine, events, options = {}) {
1582
- const packChunks = options.packChunks ?? true;
1583
- const headerOut = headerLine + "\n";
1584
- const bodyOut = eventLines(events, packChunks) + "\n";
1585
- const [headerFrame, eventFrame] = await Promise.all([compressZstdFrame(headerOut), compressZstdFrame(bodyOut)]);
1586
- return Buffer.concat([headerFrame, eventFrame]);
1587
- }
1588
-
1589
- // src/rewind-fix.ts
1590
- function isApplyInput(raw) {
1591
- return raw.trim() === "--apply";
1592
- }
1593
- function registerRewindFix(ctx, store, render) {
1594
- let persistence;
1595
- ctx.inject(["sessionPersistence"], (scope) => {
1596
- persistence = scope.sessionPersistence;
1597
- });
1598
- ctx.effect(function* () {
1599
- yield ctx.commands.register({
1600
- name: "dsh-rewind-fix",
1601
- description: render("rewindfix.description"),
1602
- input: { hint: render("rewindfix.inputHint") },
1603
- handler: (invocation) => handleRewindFix(ctx, store, persistence, invocation, render)
1604
- });
1605
- }, "dsh-rewind-fix command");
1606
- }
1607
- async function handleRewindFix(ctx, store, persistence, invocation, render) {
1608
- if (persistence === void 0) {
1609
- return { kind: "error", text: render("rewindfix.persistenceUnavailable") };
1610
- }
1611
- const sessionStore = ctx.get("sessions");
1612
- if (sessionStore === void 0) {
1613
- return { kind: "error", text: render("rewindfix.sessionStoreUnavailable") };
1614
- }
1615
- const launcher = invocation.agent?.session;
1616
- const deps = {
1617
- listSnapshots: (signal) => persistence.listSnapshots(signal),
1618
- readRaw: (id, signal) => persistence.readRaw(id, signal),
1619
- locate: (header) => persistence.locate(header),
1620
- isSessionLoaded: (id) => sessionStore.get(id) !== void 0,
1621
- clearSession: (id) => store.clearSession(id)
1622
- };
1623
- const result = await runRewindFix(deps, {
1624
- apply: isApplyInput(invocation.rawInput),
1625
- launcherHasMarkers: (launcher?.snapshotEvents() ?? []).some(isLegacyRewindMarker),
1626
- signal: invocation.signal
1627
- });
1628
- return { kind: "success", text: renderRewindFixReport(result, render) };
1629
- }
1630
- async function runRewindFix(deps, opts) {
1631
- const snapshots = await deps.listSnapshots(opts.signal);
1632
- const total = snapshots.length;
1633
- const started = Date.now();
1634
- let cancelled = false;
1635
- const concurrency = Math.max(1, Math.min(opts.concurrency ?? 4, total));
1636
- const results = new Array(total);
1637
- let cursor = 0;
1638
- const worker = async () => {
1639
- for (; ; ) {
1640
- if (opts.signal?.aborted) {
1641
- cancelled = true;
1642
- return;
1643
- }
1644
- const i = cursor++;
1645
- if (i >= total) return;
1646
- results[i] = await processOneSession(deps, opts, snapshots[i].header);
1647
- }
1648
- };
1649
- await Promise.all(Array.from({ length: concurrency }, () => worker()));
1650
- const sessions = results.filter((o) => o !== void 0);
1651
- return {
1652
- launcherHasMarkers: opts.launcherHasMarkers,
1653
- cancelled,
1654
- scanned: total,
1655
- durationMs: Date.now() - started,
1656
- apply: opts.apply,
1657
- sessions
1658
- };
1659
- }
1660
- async function processOneSession(deps, opts, header) {
1661
- const id = header.id;
1662
- if (deps.isSessionLoaded(header.id)) {
1663
- return { id, status: "skipped", a: 0, b: 0, c: 0, staleArgs: 0, reason: "loaded" };
1664
- }
1665
- let decoded;
1666
- try {
1667
- decoded = await readEvents(deps, header.id, opts.signal);
1668
- } catch (error) {
1669
- return { id, status: "failed", a: 0, b: 0, c: 0, staleArgs: 0, reason: "unreadable", error: textOf(error) };
1670
- }
1671
- if (decoded === void 0) {
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) };
1679
- }
1680
- let stale;
1681
- try {
1682
- stale = repairStaleArgs(converted.events);
1683
- } catch (error) {
1684
- return { id, status: "failed", a: 0, b: 0, c: 0, staleArgs: 0, reason: "repair", error: textOf(error) };
1685
- }
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" };
1693
- }
1694
- if (!opts.apply) {
1695
- return { id, status: "repaired", a, b, c, staleArgs };
1696
- }
1697
- const repair = { events: stale.events, a, b, c, staleArgs };
1698
- return executeSession(deps, header, decoded.headerLine, repair);
1699
- }
1700
- async function executeSession(deps, header, headerLine, repair) {
1701
- const base = {
1702
- id: header.id,
1703
- a: repair.a,
1704
- b: repair.b,
1705
- c: repair.c,
1706
- staleArgs: repair.staleArgs
1707
- };
1708
- if (deps.isSessionLoaded(header.id)) {
1709
- return { ...base, status: "skipped", reason: "loaded-between", error: "loaded between scan and write" };
1710
- }
1711
- const location = deps.locate(header);
1712
- if (location === void 0) return { ...base, status: "failed", error: "locate() returned no path" };
1713
- const path = location.path;
1714
- const lockPath = `${path}.rewind-fix.lock`;
1715
- const tmpPath = `${path}.rewind-fix.tmp`;
1716
- const bakPath = `${path}.rewind-fix.bak`;
1717
- const lock = await acquireSessionLock(lockPath);
1718
- if (lock === void 0) {
1719
- return { ...base, status: "failed", reason: "locked", error: "another process holds the session lock" };
1720
- }
1721
- try {
1722
- await copyFile(path, bakPath);
1723
- const buffer = await encodeSessionLog(headerLine, repair.events);
1724
- await writeFile2(tmpPath, buffer);
1725
- await rename2(tmpPath, path);
1726
- const verification = await verifyWritten(deps, header.id);
1727
- if (verification === void 0) throw new Error("written artifact could not be re-read");
1728
- if (!isDeepStrictEqual(normalizeForVerify(verification), normalizeForVerify(repair.events))) {
1729
- throw new Error("round-trip mismatch after write");
1730
- }
1731
- await deps.clearSession(idOf(header));
1732
- return { ...base, status: "repaired" };
1733
- } catch (error) {
1734
- await unlink(tmpPath).catch(() => {
1735
- });
1736
- await rename2(bakPath, path).catch(() => {
1737
- });
1738
- return { ...base, status: "failed", error: textOf(error) };
1739
- } finally {
1740
- await lock.close().catch(() => {
1741
- });
1742
- await unlink(lockPath).catch(() => {
1743
- });
1744
- await unlink(bakPath).catch(() => {
1745
- });
1746
- }
1747
- }
1748
- async function readEvents(deps, id, signal) {
1749
- const raw = await deps.readRaw(id, signal);
1750
- if (raw === void 0) return void 0;
1751
- const { headerLine, body } = splitSession(raw.content);
1752
- return { events: decodeEventBody(body), headerLine };
1753
- }
1754
- async function verifyWritten(deps, id) {
1755
- const raw = await deps.readRaw(id);
1756
- if (raw === void 0) return void 0;
1757
- return decodeEventBody(splitSession(raw.content).body);
1758
- }
1759
- function normalizeForVerify(events) {
1760
- return events.map((e) => ({ type: e.type, seq: e.seq, data: e.data }));
1761
- }
1762
- function textOf(error) {
1763
- return error instanceof Error ? error.message : String(error);
1764
- }
1765
- function idOf(header) {
1766
- return header.id;
1767
- }
1768
- async function acquireSessionLock(lockPath) {
1769
- for (let attempt = 0; attempt < 2; attempt += 1) {
1770
- try {
1771
- const handle = await open(lockPath, "wx");
1772
- await handle.writeFile(String(process.pid));
1773
- return handle;
1774
- } catch (error) {
1775
- const code = error.code;
1776
- if (code !== "EEXIST") throw error;
1777
- if (attempt === 1) return void 0;
1778
- if (await removeStaleLock(lockPath)) continue;
1779
- return void 0;
1780
- }
1781
- }
1782
- return void 0;
1783
- }
1784
- async function removeStaleLock(lockPath) {
1785
- let pid = 0;
1786
- try {
1787
- pid = Number((await readFile2(lockPath, "utf8")).trim());
1788
- } catch {
1789
- return false;
1790
- }
1791
- if (!Number.isSafeInteger(pid) || pid <= 0) return false;
1792
- if (pidAlive(pid)) return false;
1793
- await unlink(lockPath).catch(() => {
1794
- });
1795
- return true;
1796
- }
1797
- function pidAlive(pid) {
1798
- try {
1799
- process.kill(pid, 0);
1800
- return true;
1801
- } catch (error) {
1802
- return error.code === "EPERM";
1803
- }
1804
- }
1805
- function renderRewindFixReport(result, render) {
1806
- const parts = [];
1807
- if (result.launcherHasMarkers) parts.push(render("rewindfix.launcherGuard"));
1808
- parts.push(headline(result, render));
1809
- result.sessions.forEach((outcome, i) => parts.push(...sessionLines(outcome, i + 1, result.scanned, result.apply, render)));
1810
- return parts.join("\n");
1811
- }
1812
- function headline(result, render) {
1813
- if (result.cancelled) return render("rewindfix.cancelled");
1814
- const repaired = result.sessions.filter((o) => o.status === "repaired").length;
1815
- const skipped = result.sessions.filter((o) => o.status === "skipped").length;
1816
- const failed = result.sessions.filter((o) => o.status === "failed").length;
1817
- const secs = (result.durationMs / 1e3).toFixed(1);
1818
- if (!result.apply) {
1819
- return render("rewindfix.dryRun", { scan: result.scanned, will: repaired, skip: skipped, fail: failed, secs });
1820
- }
1821
- return render("rewindfix.done", { repair: repaired, skip: skipped, fail: failed, secs });
1822
- }
1823
- function sessionLines(outcome, index, total, apply2, render) {
1824
- const head = `[${index}/${total}] ${outcome.id}`;
1825
- if (outcome.status === "repaired") {
1826
- const counts = render("rewindfix.counts", { a: outcome.a, b: outcome.b, c: outcome.c });
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")}`];
1830
- }
1831
- if (outcome.status === "skipped") {
1832
- return [`${head} ${skipLabel(outcome, render)}`];
1833
- }
1834
- return [`${head} ${failLabel(outcome, render)}`];
1835
- }
1836
- function skipLabel(outcome, render) {
1837
- switch (outcome.reason) {
1838
- case "no-markers":
1839
- return render("rewindfix.skip.noMarkers");
1840
- case "no-artifact":
1841
- return render("rewindfix.skip.noArtifact");
1842
- case "loaded-between": {
1843
- const label = render("rewindfix.skip.loadedNow");
1844
- return outcome.error ? `${label} ${outcome.error}` : label;
1845
- }
1846
- case "loaded":
1847
- default:
1848
- return render("rewindfix.skip.loaded");
1849
- }
1850
- }
1851
- function failLabel(outcome, render) {
1852
- switch (outcome.reason) {
1853
- case "unreadable":
1854
- return render("rewindfix.fail.unreadable", { error: outcome.error ?? "" });
1855
- case "repair":
1856
- return render("rewindfix.fail.repair", { error: outcome.error ?? "" });
1857
- case "locked":
1858
- return render("rewindfix.fail.locked", {
1859
- error: `${render("rewindfix.locked")}${outcome.error ? ` (${outcome.error})` : ""}`
1860
- });
1861
- default:
1862
- return render("rewindfix.fail.generic", { error: outcome.error ?? "" });
1863
- }
1864
- }
1865
-
1866
1305
  // src/snapshot-cleanup.ts
1867
- 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";
1868
1307
  import { dirname as dirname2, join as join2 } from "node:path";
1869
1308
  import { resolveDshHome as resolveDshHome2 } from "@deepseek-ai/dsh-home-paths";
1870
1309
  import z from "@deepseek-ai/schemastery";
1871
- var CLEANUP_CONFIG_FILENAME = "snapshot-cleanup.json";
1872
1310
  var DEFAULT_MAX_AGE_DAYS = 30;
1873
1311
  var DEFAULT_CLEANUP_CONFIG = { enabled: false, maxAgeDays: DEFAULT_MAX_AGE_DAYS };
1874
1312
  var CLEANUP_SETTINGS_NAMESPACE = "dsh-rewind-snapshot-cleanup";
@@ -1886,35 +1324,14 @@ function settingsCleanupStore(scope) {
1886
1324
  }
1887
1325
  };
1888
1326
  }
1889
- async function migrateLegacyCleanupConfig(legacyPath, scope, log) {
1890
- const loaded = await loadCleanupConfig(legacyPath);
1891
- if (!loaded.ok) {
1892
- log(`[dsh-rewind] legacy snapshot-cleanup config invalid, migrating defaults and removing: ${loaded.error}`);
1893
- await scope.update({ enabled: false, maxAgeDays: DEFAULT_MAX_AGE_DAYS });
1894
- await unlink2(legacyPath).catch(() => void 0);
1895
- return true;
1896
- }
1897
- if (!loaded.fromFile) return false;
1898
- if (loaded.config.enabled === DEFAULT_CLEANUP_CONFIG.enabled && loaded.config.maxAgeDays === DEFAULT_CLEANUP_CONFIG.maxAgeDays) {
1899
- await unlink2(legacyPath).catch(() => void 0);
1900
- return true;
1901
- }
1902
- await scope.update({ enabled: loaded.config.enabled, maxAgeDays: loaded.config.maxAgeDays });
1903
- await unlink2(legacyPath).catch(() => void 0);
1904
- log(`[dsh-rewind] migrated legacy snapshot-cleanup config (enabled=${String(loaded.config.enabled)}, maxAgeDays=${String(loaded.config.maxAgeDays)})`);
1905
- return true;
1906
- }
1907
1327
  var AUTO_SWEEP_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1908
- function resolveCleanupConfigPath(dshHome) {
1909
- return join2(resolveDshHome2(dshHome), CLEANUP_CONFIG_FILENAME);
1910
- }
1911
1328
  var STATE_FILENAME = "snapshot-cleanup-last-sweep.json";
1912
1329
  function resolveCleanupStatePath(dshHome) {
1913
- return join2(dirname2(resolveCleanupConfigPath(dshHome)), STATE_FILENAME);
1330
+ return join2(resolveDshHome2(dshHome), STATE_FILENAME);
1914
1331
  }
1915
1332
  async function loadLastSweepAt(path) {
1916
1333
  try {
1917
- const raw = JSON.parse(await readFile3(path, "utf8"));
1334
+ const raw = JSON.parse(await readFile2(path, "utf8"));
1918
1335
  const value = raw["lastSweepAt"];
1919
1336
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
1920
1337
  } catch {
@@ -1924,8 +1341,8 @@ async function loadLastSweepAt(path) {
1924
1341
  async function saveLastSweepAt(path, ms) {
1925
1342
  const tmp = `${path}.tmp`;
1926
1343
  await mkdir2(dirname2(path), { recursive: true });
1927
- await writeFile3(tmp, JSON.stringify({ lastSweepAt: ms }), "utf8");
1928
- await rename3(tmp, path);
1344
+ await writeFile2(tmp, JSON.stringify({ lastSweepAt: ms }), "utf8");
1345
+ await rename2(tmp, path);
1929
1346
  }
1930
1347
  async function runAutoCleanupCheck(deps, sessionId) {
1931
1348
  try {
@@ -1962,26 +1379,6 @@ function parseCleanupConfig(raw) {
1962
1379
  }
1963
1380
  return { ok: true, config: { enabled, maxAgeDays } };
1964
1381
  }
1965
- async function loadCleanupConfig(path) {
1966
- let text;
1967
- try {
1968
- text = await readFile3(path, "utf8");
1969
- } catch (error) {
1970
- if (error.code === "ENOENT") {
1971
- return { ok: true, config: { ...DEFAULT_CLEANUP_CONFIG }, fromFile: false };
1972
- }
1973
- return { ok: false, error: `config file unreadable: ${error instanceof Error ? error.message : String(error)}` };
1974
- }
1975
- let raw;
1976
- try {
1977
- raw = JSON.parse(text);
1978
- } catch (error) {
1979
- return { ok: false, error: `config file is not valid JSON: ${error instanceof Error ? error.message : String(error)}` };
1980
- }
1981
- const parsed = parseCleanupConfig(raw);
1982
- if (!parsed.ok) return { ok: false, error: parsed.error };
1983
- return { ok: true, config: parsed.config, fromFile: true };
1984
- }
1985
1382
  function parseCleanupCommand(rawInput) {
1986
1383
  const parts = rawInput.trim().split(/\s+/).filter(Boolean);
1987
1384
  if (parts.length === 0) return { action: "status" };
@@ -2025,8 +1422,7 @@ function shouldRunAutoSweep(lastAtMs, nowMs) {
2025
1422
  // src/index.ts
2026
1423
  var name = "dsh-rewind";
2027
1424
  var inject = ["commands", "tools"];
2028
- var TRACKED_TOOLS = /* @__PURE__ */ new Set(["write", "edit", "str_replace_editor"]);
2029
- var MUTATING_EDITOR_COMMANDS = /* @__PURE__ */ new Set(["create", "str_replace", "insert"]);
1425
+ var TRACKED_TOOLS = /* @__PURE__ */ new Set(["write", "edit"]);
2030
1426
  var activeLocale = "en";
2031
1427
  var cleanupStore;
2032
1428
  function t(key, params) {
@@ -2045,10 +1441,6 @@ function mutationPathOf(exec) {
2045
1441
  if (exec.name === "write" || exec.name === "edit") {
2046
1442
  return typeof args.file_path === "string" ? args.file_path : void 0;
2047
1443
  }
2048
- if (exec.name === "str_replace_editor") {
2049
- if (typeof args.command !== "string" || !MUTATING_EDITOR_COMMANDS.has(args.command)) return void 0;
2050
- return typeof args.path === "string" ? args.path : void 0;
2051
- }
2052
1444
  return void 0;
2053
1445
  }
2054
1446
  function anchorSeqOf(session, cache) {
@@ -2119,10 +1511,11 @@ async function commitEntry(store, pending, anchorCache, trackedBySession, exec,
2119
1511
  }
2120
1512
  tracked.add(capture.path);
2121
1513
  }
2122
- var REWIND_MARKER_CONTENT2 = [];
1514
+ var REWIND_MARKER_SOURCE = { kind: "plugin", plugin: "dsh-rewind" };
1515
+ var REWIND_MARKER_CONTENT = [];
2123
1516
  function buildMarker() {
2124
1517
  return createUserMessage({
2125
- content: REWIND_MARKER_CONTENT2,
1518
+ content: REWIND_MARKER_CONTENT,
2126
1519
  source: REWIND_MARKER_SOURCE
2127
1520
  });
2128
1521
  }
@@ -2250,7 +1643,7 @@ async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflig
2250
1643
  }
2251
1644
  let restore = "";
2252
1645
  if (mode === "both") {
2253
- 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));
2254
1647
  await syncRestoreObservations(ctx, fs, agent, outcome);
2255
1648
  const parts = [];
2256
1649
  if (outcome.restored.length > 0) parts.push(t("restore.count", { count: outcome.restored.length }));
@@ -2461,13 +1854,6 @@ function apply(ctx, config) {
2461
1854
  { base: DEFAULT_CLEANUP_CONFIG }
2462
1855
  );
2463
1856
  cleanupStore = settingsCleanupStore(cleanupScope);
2464
- void migrateLegacyCleanupConfig(
2465
- resolveCleanupConfigPath(dshHome),
2466
- cleanupScope,
2467
- (msg) => ctx.logger.warn(msg)
2468
- ).catch((error) => {
2469
- ctx.logger.warn(`[dsh-rewind] snapshot cleanup migration failed: ${error instanceof Error ? error.message : String(error)}`);
2470
- });
2471
1857
  });
2472
1858
  ctx.effect(function* () {
2473
1859
  const rewindHandler = (invocation) => handleRewind(ctx, store, fsService, invocation, inflight);
@@ -2488,7 +1874,6 @@ function apply(ctx, config) {
2488
1874
  handler: (invocation) => handleSnapshotCleanup(store, invocation, dshHome, trackedBySession)
2489
1875
  });
2490
1876
  }, "dsh-rewind command");
2491
- registerRewindFix(ctx, store, (key, params) => t(key, params));
2492
1877
  ctx.on("session/event", (session, event) => {
2493
1878
  if (event.type !== "user/message") return;
2494
1879
  const header = session.header;