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