dsh-rewind-plugin 0.16.0-alpha.1 → 0.16.0-beta.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/SECURITY.md +18 -14
- package/lib/client.js +12 -3
- package/lib/index.js +40 -24
- package/lib/types/client/portals.d.ts +13 -1
- package/lib/types/index.d.ts +13 -1
- package/lib/types/snapshot-cleanup.d.ts +7 -5
- package/lib/types/snapshot.d.ts +9 -5
- package/package.json +58 -58
package/SECURITY.md
CHANGED
|
@@ -91,19 +91,21 @@ hides the `[target, marker]` span from the rendered transcript.
|
|
|
91
91
|
## Filesystem containment
|
|
92
92
|
|
|
93
93
|
- **Store root**: `<dsh home>/rewind-snapshots/` by default (resolved via
|
|
94
|
-
`resolveDshHome`, so `~/.dsh
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
`resolveDshHome`, so `$DSH_HOME` feeds the base and `~/.dsh` is the fallback);
|
|
95
|
+
the `snapshotDir` config overrides the whole path. The plugin reads no
|
|
96
|
+
environment variable of its own. Deleting the root only removes file backups
|
|
97
|
+
and the store rebuilds from scratch.
|
|
97
98
|
- **Path sanitization**: session ids and call ids are scrubbed to
|
|
98
99
|
`[a-zA-Z0-9._-]` (`safeSessionId` / `safeFileId`) before joining the store
|
|
99
100
|
root; `.` and `..` bare values are replaced — hostile ids cannot traverse
|
|
100
101
|
out of the root.
|
|
101
|
-
- **
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
- **Links and moved directories are refused**: two checks guard a tracked path.
|
|
103
|
+
(1) Planning skips a path whose last component is a symlink or a hard link
|
|
104
|
+
(`lstat().nlink > 1`) and reports it, never restoring it — a symlink would
|
|
105
|
+
redirect the write outside the checkpoint, and a hard link would clobber every
|
|
106
|
+
other name of the same inode (e.g. pnpm-installed files); a link swapped in
|
|
107
|
+
after planning is not re-checked. (2) At apply time, every record this build
|
|
108
|
+
writes — entry, link and
|
|
107
109
|
journal action — carries the `realpath` of the file's directory at commit time
|
|
108
110
|
(the location pin; best-effort, and absent on released-v1 data or a commit
|
|
109
111
|
whose parent could not be resolved), and a restore re-checks it before
|
|
@@ -145,11 +147,13 @@ hides the `[target, marker]` span from the rendered transcript.
|
|
|
145
147
|
planned path's pre-restore ("rescue") state and persists an intent journal,
|
|
146
148
|
then marks each action done as it is applied. A crash at any point leaves
|
|
147
149
|
the journal on disk.
|
|
148
|
-
- **Disk is truth**:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
reached auto-heals to
|
|
150
|
+
- **Disk is truth**: an interrupted op keeps its journal on disk, and the store's
|
|
151
|
+
recovery API re-derives the true state from the real disk — `reconcileRestores`
|
|
152
|
+
reports which paths already match the goal (restored) and which are pending,
|
|
153
|
+
`continueRestore` finishes the op, `rollbackRestore` undoes it to the exact
|
|
154
|
+
pre-restore state, and a journal whose goal is already reached auto-heals to
|
|
155
|
+
its terminal state. The plugin never runs them on its own: an interrupted op
|
|
156
|
+
stays interrupted until an explicit caller resumes or undoes it.
|
|
153
157
|
- **Fail-loud vs fail-soft**: a corrupt **journal** is reported
|
|
154
158
|
`recovery-required` — never silently dropped (dropping it would erase the
|
|
155
159
|
interrupted restore's recovery record). Corrupt **checkpoint entries** are
|
package/lib/client.js
CHANGED
|
@@ -1095,6 +1095,13 @@ function hideWithdrawnSeats(chat, seats, hiddenSeqs, hidden) {
|
|
|
1095
1095
|
}
|
|
1096
1096
|
}
|
|
1097
1097
|
}
|
|
1098
|
+
function showHiddenSeats(hidden) {
|
|
1099
|
+
for (const element of hidden) {
|
|
1100
|
+
element.style.display = "";
|
|
1101
|
+
delete element.dataset.dshRewindHidden;
|
|
1102
|
+
}
|
|
1103
|
+
hidden.clear();
|
|
1104
|
+
}
|
|
1098
1105
|
function collectTargets(chat, hiddenSeqs) {
|
|
1099
1106
|
const rows = /* @__PURE__ */ new Map();
|
|
1100
1107
|
for (const element of document.querySelectorAll(USER_SEAT_SELECTOR)) {
|
|
@@ -1168,7 +1175,7 @@ function sameTargets(left, right) {
|
|
|
1168
1175
|
}
|
|
1169
1176
|
function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchChat, t, subscribeLocale, setComposerText, restoreImages }) {
|
|
1170
1177
|
const [targets, setTargets] = (0, import_react.useState)([]);
|
|
1171
|
-
const hidden = (0, import_react.useRef)(/* @__PURE__ */ new
|
|
1178
|
+
const hidden = (0, import_react.useRef)(/* @__PURE__ */ new Set());
|
|
1172
1179
|
const [, forceRender] = (0, import_react.useReducer)((count) => count + 1, 0);
|
|
1173
1180
|
(0, import_react.useEffect)(() => subscribeLocale(() => {
|
|
1174
1181
|
forceRender();
|
|
@@ -1189,6 +1196,7 @@ function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchC
|
|
|
1189
1196
|
const snapshot = session.getSnapshot();
|
|
1190
1197
|
const chat = chatOf(session);
|
|
1191
1198
|
const hiddenSeqs = chat === void 0 ? /* @__PURE__ */ new Set() : hiddenSeqsOf(chat);
|
|
1199
|
+
for (const element of hidden.current) if (!element.isConnected) hidden.current.delete(element);
|
|
1192
1200
|
hideWithdrawnSeats(
|
|
1193
1201
|
chat,
|
|
1194
1202
|
chat === void 0 ? [] : document.querySelectorAll(CHAT_SEAT_SELECTOR),
|
|
@@ -1225,6 +1233,7 @@ function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchC
|
|
|
1225
1233
|
return () => {
|
|
1226
1234
|
active = false;
|
|
1227
1235
|
observer.disconnect();
|
|
1236
|
+
showHiddenSeats(hidden.current);
|
|
1228
1237
|
unsubscribeInbox?.();
|
|
1229
1238
|
unsubscribeInbox = void 0;
|
|
1230
1239
|
inboxOwner = void 0;
|
|
@@ -1359,9 +1368,9 @@ function createRewindBridge(deps) {
|
|
|
1359
1368
|
}
|
|
1360
1369
|
|
|
1361
1370
|
// src/client/build-info.ts
|
|
1362
|
-
var PLUGIN_VERSION = true ? "0.16.0-
|
|
1371
|
+
var PLUGIN_VERSION = true ? "0.16.0-beta.1" : "dev";
|
|
1363
1372
|
var PLUGIN_PACKAGE = "dsh-rewind-plugin";
|
|
1364
|
-
var BUILD_HASH = true ? "
|
|
1373
|
+
var BUILD_HASH = true ? "c6cfbc55" : "dev";
|
|
1365
1374
|
|
|
1366
1375
|
// src/client/locales.ts
|
|
1367
1376
|
var zh = {
|
package/lib/index.js
CHANGED
|
@@ -238,15 +238,7 @@ var LEGACY_JOURNAL_PREFIX = "restore-journal-";
|
|
|
238
238
|
var PENDING_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
239
239
|
var COMPARE_CHUNK_BYTES = 64 * 1024;
|
|
240
240
|
var REPLACEMENT_CHAR = "\uFFFD";
|
|
241
|
-
var SNAPSHOT_ROOT_ENV = "DSH_REWIND_SNAPSHOT_DIR";
|
|
242
|
-
var MAX_FILE_BYTES_ENV = "DSH_REWIND_MAX_FILE_BYTES";
|
|
243
241
|
var DEFAULT_MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
244
|
-
function maxFileBytesFromEnv() {
|
|
245
|
-
const raw = process.env[MAX_FILE_BYTES_ENV]?.trim();
|
|
246
|
-
if (raw === void 0 || raw === "") return void 0;
|
|
247
|
-
const parsed = Number(raw);
|
|
248
|
-
return Number.isInteger(parsed) ? parsed : void 0;
|
|
249
|
-
}
|
|
250
242
|
var MAX_ANCHOR_GROUPS = 100;
|
|
251
243
|
var CURRENT_STORE_VERSION = 2;
|
|
252
244
|
var UnknownStoreVersionError = class extends Error {
|
|
@@ -675,6 +667,11 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
675
667
|
* and the restore planner refuses a pre-cap record over an over-cap file.
|
|
676
668
|
*/
|
|
677
669
|
maxFileBytes;
|
|
670
|
+
/**
|
|
671
|
+
* Newest anchor groups (user messages) kept per session; `prune` evicts past
|
|
672
|
+
* this bound. A `Config` field (`maxAnchorGroups`).
|
|
673
|
+
*/
|
|
674
|
+
maxAnchorGroups;
|
|
678
675
|
/** Resolved checkpoint store root (absolute); see the constructor's fallback. */
|
|
679
676
|
root;
|
|
680
677
|
/**
|
|
@@ -700,8 +697,9 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
700
697
|
formatVersion = null;
|
|
701
698
|
constructor(root, opts) {
|
|
702
699
|
this.dedup = opts?.dedup ?? true;
|
|
703
|
-
this.maxFileBytes = opts?.maxFileBytes ??
|
|
704
|
-
this.
|
|
700
|
+
this.maxFileBytes = opts?.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
701
|
+
this.maxAnchorGroups = opts?.maxAnchorGroups ?? MAX_ANCHOR_GROUPS;
|
|
702
|
+
this.root = root ?? join(resolveDshHome(opts?.dshHome), SNAPSHOT_DIR_NAME);
|
|
705
703
|
}
|
|
706
704
|
/** Absolute path of one session's snapshot directory (id sanitized). */
|
|
707
705
|
sessionDir(sessionId) {
|
|
@@ -1679,7 +1677,7 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
1679
1677
|
* real is still on disk and the kept link still resolves, so nothing dangles
|
|
1680
1678
|
* and a later prune simply re-materializes.
|
|
1681
1679
|
*/
|
|
1682
|
-
async prune(sessionId, keep =
|
|
1680
|
+
async prune(sessionId, keep = this.maxAnchorGroups, opts) {
|
|
1683
1681
|
const sessionDir = this.sessionDir(sessionId);
|
|
1684
1682
|
let names;
|
|
1685
1683
|
try {
|
|
@@ -2244,7 +2242,7 @@ async function runAutoCleanupCheck(deps, sessionId) {
|
|
|
2244
2242
|
}
|
|
2245
2243
|
if (!loaded.config.enabled) return;
|
|
2246
2244
|
if (isAborted(deps.signal)) return;
|
|
2247
|
-
if (!shouldRunAutoSweep(await loadLastSweepAt(deps.statePath), Date.now())) return;
|
|
2245
|
+
if (!shouldRunAutoSweep(await loadLastSweepAt(deps.statePath), Date.now(), deps.intervalMs)) return;
|
|
2248
2246
|
if (isAborted(deps.signal)) return;
|
|
2249
2247
|
await deps.pruner.pruneStale({ keepActiveId: sessionId, maxAgeDays: loaded.config.maxAgeDays });
|
|
2250
2248
|
if (isAborted(deps.signal)) return;
|
|
@@ -2309,8 +2307,8 @@ function parseCleanupCommand(rawInput) {
|
|
|
2309
2307
|
return { error: `unknown /snapshot-auto-cleanup subcommand "${parts[0]}"` };
|
|
2310
2308
|
}
|
|
2311
2309
|
}
|
|
2312
|
-
function shouldRunAutoSweep(lastAtMs, nowMs) {
|
|
2313
|
-
return nowMs - lastAtMs >=
|
|
2310
|
+
function shouldRunAutoSweep(lastAtMs, nowMs, intervalMs = AUTO_SWEEP_INTERVAL_MS) {
|
|
2311
|
+
return nowMs - lastAtMs >= intervalMs;
|
|
2314
2312
|
}
|
|
2315
2313
|
|
|
2316
2314
|
// src/index.ts
|
|
@@ -2321,6 +2319,9 @@ var Config = z.object({
|
|
|
2321
2319
|
snapshotDir: z.string().required(false),
|
|
2322
2320
|
dshHome: z.string().required(false),
|
|
2323
2321
|
dedup: z.boolean().default(true),
|
|
2322
|
+
maxFileBytes: z.number().step(1).min(0).default(DEFAULT_MAX_FILE_BYTES),
|
|
2323
|
+
maxAnchorGroups: z.number().step(1).min(1).default(MAX_ANCHOR_GROUPS),
|
|
2324
|
+
autoSweepIntervalMs: z.number().step(1).min(1).default(AUTO_SWEEP_INTERVAL_MS),
|
|
2324
2325
|
autoCleanupEnabled: z.boolean().default(DEFAULT_CLEANUP_CONFIG.enabled).volatile(),
|
|
2325
2326
|
autoCleanupMaxAgeDays: z.number().step(1).min(1).default(DEFAULT_CLEANUP_CONFIG.maxAgeDays).volatile()
|
|
2326
2327
|
});
|
|
@@ -2446,11 +2447,16 @@ async function commitEntry(store, pending, anchorCache, trackedBySession, exec,
|
|
|
2446
2447
|
await discardCapture(capture);
|
|
2447
2448
|
return;
|
|
2448
2449
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2450
|
+
try {
|
|
2451
|
+
await store.recordBackup(agent.session.id, {
|
|
2452
|
+
callId: exec.callId,
|
|
2453
|
+
anchorSeq,
|
|
2454
|
+
path: capture.path
|
|
2455
|
+
}, capture.backup);
|
|
2456
|
+
} catch (error) {
|
|
2457
|
+
await discardCapture(capture);
|
|
2458
|
+
throw error;
|
|
2459
|
+
}
|
|
2454
2460
|
let tracked = trackedBySession.get(agent.session.id);
|
|
2455
2461
|
if (tracked === void 0) {
|
|
2456
2462
|
tracked = /* @__PURE__ */ new Set();
|
|
@@ -2530,7 +2536,7 @@ async function syncRestoreObservations(ctx, fs, agent, outcome) {
|
|
|
2530
2536
|
for (const path of outcome.restored) {
|
|
2531
2537
|
const target = await resolveObservationTarget(fs, path);
|
|
2532
2538
|
if (target === void 0) continue;
|
|
2533
|
-
const info = await fs.stat(target);
|
|
2539
|
+
const info = await fs.stat(target).catch(() => void 0);
|
|
2534
2540
|
if (info === void 0) continue;
|
|
2535
2541
|
ctx.emit("fs/observed", target, { kind: "present", version: info.version }, actor);
|
|
2536
2542
|
}
|
|
@@ -2688,7 +2694,7 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
|
2688
2694
|
return executeRewind(ctx, store, fs, invocation, target, mode, inflight);
|
|
2689
2695
|
}
|
|
2690
2696
|
var autoSweepChecked = false;
|
|
2691
|
-
async function maybeRunAutoCleanup(ctx, store, sessionId, dshHome, signal) {
|
|
2697
|
+
async function maybeRunAutoCleanup(ctx, store, sessionId, dshHome, sweepIntervalMs, signal) {
|
|
2692
2698
|
if (autoSweepChecked || signal?.aborted === true) return;
|
|
2693
2699
|
autoSweepChecked = true;
|
|
2694
2700
|
try {
|
|
@@ -2697,6 +2703,7 @@ async function maybeRunAutoCleanup(ctx, store, sessionId, dshHome, signal) {
|
|
|
2697
2703
|
readConfig: () => readCleanupPolicy(),
|
|
2698
2704
|
statePath: resolveCleanupStatePath(dshHome),
|
|
2699
2705
|
log: (msg) => ctx.logger.warn(msg),
|
|
2706
|
+
...sweepIntervalMs === void 0 ? {} : { intervalMs: sweepIntervalMs },
|
|
2700
2707
|
...signal === void 0 ? {} : { signal }
|
|
2701
2708
|
}, sessionId);
|
|
2702
2709
|
} catch (error) {
|
|
@@ -2834,7 +2841,12 @@ function apply(ctx, config) {
|
|
|
2834
2841
|
child.effect(() => child.settings.configure({ auto: false }, ctx.fiber), "dsh-rewind page policy");
|
|
2835
2842
|
});
|
|
2836
2843
|
const dshHome = config?.dshHome;
|
|
2837
|
-
const store = new SnapshotStore(config?.snapshotDir, {
|
|
2844
|
+
const store = new SnapshotStore(config?.snapshotDir, {
|
|
2845
|
+
dedup: config?.dedup,
|
|
2846
|
+
dshHome,
|
|
2847
|
+
maxFileBytes: config?.maxFileBytes,
|
|
2848
|
+
maxAnchorGroups: config?.maxAnchorGroups
|
|
2849
|
+
});
|
|
2838
2850
|
const configKey = cleanupConfigKey(rewindConfigKey(ctx));
|
|
2839
2851
|
if (config !== void 0 && configKey !== void 0) {
|
|
2840
2852
|
const writer = {
|
|
@@ -2930,11 +2942,12 @@ function apply(ctx, config) {
|
|
|
2930
2942
|
ctx.on("session/event", (session, event) => {
|
|
2931
2943
|
if (event.type !== "user/message") return;
|
|
2932
2944
|
if (isSubagentSession(session)) return;
|
|
2945
|
+
if (inflight.has(session.id)) return;
|
|
2933
2946
|
void (async () => {
|
|
2934
2947
|
try {
|
|
2935
2948
|
if (lifecycle.signal.aborted) return;
|
|
2936
2949
|
const sessionId = session.id;
|
|
2937
|
-
void maybeRunAutoCleanup(ctx, store, sessionId, dshHome, lifecycle.signal);
|
|
2950
|
+
void maybeRunAutoCleanup(ctx, store, sessionId, dshHome, config?.autoSweepIntervalMs, lifecycle.signal);
|
|
2938
2951
|
let tracked = trackedBySession.get(sessionId);
|
|
2939
2952
|
if (tracked === void 0) {
|
|
2940
2953
|
tracked = await store.trackedPaths(sessionId);
|
|
@@ -2952,6 +2965,9 @@ function apply(ctx, config) {
|
|
|
2952
2965
|
const fs = scope.fs;
|
|
2953
2966
|
const workingDirectory = scope.workingDirectory;
|
|
2954
2967
|
fsService = fs;
|
|
2968
|
+
scope.effect(() => () => {
|
|
2969
|
+
fsService = void 0;
|
|
2970
|
+
}, "dsh-rewind fs capture");
|
|
2955
2971
|
scope.on("tools/execute", async (exec, next) => {
|
|
2956
2972
|
try {
|
|
2957
2973
|
await captureBefore(fs, workingDirectory, store, exec, pending);
|
|
@@ -2964,7 +2980,7 @@ function apply(ctx, config) {
|
|
|
2964
2980
|
try {
|
|
2965
2981
|
const session = exec.agent?.session;
|
|
2966
2982
|
if (session !== void 0 && !isSubagentSession(session)) {
|
|
2967
|
-
void maybeRunAutoCleanup(ctx, store, session.id, dshHome, lifecycle.signal);
|
|
2983
|
+
void maybeRunAutoCleanup(ctx, store, session.id, dshHome, config?.autoSweepIntervalMs, lifecycle.signal);
|
|
2968
2984
|
}
|
|
2969
2985
|
await commitEntry(store, pending, anchorCache, trackedBySession, exec, result);
|
|
2970
2986
|
} catch (error) {
|
|
@@ -235,7 +235,19 @@ export declare function actionsContainerOf(row: HTMLElement | undefined): HTMLEl
|
|
|
235
235
|
* @param hiddenSeqs - anchor seqs withdrawn by the executed rewinds.
|
|
236
236
|
* @param hidden - elements this pass has hidden, so a re-created row is re-judged.
|
|
237
237
|
*/
|
|
238
|
-
export declare function hideWithdrawnSeats(chat: HiddenChat | undefined, seats: Iterable<HTMLElement>, hiddenSeqs: ReadonlySet<number>, hidden:
|
|
238
|
+
export declare function hideWithdrawnSeats(chat: HiddenChat | undefined, seats: Iterable<HTMLElement>, hiddenSeqs: ReadonlySet<number>, hidden: Set<HTMLElement>): void;
|
|
239
|
+
/**
|
|
240
|
+
* Undo every hide the given set recorded, so a live disable (or a session-slot
|
|
241
|
+
* collapse) cannot leave the transcript truncated.
|
|
242
|
+
*
|
|
243
|
+
* The hide is the plugin's OWN mutation of host rows ({@link hideWithdrawnSeats}),
|
|
244
|
+
* and nothing else would ever revert it: a disposed bridge runs no further
|
|
245
|
+
* refresh, so rows it hid would stay hidden with no affordance to bring them
|
|
246
|
+
* back.
|
|
247
|
+
*
|
|
248
|
+
* @param hidden - the elements this bridge currently hides; emptied.
|
|
249
|
+
*/
|
|
250
|
+
export declare function showHiddenSeats(hidden: Set<HTMLElement>): void;
|
|
239
251
|
/**
|
|
240
252
|
* Collect the portal targets of one session: user rows × snapshot nodes.
|
|
241
253
|
* Exported as a test seam — the DOM→targets pairing that drives the ↶ button
|
package/lib/types/index.d.ts
CHANGED
|
@@ -37,12 +37,18 @@ export declare const inject: string[];
|
|
|
37
37
|
export declare const ENGINE_COMMAND = "rewind-plugin";
|
|
38
38
|
/** Plugin config. */
|
|
39
39
|
export interface RewindConfig {
|
|
40
|
-
/** Checkpoint store root (exact path; beats
|
|
40
|
+
/** Checkpoint store root (exact path; beats the harness-home default). */
|
|
41
41
|
readonly snapshotDir?: string;
|
|
42
42
|
/** Harness home override (`config.dshHome` > `$DSH_HOME` > `~/.dsh`); feeds the default snapshot/cleanup paths. */
|
|
43
43
|
readonly dshHome?: string;
|
|
44
44
|
/** In-place content dedup (identical before-content → link). Default `true`. */
|
|
45
45
|
readonly dedup?: boolean;
|
|
46
|
+
/** Per-file backup cap in bytes; `0` disables the cap. Default {@link DEFAULT_MAX_FILE_BYTES}. */
|
|
47
|
+
readonly maxFileBytes?: number;
|
|
48
|
+
/** Newest anchor groups (user messages) kept per session. Default {@link MAX_ANCHOR_GROUPS}. */
|
|
49
|
+
readonly maxAnchorGroups?: number;
|
|
50
|
+
/** Automatic snapshot-sweep cadence in ms. Default {@link AUTO_SWEEP_INTERVAL_MS}. */
|
|
51
|
+
readonly autoSweepIntervalMs?: number;
|
|
46
52
|
}
|
|
47
53
|
/**
|
|
48
54
|
* The plugin's full configuration: the ordinary store options plus the
|
|
@@ -62,12 +68,18 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
62
68
|
snapshotDir: z<string, string, "plain">;
|
|
63
69
|
dshHome: z<string, string, "plain">;
|
|
64
70
|
dedup: z<boolean, boolean, "defined">;
|
|
71
|
+
maxFileBytes: z<number, number, "defined">;
|
|
72
|
+
maxAnchorGroups: z<number, number, "defined">;
|
|
73
|
+
autoSweepIntervalMs: z<number, number, "defined">;
|
|
65
74
|
autoCleanupEnabled: z<boolean, boolean, "volatile-defined">;
|
|
66
75
|
autoCleanupMaxAgeDays: z<number, number, "volatile-defined">;
|
|
67
76
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
68
77
|
snapshotDir: z<string, string, "plain">;
|
|
69
78
|
dshHome: z<string, string, "plain">;
|
|
70
79
|
dedup: z<boolean, boolean, "defined">;
|
|
80
|
+
maxFileBytes: z<number, number, "defined">;
|
|
81
|
+
maxAnchorGroups: z<number, number, "defined">;
|
|
82
|
+
autoSweepIntervalMs: z<number, number, "defined">;
|
|
71
83
|
autoCleanupEnabled: z<boolean, boolean, "volatile-defined">;
|
|
72
84
|
autoCleanupMaxAgeDays: z<number, number, "volatile-defined">;
|
|
73
85
|
}>>, "plain">;
|
|
@@ -74,7 +74,7 @@ export interface CleanupConfigStore {
|
|
|
74
74
|
* field is cleared rather than pinned.
|
|
75
75
|
*/
|
|
76
76
|
export declare function volatileCleanupStore(settings: CleanupSettings, writer: CleanupConfigWriter): CleanupConfigStore;
|
|
77
|
-
/**
|
|
77
|
+
/** Default auto-sweep cadence (once a day); `autoSweepIntervalMs` overrides it. */
|
|
78
78
|
export declare const AUTO_SWEEP_INTERVAL_MS: number;
|
|
79
79
|
/** The state file that records the last automatic-sweep wall-clock time. */
|
|
80
80
|
export declare const STATE_FILENAME = "snapshot-cleanup-last-sweep.json";
|
|
@@ -123,6 +123,8 @@ export declare function runAutoCleanupCheck(deps: {
|
|
|
123
123
|
}>;
|
|
124
124
|
statePath: string;
|
|
125
125
|
log: (msg: string) => void;
|
|
126
|
+
/** Sweep cadence in ms; defaults to {@link AUTO_SWEEP_INTERVAL_MS}. */
|
|
127
|
+
intervalMs?: number;
|
|
126
128
|
/** Optional mount lifecycle signal; absent means "never cancelled". */
|
|
127
129
|
signal?: AbortSignal;
|
|
128
130
|
}, sessionId: string | undefined): Promise<void>;
|
|
@@ -165,8 +167,8 @@ export declare function parseCleanupCommand(rawInput: string): CleanupCommand |
|
|
|
165
167
|
error: string;
|
|
166
168
|
};
|
|
167
169
|
/**
|
|
168
|
-
* The
|
|
169
|
-
* process), so the first call always sweeps; after that a call within
|
|
170
|
-
* a no-op
|
|
170
|
+
* The auto-sweep throttle: `lastAtMs` of `0` means "never ran" (a fresh
|
|
171
|
+
* process), so the first call always sweeps; after that a call within
|
|
172
|
+
* `intervalMs` is a no-op. `autoSweepIntervalMs` sets the cadence.
|
|
171
173
|
*/
|
|
172
|
-
export declare function shouldRunAutoSweep(lastAtMs: number, nowMs: number): boolean;
|
|
174
|
+
export declare function shouldRunAutoSweep(lastAtMs: number, nowMs: number, intervalMs?: number): boolean;
|
package/lib/types/snapshot.d.ts
CHANGED
|
@@ -36,15 +36,12 @@
|
|
|
36
36
|
*
|
|
37
37
|
* @module dsh-rewind/snapshot
|
|
38
38
|
*/
|
|
39
|
-
/** Environment variable overriding the store root (tests, exotic homes). */
|
|
40
|
-
export declare const SNAPSHOT_ROOT_ENV = "DSH_REWIND_SNAPSHOT_DIR";
|
|
41
|
-
/** Env override for the per-file backup cap (`0` = unlimited); tests set it too. */
|
|
42
|
-
export declare const MAX_FILE_BYTES_ENV = "DSH_REWIND_MAX_FILE_BYTES";
|
|
43
39
|
/**
|
|
44
40
|
* A regular file larger than this is NOT backed up. This plugin is a
|
|
45
41
|
* lightweight, conversation-scoped undo, not a backup system: copying a very
|
|
46
42
|
* large file on every edit is what grows the store without bound and saturates
|
|
47
|
-
* IO (issue #39). The cap is a declared limitation, not a delta scheme
|
|
43
|
+
* IO (issue #39). The cap is a declared limitation, not a delta scheme, and is
|
|
44
|
+
* exposed as the `maxFileBytes` config field (`0` disables the cap).
|
|
48
45
|
*/
|
|
49
46
|
export declare const DEFAULT_MAX_FILE_BYTES: number;
|
|
50
47
|
/** Number of newest anchor groups (user messages) kept per session. */
|
|
@@ -453,6 +450,11 @@ export declare class SnapshotStore {
|
|
|
453
450
|
* and the restore planner refuses a pre-cap record over an over-cap file.
|
|
454
451
|
*/
|
|
455
452
|
private readonly maxFileBytes;
|
|
453
|
+
/**
|
|
454
|
+
* Newest anchor groups (user messages) kept per session; `prune` evicts past
|
|
455
|
+
* this bound. A `Config` field (`maxAnchorGroups`).
|
|
456
|
+
*/
|
|
457
|
+
private readonly maxAnchorGroups;
|
|
456
458
|
/** Resolved checkpoint store root (absolute); see the constructor's fallback. */
|
|
457
459
|
readonly root: string;
|
|
458
460
|
/**
|
|
@@ -481,6 +483,8 @@ export declare class SnapshotStore {
|
|
|
481
483
|
readonly dshHome?: string;
|
|
482
484
|
/** Per-file backup cap in bytes; `<= 0` disables it. */
|
|
483
485
|
readonly maxFileBytes?: number;
|
|
486
|
+
/** Newest anchor groups kept per session; see {@link MAX_ANCHOR_GROUPS}. */
|
|
487
|
+
readonly maxAnchorGroups?: number;
|
|
484
488
|
});
|
|
485
489
|
/** Absolute path of one session's snapshot directory (id sanitized). */
|
|
486
490
|
sessionDir(sessionId: string): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rewind-plugin",
|
|
3
|
-
"version": "0.16.0-
|
|
3
|
+
"version": "0.16.0-beta.1",
|
|
4
4
|
"description": "In-window conversation rewind with workspace file restore · 同窗口内对话回退并可还原工作区文件",
|
|
5
5
|
"icon": "./assets/icon.svg",
|
|
6
6
|
"keywords": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": "^22.19.0 || >=24.0.0",
|
|
23
|
-
"dsh": ">=0.2.1-
|
|
23
|
+
"dsh": ">=0.2.1-rc.1 <0.3.0-0"
|
|
24
24
|
},
|
|
25
25
|
"type": "module",
|
|
26
26
|
"main": "lib/index.js",
|
|
@@ -73,23 +73,23 @@
|
|
|
73
73
|
"access": "public"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
77
|
-
"@deepseek-ai/dsh-client-locale": "^0.2.1-
|
|
78
|
-
"@deepseek-ai/dsh-client-ui-commands": "^0.2.1-
|
|
79
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.1-
|
|
80
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.2.1-
|
|
81
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.2.1-
|
|
82
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.2.1-
|
|
83
|
-
"@deepseek-ai/dsh-commands": "^0.2.1-
|
|
84
|
-
"@deepseek-ai/dsh-fs": "^0.2.1-
|
|
85
|
-
"@deepseek-ai/dsh-home-paths": "^0.2.1-
|
|
86
|
-
"@deepseek-ai/dsh-llm": "^0.2.1-
|
|
87
|
-
"@deepseek-ai/dsh-sandbox": "^0.2.1-
|
|
88
|
-
"@deepseek-ai/dsh-session": "^0.2.1-
|
|
89
|
-
"@deepseek-ai/dsh-settings": "^0.2.1-
|
|
90
|
-
"@deepseek-ai/dsh-tools": "^0.2.1-
|
|
91
|
-
"@deepseek-ai/dsh-working-directory": "^0.2.1-
|
|
92
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
76
|
+
"@deepseek-ai/cordis": "^4.0.5",
|
|
77
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.1-rc.1",
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-commands": "^0.2.1-rc.1",
|
|
79
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.1-rc.1",
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.2.1-rc.1",
|
|
81
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.1-rc.1",
|
|
82
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.1-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-commands": "^0.2.1-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-fs": "^0.2.1-rc.1",
|
|
85
|
+
"@deepseek-ai/dsh-home-paths": "^0.2.1-rc.1",
|
|
86
|
+
"@deepseek-ai/dsh-llm": "^0.2.1-rc.1",
|
|
87
|
+
"@deepseek-ai/dsh-sandbox": "^0.2.1-rc.1",
|
|
88
|
+
"@deepseek-ai/dsh-session": "^0.2.1-rc.1",
|
|
89
|
+
"@deepseek-ai/dsh-settings": "^0.2.1-rc.1",
|
|
90
|
+
"@deepseek-ai/dsh-tools": "^0.2.1-rc.1",
|
|
91
|
+
"@deepseek-ai/dsh-working-directory": "^0.2.1-rc.1",
|
|
92
|
+
"@deepseek-ai/schemastery": "^3.18.5"
|
|
93
93
|
},
|
|
94
94
|
"peerDependenciesMeta": {
|
|
95
95
|
"@deepseek-ai/cordis": {
|
|
@@ -145,45 +145,45 @@
|
|
|
145
145
|
}
|
|
146
146
|
},
|
|
147
147
|
"devDependencies": {
|
|
148
|
-
"@deepseek-ai/cordis": "4.0.
|
|
149
|
-
"@deepseek-ai/cosmokit": "1.8.
|
|
150
|
-
"@deepseek-ai/dsh-agent": "0.2.1-
|
|
151
|
-
"@deepseek-ai/dsh-api-session-controller": "0.2.1-
|
|
152
|
-
"@deepseek-ai/dsh-attachment": "0.2.1-
|
|
153
|
-
"@deepseek-ai/dsh-client-connection": "0.2.1-
|
|
154
|
-
"@deepseek-ai/dsh-client-locale": "0.2.1-
|
|
155
|
-
"@deepseek-ai/dsh-client-store": "0.2.1-
|
|
156
|
-
"@deepseek-ai/dsh-client-ui-commands": "0.2.1-
|
|
157
|
-
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-
|
|
158
|
-
"@deepseek-ai/dsh-client-ui-input-trigger": "0.2.1-
|
|
159
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-
|
|
160
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-
|
|
161
|
-
"@deepseek-ai/dsh-client-ui-session": "0.2.1-
|
|
162
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.2.1-
|
|
163
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-
|
|
164
|
-
"@deepseek-ai/dsh-command-compact": "0.2.1-
|
|
165
|
-
"@deepseek-ai/dsh-commands": "0.2.1-
|
|
166
|
-
"@deepseek-ai/dsh-compaction": "0.2.1-
|
|
167
|
-
"@deepseek-ai/dsh-compaction-basic": "0.2.1-
|
|
168
|
-
"@deepseek-ai/dsh-compaction-image-offload": "0.2.1-
|
|
169
|
-
"@deepseek-ai/dsh-fs": "0.2.1-
|
|
170
|
-
"@deepseek-ai/dsh-goal": "0.2.1-
|
|
171
|
-
"@deepseek-ai/dsh-home-paths": "0.2.1-
|
|
172
|
-
"@deepseek-ai/dsh-llm": "0.2.1-
|
|
173
|
-
"@deepseek-ai/dsh-plan-mode": "0.2.1-
|
|
174
|
-
"@deepseek-ai/dsh-sandbox": "0.2.1-
|
|
175
|
-
"@deepseek-ai/dsh-scope": "0.2.1-
|
|
176
|
-
"@deepseek-ai/dsh-session": "0.2.1-
|
|
177
|
-
"@deepseek-ai/dsh-session-projection": "0.2.1-
|
|
178
|
-
"@deepseek-ai/dsh-session-stats": "0.2.1-
|
|
179
|
-
"@deepseek-ai/dsh-session-title": "0.2.1-
|
|
180
|
-
"@deepseek-ai/dsh-settings": "0.2.1-
|
|
181
|
-
"@deepseek-ai/dsh-token-meter": "0.2.1-
|
|
182
|
-
"@deepseek-ai/dsh-tools": "0.2.1-
|
|
183
|
-
"@deepseek-ai/dsh-typert-protocol": "0.2.1-
|
|
184
|
-
"@deepseek-ai/dsh-user-questions": "0.2.1-
|
|
185
|
-
"@deepseek-ai/dsh-working-directory": "0.2.1-
|
|
186
|
-
"@deepseek-ai/schemastery": "3.18.
|
|
148
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
149
|
+
"@deepseek-ai/cosmokit": "1.8.6",
|
|
150
|
+
"@deepseek-ai/dsh-agent": "0.2.1-rc.1",
|
|
151
|
+
"@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.1",
|
|
152
|
+
"@deepseek-ai/dsh-attachment": "0.2.1-rc.1",
|
|
153
|
+
"@deepseek-ai/dsh-client-connection": "0.2.1-rc.1",
|
|
154
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.1",
|
|
155
|
+
"@deepseek-ai/dsh-client-store": "0.2.1-rc.1",
|
|
156
|
+
"@deepseek-ai/dsh-client-ui-commands": "0.2.1-rc.1",
|
|
157
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.1",
|
|
158
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": "0.2.1-rc.1",
|
|
159
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-rc.1",
|
|
160
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.1",
|
|
161
|
+
"@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.1",
|
|
162
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.1",
|
|
163
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.1",
|
|
164
|
+
"@deepseek-ai/dsh-command-compact": "0.2.1-rc.1",
|
|
165
|
+
"@deepseek-ai/dsh-commands": "0.2.1-rc.1",
|
|
166
|
+
"@deepseek-ai/dsh-compaction": "0.2.1-rc.1",
|
|
167
|
+
"@deepseek-ai/dsh-compaction-basic": "0.2.1-rc.1",
|
|
168
|
+
"@deepseek-ai/dsh-compaction-image-offload": "0.2.1-rc.1",
|
|
169
|
+
"@deepseek-ai/dsh-fs": "0.2.1-rc.1",
|
|
170
|
+
"@deepseek-ai/dsh-goal": "0.2.1-rc.1",
|
|
171
|
+
"@deepseek-ai/dsh-home-paths": "0.2.1-rc.1",
|
|
172
|
+
"@deepseek-ai/dsh-llm": "0.2.1-rc.1",
|
|
173
|
+
"@deepseek-ai/dsh-plan-mode": "0.2.1-rc.1",
|
|
174
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-rc.1",
|
|
175
|
+
"@deepseek-ai/dsh-scope": "0.2.1-rc.1",
|
|
176
|
+
"@deepseek-ai/dsh-session": "0.2.1-rc.1",
|
|
177
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-rc.1",
|
|
178
|
+
"@deepseek-ai/dsh-session-stats": "0.2.1-rc.1",
|
|
179
|
+
"@deepseek-ai/dsh-session-title": "0.2.1-rc.1",
|
|
180
|
+
"@deepseek-ai/dsh-settings": "0.2.1-rc.1",
|
|
181
|
+
"@deepseek-ai/dsh-token-meter": "0.2.1-rc.1",
|
|
182
|
+
"@deepseek-ai/dsh-tools": "0.2.1-rc.1",
|
|
183
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.1",
|
|
184
|
+
"@deepseek-ai/dsh-user-questions": "0.2.1-rc.1",
|
|
185
|
+
"@deepseek-ai/dsh-working-directory": "0.2.1-rc.1",
|
|
186
|
+
"@deepseek-ai/schemastery": "3.18.5",
|
|
187
187
|
"@types/node": "^24.0.0",
|
|
188
188
|
"@types/react": "^18.3.31",
|
|
189
189
|
"@types/react-dom": "^18.3.7",
|