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/README.en.md +2 -2
- package/README.md +2 -2
- package/docs/architecture.md +24 -16
- package/docs/compat/audit.md +4 -3
- package/docs/format.md +3 -4
- package/docs/harness-reference.md +1 -4
- package/docs/release/release.md +7 -0
- package/docs/release/release.zh.md +5 -0
- package/docs/rewind-fix.md +1 -1
- package/docs/rewind-fix.zh.md +1 -1
- package/lib/client.js +12 -6
- package/lib/index.js +12 -646
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/locales.d.ts +0 -20
- package/lib/types/snapshot-cleanup.d.ts +12 -48
- package/package.json +47 -44
- package/lib/types/rewind-fix.d.ts +0 -111
- package/lib/types/rewind-marker-repair.d.ts +0 -129
- package/lib/types/session-log-io.d.ts +0 -57
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
|
|
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
|
|
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(
|
|
1330
|
+
return join2(resolveDshHome2(dshHome), STATE_FILENAME);
|
|
1934
1331
|
}
|
|
1935
1332
|
async function loadLastSweepAt(path) {
|
|
1936
1333
|
try {
|
|
1937
|
-
const raw = JSON.parse(await
|
|
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
|
|
1948
|
-
await
|
|
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"
|
|
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) =>
|
|
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;
|