dsh-rewind-plugin 0.4.2 → 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.
- package/CONTRIBUTING.md +72 -0
- package/README.en.md +37 -39
- package/README.md +43 -39
- package/SECURITY.md +154 -0
- package/docs/README.md +29 -0
- package/docs/architecture.md +128 -0
- package/docs/compat/tracking-boundary.md +103 -0
- package/docs/compat/tracking-boundary.zh.md +55 -0
- package/docs/compat/troubleshooting.md +41 -0
- package/docs/{troubleshooting.zh.md → compat/troubleshooting.zh.md} +15 -0
- package/docs/format.md +150 -0
- package/docs/harness-reference.md +7 -3
- package/docs/release/release.md +69 -0
- package/docs/{release.md → release/release.zh.md} +6 -52
- package/docs/snapshot-auto-cleanup.md +71 -0
- package/docs/snapshot-auto-cleanup.zh.md +42 -0
- package/lib/index.js +520 -37
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/index.d.ts +3 -1
- package/lib/types/locales.d.ts +17 -0
- package/lib/types/snapshot-cleanup.d.ts +125 -0
- package/lib/types/snapshot.d.ts +146 -11
- package/package.json +4 -1
- package/docs/troubleshooting.md +0 -44
- /package/docs/{compat-audit.md → compat/audit.md} +0 -0
- /package/docs/{client-contract.md → contract/client-contract.md} +0 -0
- /package/docs/{client-contract.zh.md → contract/client-contract.zh.md} +0 -0
|
@@ -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';
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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, RestoreJournal, RestoreJournalState, RestoreReconcileReport } 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
|
package/lib/types/locales.d.ts
CHANGED
|
@@ -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;
|
|
@@ -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;
|
package/lib/types/snapshot.d.ts
CHANGED
|
@@ -66,6 +66,30 @@ export interface CheckpointEntry {
|
|
|
66
66
|
/** Epoch ms the entry was committed (stable ordering within a group). */
|
|
67
67
|
readonly time: number;
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* One in-place dedup link, keyed by tool call. When a tracked file is
|
|
71
|
+
* recorded with a `before` content identical to the immediately-prior entry
|
|
72
|
+
* for that path, the entry is stored as a LINK instead of a full copy: it
|
|
73
|
+
* carries no `before`, only a `ref` naming the prior entry file
|
|
74
|
+
* (`<anchorSeq>/<callId>.json`). The linear (predecessor-chained) ref makes
|
|
75
|
+
* restore resolution and prune materialization rewrite-free.
|
|
76
|
+
*
|
|
77
|
+
* The real-entry format ({@link CheckpointEntry}) is unchanged so existing
|
|
78
|
+
* data reads identically; links are a NEW entry kind only the current build
|
|
79
|
+
* understands (old-build reads of links are explicitly out of scope).
|
|
80
|
+
*/
|
|
81
|
+
export interface LinkEntry {
|
|
82
|
+
readonly callId: string;
|
|
83
|
+
readonly anchorSeq: number;
|
|
84
|
+
readonly path: string;
|
|
85
|
+
/** `<anchorSeq>/<callId>.json` of the immediately-prior entry for the path. */
|
|
86
|
+
readonly ref: string;
|
|
87
|
+
readonly time: number;
|
|
88
|
+
}
|
|
89
|
+
/** Any on-disk entry: a full before-backup or an in-place dedup link. */
|
|
90
|
+
export type StoredEntry = CheckpointEntry | LinkEntry;
|
|
91
|
+
/** True when an entry is a dedup link (carries `ref`, not `before`). */
|
|
92
|
+
export declare function isLinkEntry(entry: StoredEntry): entry is LinkEntry;
|
|
69
93
|
/** Per-file restore impact preview (`/rewind preview @seq both`). */
|
|
70
94
|
export interface FileImpact {
|
|
71
95
|
readonly path: string;
|
|
@@ -199,6 +223,32 @@ export type PlannedAction = {
|
|
|
199
223
|
};
|
|
200
224
|
/** Production probe: real reads via node:fs, links detected by lstat + nlink. */
|
|
201
225
|
export declare const defaultProbe: DiskProbe;
|
|
226
|
+
/**
|
|
227
|
+
* Result of a stale-session cleanup sweep ({@link SnapshotStore.pruneStale}).
|
|
228
|
+
*
|
|
229
|
+
* The sweep is ANTI-DELETE: it only ever removes WHOLE session directories
|
|
230
|
+
* that have been idle past `maxAgeDays`. `scanned` counts every session dir
|
|
231
|
+
* evaluated; `kept` + `skippedActive` + `deleted` sum to it. `remainingBytes`
|
|
232
|
+
* is the total of directories that SURVIVE the policy (when `dryRun` it is the
|
|
233
|
+
* would-be total, not the current on-disk total), so it is comparable across
|
|
234
|
+
* dry and real runs.
|
|
235
|
+
*/
|
|
236
|
+
export interface PruneStaleReport {
|
|
237
|
+
/** Number of session directories evaluated. */
|
|
238
|
+
readonly scanned: number;
|
|
239
|
+
/** Session directories removed (would-be count when `dryRun`). */
|
|
240
|
+
readonly deleted: number;
|
|
241
|
+
/** Bytes reclaimed (would-be bytes when `dryRun`). */
|
|
242
|
+
readonly freedBytes: number;
|
|
243
|
+
/** Session directories retained (not past the cutoff, not the active one). */
|
|
244
|
+
readonly kept: number;
|
|
245
|
+
/** Bytes across the retained + skipped-active directories. */
|
|
246
|
+
readonly remainingBytes: number;
|
|
247
|
+
/** Directories skipped because they are the active session. */
|
|
248
|
+
readonly skippedActive: number;
|
|
249
|
+
/** Whether nothing was really removed (the sweep only reported). */
|
|
250
|
+
readonly dryRun: boolean;
|
|
251
|
+
}
|
|
202
252
|
/**
|
|
203
253
|
* On-disk checkpoint store. Every write goes straight through `node:fs`, so a
|
|
204
254
|
* restore reliably lands on the real file system.
|
|
@@ -221,15 +271,56 @@ export declare class SnapshotStore {
|
|
|
221
271
|
* it, and even then the in-process order still holds.
|
|
222
272
|
*/
|
|
223
273
|
private lastEntryTime;
|
|
224
|
-
|
|
274
|
+
/** Store options; `dedup` toggles in-place content dedup (default on). */
|
|
275
|
+
private readonly dedup;
|
|
276
|
+
/**
|
|
277
|
+
* In-memory per-path "most recent entry" for content dedup, keyed by
|
|
278
|
+
* `<sessionId>\0<path>`. Each value holds the entry's effective `before`
|
|
279
|
+
* content and its own file ref, so a new record with the same content links
|
|
280
|
+
* to the immediately-prior entry (linear chain). Seeded lazily per session
|
|
281
|
+
* from the bounded on-disk window, so dedup survives a host restart.
|
|
282
|
+
*/
|
|
283
|
+
private readonly lastEntry;
|
|
284
|
+
/** Sessions whose dedup state has been seeded from disk this process. */
|
|
285
|
+
private readonly seededSessions;
|
|
286
|
+
constructor(root?: string, opts?: {
|
|
287
|
+
readonly dedup?: boolean;
|
|
288
|
+
});
|
|
225
289
|
/** Absolute path of one session's snapshot directory (id sanitized). */
|
|
226
290
|
sessionDir(sessionId: string): string;
|
|
227
291
|
/** Absolute path of one anchor group directory. */
|
|
228
292
|
anchorDir(sessionId: string, anchorSeq: number): string;
|
|
229
|
-
/**
|
|
293
|
+
/** Absolute file ref (relative to the session dir) of an entry. */
|
|
294
|
+
private entryRefOf;
|
|
295
|
+
/**
|
|
296
|
+
* Seed a session's dedup state from the existing (bounded) on-disk window:
|
|
297
|
+
* scan entries newest-first and record the most recent entry per path. This
|
|
298
|
+
* makes content dedup survive a host restart within the session window. A
|
|
299
|
+
* no-op after the first seed (or when `dedup` is disabled).
|
|
300
|
+
*/
|
|
301
|
+
private ensureDedupSeeded;
|
|
302
|
+
/**
|
|
303
|
+
* Resolve an entry's effective `before` content, following a link chain to
|
|
304
|
+
* its terminal real snapshot. Refs are strictly backward in
|
|
305
|
+
* `(anchorSeq, time)`, so the chain is acyclic and finite. A dangling or
|
|
306
|
+
* cyclic link throws — callers fail per-file (never silently dropping the
|
|
307
|
+
* path from a restore).
|
|
308
|
+
*/
|
|
309
|
+
private resolveBefore;
|
|
310
|
+
/** Commit one before-backup (or an in-place dedup link) under its anchor. */
|
|
230
311
|
recordEntry(sessionId: string, entry: Omit<CheckpointEntry, 'time'>, opts?: {
|
|
312
|
+
readonly dedup?: boolean;
|
|
231
313
|
readonly crash?: (point: CrashPoint) => void;
|
|
232
314
|
}): Promise<void>;
|
|
315
|
+
/**
|
|
316
|
+
* The effective content recorded by the path's MOST RECENT entry, or
|
|
317
|
+
* undefined when the path has never been recorded (a fresh tracking sight).
|
|
318
|
+
* This is the single in-memory "last known state" the boundary uses to
|
|
319
|
+
* decide whether a tracked file changed — the same source `recordEntry`
|
|
320
|
+
* dedups against, so there is one content copy and one comparison per
|
|
321
|
+
* decision, not two. Seeding is idempotent (once per session from disk).
|
|
322
|
+
*/
|
|
323
|
+
lastKnownContent(sessionId: string, path: string): Promise<string | null | undefined>;
|
|
233
324
|
/**
|
|
234
325
|
* All committed entries anchored at or after `targetSeq`, newest first (for
|
|
235
326
|
* preview ordering). The boundary is inclusive: rewinding to a message also
|
|
@@ -237,7 +328,7 @@ export declare class SnapshotStore {
|
|
|
237
328
|
* turn's assistant response and tool calls), so only entries anchored at
|
|
238
329
|
* earlier messages survive.
|
|
239
330
|
*/
|
|
240
|
-
entriesAfter(sessionId: string, targetSeq: number): Promise<
|
|
331
|
+
entriesAfter(sessionId: string, targetSeq: number): Promise<StoredEntry[]>;
|
|
241
332
|
/**
|
|
242
333
|
* Per-path EARLIEST committed entry anchored at or after the target — the
|
|
243
334
|
* single source of truth for both restore and impact preview.
|
|
@@ -269,7 +360,7 @@ export declare class SnapshotStore {
|
|
|
269
360
|
* @param sessionId - session whose snapshot store to plan against.
|
|
270
361
|
* @param targetSeq - rewind target; entries anchored at/after it apply.
|
|
271
362
|
* @param probe - current-disk state probe (defaults to the real FS).
|
|
272
|
-
* @returns the planned actions
|
|
363
|
+
* @returns the planned actions, the link paths skipped, and per-file failures.
|
|
273
364
|
*/
|
|
274
365
|
private planRestore;
|
|
275
366
|
/** Per-file restore impact: only actions that would actually change the disk. */
|
|
@@ -384,8 +475,23 @@ export declare class SnapshotStore {
|
|
|
384
475
|
* recycles terminal restore journals (see {@link pruneTerminalJournals}),
|
|
385
476
|
* so the per-commit cap bounds BOTH the checkpoint entries and the journal
|
|
386
477
|
* accumulation.
|
|
478
|
+
*
|
|
479
|
+
* Because dedup links reference prior entries, eviction is LINK-AWARE: before
|
|
480
|
+
* deleting the oldest groups, any SURVIVING (kept-group) link whose `ref`
|
|
481
|
+
* lands on a real snapshot inside a doomed group is MATERIALIZED (rewritten
|
|
482
|
+
* as a real snapshot carrying the resolved content), so no kept link is left
|
|
483
|
+
* dangling. Links form a linear predecessor chain, so materializing the first
|
|
484
|
+
* link after each doomed real is enough — later links already point at that
|
|
485
|
+
* materialized entry (or at other kept links), requiring no rewrite.
|
|
486
|
+
*
|
|
487
|
+
* `opts.crash` is the test-only seam: a crash fired inside a materialization
|
|
488
|
+
* write (between its temp write and rename) leaves ONLY a `.tmp` — the doomed
|
|
489
|
+
* real is still on disk and the kept link still resolves, so nothing dangles
|
|
490
|
+
* and a later prune simply re-materializes.
|
|
387
491
|
*/
|
|
388
|
-
prune(sessionId: string, keep?: number
|
|
492
|
+
prune(sessionId: string, keep?: number, opts?: {
|
|
493
|
+
readonly crash?: (point: CrashPoint) => void;
|
|
494
|
+
}): Promise<void>;
|
|
389
495
|
/**
|
|
390
496
|
* Recycle terminal restore journals (`completed` / `rolled-back`): once an
|
|
391
497
|
* op finished, its journal's before + rescue content is dead weight that
|
|
@@ -397,6 +503,33 @@ export declare class SnapshotStore {
|
|
|
397
503
|
private pruneTerminalJournals;
|
|
398
504
|
/** True when a path exists on disk (used by tests and diagnostics). */
|
|
399
505
|
exists(path: string): Promise<boolean>;
|
|
506
|
+
/**
|
|
507
|
+
* Cross-session retention sweep: remove WHOLE session directories whose
|
|
508
|
+
* newest member stamp is older than `maxAgeDays` days of idle, keeping the
|
|
509
|
+
* active session (`keepActiveId`) untouched. This is the anti-growth policy
|
|
510
|
+
* for finished sessions (rewind only ever reads the active session, so a
|
|
511
|
+
* finished session's backups are provably dead weight).
|
|
512
|
+
*
|
|
513
|
+
* SAFETY:
|
|
514
|
+
* - Only whole session directories are removed (dedup refs are
|
|
515
|
+
* session-relative, so there is no cross-session dangling to materialize);
|
|
516
|
+
* - the active session is never targeted (`keepActiveId`), and everything
|
|
517
|
+
* else is protected by its own mtime — a session that is still written to
|
|
518
|
+
* keeps scrolling its newest member stamp forward, so it is never old
|
|
519
|
+
* enough to be pruned;
|
|
520
|
+
* - a non-positive `maxAgeDays` throws instead of degenerating into a
|
|
521
|
+
* mass-destructive `cutoff` in the far future;
|
|
522
|
+
* - the walk uses `lstat` (no symlink following) and skips dot-prefixed
|
|
523
|
+
* temp left overs, so measurement stays inside the store root.
|
|
524
|
+
*
|
|
525
|
+
* `dryRun` computes and reports exactly what would be removed without
|
|
526
|
+
* deleting anything — the `/snapshot-auto-cleanup run` preview.
|
|
527
|
+
*/
|
|
528
|
+
pruneStale(opts: {
|
|
529
|
+
readonly keepActiveId?: string;
|
|
530
|
+
readonly maxAgeDays: number;
|
|
531
|
+
readonly dryRun?: boolean;
|
|
532
|
+
}): Promise<PruneStaleReport>;
|
|
400
533
|
/**
|
|
401
534
|
* All distinct paths ever recorded for a session — the "tracked files"
|
|
402
535
|
* set. Mirrors Claude Code's global `trackedFiles` collection (files stay
|
|
@@ -418,10 +551,13 @@ export declare class SnapshotStore {
|
|
|
418
551
|
* Semantics: the recorded `before` is the file's state at the boundary —
|
|
419
552
|
* the state the boundary message's turn starts from, exactly like the
|
|
420
553
|
* tool-captured entries. An entry is written only when the state differs
|
|
421
|
-
* from the
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
554
|
+
* from the path's most-recent recorded content (`lastKnownContent`); a fresh
|
|
555
|
+
* sighting (never recorded) always records. The state is compared against the
|
|
556
|
+
* SAME single in-memory source `recordEntry` dedups against, so there is one
|
|
557
|
+
* content copy and one comparison — not the two (a boundary map plus the
|
|
558
|
+
* dedup map) the previous model held. Only CHANGED files are recorded, and
|
|
559
|
+
* each is a full snapshot (`dedup: false`): a changed state always differs
|
|
560
|
+
* from the recent record, so the link decision would never apply there.
|
|
425
561
|
*
|
|
426
562
|
* Symlinked / hard-linked paths are never re-checked (restores skip them).
|
|
427
563
|
* A probe failure skips the file with a warning-level no-op; it never
|
|
@@ -431,8 +567,7 @@ export declare class SnapshotStore {
|
|
|
431
567
|
* @param sessionId - session whose tracked files to re-check.
|
|
432
568
|
* @param anchorSeq - the boundary user-message seq (entry anchor).
|
|
433
569
|
* @param tracked - the session's tracked path set (read-only here).
|
|
434
|
-
* @param states - per-path last-seen state (path → content, null = absent).
|
|
435
570
|
* @param probe - current-disk state probe (defaults to the real FS).
|
|
436
571
|
* @returns the number of entries recorded.
|
|
437
572
|
*/
|
|
438
|
-
export declare function reconcileTracked(store: SnapshotStore, sessionId: string, anchorSeq: number, tracked: ReadonlySet<string>,
|
|
573
|
+
export declare function reconcileTracked(store: SnapshotStore, sessionId: string, anchorSeq: number, tracked: ReadonlySet<string>, probe?: DiskProbe): Promise<number>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rewind-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
"cordis.patch.yml",
|
|
40
40
|
"README.md",
|
|
41
41
|
"README.en.md",
|
|
42
|
+
"SECURITY.md",
|
|
43
|
+
"CONTRIBUTING.md",
|
|
42
44
|
"docs",
|
|
43
45
|
"assets",
|
|
44
46
|
"LICENSE"
|
|
@@ -62,6 +64,7 @@
|
|
|
62
64
|
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.client.json && tsc --noEmit -p tsconfig.client-test.json",
|
|
63
65
|
"test": "vitest run",
|
|
64
66
|
"verify:host": "node scripts/verify-host.mjs",
|
|
67
|
+
"check": "npm run typecheck && npm test && npm run build && node scripts/verify-host.mjs && npm pack --dry-run",
|
|
65
68
|
"prepare": "npm run build"
|
|
66
69
|
},
|
|
67
70
|
"publishConfig": {
|
package/docs/troubleshooting.md
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# Troubleshooting
|
|
2
|
-
|
|
3
|
-
[简体中文](troubleshooting.zh.md)
|
|
4
|
-
|
|
5
|
-
## History load failure: `…turn-tail… received an update before its start Match`
|
|
6
|
-
|
|
7
|
-
Rewinds from `≤ 0.2.4` collided with the next real turn's `turn/start`, so reopening the session showed
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
Failed to load history: conversation Context …:turn-tail… received an update before its start Match (internal)
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
and the history vanished. Rewinds from `0.2.5` on no longer collide; already-corrupted sessions need an offline repair (the log is append-only). The repair tool (`dsh-rewind-repair`) is no longer shipped from v0.4.0 — install a pre-v0.4.0 release to get it (fully quit dsh web / host first, then):
|
|
14
|
-
|
|
15
|
-
```sh
|
|
16
|
-
npm exec --yes --package=dsh-rewind-plugin@0.3.3 -- dsh-rewind-repair
|
|
17
|
-
npm exec --yes --package=dsh-rewind-plugin@0.3.3 -- dsh-rewind-repair -- --dry-run # preview only
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
It only rewrites the marker events' `data.turn` (seqs, order, and zstd frame structure intact) and backs up the original file before writing — safe to run repeatedly. From a source checkout of a pre-v0.4.0 tag: `node scripts/repair-markers.mjs` (identical flags).
|
|
21
|
-
|
|
22
|
-
## Known compatibility boundaries
|
|
23
|
-
|
|
24
|
-
Behavioral notes verified by the compatibility probe suites
|
|
25
|
-
([compat-audit.md](compat-audit.md)) — none of these is a crash:
|
|
26
|
-
|
|
27
|
-
- **Session stats / telemetry do not roll back**: whole-log folds count the
|
|
28
|
-
withdrawn turns, and telemetry records the rewind's marker and ghost-step
|
|
29
|
-
frame under the reused turn. Expected whole-log semantics.
|
|
30
|
-
- **Withdrawn messages stay searchable and exported**: rewind cuts only the
|
|
31
|
-
model-visible surface; full-text search and `/export` still see the raw log.
|
|
32
|
-
- **Session titles may regenerate** after a rewind (title derives from the
|
|
33
|
-
current surface).
|
|
34
|
-
- **Files written by a cancelled tool call** (write happened, no result, no
|
|
35
|
-
snapshot commit) cannot be restored by "conversation and code".
|
|
36
|
-
|
|
37
|
-
**R-OPENSTEP** (harness-side, plugin does not guard): a session log carrying
|
|
38
|
-
an *unclosed* `step/start` (a crash before the agent loop's `finally` closed
|
|
39
|
-
the step) makes any later step activity break token-meter replay (and
|
|
40
|
-
`/compact`). Harness `0.1.1-rc.2` fixes the crash path on load
|
|
41
|
-
(`interruptedTurnClosers` closes leftover step/turn boundaries). A plugin-side
|
|
42
|
-
up-front rejection was implemented and **reverted** (false positives broke the
|
|
43
|
-
rewind feature on real session logs, `177ec14`); the residual runtime-produced
|
|
44
|
-
risk is accepted. Tracked in the compat-audit.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|