dsh-rewind-plugin 0.5.0 → 0.6.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.
@@ -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: optional override of the checkpoint store root. */
32
+ /** Plugin config. */
33
33
  export interface RewindConfig {
34
- /** Checkpoint store root (defaults to `~/.dsh/rewind-snapshots`). */
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 override of the checkpoint store root.
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;
@@ -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. */
@@ -32,8 +32,13 @@ export declare const DEFAULT_MAX_AGE_DAYS = 30;
32
32
  export declare const DEFAULT_CLEANUP_CONFIG: CleanupConfig;
33
33
  /** Auto-sweep cadence (the user's hardcoded 24h rhythm — not user-set). */
34
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;
35
+ /**
36
+ * Resolve the config file path (highest first): the `DSH_SNAPSHOT_CLEANUP_CONFIG`
37
+ * env override, else `<harness home>/snapshot-cleanup.json` — derived from
38
+ * `dshHome` (config.dshHome > `$DSH_HOME` > `~/.dsh`) so the plugin follows the
39
+ * harness home instead of hardcoding `~/.dsh`.
40
+ */
41
+ export declare function resolveCleanupConfigPath(dshHome?: string): string;
37
42
  /** The state file that records the last automatic-sweep wall-clock time. */
38
43
  export declare const STATE_FILENAME = "snapshot-cleanup-last-sweep.json";
39
44
  /**
@@ -41,7 +46,7 @@ export declare const STATE_FILENAME = "snapshot-cleanup-last-sweep.json";
41
46
  * 24h cadence SURVIVES a host restart (a real deployment is rarely up 24/7,
42
47
  * so an in-memory timestamp would reset on every boot and re-sweep too often).
43
48
  */
44
- export declare function resolveCleanupStatePath(): string;
49
+ export declare function resolveCleanupStatePath(dshHome?: string): string;
45
50
  /**
46
51
  * Read the persisted last-sweep time (epoch ms). A missing or corrupt file
47
52
  * reads as `0` ("never swept"), so the next activity runs the sweep — which is
@@ -105,16 +110,31 @@ export declare function loadCleanupConfig(path: string): Promise<{
105
110
  */
106
111
  export declare function saveCleanupConfig(path: string, config: CleanupConfig): Promise<void>;
107
112
  /** The `/snapshot-auto-cleanup` sub-command the parser can resolve to. */
108
- export type CleanupCommandAction = 'status' | 'on' | 'off' | 'max-age' | 'run' | 'run-apply';
113
+ export type CleanupCommandAction = 'status' | 'on' | 'off' | 'max-age' | 'run';
114
+ /** A parsed `/snapshot-auto-cleanup` command (excludes the error branch). */
115
+ export type CleanupCommand = {
116
+ action: 'status' | 'on' | 'off';
117
+ } | {
118
+ action: 'max-age';
119
+ value: number;
120
+ } | {
121
+ action: 'run';
122
+ target: 'rules' | 'current';
123
+ apply: boolean;
124
+ };
109
125
  /**
110
126
  * Parse the free-form text after `/snapshot-auto-cleanup`. Pure so it is
111
127
  * unit-testable; `src/index.ts` maps the resolved action onto the store / the
112
128
  * config file. `max-age` returns the validated positive day count.
129
+ *
130
+ * The `run` verb is the single manual-cleanup action. `--apply` is the ONLY
131
+ * execute-vs-dry-run switch (position-independent): without it the action is a
132
+ * dry-run preview. `--current` re-targets the action to the ACTIVE session's
133
+ * snapshots (the manual "clear this session now"); without it, `run` keeps its
134
+ * age-based stale-session sweep semantics. The old `run-apply` abbreviation is
135
+ * gone — use `run --apply`.
113
136
  */
114
- export declare function parseCleanupCommand(rawInput: string): {
115
- action: CleanupCommandAction;
116
- value?: number;
117
- } | {
137
+ export declare function parseCleanupCommand(rawInput: string): CleanupCommand | {
118
138
  error: string;
119
139
  };
120
140
  /**
@@ -20,6 +20,11 @@
20
20
  * kept), and restores read/write the real file system with plain `node:fs`
21
21
  * — independent of the fs service.
22
22
  *
23
+ * Security note: this `node:fs` authority is the DSH host authority every host
24
+ * plugin holds — the model-facing fences constrain the model's tools, not this
25
+ * code. The store stays bounded to the model-touched paths, so excluding a
26
+ * file (e.g. `.env`) is a model-permission concern (see `SECURITY.md`).
27
+ *
23
28
  * Crash safety (this module's own engineering asset):
24
29
  * - Checkpoint commits are ATOMIC: the entry JSON is written to a sibling
25
30
  * temp file and renamed over the target, so a host crash mid-write can
@@ -48,7 +53,12 @@
48
53
  *
49
54
  * @module dsh-rewind/snapshot
50
55
  */
51
- /** Default store root: the dsh data directory. */
56
+ /**
57
+ * Default store root: `<harness home>/rewind-snapshots`. Resolved through
58
+ * {@link resolveDshHome} so the plugin follows `$DSH_HOME` (or a configured
59
+ * harness home) rather than hardcoding `~/.dsh` — matching the other
60
+ * first-party DSH packages. See `SECURITY.md` "Sensitive files".
61
+ */
52
62
  export declare const DEFAULT_SNAPSHOT_ROOT: string;
53
63
  /** Environment variable overriding the store root (tests, exotic homes). */
54
64
  export declare const SNAPSHOT_ROOT_ENV = "DSH_REWIND_SNAPSHOT_DIR";
@@ -249,12 +259,34 @@ export interface PruneStaleReport {
249
259
  /** Whether nothing was really removed (the sweep only reported). */
250
260
  readonly dryRun: boolean;
251
261
  }
262
+ /**
263
+ * Result of a manual whole-session clear ({@link SnapshotStore.clearSession}).
264
+ *
265
+ * Unlike the age-based sweep, a clear removes EVERY snapshot of ONE session
266
+ * (all anchor groups, all checkpoint entries, all restore journals) on demand —
267
+ * the active session the user is driving, to drop the rewind overhead or to
268
+ * archive a conversation immediately. `dryRun` reports what would be removed
269
+ * without touching disk or memory.
270
+ */
271
+ export interface ClearSessionReport {
272
+ /** The session whose records were (or would be) cleared. */
273
+ readonly sessionId: string;
274
+ /** Number of anchor-group (user-message) directories present. */
275
+ readonly anchorGroups: number;
276
+ /** Number of committed checkpoint entries (full backups + dedup links). */
277
+ readonly entries: number;
278
+ /** Number of restore-journal files (terminal + pending). */
279
+ readonly journals: number;
280
+ /** Bytes occupied by the session directory (the amount freed). */
281
+ readonly bytes: number;
282
+ /** Whether nothing was really removed (the clear only reported). */
283
+ readonly dryRun: boolean;
284
+ }
252
285
  /**
253
286
  * On-disk checkpoint store. Every write goes straight through `node:fs`, so a
254
287
  * restore reliably lands on the real file system.
255
288
  */
256
289
  export declare class SnapshotStore {
257
- readonly root: string;
258
290
  /** Debounce window for the per-commit prune (keeps the readdir+sort off the hot path). */
259
291
  private static readonly PRUNE_INTERVAL_MS;
260
292
  private lastPruneAt;
@@ -273,6 +305,8 @@ export declare class SnapshotStore {
273
305
  private lastEntryTime;
274
306
  /** Store options; `dedup` toggles in-place content dedup (default on). */
275
307
  private readonly dedup;
308
+ /** Resolved checkpoint store root (absolute); see the constructor's fallback. */
309
+ readonly root: string;
276
310
  /**
277
311
  * In-memory per-path "most recent entry" for content dedup, keyed by
278
312
  * `<sessionId>\0<path>`. Each value holds the entry's effective `before`
@@ -285,6 +319,7 @@ export declare class SnapshotStore {
285
319
  private readonly seededSessions;
286
320
  constructor(root?: string, opts?: {
287
321
  readonly dedup?: boolean;
322
+ readonly dshHome?: string;
288
323
  });
289
324
  /** Absolute path of one session's snapshot directory (id sanitized). */
290
325
  sessionDir(sessionId: string): string;
@@ -537,6 +572,49 @@ export declare class SnapshotStore {
537
572
  * entries so no extra persistence is needed.
538
573
  */
539
574
  trackedPaths(sessionId: string): Promise<Set<string>>;
575
+ /**
576
+ * Summarize a session's on-disk footprint for a clear dry-run: anchor-group
577
+ * count, committed checkpoint-entry count, restore-journal count, and total
578
+ * bytes. Walks with `lstat` (never follows a symlink, so a hostile symlink
579
+ * cannot escape the store root or inflate the measurement) and skips
580
+ * dot-prefixed temp leftovers and non-`.json` members — they are never
581
+ * checkpoint entries.
582
+ */
583
+ private sessionStats;
584
+ /**
585
+ * Remove a session's ENTIRE snapshot directory — every anchor group, every
586
+ * checkpoint entry, and every restore journal — and reset the store's
587
+ * in-memory dedup state so the session starts recording fresh from the
588
+ * current workspace state. This is the manual "get rid of this session's
589
+ * records NOW" action on the ACTIVE session the user is driving (it is never
590
+ * targetable by id; that is a directory-manipulation concern the user can do
591
+ * directly).
592
+ *
593
+ * SEMANTICS — clearing is an explicit abandonment: issuing the command means
594
+ * the user accepts that this session's snapshot archive goes away. It is
595
+ * therefore NOT gated on the state of any restore journal. A clear and a
596
+ * restore are both slash commands the host runs to completion for an agent,
597
+ * so they never interleave — any non-terminal journal present on disk is a
598
+ * stale orphan from a previous (crashed) process, and discarding it is the
599
+ * correct, safe resolution of that abandoned restore.
600
+ *
601
+ * SAFETY (this module's real concern is the plugin's ongoing BEHAVIOR, not
602
+ * losing snapshots):
603
+ * - Only the session dir is removed; dedup refs are session-relative, so
604
+ * there is no cross-session dangling to materialize (the same rationale as
605
+ * {@link pruneStale}'s whole-dir removal).
606
+ * - The in-memory dedup state (`lastEntry` / `seededSessions`) is ALWAYS
607
+ * reset on an apply — even when the dir was already empty. A stale
608
+ * in-memory entry (e.g. a session whose dir was removed out-of-band) would
609
+ * otherwise link a later `recordEntry` to a deleted prior entry, leaving a
610
+ * dangling ref that breaks restore resolution. This is the primary
611
+ * correctness guarantee.
612
+ *
613
+ * `dryRun` computes the report without touching disk or memory.
614
+ */
615
+ clearSession(sessionId: string, opts?: {
616
+ readonly dryRun?: boolean;
617
+ }): Promise<ClearSessionReport>;
540
618
  }
541
619
  /**
542
620
  * Re-check every tracked file at a user-message boundary and record the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rewind-plugin",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "DSH 插件:真正便捷无感的同窗口内对话回退,从不新建分支;自带轻量工作区备份,可一并还原文件(完整 Claude Code /rewind 语义)。 · DSH plugin: genuinely effortless in-window conversation rewind — never forking a new session; ships a lightweight workspace backup that restores files together with the rewind (full Claude Code /rewind semantics).",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -78,6 +78,7 @@
78
78
  "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6 || ^0.1.1-rc.2",
79
79
  "@deepseek-ai/dsh-commands": "^0.1.0-rc.6 || ^0.1.1-rc.2",
80
80
  "@deepseek-ai/dsh-fs": "^0.1.0-rc.6 || ^0.1.1-rc.2",
81
+ "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-rc.2",
81
82
  "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-rc.2",
82
83
  "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.6 || ^0.1.1-rc.2",
83
84
  "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.2",
@@ -106,6 +107,9 @@
106
107
  "@deepseek-ai/dsh-fs": {
107
108
  "optional": true
108
109
  },
110
+ "@deepseek-ai/dsh-home-paths": {
111
+ "optional": true
112
+ },
109
113
  "@deepseek-ai/dsh-llm": {
110
114
  "optional": true
111
115
  },
@@ -136,6 +140,7 @@
136
140
  "@deepseek-ai/dsh-compaction-basic": "^0.1.1-rc.2",
137
141
  "@deepseek-ai/dsh-fs": "^0.1.1-rc.2",
138
142
  "@deepseek-ai/dsh-goal": "^0.1.1-rc.2",
143
+ "@deepseek-ai/dsh-home-paths": "^0.1.1-rc.2",
139
144
  "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
140
145
  "@deepseek-ai/dsh-sandbox": "^0.1.1-rc.2",
141
146
  "@deepseek-ai/dsh-session": "^0.1.1-rc.2",