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 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/...` when `DSH_HOME` is unset); the `snapshotDir`
95
- config, then `DSH_REWIND_SNAPSHOT_DIR` env, override it. Deleting it only
96
- removes file backups and the store rebuilds from scratch.
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
- - **Never written through a link or a moved directory**: two checks guard a
102
- tracked path before any write or unlink. (1) A path whose last component is a
103
- symlink or a hard link (`lstat().nlink > 1`) is skipped and reported, never
104
- restored — a symlink would redirect the write outside the checkpoint, and a
105
- hard link would clobber every other name of the same inode (e.g.
106
- pnpm-installed files). (2) Every record this build writes — entry, link and
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**: after a restart, `reconcileRestores` re-derives from the
149
- real disk which paths already match the goal (restored) and which are
150
- pending; `continueRestore` finishes the interrupted op, `rollbackRestore`
151
- undoes it to the exact pre-restore state. A journal whose goal is already
152
- reached auto-heals to its terminal state.
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 WeakSet());
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-alpha.1" : "dev";
1371
+ var PLUGIN_VERSION = true ? "0.16.0-beta.1" : "dev";
1363
1372
  var PLUGIN_PACKAGE = "dsh-rewind-plugin";
1364
- var BUILD_HASH = true ? "f36f8be8" : "dev";
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 ?? maxFileBytesFromEnv() ?? DEFAULT_MAX_FILE_BYTES;
704
- this.root = root ?? process.env[SNAPSHOT_ROOT_ENV] ?? join(resolveDshHome(opts?.dshHome), SNAPSHOT_DIR_NAME);
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 = MAX_ANCHOR_GROUPS, opts) {
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 >= AUTO_SWEEP_INTERVAL_MS;
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
- await store.recordBackup(agent.session.id, {
2450
- callId: exec.callId,
2451
- anchorSeq,
2452
- path: capture.path
2453
- }, capture.backup);
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, { dedup: config?.dedup, dshHome });
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: WeakSet<HTMLElement>): void;
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
@@ -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 `DSH_REWIND_SNAPSHOT_DIR` and the harness-home default). */
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
- /** Auto-sweep cadence (the user's hardcoded 24h rhythm — not user-set). */
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 24h auto-sweep throttle. `lastAtMs` of `0` means "never ran" (a fresh
169
- * process), so the first call always sweeps; after that a call within 24h is
170
- * a no-op, matching the "every machine at most once per day" model.
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;
@@ -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-alpha.1",
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-alpha.2"
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.1",
77
- "@deepseek-ai/dsh-client-locale": "^0.2.1-alpha.2",
78
- "@deepseek-ai/dsh-client-ui-commands": "^0.2.1-alpha.2",
79
- "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.1-alpha.2",
80
- "@deepseek-ai/dsh-client-ui-primitives": "^0.2.1-alpha.2",
81
- "@deepseek-ai/dsh-client-ui-settings": "^0.2.1-alpha.2",
82
- "@deepseek-ai/dsh-client-ui-slots": "^0.2.1-alpha.2",
83
- "@deepseek-ai/dsh-commands": "^0.2.1-alpha.2",
84
- "@deepseek-ai/dsh-fs": "^0.2.1-alpha.2",
85
- "@deepseek-ai/dsh-home-paths": "^0.2.1-alpha.2",
86
- "@deepseek-ai/dsh-llm": "^0.2.1-alpha.2",
87
- "@deepseek-ai/dsh-sandbox": "^0.2.1-alpha.2",
88
- "@deepseek-ai/dsh-session": "^0.2.1-alpha.2",
89
- "@deepseek-ai/dsh-settings": "^0.2.1-alpha.2",
90
- "@deepseek-ai/dsh-tools": "^0.2.1-alpha.2",
91
- "@deepseek-ai/dsh-working-directory": "^0.2.1-alpha.2",
92
- "@deepseek-ai/schemastery": "^3.18.4"
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.3",
149
- "@deepseek-ai/cosmokit": "1.8.4",
150
- "@deepseek-ai/dsh-agent": "0.2.1-alpha.2",
151
- "@deepseek-ai/dsh-api-session-controller": "0.2.1-alpha.2",
152
- "@deepseek-ai/dsh-attachment": "0.2.1-alpha.2",
153
- "@deepseek-ai/dsh-client-connection": "0.2.1-alpha.2",
154
- "@deepseek-ai/dsh-client-locale": "0.2.1-alpha.2",
155
- "@deepseek-ai/dsh-client-store": "0.2.1-alpha.2",
156
- "@deepseek-ai/dsh-client-ui-commands": "0.2.1-alpha.2",
157
- "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-alpha.2",
158
- "@deepseek-ai/dsh-client-ui-input-trigger": "0.2.1-alpha.2",
159
- "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-alpha.2",
160
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-alpha.2",
161
- "@deepseek-ai/dsh-client-ui-session": "0.2.1-alpha.2",
162
- "@deepseek-ai/dsh-client-ui-settings": "0.2.1-alpha.2",
163
- "@deepseek-ai/dsh-client-ui-slots": "0.2.1-alpha.2",
164
- "@deepseek-ai/dsh-command-compact": "0.2.1-alpha.2",
165
- "@deepseek-ai/dsh-commands": "0.2.1-alpha.2",
166
- "@deepseek-ai/dsh-compaction": "0.2.1-alpha.2",
167
- "@deepseek-ai/dsh-compaction-basic": "0.2.1-alpha.2",
168
- "@deepseek-ai/dsh-compaction-image-offload": "0.2.1-alpha.2",
169
- "@deepseek-ai/dsh-fs": "0.2.1-alpha.2",
170
- "@deepseek-ai/dsh-goal": "0.2.1-alpha.2",
171
- "@deepseek-ai/dsh-home-paths": "0.2.1-alpha.2",
172
- "@deepseek-ai/dsh-llm": "0.2.1-alpha.2",
173
- "@deepseek-ai/dsh-plan-mode": "0.2.1-alpha.2",
174
- "@deepseek-ai/dsh-sandbox": "0.2.1-alpha.2",
175
- "@deepseek-ai/dsh-scope": "0.2.1-alpha.2",
176
- "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
177
- "@deepseek-ai/dsh-session-projection": "0.2.1-alpha.2",
178
- "@deepseek-ai/dsh-session-stats": "0.2.1-alpha.2",
179
- "@deepseek-ai/dsh-session-title": "0.2.1-alpha.2",
180
- "@deepseek-ai/dsh-settings": "0.2.1-alpha.2",
181
- "@deepseek-ai/dsh-token-meter": "0.2.1-alpha.2",
182
- "@deepseek-ai/dsh-tools": "0.2.1-alpha.2",
183
- "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2",
184
- "@deepseek-ai/dsh-user-questions": "0.2.1-alpha.2",
185
- "@deepseek-ai/dsh-working-directory": "0.2.1-alpha.2",
186
- "@deepseek-ai/schemastery": "3.18.4",
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",