dsh-rewind-plugin 0.5.0 → 0.6.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 +18 -3
- package/README.md +4 -4
- package/SECURITY.md +9 -3
- package/docs/README.md +1 -0
- package/docs/compat/audit.md +152 -124
- package/docs/format.md +5 -4
- package/docs/harness-reference.md +1 -1
- package/docs/release/release.md +8 -3
- package/docs/release/release.zh.md +4 -2
- package/docs/snapshot-auto-cleanup.md +75 -56
- package/docs/snapshot-auto-cleanup.zh.md +36 -21
- package/lib/client.js +75 -112
- package/lib/index.js +206 -39
- package/lib/types/client/hidden.d.ts +28 -0
- package/lib/types/client/index.d.ts +12 -12
- package/lib/types/client/locales.d.ts +0 -2
- package/lib/types/client/popover.d.ts +9 -2
- package/lib/types/client/portals.d.ts +15 -2
- package/lib/types/index.d.ts +6 -3
- package/lib/types/locales.d.ts +5 -0
- package/lib/types/snapshot-cleanup.d.ts +28 -8
- package/lib/types/snapshot.d.ts +87 -2
- package/package.json +7 -1
package/lib/index.js
CHANGED
|
@@ -8,7 +8,7 @@ var en = {
|
|
|
8
8
|
"usage.title": "Usage:",
|
|
9
9
|
"usage.noArgs": " /rewind (no args) withdraw the most recent user message",
|
|
10
10
|
"usage.seq": " /rewind @<seq> chat|both rewind to the given message (chat = conversation only / both = conversation + files)",
|
|
11
|
-
"usage.blocked": "
|
|
11
|
+
"usage.blocked": " /rewind or /undo open the rewind picker",
|
|
12
12
|
"describeTarget.seq": "seq {seq}",
|
|
13
13
|
"describeTarget.index": "message {index}",
|
|
14
14
|
"plan.rewinding": "Rewind to seq {targetSeq}, removing {count} node(s) from the model context (conversation log kept).",
|
|
@@ -32,7 +32,7 @@ var en = {
|
|
|
32
32
|
"chooseMode": "Rewind to {target}. Choose a mode:\n /rewind {target} chat conversation only\n /rewind {target} both conversation + file restore",
|
|
33
33
|
"command.description": "Rewind the conversation back to an earlier user message (optionally restoring files)",
|
|
34
34
|
"cleanup.description": "Manage automatic cleanup of session snapshot backups",
|
|
35
|
-
"cleanup.inputHint": "on | off | max-age <days> | run [--apply]
|
|
35
|
+
"cleanup.inputHint": "on | off | max-age <days> | run [--apply] [--current]",
|
|
36
36
|
"cleanup.status": "Auto-cleanup: {state}. Max age: {days} day(s). Config: {path} ({present}).",
|
|
37
37
|
"cleanup.enabled": "enabled",
|
|
38
38
|
"cleanup.disabled": "disabled",
|
|
@@ -41,19 +41,24 @@ var en = {
|
|
|
41
41
|
"cleanup.onOk": "Auto-cleanup enabled.",
|
|
42
42
|
"cleanup.offOk": "Auto-cleanup disabled \u2014 all snapshots kept.",
|
|
43
43
|
"cleanup.maxAgeOk": "Auto-cleanup max age set to {days} day(s).",
|
|
44
|
-
"cleanup.cfgInvalid":
|
|
44
|
+
"cleanup.cfgInvalid": 'Snapshot cleanup config invalid: {detail}. Nothing was executed; use "on|off|max-age" to reset the config.',
|
|
45
45
|
"cleanup.saveFailed": "Could not save cleanup config: {detail}.",
|
|
46
|
-
"cleanup.runDry": "Dry-run: would remove {deleted} session snapshot backup(s). Re-run with --apply to delete.",
|
|
46
|
+
"cleanup.runDry": "Dry-run: would remove {deleted} session snapshot backup(s), freeing {freed} bytes. Re-run with --apply to delete.",
|
|
47
47
|
"cleanup.runApply": "Removed {deleted} session snapshot backup(s), freeing {freed} bytes; {kept} kept, {remaining} bytes remain.",
|
|
48
48
|
"cleanup.runFailed": "Cleanup failed: {detail}.",
|
|
49
49
|
"cleanup.skipped": "({skipped} active session(s) skipped.)",
|
|
50
|
-
"cleanup.
|
|
50
|
+
"cleanup.clearDry": "Dry-run: would clear {entries} snapshot(s) of the current session, freeing {bytes} bytes. Re-run with --apply to delete.",
|
|
51
|
+
"cleanup.clearApply": "Cleared {entries} snapshot(s) of the current session, freeing {bytes} bytes. This session now records snapshots fresh from its current state.",
|
|
52
|
+
"cleanup.clearActive": "Could not clear session {sessionId}: the session is still running and could not be stopped. Try again once it is idle.",
|
|
53
|
+
"cleanup.clearCancelled": "Clear cancelled.",
|
|
54
|
+
"cleanup.clearFailed": "Could not clear session {sessionId}: {detail}.",
|
|
55
|
+
"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"
|
|
51
56
|
};
|
|
52
57
|
var zh = {
|
|
53
58
|
"usage.title": "\u7528\u6CD5\uFF1A",
|
|
54
59
|
"usage.noArgs": " /rewind \uFF08\u65E0\u53C2\u6570\uFF09\u64A4\u56DE\u6700\u8FD1\u4E00\u6761\u7528\u6237\u6D88\u606F",
|
|
55
60
|
"usage.seq": " /rewind @<seq> chat|both \u56DE\u9000\u5230\u6307\u5B9A\u6D88\u606F\uFF08chat \u4EC5\u5BF9\u8BDD / both \u5BF9\u8BDD+\u6587\u4EF6\uFF09",
|
|
56
|
-
"usage.blocked": "
|
|
61
|
+
"usage.blocked": " /rewind \u6216 /undo \u6253\u5F00\u56DE\u9000\u9009\u62E9\u9762\u677F",
|
|
57
62
|
"describeTarget.seq": "seq {seq}",
|
|
58
63
|
"describeTarget.index": "\u7B2C {index} \u6761\u6D88\u606F",
|
|
59
64
|
"plan.rewinding": "\u5C06\u56DE\u9000\u5230 seq {targetSeq}\uFF0C\u4ECE\u6A21\u578B\u4E0A\u4E0B\u6587\u79FB\u9664 {count} \u4E2A\u8282\u70B9\uFF08\u5BF9\u8BDD\u65E5\u5FD7\u4FDD\u7559\uFF09\u3002",
|
|
@@ -77,7 +82,7 @@ var zh = {
|
|
|
77
82
|
"chooseMode": "\u5C06\u56DE\u9000\u5230 {target}\u3002\u9009\u62E9\u6A21\u5F0F\uFF1A\n /rewind {target} chat \u4EC5\u56DE\u9000\u5BF9\u8BDD\n /rewind {target} both \u56DE\u9000\u5BF9\u8BDD\u5E76\u8FD8\u539F\u6587\u4EF6",
|
|
78
83
|
"command.description": "\u5728\u540C\u7A97\u53E3\u5185\u5C06\u5BF9\u8BDD\u56DE\u9000\u5230\u66F4\u65E9\u7684\u7528\u6237\u6D88\u606F\uFF08\u53EF\u540C\u65F6\u8FD8\u539F\u6587\u4EF6\uFF09",
|
|
79
84
|
"cleanup.description": "\u7BA1\u7406\u4F1A\u8BDD\u5FEB\u7167\u5907\u4EFD\u7684\u81EA\u52A8\u6E05\u7406",
|
|
80
|
-
"cleanup.inputHint": "on | off | max-age <\u5929\u6570> | run [--apply]
|
|
85
|
+
"cleanup.inputHint": "on | off | max-age <\u5929\u6570> | run [--apply] [--current]",
|
|
81
86
|
"cleanup.status": "\u81EA\u52A8\u6E05\u7406\uFF1A{state}\u3002\u6700\u5927\u4FDD\u7559\u5929\u6570\uFF1A{days} \u5929\u3002\u914D\u7F6E\uFF1A{path}\uFF08{present}\uFF09\u3002",
|
|
82
87
|
"cleanup.enabled": "\u5DF2\u5F00\u542F",
|
|
83
88
|
"cleanup.disabled": "\u5DF2\u5173\u95ED",
|
|
@@ -86,13 +91,18 @@ var zh = {
|
|
|
86
91
|
"cleanup.onOk": "\u5DF2\u5F00\u542F\u81EA\u52A8\u6E05\u7406\u3002",
|
|
87
92
|
"cleanup.offOk": "\u5DF2\u5173\u95ED\u81EA\u52A8\u6E05\u7406\u2014\u2014\u4FDD\u7559\u5168\u90E8\u5FEB\u7167\u3002",
|
|
88
93
|
"cleanup.maxAgeOk": "\u5DF2\u5C06\u81EA\u52A8\u6E05\u7406\u7684\u6700\u5927\u4FDD\u7559\u5929\u6570\u8BBE\u4E3A {days} \u5929\u3002",
|
|
89
|
-
"cleanup.cfgInvalid": "\u5FEB\u7167\u6E05\u7406\u914D\u7F6E\u65E0\u6548\uFF1A{detail}\u3002\
|
|
94
|
+
"cleanup.cfgInvalid": "\u5FEB\u7167\u6E05\u7406\u914D\u7F6E\u65E0\u6548\uFF1A{detail}\u3002\u672A\u6267\u884C\u4EFB\u4F55\u64CD\u4F5C\uFF1B\u8BF7\u7528\u300Con|off|max-age\u300D\u91CD\u8BBE\u914D\u7F6E\u4EE5\u4FEE\u590D\u3002",
|
|
90
95
|
"cleanup.saveFailed": "\u65E0\u6CD5\u4FDD\u5B58\u6E05\u7406\u914D\u7F6E\uFF1A{detail}\u3002",
|
|
91
|
-
"cleanup.runDry": "\u9884\u6F14\uFF1A\u5C06\u5220\u9664 {deleted} \u4E2A\u4F1A\u8BDD\u7684\u5FEB\u7167\u5907\u4EFD\u3002\u52A0 --apply \u6B63\u5F0F\u5220\u9664\u3002",
|
|
96
|
+
"cleanup.runDry": "\u9884\u6F14\uFF1A\u5C06\u5220\u9664 {deleted} \u4E2A\u4F1A\u8BDD\u7684\u5FEB\u7167\u5907\u4EFD\uFF0C\u91CA\u653E {freed} \u5B57\u8282\u3002\u52A0 --apply \u6B63\u5F0F\u5220\u9664\u3002",
|
|
92
97
|
"cleanup.runApply": "\u5DF2\u5220\u9664 {deleted} \u4E2A\u4F1A\u8BDD\u7684\u5FEB\u7167\u5907\u4EFD\uFF0C\u91CA\u653E {freed} \u5B57\u8282\uFF1B\u4FDD\u7559 {kept} \u4E2A\uFF0C\u5269\u4F59 {remaining} \u5B57\u8282\u3002",
|
|
93
98
|
"cleanup.runFailed": "\u6E05\u7406\u5931\u8D25\uFF1A{detail}\u3002",
|
|
94
99
|
"cleanup.skipped": "\uFF08\u8DF3\u8FC7\u4E86 {skipped} \u4E2A\u6D3B\u52A8\u4F1A\u8BDD\u3002\uFF09",
|
|
95
|
-
"cleanup.
|
|
100
|
+
"cleanup.clearDry": "\u9884\u6F14\uFF1A\u5C06\u6E05\u9664\u5F53\u524D\u4F1A\u8BDD\u7684 {entries} \u4E2A\u5FEB\u7167\uFF0C\u91CA\u653E {bytes} \u5B57\u8282\u3002\u52A0 --apply \u6B63\u5F0F\u5220\u9664\u3002",
|
|
101
|
+
"cleanup.clearApply": "\u5DF2\u6E05\u9664\u5F53\u524D\u4F1A\u8BDD\u7684 {entries} \u4E2A\u5FEB\u7167\uFF0C\u91CA\u653E {bytes} \u5B57\u8282\u3002\u8BE5\u4F1A\u8BDD\u5DF2\u91CD\u7F6E\u4E3A\u4ECE\u5F53\u524D\u72B6\u6001\u91CD\u65B0\u8BB0\u5F55\u5FEB\u7167\u3002",
|
|
102
|
+
"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",
|
|
103
|
+
"cleanup.clearCancelled": "\u6E05\u7A7A\u5DF2\u53D6\u6D88\u3002",
|
|
104
|
+
"cleanup.clearFailed": "\u65E0\u6CD5\u6E05\u9664\u4F1A\u8BDD {sessionId}\uFF1A{detail}\u3002",
|
|
105
|
+
"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"
|
|
96
106
|
};
|
|
97
107
|
var HOST_DICTS = { en, zh };
|
|
98
108
|
function translate(lang, key, params = {}) {
|
|
@@ -231,8 +241,9 @@ function execSessionCwd(exec, requestedPath) {
|
|
|
231
241
|
import { createHash } from "node:crypto";
|
|
232
242
|
import { lstat, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
233
243
|
import { dirname, join } from "node:path";
|
|
234
|
-
import {
|
|
235
|
-
var
|
|
244
|
+
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
245
|
+
var SNAPSHOT_DIR_NAME = "rewind-snapshots";
|
|
246
|
+
var DEFAULT_SNAPSHOT_ROOT = join(resolveDshHome(), SNAPSHOT_DIR_NAME);
|
|
236
247
|
var SNAPSHOT_ROOT_ENV = "DSH_REWIND_SNAPSHOT_DIR";
|
|
237
248
|
var MAX_ANCHOR_GROUPS = 100;
|
|
238
249
|
function isLinkEntry(entry) {
|
|
@@ -337,11 +348,6 @@ async function dirSizeAndLastActive(dir) {
|
|
|
337
348
|
return { size, lastActiveMs };
|
|
338
349
|
}
|
|
339
350
|
var SnapshotStore = class _SnapshotStore {
|
|
340
|
-
constructor(root = process.env[SNAPSHOT_ROOT_ENV] ?? DEFAULT_SNAPSHOT_ROOT, opts) {
|
|
341
|
-
this.root = root;
|
|
342
|
-
this.dedup = opts?.dedup ?? true;
|
|
343
|
-
}
|
|
344
|
-
root;
|
|
345
351
|
/** Debounce window for the per-commit prune (keeps the readdir+sort off the hot path). */
|
|
346
352
|
static PRUNE_INTERVAL_MS = 1e3;
|
|
347
353
|
lastPruneAt = 0;
|
|
@@ -360,6 +366,8 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
360
366
|
lastEntryTime = 0;
|
|
361
367
|
/** Store options; `dedup` toggles in-place content dedup (default on). */
|
|
362
368
|
dedup;
|
|
369
|
+
/** Resolved checkpoint store root (absolute); see the constructor's fallback. */
|
|
370
|
+
root;
|
|
363
371
|
/**
|
|
364
372
|
* In-memory per-path "most recent entry" for content dedup, keyed by
|
|
365
373
|
* `<sessionId>\0<path>`. Each value holds the entry's effective `before`
|
|
@@ -370,6 +378,10 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
370
378
|
lastEntry = /* @__PURE__ */ new Map();
|
|
371
379
|
/** Sessions whose dedup state has been seeded from disk this process. */
|
|
372
380
|
seededSessions = /* @__PURE__ */ new Set();
|
|
381
|
+
constructor(root, opts) {
|
|
382
|
+
this.dedup = opts?.dedup ?? true;
|
|
383
|
+
this.root = root ?? process.env[SNAPSHOT_ROOT_ENV] ?? join(resolveDshHome(opts?.dshHome), SNAPSHOT_DIR_NAME);
|
|
384
|
+
}
|
|
373
385
|
/** Absolute path of one session's snapshot directory (id sanitized). */
|
|
374
386
|
sessionDir(sessionId) {
|
|
375
387
|
return join(this.root, safeSessionId(sessionId));
|
|
@@ -760,6 +772,13 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
760
772
|
* already absent, i.e. the target state is reached), a restore is a plain
|
|
761
773
|
* writeFile with a recursive mkdir of the parent. Returns how the outcome
|
|
762
774
|
* should record it.
|
|
775
|
+
*
|
|
776
|
+
* This is the only place the store writes restored content to the real FS,
|
|
777
|
+
* and it is deliberately a raw `writeFile`/`unlink` rather than the fs
|
|
778
|
+
* service: the caller only ever hands it a path from `planRestore` — one the
|
|
779
|
+
* session's own write-class tool call recorded and resolved (never a
|
|
780
|
+
* symlink/hard link) and only when it differs from the live disk. So no
|
|
781
|
+
* arbitrary path, no model input, never automatic.
|
|
763
782
|
*/
|
|
764
783
|
async applyActionToDisk(kind, path, content, deleteFile) {
|
|
765
784
|
if (kind === "delete") {
|
|
@@ -1181,6 +1200,103 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
1181
1200
|
}
|
|
1182
1201
|
return paths;
|
|
1183
1202
|
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Summarize a session's on-disk footprint for a clear dry-run: anchor-group
|
|
1205
|
+
* count, committed checkpoint-entry count, restore-journal count, and total
|
|
1206
|
+
* bytes. Walks with `lstat` (never follows a symlink, so a hostile symlink
|
|
1207
|
+
* cannot escape the store root or inflate the measurement) and skips
|
|
1208
|
+
* dot-prefixed temp leftovers and non-`.json` members — they are never
|
|
1209
|
+
* checkpoint entries.
|
|
1210
|
+
*/
|
|
1211
|
+
async sessionStats(sessionId) {
|
|
1212
|
+
const sessionDir = this.sessionDir(sessionId);
|
|
1213
|
+
let names;
|
|
1214
|
+
try {
|
|
1215
|
+
names = await readdir(sessionDir);
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
if (error.code === "ENOENT") return { anchorGroups: 0, entries: 0, journals: 0, bytes: 0 };
|
|
1218
|
+
throw error;
|
|
1219
|
+
}
|
|
1220
|
+
let anchorGroups = 0;
|
|
1221
|
+
let entries = 0;
|
|
1222
|
+
let journals = 0;
|
|
1223
|
+
let bytes = 0;
|
|
1224
|
+
for (const name2 of names) {
|
|
1225
|
+
if (name2.startsWith(".")) continue;
|
|
1226
|
+
const full = join(sessionDir, name2);
|
|
1227
|
+
let st;
|
|
1228
|
+
try {
|
|
1229
|
+
st = await lstat(full);
|
|
1230
|
+
} catch {
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
if (st.isDirectory()) {
|
|
1234
|
+
if (!Number.isSafeInteger(Number(name2))) continue;
|
|
1235
|
+
anchorGroups++;
|
|
1236
|
+
let files;
|
|
1237
|
+
try {
|
|
1238
|
+
files = await readdir(full);
|
|
1239
|
+
} catch {
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
for (const file of files) {
|
|
1243
|
+
if (!file.endsWith(".json")) continue;
|
|
1244
|
+
entries++;
|
|
1245
|
+
const fileSt = await lstat(join(full, file)).catch(() => void 0);
|
|
1246
|
+
if (fileSt !== void 0) bytes += fileSt.size;
|
|
1247
|
+
}
|
|
1248
|
+
} else if (name2.startsWith(_SnapshotStore.JOURNAL_PREFIX) && name2.endsWith(".json")) {
|
|
1249
|
+
journals++;
|
|
1250
|
+
bytes += st.size;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return { anchorGroups, entries, journals, bytes };
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Remove a session's ENTIRE snapshot directory — every anchor group, every
|
|
1257
|
+
* checkpoint entry, and every restore journal — and reset the store's
|
|
1258
|
+
* in-memory dedup state so the session starts recording fresh from the
|
|
1259
|
+
* current workspace state. This is the manual "get rid of this session's
|
|
1260
|
+
* records NOW" action on the ACTIVE session the user is driving (it is never
|
|
1261
|
+
* targetable by id; that is a directory-manipulation concern the user can do
|
|
1262
|
+
* directly).
|
|
1263
|
+
*
|
|
1264
|
+
* SEMANTICS — clearing is an explicit abandonment: issuing the command means
|
|
1265
|
+
* the user accepts that this session's snapshot archive goes away. It is
|
|
1266
|
+
* therefore NOT gated on the state of any restore journal. A clear and a
|
|
1267
|
+
* restore are both slash commands the host runs to completion for an agent,
|
|
1268
|
+
* so they never interleave — any non-terminal journal present on disk is a
|
|
1269
|
+
* stale orphan from a previous (crashed) process, and discarding it is the
|
|
1270
|
+
* correct, safe resolution of that abandoned restore.
|
|
1271
|
+
*
|
|
1272
|
+
* SAFETY (this module's real concern is the plugin's ongoing BEHAVIOR, not
|
|
1273
|
+
* losing snapshots):
|
|
1274
|
+
* - Only the session dir is removed; dedup refs are session-relative, so
|
|
1275
|
+
* there is no cross-session dangling to materialize (the same rationale as
|
|
1276
|
+
* {@link pruneStale}'s whole-dir removal).
|
|
1277
|
+
* - The in-memory dedup state (`lastEntry` / `seededSessions`) is ALWAYS
|
|
1278
|
+
* reset on an apply — even when the dir was already empty. A stale
|
|
1279
|
+
* in-memory entry (e.g. a session whose dir was removed out-of-band) would
|
|
1280
|
+
* otherwise link a later `recordEntry` to a deleted prior entry, leaving a
|
|
1281
|
+
* dangling ref that breaks restore resolution. This is the primary
|
|
1282
|
+
* correctness guarantee.
|
|
1283
|
+
*
|
|
1284
|
+
* `dryRun` computes the report without touching disk or memory.
|
|
1285
|
+
*/
|
|
1286
|
+
async clearSession(sessionId, opts) {
|
|
1287
|
+
const dryRun = opts?.dryRun ?? false;
|
|
1288
|
+
const stats = await this.sessionStats(sessionId);
|
|
1289
|
+
if (!dryRun) {
|
|
1290
|
+
if (stats.anchorGroups > 0 || stats.journals > 0) {
|
|
1291
|
+
await rm(this.sessionDir(sessionId), { recursive: true, force: true });
|
|
1292
|
+
}
|
|
1293
|
+
this.seededSessions.delete(sessionId);
|
|
1294
|
+
for (const key of this.lastEntry.keys()) {
|
|
1295
|
+
if (key.startsWith(`${sessionId}\0`)) this.lastEntry.delete(key);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
return { sessionId, ...stats, dryRun };
|
|
1299
|
+
}
|
|
1184
1300
|
};
|
|
1185
1301
|
function hashPath(path) {
|
|
1186
1302
|
return createHash("sha256").update(path).digest("hex").slice(0, 8);
|
|
@@ -1211,18 +1327,18 @@ async function reconcileTracked(store, sessionId, anchorSeq, tracked, probe = de
|
|
|
1211
1327
|
// src/snapshot-cleanup.ts
|
|
1212
1328
|
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1213
1329
|
import { dirname as dirname2, join as join2 } from "node:path";
|
|
1214
|
-
import {
|
|
1330
|
+
import { resolveDshHome as resolveDshHome2 } from "@deepseek-ai/dsh-home-paths";
|
|
1215
1331
|
var CLEANUP_CONFIG_FILENAME = "snapshot-cleanup.json";
|
|
1216
1332
|
var CLEANUP_CONFIG_ENV = "DSH_SNAPSHOT_CLEANUP_CONFIG";
|
|
1217
1333
|
var DEFAULT_MAX_AGE_DAYS = 30;
|
|
1218
1334
|
var DEFAULT_CLEANUP_CONFIG = { enabled: false, maxAgeDays: DEFAULT_MAX_AGE_DAYS };
|
|
1219
1335
|
var AUTO_SWEEP_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1220
|
-
function resolveCleanupConfigPath() {
|
|
1221
|
-
return process.env[CLEANUP_CONFIG_ENV] ?? join2(
|
|
1336
|
+
function resolveCleanupConfigPath(dshHome) {
|
|
1337
|
+
return process.env[CLEANUP_CONFIG_ENV] ?? join2(resolveDshHome2(dshHome), CLEANUP_CONFIG_FILENAME);
|
|
1222
1338
|
}
|
|
1223
1339
|
var STATE_FILENAME = "snapshot-cleanup-last-sweep.json";
|
|
1224
|
-
function resolveCleanupStatePath() {
|
|
1225
|
-
return join2(dirname2(resolveCleanupConfigPath()), STATE_FILENAME);
|
|
1340
|
+
function resolveCleanupStatePath(dshHome) {
|
|
1341
|
+
return join2(dirname2(resolveCleanupConfigPath(dshHome)), STATE_FILENAME);
|
|
1226
1342
|
}
|
|
1227
1343
|
async function loadLastSweepAt(path) {
|
|
1228
1344
|
try {
|
|
@@ -1319,10 +1435,21 @@ function parseCleanupCommand(rawInput) {
|
|
|
1319
1435
|
if (!Number.isInteger(days) || days <= 0) return { error: '"max-age" must be a positive integer (days)' };
|
|
1320
1436
|
return { action: "max-age", value: days };
|
|
1321
1437
|
}
|
|
1438
|
+
case "run-apply":
|
|
1439
|
+
return { error: 'the "run-apply" abbreviation was removed; use "run --apply"' };
|
|
1322
1440
|
case "run": {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1441
|
+
let apply2 = false;
|
|
1442
|
+
let current = false;
|
|
1443
|
+
for (const rawFlag of parts.slice(1)) {
|
|
1444
|
+
if (rawFlag === "--apply") {
|
|
1445
|
+
apply2 = true;
|
|
1446
|
+
} else if (rawFlag === "--current") {
|
|
1447
|
+
current = true;
|
|
1448
|
+
} else {
|
|
1449
|
+
return { error: `unknown /snapshot-auto-cleanup run flag "${rawFlag}"` };
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
return { action: "run", target: current ? "current" : "rules", apply: apply2 };
|
|
1326
1453
|
}
|
|
1327
1454
|
default:
|
|
1328
1455
|
return { error: `unknown /snapshot-auto-cleanup subcommand "${parts[0]}"` };
|
|
@@ -1640,13 +1767,13 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
|
1640
1767
|
return executeRewind(ctx, store, fs, invocation, target, mode, inflight);
|
|
1641
1768
|
}
|
|
1642
1769
|
var autoSweepChecked = false;
|
|
1643
|
-
async function maybeRunAutoCleanup(ctx, store, sessionId) {
|
|
1770
|
+
async function maybeRunAutoCleanup(ctx, store, sessionId, dshHome) {
|
|
1644
1771
|
if (autoSweepChecked) return;
|
|
1645
1772
|
autoSweepChecked = true;
|
|
1646
1773
|
await runAutoCleanupCheck({
|
|
1647
1774
|
pruner: store,
|
|
1648
|
-
configPath: resolveCleanupConfigPath(),
|
|
1649
|
-
statePath: resolveCleanupStatePath(),
|
|
1775
|
+
configPath: resolveCleanupConfigPath(dshHome),
|
|
1776
|
+
statePath: resolveCleanupStatePath(dshHome),
|
|
1650
1777
|
log: (msg) => ctx.logger.warn(msg)
|
|
1651
1778
|
}, sessionId);
|
|
1652
1779
|
}
|
|
@@ -1661,10 +1788,10 @@ function formatCleanupReport(report) {
|
|
|
1661
1788
|
return report.skippedActive > 0 ? `${text}
|
|
1662
1789
|
${t("cleanup.skipped", { skipped: report.skippedActive })}` : text;
|
|
1663
1790
|
}
|
|
1664
|
-
async function handleSnapshotCleanup(store, invocation) {
|
|
1791
|
+
async function handleSnapshotCleanup(store, invocation, dshHome, trackedBySession) {
|
|
1665
1792
|
const parsed = parseCleanupCommand(invocation.rawInput);
|
|
1666
1793
|
if ("error" in parsed) return { kind: "error", text: t("cleanup.usage") };
|
|
1667
|
-
const configPath = resolveCleanupConfigPath();
|
|
1794
|
+
const configPath = resolveCleanupConfigPath(dshHome);
|
|
1668
1795
|
switch (parsed.action) {
|
|
1669
1796
|
case "status": {
|
|
1670
1797
|
const loaded = await loadCleanupConfig(configPath);
|
|
@@ -1700,17 +1827,20 @@ async function handleSnapshotCleanup(store, invocation) {
|
|
|
1700
1827
|
}
|
|
1701
1828
|
return { kind: "success", text: t("cleanup.maxAgeOk", { days: parsed.value }) };
|
|
1702
1829
|
}
|
|
1703
|
-
case "run":
|
|
1704
|
-
|
|
1830
|
+
case "run": {
|
|
1831
|
+
const apply2 = parsed.apply;
|
|
1832
|
+
if (parsed.target === "current") {
|
|
1833
|
+
return handleClearCurrent(store, invocation, apply2, trackedBySession);
|
|
1834
|
+
}
|
|
1705
1835
|
const loaded = await loadCleanupConfig(configPath);
|
|
1706
1836
|
if (!loaded.ok) return { kind: "error", text: t("cleanup.cfgInvalid", { detail: loaded.error }) };
|
|
1707
1837
|
try {
|
|
1708
1838
|
const report = await store.pruneStale({
|
|
1709
1839
|
keepActiveId: invocation.agent.session.id,
|
|
1710
1840
|
maxAgeDays: loaded.config.maxAgeDays,
|
|
1711
|
-
dryRun:
|
|
1841
|
+
dryRun: !apply2
|
|
1712
1842
|
});
|
|
1713
|
-
if (!report.dryRun) await saveLastSweepAt(resolveCleanupStatePath(), Date.now());
|
|
1843
|
+
if (!report.dryRun) await saveLastSweepAt(resolveCleanupStatePath(dshHome), Date.now());
|
|
1714
1844
|
return { kind: "success", text: formatCleanupReport(report) };
|
|
1715
1845
|
} catch (error) {
|
|
1716
1846
|
return { kind: "error", text: t("cleanup.runFailed", { detail: error instanceof Error ? error.message : String(error) }) };
|
|
@@ -1718,8 +1848,39 @@ async function handleSnapshotCleanup(store, invocation) {
|
|
|
1718
1848
|
}
|
|
1719
1849
|
}
|
|
1720
1850
|
}
|
|
1851
|
+
function formatClearReport(report) {
|
|
1852
|
+
const key = report.dryRun ? "cleanup.clearDry" : "cleanup.clearApply";
|
|
1853
|
+
return t(key, {
|
|
1854
|
+
entries: report.entries,
|
|
1855
|
+
bytes: report.bytes
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
async function handleClearCurrent(store, invocation, apply2, trackedBySession) {
|
|
1859
|
+
const { agent } = invocation;
|
|
1860
|
+
const sessionId = agent.session.id;
|
|
1861
|
+
if (apply2) {
|
|
1862
|
+
if (agent.status !== "idle") {
|
|
1863
|
+
agent.cancel({ kind: "user" }, { keepInbox: true });
|
|
1864
|
+
const stopped = await waitForAgentIdle(agent, invocation.signal);
|
|
1865
|
+
if (!stopped) {
|
|
1866
|
+
return { kind: "error", text: t("cleanup.clearActive", { sessionId }) };
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
if (invocation.signal.aborted) {
|
|
1870
|
+
return { kind: "error", text: t("cleanup.clearCancelled") };
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
try {
|
|
1874
|
+
const report = await store.clearSession(sessionId, { dryRun: !apply2 });
|
|
1875
|
+
if (!report.dryRun) trackedBySession.delete(sessionId);
|
|
1876
|
+
return { kind: "success", text: formatClearReport(report) };
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
return { kind: "error", text: t("cleanup.clearFailed", { detail: error instanceof Error ? error.message : String(error), sessionId }) };
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1721
1881
|
function apply(ctx, config) {
|
|
1722
|
-
const
|
|
1882
|
+
const dshHome = config?.dshHome;
|
|
1883
|
+
const store = new SnapshotStore(config?.snapshotDir, { dedup: config?.dedup, dshHome });
|
|
1723
1884
|
const pending = /* @__PURE__ */ new Map();
|
|
1724
1885
|
const anchorCache = /* @__PURE__ */ new WeakMap();
|
|
1725
1886
|
const inflight = /* @__PURE__ */ new Set();
|
|
@@ -1732,16 +1893,22 @@ function apply(ctx, config) {
|
|
|
1732
1893
|
}
|
|
1733
1894
|
});
|
|
1734
1895
|
ctx.effect(function* () {
|
|
1896
|
+
const rewindHandler = (invocation) => handleRewind(ctx, store, fsService, invocation, inflight);
|
|
1735
1897
|
yield ctx.commands.register({
|
|
1736
1898
|
name: "rewind",
|
|
1737
1899
|
description: t("command.description"),
|
|
1738
|
-
handler:
|
|
1900
|
+
handler: rewindHandler
|
|
1901
|
+
});
|
|
1902
|
+
yield ctx.commands.register({
|
|
1903
|
+
name: "undo",
|
|
1904
|
+
description: t("command.description"),
|
|
1905
|
+
handler: rewindHandler
|
|
1739
1906
|
});
|
|
1740
1907
|
yield ctx.commands.register({
|
|
1741
1908
|
name: "snapshot-auto-cleanup",
|
|
1742
1909
|
description: t("cleanup.description"),
|
|
1743
1910
|
input: { hint: t("cleanup.inputHint") },
|
|
1744
|
-
handler: (invocation) => handleSnapshotCleanup(store, invocation)
|
|
1911
|
+
handler: (invocation) => handleSnapshotCleanup(store, invocation, dshHome, trackedBySession)
|
|
1745
1912
|
});
|
|
1746
1913
|
}, "dsh-rewind command");
|
|
1747
1914
|
ctx.on("session/event", (session, event) => {
|
|
@@ -1751,7 +1918,7 @@ function apply(ctx, config) {
|
|
|
1751
1918
|
void (async () => {
|
|
1752
1919
|
try {
|
|
1753
1920
|
const sessionId = session.id;
|
|
1754
|
-
void maybeRunAutoCleanup(ctx, store, sessionId);
|
|
1921
|
+
void maybeRunAutoCleanup(ctx, store, sessionId, dshHome);
|
|
1755
1922
|
let tracked = trackedBySession.get(sessionId);
|
|
1756
1923
|
if (tracked === void 0) {
|
|
1757
1924
|
tracked = await store.trackedPaths(sessionId);
|
|
@@ -1777,7 +1944,7 @@ function apply(ctx, config) {
|
|
|
1777
1944
|
});
|
|
1778
1945
|
scope.on("tools/post-execute", async (exec, result, next) => {
|
|
1779
1946
|
try {
|
|
1780
|
-
void maybeRunAutoCleanup(ctx, store, exec.agent?.session?.id);
|
|
1947
|
+
void maybeRunAutoCleanup(ctx, store, exec.agent?.session?.id, dshHome);
|
|
1781
1948
|
await commitEntry(store, pending, anchorCache, trackedBySession, exec, result);
|
|
1782
1949
|
} catch (error) {
|
|
1783
1950
|
ctx.logger.warn(`[dsh-rewind] checkpoint commit failed for ${exec.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -13,6 +13,34 @@ export interface HiddenChat {
|
|
|
13
13
|
get(key: string): ChatConversationViewNode | undefined;
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Reader for one session's live chat snapshot. The dual channel hides the
|
|
18
|
+
* harness split behind SiriLee/dsh-rewind#7: rc.2 serves the chat from the
|
|
19
|
+
* session face snapshot, while 0.1.2-alpha.1+ serves it from the
|
|
20
|
+
* `uiConversation` service's named "chat" view (contributed by
|
|
21
|
+
* dsh-client-ui-chat through the uiSession slot hook).
|
|
22
|
+
*/
|
|
23
|
+
export type ChatOf = (session: {
|
|
24
|
+
readonly sessionId: string;
|
|
25
|
+
getSnapshot(): {
|
|
26
|
+
chat?: unknown;
|
|
27
|
+
};
|
|
28
|
+
} | undefined) => HiddenChat | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the chat snapshot across the two harness channels: the session-face
|
|
31
|
+
* snapshot first (rc.2 — on alpha.1+ the face no longer carries `chat`, so the
|
|
32
|
+
* field reads `undefined`), then the `uiConversation` "chat" view. The view's
|
|
33
|
+
* `getSnapshot()` returns undefined until the named view is registered, so
|
|
34
|
+
* both channels missing degrades to `undefined` (no targets, no hiding —
|
|
35
|
+
* never a crash).
|
|
36
|
+
*/
|
|
37
|
+
export declare function chatSnapshotOf(face: {
|
|
38
|
+
getSnapshot(): {
|
|
39
|
+
chat?: unknown;
|
|
40
|
+
};
|
|
41
|
+
} | undefined, chatView: {
|
|
42
|
+
getSnapshot(): unknown;
|
|
43
|
+
} | undefined): HiddenChat | undefined;
|
|
16
44
|
/**
|
|
17
45
|
* Extract the rewind target seq from a `/rewind` command's structured `args`
|
|
18
46
|
* (e.g. `@5 chat`, `preview @5 both`). Locale-independent — never parses the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-rewind client half: the `/rewind` command decoration, the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* dsh-rewind client half: the `/rewind` command decoration, the locale
|
|
3
|
+
* registration, and the session-scoped portal bridge that renders the
|
|
4
|
+
* per-message ↶ rewind button (see
|
|
5
5
|
* `portals.tsx` for the button itself).
|
|
6
6
|
*
|
|
7
7
|
* The button is NOT injected by hand into the DOM anymore: the plugin
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
* drift.
|
|
15
15
|
*
|
|
16
16
|
* The text-driven flow is the harness's STANDARD command decoration
|
|
17
|
-
* (`ctx.commandUi.decorate`): a bare `/rewind`
|
|
18
|
-
* completion, or typed in full and Entered —
|
|
19
|
-
* popupSelect shell (search, ↑↓/Enter, Esc) listing
|
|
20
|
-
* instead of executing the command. Picking one
|
|
21
|
-
* the ↶ button: the mode popover, both-impact
|
|
22
|
-
* hiding and the composer refill
|
|
23
|
-
* forms (`/rewind @<seq> chat|both`,
|
|
24
|
-
* channels the ↶ button and the popover
|
|
25
|
-
*
|
|
17
|
+
* (`ctx.commandUi.decorate`): a bare `/rewind` (or its alias `/undo`) —
|
|
18
|
+
* picked from the slash-menu completion, or typed in full and Entered —
|
|
19
|
+
* opens the harness's own popupSelect shell (search, ↑↓/Enter, Esc) listing
|
|
20
|
+
* the rewind candidates instead of executing the command. Picking one
|
|
21
|
+
* continues the SAME flow as the ↶ button: the mode popover, both-impact
|
|
22
|
+
* confirmation, execution, row hiding and the composer refill
|
|
23
|
+
* (`runRewindAndFill`). The parameterized forms (`/rewind @<seq> chat|both`,
|
|
24
|
+
* `/rewind preview …`) stay internal channels the ↶ button and the popover
|
|
25
|
+
* drive through `session.command`.
|
|
26
26
|
*
|
|
27
27
|
* @module dsh-rewind/client
|
|
28
28
|
*/
|
|
@@ -25,7 +25,6 @@ export declare const zh: {
|
|
|
25
25
|
'popover.impact.delete': string;
|
|
26
26
|
'popover.confirm': string;
|
|
27
27
|
'popover.back': string;
|
|
28
|
-
'guard.hint': string;
|
|
29
28
|
};
|
|
30
29
|
/** The rewind namespace key union. */
|
|
31
30
|
export type RewindKey = keyof typeof zh;
|
|
@@ -61,5 +60,4 @@ export declare const en: {
|
|
|
61
60
|
'popover.impact.delete': string;
|
|
62
61
|
'popover.confirm': string;
|
|
63
62
|
'popover.back': string;
|
|
64
|
-
'guard.hint': string;
|
|
65
63
|
};
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import type { SessionFace } from '@deepseek-ai/dsh-client-runtime/client';
|
|
17
17
|
import type { CommandNode } from '@deepseek-ai/dsh-client-runtime/client';
|
|
18
|
+
import { type ChatOf } from './hidden.ts';
|
|
18
19
|
import type { RewindKey } from './locales.ts';
|
|
19
20
|
type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
|
|
20
21
|
export interface PopoverOptions {
|
|
@@ -31,6 +32,12 @@ export interface PopoverOptions {
|
|
|
31
32
|
/** Pending variant: executed after the retract confirm closes the popover. */
|
|
32
33
|
readonly onRetract?: () => void;
|
|
33
34
|
readonly preview: string;
|
|
35
|
+
/**
|
|
36
|
+
* Dual-channel chat reader (rc.2 session face / alpha.1+ uiConversation
|
|
37
|
+
* view): the durable variant's command probes scan the chat through it.
|
|
38
|
+
* Unused by the pending-retract variant.
|
|
39
|
+
*/
|
|
40
|
+
readonly chatOf: ChatOf;
|
|
34
41
|
/** The button that opened the popover (outside-click ignore target). */
|
|
35
42
|
readonly anchor: HTMLElement;
|
|
36
43
|
readonly t: Translate;
|
|
@@ -50,13 +57,13 @@ export declare function closePopover(): void;
|
|
|
50
57
|
* command's stale outcome (e.g. an older preview that found file changes,
|
|
51
58
|
* after those changes were already restored).
|
|
52
59
|
*/
|
|
53
|
-
export declare function knownCommandSeqs(session: SessionFace, match: (node: CommandNode) => boolean): Set<number>;
|
|
60
|
+
export declare function knownCommandSeqs(session: SessionFace, chatOf: ChatOf, match: (node: CommandNode) => boolean): Set<number>;
|
|
54
61
|
/**
|
|
55
62
|
* Resolve the outcome of the newest matching rewind command by watching the
|
|
56
63
|
* session snapshot (command/run + command/done land as one CommandNode).
|
|
57
64
|
* @returns the outcome text-bearing node, or null on timeout.
|
|
58
65
|
*/
|
|
59
|
-
export declare function waitForCommand(session: SessionFace, match: (node: CommandNode) => boolean, timeoutMs?: number): Promise<{
|
|
66
|
+
export declare function waitForCommand(session: SessionFace, chatOf: ChatOf, match: (node: CommandNode) => boolean, timeoutMs?: number): Promise<{
|
|
60
67
|
kind: 'success' | 'error';
|
|
61
68
|
text?: string;
|
|
62
69
|
} | null>;
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { type ReactNode } from 'react';
|
|
26
26
|
import type { SessionFace } from '@deepseek-ai/dsh-client-runtime/client';
|
|
27
|
+
import { type ChatOf, type HiddenChat } from './hidden.ts';
|
|
27
28
|
import type { RewindKey } from './locales.ts';
|
|
28
29
|
type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
|
|
29
30
|
/** One portal target: the actions row of a user/steering seat + its durable node. */
|
|
@@ -51,6 +52,12 @@ export type PortalTarget = {
|
|
|
51
52
|
/** Capabilities the session-scoped bridge receives from the plugin apply(). */
|
|
52
53
|
export interface RewindBridgeDeps {
|
|
53
54
|
readonly sessionOf: (sessionId: string) => SessionFace | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Dual-channel chat reader (rc.2 session face / alpha.1+ uiConversation
|
|
57
|
+
* "chat" view): every chat snapshot read goes through it. See
|
|
58
|
+
* `chatSnapshotOf` in hidden.ts for the channel precedence.
|
|
59
|
+
*/
|
|
60
|
+
readonly chatOf: ChatOf;
|
|
54
61
|
readonly currentSessionId: () => string | undefined;
|
|
55
62
|
readonly t: Translate;
|
|
56
63
|
readonly subscribeLocale: (cb: () => void) => () => void;
|
|
@@ -87,7 +94,13 @@ export declare function fillComposer(text: string): boolean;
|
|
|
87
94
|
* the old baseline heuristic refilled withdrawn text into the composer
|
|
88
95
|
* after switching sessions or restarting dsh.
|
|
89
96
|
*/
|
|
90
|
-
export declare function runRewindAndFill(session: SessionFace, seq: number, mode: 'chat' | 'both', currentSessionId: () => string | undefined): Promise<void>;
|
|
97
|
+
export declare function runRewindAndFill(session: SessionFace, seq: number, mode: 'chat' | 'both', currentSessionId: () => string | undefined, chatOf: ChatOf): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Collect the portal targets of one session: user rows × snapshot nodes.
|
|
100
|
+
* Exported as a test seam — the DOM→targets pairing that drives the ↶ button
|
|
101
|
+
* is otherwise only reachable through a full React portal render.
|
|
102
|
+
*/
|
|
103
|
+
export declare function collectTargets(chat: HiddenChat, hiddenSeqs: ReadonlySet<number>): readonly PortalTarget[];
|
|
91
104
|
interface RewindPortalsProps extends RewindBridgeDeps {
|
|
92
105
|
readonly sessionId: string;
|
|
93
106
|
}
|
|
@@ -98,7 +111,7 @@ interface RewindPortalsProps extends RewindBridgeDeps {
|
|
|
98
111
|
* skipped when the target set is unchanged), so the plugin never runs a
|
|
99
112
|
* synchronous full-transcript scan inside a commit microtask.
|
|
100
113
|
*/
|
|
101
|
-
export declare function RewindPortals({ sessionId, sessionOf, currentSessionId, t, subscribeLocale }: RewindPortalsProps): ReactNode;
|
|
114
|
+
export declare function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subscribeLocale }: RewindPortalsProps): ReactNode;
|
|
102
115
|
/**
|
|
103
116
|
* Build the slot-entry component for the plugin apply(): a tiny bridge that
|
|
104
117
|
* injects the apply-time capabilities (session resolution, locale, rewind
|
package/lib/types/index.d.ts
CHANGED
|
@@ -29,10 +29,12 @@ export { SnapshotStore } from './snapshot.ts';
|
|
|
29
29
|
export type { CheckpointEntry, FileImpact, PruneStaleReport, RestoreOutcome, RestoreJournal, RestoreJournalState, RestoreReconcileReport } from './snapshot.ts';
|
|
30
30
|
export declare const name = "dsh-rewind";
|
|
31
31
|
export declare const inject: string[];
|
|
32
|
-
/** Plugin config
|
|
32
|
+
/** Plugin config. */
|
|
33
33
|
export interface RewindConfig {
|
|
34
|
-
/** Checkpoint store root (
|
|
34
|
+
/** Checkpoint store root (exact path; beats `DSH_REWIND_SNAPSHOT_DIR` and the harness-home default). */
|
|
35
35
|
readonly snapshotDir?: string;
|
|
36
|
+
/** Harness home override (`config.dshHome` > `$DSH_HOME` > `~/.dsh`); feeds the default snapshot/cleanup paths. */
|
|
37
|
+
readonly dshHome?: string;
|
|
36
38
|
/** In-place content dedup (identical before-content → link). Default `true`. */
|
|
37
39
|
readonly dedup?: boolean;
|
|
38
40
|
}
|
|
@@ -55,6 +57,7 @@ export interface RewindConfig {
|
|
|
55
57
|
* disk at `tools/post-execute` under the turn's anchor message seq.
|
|
56
58
|
*
|
|
57
59
|
* @param ctx - context carrying `commands`, `tools`, and an optional `fs`.
|
|
58
|
-
* @param config - optional
|
|
60
|
+
* @param config - optional plugin config: `snapshotDir` (exact store-root override),
|
|
61
|
+
* `dshHome` (harness-home override feeding the default paths), `dedup`.
|
|
59
62
|
*/
|
|
60
63
|
export declare function apply(ctx: Context, config?: RewindConfig): void;
|
package/lib/types/locales.d.ts
CHANGED
|
@@ -65,6 +65,11 @@ export declare const en: {
|
|
|
65
65
|
'cleanup.runApply': string;
|
|
66
66
|
'cleanup.runFailed': string;
|
|
67
67
|
'cleanup.skipped': string;
|
|
68
|
+
'cleanup.clearDry': string;
|
|
69
|
+
'cleanup.clearApply': string;
|
|
70
|
+
'cleanup.clearActive': string;
|
|
71
|
+
'cleanup.clearCancelled': string;
|
|
72
|
+
'cleanup.clearFailed': string;
|
|
68
73
|
'cleanup.usage': string;
|
|
69
74
|
};
|
|
70
75
|
/** The host rewind dictionary key union. */
|