dsh-rewind-plugin 0.4.1 → 0.5.0

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.
@@ -18,11 +18,12 @@ export declare const PREVIEW_CHARS = 80;
18
18
  /**
19
19
  * Default cap on how many user messages the rewind picker lists (newest kept).
20
20
  *
21
- * A fixed 10 made long sessions look "incomplete" (only the newest 10 shown).
22
- * 50 keeps the picker scrollable/searchable via the popupSelect shell while
23
- * covering far longer sessions; callers can still pass an explicit `limit`.
21
+ * Matches the snapshot store's MAX_ANCHOR_GROUPS (100), so the picker shows
22
+ * every anchor group that can still restore file backups; 100 stays
23
+ * scrollable/searchable via the popupSelect shell, and callers can still
24
+ * pass an explicit `limit`.
24
25
  */
25
- export declare const DEFAULT_CANDIDATE_LIMIT = 50;
26
+ export declare const DEFAULT_CANDIDATE_LIMIT = 100;
26
27
  /** One selectable rewind target. */
27
28
  export interface RewindCandidate {
28
29
  /** Absolute log seq of the `user/message` event. */
@@ -37,6 +37,6 @@ export declare const inject: string[];
37
37
  export declare function apply(ctx: ClientContext): void;
38
38
  /**
39
39
  * Public contract — rewind visibility. Stable, semver-protected; the rest of
40
- * this module is internal. See `docs/client-contract.md`.
40
+ * this module is internal. See `docs/contract/client-contract.md`.
41
41
  */
42
42
  export { hiddenSeqsOf, targetSeqOfArgs, type HiddenChat } from './hidden.ts';
@@ -26,13 +26,15 @@
26
26
  */
27
27
  import type { Context } from '@deepseek-ai/cordis';
28
28
  export { SnapshotStore } from './snapshot.ts';
29
- export type { CheckpointEntry, FileImpact, RestoreOutcome } from './snapshot.ts';
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
32
  /** Plugin config: optional override of the checkpoint store root. */
33
33
  export interface RewindConfig {
34
34
  /** Checkpoint store root (defaults to `~/.dsh/rewind-snapshots`). */
35
35
  readonly snapshotDir?: string;
36
+ /** In-place content dedup (identical before-content → link). Default `true`. */
37
+ readonly dedup?: boolean;
36
38
  }
37
39
  /**
38
40
  * Register the `/rewind` command and the checkpoint pipeline (before-capture
@@ -49,6 +49,23 @@ export declare const en: {
49
49
  noUserMessages: string;
50
50
  chooseMode: string;
51
51
  'command.description': string;
52
+ 'cleanup.description': string;
53
+ 'cleanup.inputHint': string;
54
+ 'cleanup.status': string;
55
+ 'cleanup.enabled': string;
56
+ 'cleanup.disabled': string;
57
+ 'cleanup.present': string;
58
+ 'cleanup.absent': string;
59
+ 'cleanup.onOk': string;
60
+ 'cleanup.offOk': string;
61
+ 'cleanup.maxAgeOk': string;
62
+ 'cleanup.cfgInvalid': string;
63
+ 'cleanup.saveFailed': string;
64
+ 'cleanup.runDry': string;
65
+ 'cleanup.runApply': string;
66
+ 'cleanup.runFailed': string;
67
+ 'cleanup.skipped': string;
68
+ 'cleanup.usage': string;
52
69
  };
53
70
  /** The host rewind dictionary key union. */
54
71
  export type HostKey = keyof typeof en;
@@ -80,10 +80,11 @@ export interface RewindPlan {
80
80
  export declare const CANDIDATE_PREVIEW_CHARS = 80;
81
81
  /**
82
82
  * Default cap on how many user messages a candidate listing returns (newest
83
- * kept). Raised from 10 so long sessions don't look incomplete; callers can
83
+ * kept). Matches the snapshot store's MAX_ANCHOR_GROUPS (100), so every
84
+ * anchor group that still has restorable file backups is listed; callers can
84
85
  * still pass an explicit `limit`.
85
86
  */
86
- export declare const DEFAULT_CANDIDATE_LIMIT = 50;
87
+ export declare const DEFAULT_CANDIDATE_LIMIT = 100;
87
88
  /**
88
89
  * Turn number for the rewind marker.
89
90
  *
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Snapshot cleanup policy: the persisted config file, its validation, the
3
+ * `/snapshot-auto-cleanup` command's argument grammar, and the auto-sweep
4
+ * throttle. Kept free of host wiring so the policy and the parser are
5
+ * unit-testable in isolation; `src/index.ts` is the only consumer.
6
+ *
7
+ * Semantics (the "cleanup" vocabulary deliberately avoids "retention"):
8
+ * - `enabled` toggles the AUTOMATIC (24h) sweep. `false` (the default) keeps
9
+ * every snapshot — the pre-feature behavior — and never writes a file.
10
+ * - `maxAgeDays` is the only "keep" knob: a finished session dir whose newest
11
+ * member stamp is older than this many days of idle is removed by a sweep.
12
+ * `0`/negative/non-integer are rejected, so a broken file can never steer
13
+ * the sweep into deleting everything.
14
+ * - The config file is created ONLY by an explicit `/snapshot-auto-cleanup`
15
+ * write. An absent file reads as the safe default (off); an unreadable or
16
+ * invalid file reports `ok:false` so a sweep fail-closes (deletes nothing)
17
+ * instead of guessing.
18
+ *
19
+ * @module dsh-rewind/snapshot-cleanup
20
+ */
21
+ /** The cleanup policy, as persisted under `~/.dsh/snapshot-cleanup.json`. */
22
+ export interface CleanupConfig {
23
+ readonly enabled: boolean;
24
+ readonly maxAgeDays: number;
25
+ }
26
+ export declare const CLEANUP_CONFIG_FILENAME = "snapshot-cleanup.json";
27
+ /** Environment variable overriding the config file path. */
28
+ export declare const CLEANUP_CONFIG_ENV = "DSH_SNAPSHOT_CLEANUP_CONFIG";
29
+ /** The default keep threshold: finished sessions idle > 30 days are pruned. */
30
+ export declare const DEFAULT_MAX_AGE_DAYS = 30;
31
+ /** The safe default policy (off) — a missing/corrupt file behaves like this. */
32
+ export declare const DEFAULT_CLEANUP_CONFIG: CleanupConfig;
33
+ /** Auto-sweep cadence (the user's hardcoded 24h rhythm — not user-set). */
34
+ export declare const AUTO_SWEEP_INTERVAL_MS: number;
35
+ /** Resolve the config file path: env override, else `~/.dsh/snapshot-cleanup.json`. */
36
+ export declare function resolveCleanupConfigPath(): string;
37
+ /** The state file that records the last automatic-sweep wall-clock time. */
38
+ export declare const STATE_FILENAME = "snapshot-cleanup-last-sweep.json";
39
+ /**
40
+ * Resolve the last-sweep state path. It sits beside the config file so the
41
+ * 24h cadence SURVIVES a host restart (a real deployment is rarely up 24/7,
42
+ * so an in-memory timestamp would reset on every boot and re-sweep too often).
43
+ */
44
+ export declare function resolveCleanupStatePath(): string;
45
+ /**
46
+ * Read the persisted last-sweep time (epoch ms). A missing or corrupt file
47
+ * reads as `0` ("never swept"), so the next activity runs the sweep — which is
48
+ * safe because the sweep is idempotent and never deletes the active session.
49
+ */
50
+ export declare function loadLastSweepAt(path: string): Promise<number>;
51
+ /** Persist the last-sweep time, atomically (temp + rename). */
52
+ export declare function saveLastSweepAt(path: string, ms: number): Promise<void>;
53
+ /** The slice of a store `runAutoCleanupCheck` needs (pruneStale). */
54
+ export interface AutoCleanupPruner {
55
+ pruneStale(opts: {
56
+ keepActiveId?: string;
57
+ maxAgeDays: number;
58
+ dryRun?: boolean;
59
+ }): Promise<unknown>;
60
+ }
61
+ /**
62
+ * The one-shot auto-cleanup check. Loads the policy + persisted last-sweep time
63
+ * and, only when enabled AND >=24h since the last sweep, runs the sweep and
64
+ * re-anchors the window on disk. Dependencies (store, paths, logger) are
65
+ * injected so the composition is unit-testable without a host. Never rejects:
66
+ * a corrupt config fail-closes (no deletion) and logs, a prune failure logs.
67
+ *
68
+ * `sessionId` is the active session directory that must never be pruned.
69
+ */
70
+ export declare function runAutoCleanupCheck(deps: {
71
+ pruner: AutoCleanupPruner;
72
+ configPath: string;
73
+ statePath: string;
74
+ log: (msg: string) => void;
75
+ }, sessionId: string | undefined): Promise<void>;
76
+ /**
77
+ * Validate one parsed JSON value into a {@link CleanupConfig}. Tolerates
78
+ * unknown extra keys; rejects a present-but-wrong-typed known key. Missing
79
+ * known keys fall back to the safe default.
80
+ */
81
+ export declare function parseCleanupConfig(raw: unknown): {
82
+ ok: true;
83
+ config: CleanupConfig;
84
+ } | {
85
+ ok: false;
86
+ error: string;
87
+ };
88
+ /**
89
+ * Load and validate the config file. A missing file is NOT an error: it reads
90
+ * as the safe default (off, `fromFile:false`). An unreadable, non-JSON, or
91
+ * structurally-invalid file is `ok:false` so a sweep fail-closes.
92
+ */
93
+ export declare function loadCleanupConfig(path: string): Promise<{
94
+ ok: true;
95
+ config: CleanupConfig;
96
+ fromFile: boolean;
97
+ } | {
98
+ ok: false;
99
+ error: string;
100
+ }>;
101
+ /**
102
+ * Persist a validated {@link CleanupConfig}, atomically (temp + rename). Any
103
+ * invalid value throws before the file is touched, so the command can never
104
+ * write a broken policy.
105
+ */
106
+ export declare function saveCleanupConfig(path: string, config: CleanupConfig): Promise<void>;
107
+ /** The `/snapshot-auto-cleanup` sub-command the parser can resolve to. */
108
+ export type CleanupCommandAction = 'status' | 'on' | 'off' | 'max-age' | 'run' | 'run-apply';
109
+ /**
110
+ * Parse the free-form text after `/snapshot-auto-cleanup`. Pure so it is
111
+ * unit-testable; `src/index.ts` maps the resolved action onto the store / the
112
+ * config file. `max-age` returns the validated positive day count.
113
+ */
114
+ export declare function parseCleanupCommand(rawInput: string): {
115
+ action: CleanupCommandAction;
116
+ value?: number;
117
+ } | {
118
+ error: string;
119
+ };
120
+ /**
121
+ * The 24h auto-sweep throttle. `lastAtMs` of `0` means "never ran" (a fresh
122
+ * process), so the first call always sweeps; after that a call within 24h is
123
+ * a no-op, matching the "every machine at most once per day" model.
124
+ */
125
+ export declare function shouldRunAutoSweep(lastAtMs: number, nowMs: number): boolean;