@dzhechkov/harness-core 0.8.30 → 0.8.31
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/.dz-manifest.json +233 -93
- package/README.md +269 -0
- package/dist/agentdb-index.d.ts +45 -0
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +345 -85
- package/dist/agentdb-index.js.map +1 -1
- package/dist/agentdb-reindex-marker.d.ts +170 -0
- package/dist/agentdb-reindex-marker.d.ts.map +1 -0
- package/dist/agentdb-reindex-marker.js +293 -0
- package/dist/agentdb-reindex-marker.js.map +1 -0
- package/dist/agentdb-snapshot-rotation.d.ts +169 -0
- package/dist/agentdb-snapshot-rotation.d.ts.map +1 -0
- package/dist/agentdb-snapshot-rotation.js +322 -0
- package/dist/agentdb-snapshot-rotation.js.map +1 -0
- package/dist/agentdb-snapshot.d.ts +126 -0
- package/dist/agentdb-snapshot.d.ts.map +1 -0
- package/dist/agentdb-snapshot.js +195 -0
- package/dist/agentdb-snapshot.js.map +1 -0
- package/dist/apply-leg.d.ts +202 -0
- package/dist/apply-leg.d.ts.map +1 -0
- package/dist/apply-leg.js +975 -0
- package/dist/apply-leg.js.map +1 -0
- package/dist/book-kb.d.ts.map +1 -1
- package/dist/book-kb.js +57 -1
- package/dist/book-kb.js.map +1 -1
- package/dist/brain.d.ts +3 -0
- package/dist/brain.d.ts.map +1 -1
- package/dist/brain.js +1 -1
- package/dist/brain.js.map +1 -1
- package/dist/cadence.d.ts +30 -1
- package/dist/cadence.d.ts.map +1 -1
- package/dist/cadence.js +66 -2
- package/dist/cadence.js.map +1 -1
- package/dist/guard.d.ts +14 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +36 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +23 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -5
- package/dist/index.js.map +1 -1
- package/dist/integration-probe-worker.d.ts.map +1 -1
- package/dist/integration-probe-worker.js +4 -6
- package/dist/integration-probe-worker.js.map +1 -1
- package/dist/integrations-verify.d.ts.map +1 -1
- package/dist/integrations-verify.js +3 -1
- package/dist/integrations-verify.js.map +1 -1
- package/dist/managed-hooks.d.ts +11 -2
- package/dist/managed-hooks.d.ts.map +1 -1
- package/dist/managed-hooks.js +33 -7
- package/dist/managed-hooks.js.map +1 -1
- package/dist/mutation-gate.d.ts +35 -1
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +49 -9
- package/dist/mutation-gate.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +94 -0
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +23 -4
- package/dist/patterns.js.map +1 -1
- package/dist/repository-origin.d.ts +1 -1
- package/dist/repository-origin.d.ts.map +1 -1
- package/dist/repository-origin.js +7 -1
- package/dist/repository-origin.js.map +1 -1
- package/dist/round-exec.d.ts +41 -0
- package/dist/round-exec.d.ts.map +1 -0
- package/dist/round-exec.js +45 -0
- package/dist/round-exec.js.map +1 -0
- package/dist/round.d.ts +109 -0
- package/dist/round.d.ts.map +1 -0
- package/dist/round.js +148 -0
- package/dist/round.js.map +1 -0
- package/dist/run-records.d.ts +8 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +69 -3
- package/dist/run-records.js.map +1 -1
- package/dist/setup.d.ts +8 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +166 -3
- package/dist/setup.js.map +1 -1
- package/dist/sqlite-read-helpers.d.ts +60 -0
- package/dist/sqlite-read-helpers.d.ts.map +1 -0
- package/dist/sqlite-read-helpers.js +118 -0
- package/dist/sqlite-read-helpers.js.map +1 -0
- package/dist/statusline.d.ts +20 -3
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +40 -7
- package/dist/statusline.js.map +1 -1
- package/dist/store-counts.d.ts +30 -0
- package/dist/store-counts.d.ts.map +1 -1
- package/dist/store-counts.js +189 -0
- package/dist/store-counts.js.map +1 -1
- package/dist/usage.d.ts +55 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +184 -33
- package/dist/usage.js.map +1 -1
- package/dist/vector-tier.d.ts +18 -1
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +26 -2
- package/dist/vector-tier.js.map +1 -1
- package/package.json +6 -6
- package/sbom.json +442 -92
- package/src/agentdb-index.ts +315 -32
- package/src/agentdb-reindex-marker.ts +330 -0
- package/src/agentdb-snapshot-rotation.ts +393 -0
- package/src/agentdb-snapshot.ts +222 -0
- package/src/apply-leg.ts +1046 -0
- package/src/book-kb.ts +64 -1
- package/src/brain.ts +11 -2
- package/src/cadence.ts +87 -2
- package/src/guard.ts +46 -0
- package/src/index.ts +58 -5
- package/src/integration-probe-worker.ts +4 -6
- package/src/integrations-verify.ts +3 -1
- package/src/managed-hooks.ts +32 -7
- package/src/mutation-gate.ts +87 -8
- package/src/operations.ts +89 -0
- package/src/patterns.ts +23 -4
- package/src/repository-origin.ts +7 -1
- package/src/round-exec.ts +85 -0
- package/src/round.ts +228 -0
- package/src/run-records.ts +77 -2
- package/src/setup.ts +198 -3
- package/src/sqlite-read-helpers.ts +128 -0
- package/src/statusline.ts +57 -9
- package/src/store-counts.ts +245 -1
- package/src/usage.ts +243 -29
- package/src/vector-tier.ts +45 -4
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agentdb snapshot lock + reindex-in-progress marker (feature `agentdb-snapshot-lock`,
|
|
3
|
+
* FR-1..FR-4; fix-round AM-1..AM-4/AM-7 after Codex review Grade D).
|
|
4
|
+
*
|
|
5
|
+
* WHY. Snapshot creation (in `reindexAgentdbRows`), snapshot rotation (`rotatePreReindexSnapshots`,
|
|
6
|
+
* including `dz brain snapshots --prune`) and snapshot restore (`restoreSqliteSnapshot` via
|
|
7
|
+
* `reindexAgentdbRows`'s rollback) are three writers of ONE directory with no mutual exclusion: a
|
|
8
|
+
* concurrent `rotate --keep 0` can delete the very snapshot family a live reindex is relying on as
|
|
9
|
+
* its undo point, with only a 10-minute grace period standing in the way.
|
|
10
|
+
*
|
|
11
|
+
* {@link withAgentdbSnapshotLock} is a thin, dbFile-addressed wrapper over `withNamedLockSync`: the
|
|
12
|
+
* lock lives at `<dirname(dbFile)>/.dz/locks/agentdb-snapshot.lock` — a pure function of the
|
|
13
|
+
* database's OWN directory, never of the caller's cwd, so a project store and the home brain each
|
|
14
|
+
* get their own lock (AC-5) regardless of where `dz` happens to be invoked from. AM-7: it defaults
|
|
15
|
+
* `staleMs` to {@link AGENTDB_SNAPSHOT_LOCK_STALE_MS} (5 minutes) rather than named-lock's ordinary
|
|
16
|
+
* 30-second default — MEASURED (lead, this fix round): `VACUUM INTO` on the owner's hub store
|
|
17
|
+
* (8.45 MB) took 96 ms, so 5 minutes leaves roughly 3000x headroom while staying inside
|
|
18
|
+
* named-lock's own 600 000 ms environment-override ceiling. A caller doing something unusually
|
|
19
|
+
* large may still override via `opts.staleMs`.
|
|
20
|
+
*
|
|
21
|
+
* The critical section this lock guards must stay SHORT and SYNCHRONOUS — file operations only
|
|
22
|
+
* (`VACUUM INTO` / copies / unlink / restore). MEASURED (named-lock.ts round-2, reused verbatim
|
|
23
|
+
* here): a 6s synchronous body under a 2s stale threshold let a THIRD process break in while the
|
|
24
|
+
* second was still inside its own critical section — a long body doesn't just block callers, it
|
|
25
|
+
* breaks the lock's own guarantee. The actual re-embedding (network/CPU-bound `EmbeddingService`
|
|
26
|
+
* calls, row writes) therefore runs OUTSIDE this lock — see `reindexAgentdbRows`.
|
|
27
|
+
*
|
|
28
|
+
* {@link writeReindexMarker} / {@link clearReindexMarker} / {@link readLiveReindexMarkers} implement
|
|
29
|
+
* the SECOND protection this feature adds — a "reindex is currently in flight" marker file beside
|
|
30
|
+
* the database, so a family a live reindex just created is protected from rotation for the
|
|
31
|
+
* DURATION of the reindex, not merely for the few milliseconds the snapshot lock itself is held
|
|
32
|
+
* (the embedding phase can run for a while, unlocked, per FR-2). The marker's liveness is judged by
|
|
33
|
+
* `startedAt` (a wall-clock age, {@link REINDEX_MARKER_TTL_MS}), never by `pid` — a pid is recorded
|
|
34
|
+
* for operator debugging only; a live pid on a different host, or a recycled pid, proves nothing
|
|
35
|
+
* ("pid is not authority over liveness" — the same lesson `store-lock.ts`/`named-lock.ts` already
|
|
36
|
+
* encode for lock staleness). A marker older than the TTL is treated as abandoned: it protects
|
|
37
|
+
* nothing, and rotation removes it, recording why in the report's `notes` — that is an honest
|
|
38
|
+
* observation about a stale marker, never a scan error.
|
|
39
|
+
*
|
|
40
|
+
* AM-1 (fix-round): {@link writeReindexMarker} creates the marker file EXCLUSIVELY
|
|
41
|
+
* (`openSync(path, 'wx')`) and stamps it with an ownership `token` (16 random hex chars). A LIVE
|
|
42
|
+
* marker already at that path (another reindex genuinely in flight) refuses this call outright —
|
|
43
|
+
* `{ ok: false, error }`, no snapshot ever taken for the refused call. A marker at or past the TTL
|
|
44
|
+
* is treated as abandoned and replaced. {@link clearReindexMarker} is compare-and-delete: it removes
|
|
45
|
+
* the marker only when the caller's `token` matches the one on disk, so a process can never tear
|
|
46
|
+
* down a marker it does not own (`{ cleared: false, reason }` otherwise). The narrow race this
|
|
47
|
+
* leaves — two `writeReindexMarker` calls racing the exclusive-create step itself — cannot happen
|
|
48
|
+
* in practice: every caller in this codebase invokes it from INSIDE the very snapshot lock this
|
|
49
|
+
* module also provides (see `reindexAgentdbRows`), so at most one writer is ever in this function
|
|
50
|
+
* at a time.
|
|
51
|
+
*
|
|
52
|
+
* @packageDocumentation
|
|
53
|
+
*/
|
|
54
|
+
import type { StoreLockOptions } from './store-lock.js';
|
|
55
|
+
/** A marker older than this is abandoned — its family is no longer protected (FR-3). `pid` is
|
|
56
|
+
* recorded for operator debugging only and is NEVER consulted to decide liveness. */
|
|
57
|
+
export declare const REINDEX_MARKER_TTL_MS: number;
|
|
58
|
+
/** AM-7: MEASURED (lead, this fix round) — `VACUUM INTO` on the hub's 8.45 MB agentdb store took
|
|
59
|
+
* 96 ms. 5 minutes is ~3000x that, and stays inside named-lock's 600 000 ms override ceiling. A
|
|
60
|
+
* base whose snapshot genuinely needs longer than this needs external coordination, not a bigger
|
|
61
|
+
* default — see the README. */
|
|
62
|
+
export declare const AGENTDB_SNAPSHOT_LOCK_STALE_MS = 300000;
|
|
63
|
+
/** The reindex-in-progress marker written beside the database while a reindex is running. */
|
|
64
|
+
export interface ReindexMarker {
|
|
65
|
+
/** The `<ms>` timestamp of THIS reindex's pre-reindex snapshot family — the value rotation must
|
|
66
|
+
* protect while the marker is live. AM-4: `null` when the reindex's `backupPath` does not match
|
|
67
|
+
* the standard `<db>.pre-reindex-<ms>.bak` family shape (a non-standard target names no family,
|
|
68
|
+
* so it is never a rotation candidate in the first place). */
|
|
69
|
+
readonly ms: number | null;
|
|
70
|
+
/** Operator-debugging only — never consulted for liveness (pid is not authority). */
|
|
71
|
+
readonly pid: number;
|
|
72
|
+
/** Wall-clock start time (`Date.now()`); liveness = `now - startedAt < REINDEX_MARKER_TTL_MS`. */
|
|
73
|
+
readonly startedAt: number;
|
|
74
|
+
/** AM-1: ownership token (16 random hex chars) — {@link clearReindexMarker} removes the marker
|
|
75
|
+
* only when the caller presents this exact token back. */
|
|
76
|
+
readonly token: string;
|
|
77
|
+
/** AM-4: the reindex's actual backup path, always recorded — the only way to identify the
|
|
78
|
+
* snapshot a `ms: null` marker is protecting (an operator debugging a stuck non-standard target
|
|
79
|
+
* has nothing else to go on). */
|
|
80
|
+
readonly backupPath?: string;
|
|
81
|
+
/** Lead edit after re-review (Codex D): set by {@link markReindexMarkerRecoveryRequired} when a
|
|
82
|
+
* rollback FAILED. Such a marker never expires — its family stays protected and every new reindex
|
|
83
|
+
* of this store is refused until an operator restores the snapshot and removes the marker. */
|
|
84
|
+
readonly requiresRecovery?: boolean;
|
|
85
|
+
readonly recoveryNote?: string;
|
|
86
|
+
}
|
|
87
|
+
/** The marker's path — always beside the database, never in a separate directory. */
|
|
88
|
+
export declare function reindexMarkerPath(dbFile: string): string;
|
|
89
|
+
/**
|
|
90
|
+
* AM-4: recompute a marker's `ms` from the ACTUAL backup path's filename — decoupled from whatever
|
|
91
|
+
* internal `ms` variable a caller used to build a DEFAULT `backupPath`. A caller-supplied
|
|
92
|
+
* `opts.backupPath` that does not end in the standard `.pre-reindex-<digits>.bak` shape (or whose
|
|
93
|
+
* digits overflow a safe integer) names no family at all, so it maps to `null` — such a target is
|
|
94
|
+
* never a rotation candidate, and the marker must not claim to protect one.
|
|
95
|
+
*/
|
|
96
|
+
export declare function msFromBackupPath(backupPath: string): number | null;
|
|
97
|
+
/**
|
|
98
|
+
* Run `fn` while holding the agentdb snapshot lock for `dbFile` — a pure function of the
|
|
99
|
+
* database's own directory (AC-5), never of `process.cwd()`. Propagates `NamedLockTimeoutError`
|
|
100
|
+
* (FR-4): a caller that cannot acquire the lock by the deadline must report the failure explicitly,
|
|
101
|
+
* never proceed unlocked and never silently skip its work. AM-7: `staleMs` defaults to
|
|
102
|
+
* {@link AGENTDB_SNAPSHOT_LOCK_STALE_MS} unless the caller names its own.
|
|
103
|
+
*/
|
|
104
|
+
export declare function withAgentdbSnapshotLock<T>(dbFile: string, fn: () => T, opts?: StoreLockOptions): T;
|
|
105
|
+
/**
|
|
106
|
+
* AM-1: write the reindex-in-progress marker EXCLUSIVELY. A marker already at this path that is
|
|
107
|
+
* still LIVE (younger than {@link REINDEX_MARKER_TTL_MS}) refuses this write outright —
|
|
108
|
+
* `{ ok: false, error }` naming the marker path, with no snapshot ever attempted by the caller for
|
|
109
|
+
* this call (the caller checks `ok` BEFORE touching the database — see `reindexAgentdbRows`). A
|
|
110
|
+
* marker that is missing, unreadable/corrupt, or past the TTL is replaced. The written marker
|
|
111
|
+
* carries a fresh ownership `token`; only {@link clearReindexMarker} presenting that SAME token may
|
|
112
|
+
* remove it.
|
|
113
|
+
*/
|
|
114
|
+
export declare function writeReindexMarker(dbFile: string, marker: {
|
|
115
|
+
readonly ms: number | null;
|
|
116
|
+
readonly pid: number;
|
|
117
|
+
readonly startedAt: number;
|
|
118
|
+
readonly backupPath?: string;
|
|
119
|
+
}): {
|
|
120
|
+
ok: true;
|
|
121
|
+
token: string;
|
|
122
|
+
} | {
|
|
123
|
+
ok: false;
|
|
124
|
+
error: string;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Lead edit after re-review (Codex D, finding 1): a marker left behind by a FAILED rollback must not
|
|
128
|
+
* quietly expire after the TTL — the snapshot it names may be the only good copy of the store. The
|
|
129
|
+
* owner (token) rewrites the marker atomically (tmp + rename) with `requiresRecovery: true`; from
|
|
130
|
+
* then on {@link readLiveReindexMarkers} protects its family without expiry and
|
|
131
|
+
* {@link writeReindexMarker} refuses every new reindex until an operator removes the marker.
|
|
132
|
+
* Call it under the snapshot lock, like every other marker mutation.
|
|
133
|
+
*/
|
|
134
|
+
export declare function markReindexMarkerRecoveryRequired(dbFile: string, token: string, recoveryNote: string): {
|
|
135
|
+
ok: true;
|
|
136
|
+
} | {
|
|
137
|
+
ok: false;
|
|
138
|
+
reason: string;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* AM-1: compare-and-delete. Removes the marker ONLY when its `token` matches the one on disk — a
|
|
142
|
+
* caller can never tear down a marker it does not own. Absence of the marker file is treated as an
|
|
143
|
+
* already-cleared success (idempotent). Called from `reindexAgentdbRows`'s cleanup on every path
|
|
144
|
+
* that does NOT leave a failed rollback behind (AM-3) — "absence of a receipt is not success" cuts
|
|
145
|
+
* the other way here too: a marker left behind after its owner finished would falsely protect a
|
|
146
|
+
* family forever (until the TTL), so ownership-checked removal must still run unconditionally on
|
|
147
|
+
* every path that is safe to clear.
|
|
148
|
+
*/
|
|
149
|
+
export declare function clearReindexMarker(dbFile: string, token: string): {
|
|
150
|
+
cleared: true;
|
|
151
|
+
} | {
|
|
152
|
+
cleared: false;
|
|
153
|
+
reason: string;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Read the marker beside `dbFile` and classify it: a marker younger than
|
|
157
|
+
* {@link REINDEX_MARKER_TTL_MS} protects its `ms` (FR-3) — but ONLY when `ms` is an actual number
|
|
158
|
+
* (AM-4: a `ms: null` marker names no family, so it protects nothing, live or not). A marker at or
|
|
159
|
+
* past the TTL is abandoned — it is removed here (rotation must not act on stale-but-still-present
|
|
160
|
+
* state on its NEXT call). AM-6: the note reports "ignored and removed" ONLY once `rmSync` actually
|
|
161
|
+
* succeeded; a removal failure (e.g. a permission error) is reported as "ignored, removal failed:
|
|
162
|
+
* <err>" — never silently claimed as removed. No marker, or a marker this function cannot parse
|
|
163
|
+
* (corrupt / missing fields), yields `{ protectedMs: [], notes: [] }` — never a throw: a malformed
|
|
164
|
+
* marker must not abort rotation, and it also cannot honestly claim to protect anything.
|
|
165
|
+
*/
|
|
166
|
+
export declare function readLiveReindexMarkers(dbFile: string, now?: number): {
|
|
167
|
+
protectedMs: number[];
|
|
168
|
+
notes: string[];
|
|
169
|
+
};
|
|
170
|
+
//# sourceMappingURL=agentdb-reindex-marker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentdb-reindex-marker.d.ts","sourceRoot":"","sources":["../src/agentdb-reindex-marker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAOH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD;qFACqF;AACrF,eAAO,MAAM,qBAAqB,QAAiB,CAAC;AAEpD;;;+BAG+B;AAC/B,eAAO,MAAM,8BAA8B,SAAU,CAAC;AAEtD,6FAA6F;AAC7F,MAAM,WAAW,aAAa;IAC5B;;;kEAG8D;IAC9D,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,qFAAqF;IACrF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,kGAAkG;IAClG,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;8DAC0D;IAC1D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;qCAEiC;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;kGAE8F;IAC9F,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IACpC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKlE;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EACvC,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,CAAC,EACX,IAAI,GAAE,gBAAqB,GAC1B,CAAC,CAKH;AAaD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GACrH;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAyC5D;AAWD;;;;;;;GAOG;AACH,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,GACnB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAe9C;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAyBxD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,MAAmB,GACvB;IAAE,WAAW,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAuC5C"}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agentdb snapshot lock + reindex-in-progress marker (feature `agentdb-snapshot-lock`,
|
|
3
|
+
* FR-1..FR-4; fix-round AM-1..AM-4/AM-7 after Codex review Grade D).
|
|
4
|
+
*
|
|
5
|
+
* WHY. Snapshot creation (in `reindexAgentdbRows`), snapshot rotation (`rotatePreReindexSnapshots`,
|
|
6
|
+
* including `dz brain snapshots --prune`) and snapshot restore (`restoreSqliteSnapshot` via
|
|
7
|
+
* `reindexAgentdbRows`'s rollback) are three writers of ONE directory with no mutual exclusion: a
|
|
8
|
+
* concurrent `rotate --keep 0` can delete the very snapshot family a live reindex is relying on as
|
|
9
|
+
* its undo point, with only a 10-minute grace period standing in the way.
|
|
10
|
+
*
|
|
11
|
+
* {@link withAgentdbSnapshotLock} is a thin, dbFile-addressed wrapper over `withNamedLockSync`: the
|
|
12
|
+
* lock lives at `<dirname(dbFile)>/.dz/locks/agentdb-snapshot.lock` — a pure function of the
|
|
13
|
+
* database's OWN directory, never of the caller's cwd, so a project store and the home brain each
|
|
14
|
+
* get their own lock (AC-5) regardless of where `dz` happens to be invoked from. AM-7: it defaults
|
|
15
|
+
* `staleMs` to {@link AGENTDB_SNAPSHOT_LOCK_STALE_MS} (5 minutes) rather than named-lock's ordinary
|
|
16
|
+
* 30-second default — MEASURED (lead, this fix round): `VACUUM INTO` on the owner's hub store
|
|
17
|
+
* (8.45 MB) took 96 ms, so 5 minutes leaves roughly 3000x headroom while staying inside
|
|
18
|
+
* named-lock's own 600 000 ms environment-override ceiling. A caller doing something unusually
|
|
19
|
+
* large may still override via `opts.staleMs`.
|
|
20
|
+
*
|
|
21
|
+
* The critical section this lock guards must stay SHORT and SYNCHRONOUS — file operations only
|
|
22
|
+
* (`VACUUM INTO` / copies / unlink / restore). MEASURED (named-lock.ts round-2, reused verbatim
|
|
23
|
+
* here): a 6s synchronous body under a 2s stale threshold let a THIRD process break in while the
|
|
24
|
+
* second was still inside its own critical section — a long body doesn't just block callers, it
|
|
25
|
+
* breaks the lock's own guarantee. The actual re-embedding (network/CPU-bound `EmbeddingService`
|
|
26
|
+
* calls, row writes) therefore runs OUTSIDE this lock — see `reindexAgentdbRows`.
|
|
27
|
+
*
|
|
28
|
+
* {@link writeReindexMarker} / {@link clearReindexMarker} / {@link readLiveReindexMarkers} implement
|
|
29
|
+
* the SECOND protection this feature adds — a "reindex is currently in flight" marker file beside
|
|
30
|
+
* the database, so a family a live reindex just created is protected from rotation for the
|
|
31
|
+
* DURATION of the reindex, not merely for the few milliseconds the snapshot lock itself is held
|
|
32
|
+
* (the embedding phase can run for a while, unlocked, per FR-2). The marker's liveness is judged by
|
|
33
|
+
* `startedAt` (a wall-clock age, {@link REINDEX_MARKER_TTL_MS}), never by `pid` — a pid is recorded
|
|
34
|
+
* for operator debugging only; a live pid on a different host, or a recycled pid, proves nothing
|
|
35
|
+
* ("pid is not authority over liveness" — the same lesson `store-lock.ts`/`named-lock.ts` already
|
|
36
|
+
* encode for lock staleness). A marker older than the TTL is treated as abandoned: it protects
|
|
37
|
+
* nothing, and rotation removes it, recording why in the report's `notes` — that is an honest
|
|
38
|
+
* observation about a stale marker, never a scan error.
|
|
39
|
+
*
|
|
40
|
+
* AM-1 (fix-round): {@link writeReindexMarker} creates the marker file EXCLUSIVELY
|
|
41
|
+
* (`openSync(path, 'wx')`) and stamps it with an ownership `token` (16 random hex chars). A LIVE
|
|
42
|
+
* marker already at that path (another reindex genuinely in flight) refuses this call outright —
|
|
43
|
+
* `{ ok: false, error }`, no snapshot ever taken for the refused call. A marker at or past the TTL
|
|
44
|
+
* is treated as abandoned and replaced. {@link clearReindexMarker} is compare-and-delete: it removes
|
|
45
|
+
* the marker only when the caller's `token` matches the one on disk, so a process can never tear
|
|
46
|
+
* down a marker it does not own (`{ cleared: false, reason }` otherwise). The narrow race this
|
|
47
|
+
* leaves — two `writeReindexMarker` calls racing the exclusive-create step itself — cannot happen
|
|
48
|
+
* in practice: every caller in this codebase invokes it from INSIDE the very snapshot lock this
|
|
49
|
+
* module also provides (see `reindexAgentdbRows`), so at most one writer is ever in this function
|
|
50
|
+
* at a time.
|
|
51
|
+
*
|
|
52
|
+
* @packageDocumentation
|
|
53
|
+
*/
|
|
54
|
+
import { closeSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
55
|
+
import { randomBytes } from 'node:crypto';
|
|
56
|
+
import { basename, dirname } from 'node:path';
|
|
57
|
+
import { withNamedLockSync } from './named-lock.js';
|
|
58
|
+
/** A marker older than this is abandoned — its family is no longer protected (FR-3). `pid` is
|
|
59
|
+
* recorded for operator debugging only and is NEVER consulted to decide liveness. */
|
|
60
|
+
export const REINDEX_MARKER_TTL_MS = 60 * 60 * 1000;
|
|
61
|
+
/** AM-7: MEASURED (lead, this fix round) — `VACUUM INTO` on the hub's 8.45 MB agentdb store took
|
|
62
|
+
* 96 ms. 5 minutes is ~3000x that, and stays inside named-lock's 600 000 ms override ceiling. A
|
|
63
|
+
* base whose snapshot genuinely needs longer than this needs external coordination, not a bigger
|
|
64
|
+
* default — see the README. */
|
|
65
|
+
export const AGENTDB_SNAPSHOT_LOCK_STALE_MS = 300_000;
|
|
66
|
+
/** The marker's path — always beside the database, never in a separate directory. */
|
|
67
|
+
export function reindexMarkerPath(dbFile) {
|
|
68
|
+
return `${dbFile}.reindex-inprogress.json`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* AM-4: recompute a marker's `ms` from the ACTUAL backup path's filename — decoupled from whatever
|
|
72
|
+
* internal `ms` variable a caller used to build a DEFAULT `backupPath`. A caller-supplied
|
|
73
|
+
* `opts.backupPath` that does not end in the standard `.pre-reindex-<digits>.bak` shape (or whose
|
|
74
|
+
* digits overflow a safe integer) names no family at all, so it maps to `null` — such a target is
|
|
75
|
+
* never a rotation candidate, and the marker must not claim to protect one.
|
|
76
|
+
*/
|
|
77
|
+
export function msFromBackupPath(backupPath) {
|
|
78
|
+
const match = /\.pre-reindex-(\d+)\.bak$/.exec(basename(backupPath));
|
|
79
|
+
if (match === null)
|
|
80
|
+
return null;
|
|
81
|
+
const ms = Number(match[1]);
|
|
82
|
+
return Number.isSafeInteger(ms) ? ms : null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Run `fn` while holding the agentdb snapshot lock for `dbFile` — a pure function of the
|
|
86
|
+
* database's own directory (AC-5), never of `process.cwd()`. Propagates `NamedLockTimeoutError`
|
|
87
|
+
* (FR-4): a caller that cannot acquire the lock by the deadline must report the failure explicitly,
|
|
88
|
+
* never proceed unlocked and never silently skip its work. AM-7: `staleMs` defaults to
|
|
89
|
+
* {@link AGENTDB_SNAPSHOT_LOCK_STALE_MS} unless the caller names its own.
|
|
90
|
+
*/
|
|
91
|
+
export function withAgentdbSnapshotLock(dbFile, fn, opts = {}) {
|
|
92
|
+
return withNamedLockSync(dirname(dbFile), 'agentdb-snapshot', fn, {
|
|
93
|
+
staleMs: AGENTDB_SNAPSHOT_LOCK_STALE_MS,
|
|
94
|
+
...opts,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** Read + best-effort-parse the marker at `path`; `undefined` on any missing/unreadable/corrupt
|
|
98
|
+
* field — callers treat that identically to "no marker" or "not provably live", never as live. */
|
|
99
|
+
function tryReadStartedAt(path) {
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
102
|
+
return typeof parsed.startedAt === 'number' && Number.isFinite(parsed.startedAt) ? parsed.startedAt : undefined;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* AM-1: write the reindex-in-progress marker EXCLUSIVELY. A marker already at this path that is
|
|
110
|
+
* still LIVE (younger than {@link REINDEX_MARKER_TTL_MS}) refuses this write outright —
|
|
111
|
+
* `{ ok: false, error }` naming the marker path, with no snapshot ever attempted by the caller for
|
|
112
|
+
* this call (the caller checks `ok` BEFORE touching the database — see `reindexAgentdbRows`). A
|
|
113
|
+
* marker that is missing, unreadable/corrupt, or past the TTL is replaced. The written marker
|
|
114
|
+
* carries a fresh ownership `token`; only {@link clearReindexMarker} presenting that SAME token may
|
|
115
|
+
* remove it.
|
|
116
|
+
*/
|
|
117
|
+
export function writeReindexMarker(dbFile, marker) {
|
|
118
|
+
const path = reindexMarkerPath(dbFile);
|
|
119
|
+
const token = randomBytes(8).toString('hex'); // 16 hex chars
|
|
120
|
+
const full = { ...marker, token };
|
|
121
|
+
const content = `${JSON.stringify(full)}\n`;
|
|
122
|
+
const tryCreate = () => {
|
|
123
|
+
let fd;
|
|
124
|
+
try {
|
|
125
|
+
fd = openSync(path, 'wx'); // exclusive create — EEXIST if a marker is already there
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (err.code === 'EEXIST')
|
|
129
|
+
return false;
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
writeFileSync(fd, content);
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
closeSync(fd);
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
};
|
|
140
|
+
if (tryCreate())
|
|
141
|
+
return { ok: true, token };
|
|
142
|
+
const existing = tryReadMarker(path);
|
|
143
|
+
if (existing?.requiresRecovery === true) {
|
|
144
|
+
return { ok: false, error: `recovery required: a previous reindex of this store failed to roll back — restore ${existing.backupPath ?? 'its snapshot'} manually, then remove marker ${path}` };
|
|
145
|
+
}
|
|
146
|
+
const existingStartedAt = tryReadStartedAt(path);
|
|
147
|
+
const live = existingStartedAt !== undefined && Date.now() - existingStartedAt < REINDEX_MARKER_TTL_MS;
|
|
148
|
+
if (live) {
|
|
149
|
+
return { ok: false, error: `reindex already in progress (marker ${path})` };
|
|
150
|
+
}
|
|
151
|
+
// Stale, missing-field, or unparseable — treated as abandoned and replaced.
|
|
152
|
+
rmSync(path, { force: true });
|
|
153
|
+
if (!tryCreate()) {
|
|
154
|
+
// Practically unreachable (nothing else writes this path outside this module's own lock), but
|
|
155
|
+
// never silently claim success over an unexplained race.
|
|
156
|
+
return { ok: false, error: `reindex already in progress (marker ${path})` };
|
|
157
|
+
}
|
|
158
|
+
return { ok: true, token };
|
|
159
|
+
}
|
|
160
|
+
function tryReadMarker(path) {
|
|
161
|
+
try {
|
|
162
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
163
|
+
return parsed !== null && typeof parsed === 'object' ? parsed : undefined;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Lead edit after re-review (Codex D, finding 1): a marker left behind by a FAILED rollback must not
|
|
171
|
+
* quietly expire after the TTL — the snapshot it names may be the only good copy of the store. The
|
|
172
|
+
* owner (token) rewrites the marker atomically (tmp + rename) with `requiresRecovery: true`; from
|
|
173
|
+
* then on {@link readLiveReindexMarkers} protects its family without expiry and
|
|
174
|
+
* {@link writeReindexMarker} refuses every new reindex until an operator removes the marker.
|
|
175
|
+
* Call it under the snapshot lock, like every other marker mutation.
|
|
176
|
+
*/
|
|
177
|
+
export function markReindexMarkerRecoveryRequired(dbFile, token, recoveryNote) {
|
|
178
|
+
const path = reindexMarkerPath(dbFile);
|
|
179
|
+
const existing = tryReadMarker(path);
|
|
180
|
+
if (existing === undefined)
|
|
181
|
+
return { ok: false, reason: `marker at ${path} is missing or unreadable` };
|
|
182
|
+
if (existing.token !== token)
|
|
183
|
+
return { ok: false, reason: `marker at ${path} is owned by a different token` };
|
|
184
|
+
const full = { ...existing, requiresRecovery: true, recoveryNote };
|
|
185
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
186
|
+
try {
|
|
187
|
+
writeFileSync(tmp, `${JSON.stringify(full)}\n`);
|
|
188
|
+
renameSync(tmp, path);
|
|
189
|
+
return { ok: true };
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
try {
|
|
193
|
+
rmSync(tmp, { force: true });
|
|
194
|
+
}
|
|
195
|
+
catch { /* best-effort */ }
|
|
196
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* AM-1: compare-and-delete. Removes the marker ONLY when its `token` matches the one on disk — a
|
|
201
|
+
* caller can never tear down a marker it does not own. Absence of the marker file is treated as an
|
|
202
|
+
* already-cleared success (idempotent). Called from `reindexAgentdbRows`'s cleanup on every path
|
|
203
|
+
* that does NOT leave a failed rollback behind (AM-3) — "absence of a receipt is not success" cuts
|
|
204
|
+
* the other way here too: a marker left behind after its owner finished would falsely protect a
|
|
205
|
+
* family forever (until the TTL), so ownership-checked removal must still run unconditionally on
|
|
206
|
+
* every path that is safe to clear.
|
|
207
|
+
*/
|
|
208
|
+
export function clearReindexMarker(dbFile, token) {
|
|
209
|
+
const path = reindexMarkerPath(dbFile);
|
|
210
|
+
let raw;
|
|
211
|
+
try {
|
|
212
|
+
raw = readFileSync(path, 'utf8');
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
if (err.code === 'ENOENT')
|
|
216
|
+
return { cleared: true };
|
|
217
|
+
return { cleared: false, reason: `cannot read marker: ${err instanceof Error ? err.message : String(err)}` };
|
|
218
|
+
}
|
|
219
|
+
let parsed;
|
|
220
|
+
try {
|
|
221
|
+
parsed = JSON.parse(raw);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return { cleared: false, reason: 'marker is not owned by this token (unparseable marker content)' };
|
|
225
|
+
}
|
|
226
|
+
const owner = parsed !== null && typeof parsed === 'object' ? parsed.token : undefined;
|
|
227
|
+
if (owner !== token) {
|
|
228
|
+
return { cleared: false, reason: `marker at ${path} is owned by a different token` };
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
rmSync(path, { force: true });
|
|
232
|
+
return { cleared: true };
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
return { cleared: false, reason: `removal failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Read the marker beside `dbFile` and classify it: a marker younger than
|
|
240
|
+
* {@link REINDEX_MARKER_TTL_MS} protects its `ms` (FR-3) — but ONLY when `ms` is an actual number
|
|
241
|
+
* (AM-4: a `ms: null` marker names no family, so it protects nothing, live or not). A marker at or
|
|
242
|
+
* past the TTL is abandoned — it is removed here (rotation must not act on stale-but-still-present
|
|
243
|
+
* state on its NEXT call). AM-6: the note reports "ignored and removed" ONLY once `rmSync` actually
|
|
244
|
+
* succeeded; a removal failure (e.g. a permission error) is reported as "ignored, removal failed:
|
|
245
|
+
* <err>" — never silently claimed as removed. No marker, or a marker this function cannot parse
|
|
246
|
+
* (corrupt / missing fields), yields `{ protectedMs: [], notes: [] }` — never a throw: a malformed
|
|
247
|
+
* marker must not abort rotation, and it also cannot honestly claim to protect anything.
|
|
248
|
+
*/
|
|
249
|
+
export function readLiveReindexMarkers(dbFile, now = Date.now()) {
|
|
250
|
+
const path = reindexMarkerPath(dbFile);
|
|
251
|
+
let raw;
|
|
252
|
+
try {
|
|
253
|
+
raw = readFileSync(path, 'utf8');
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return { protectedMs: [], notes: [] };
|
|
257
|
+
}
|
|
258
|
+
let parsed;
|
|
259
|
+
try {
|
|
260
|
+
parsed = JSON.parse(raw);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return { protectedMs: [], notes: [] };
|
|
264
|
+
}
|
|
265
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
266
|
+
return { protectedMs: [], notes: [] };
|
|
267
|
+
const { ms, startedAt } = parsed;
|
|
268
|
+
if (typeof startedAt !== 'number' || !Number.isFinite(startedAt)) {
|
|
269
|
+
return { protectedMs: [], notes: [] };
|
|
270
|
+
}
|
|
271
|
+
const hasFamily = typeof ms === 'number' && Number.isFinite(ms);
|
|
272
|
+
if (parsed.requiresRecovery === true) {
|
|
273
|
+
const label = hasFamily ? `ms=${ms}` : 'ms=(none — non-standard backupPath)';
|
|
274
|
+
return { protectedMs: hasFamily ? [ms] : [], notes: [`recovery-required reindex marker (${label}) protects its family without expiry — restore the snapshot manually, then remove ${path}`] };
|
|
275
|
+
}
|
|
276
|
+
if (now - startedAt < REINDEX_MARKER_TTL_MS) {
|
|
277
|
+
return { protectedMs: hasFamily ? [ms] : [], notes: [] };
|
|
278
|
+
}
|
|
279
|
+
// AM-6: only claim "removed" once `rmSync` actually succeeded.
|
|
280
|
+
let removalError;
|
|
281
|
+
try {
|
|
282
|
+
rmSync(path, { force: true });
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
removalError = err instanceof Error ? err.message : String(err);
|
|
286
|
+
}
|
|
287
|
+
const label = hasFamily ? `ms=${ms}` : 'ms=(none — non-standard backupPath)';
|
|
288
|
+
const note = removalError === undefined
|
|
289
|
+
? `stale reindex marker (${label}, startedAt=${new Date(startedAt).toISOString()}) ignored and removed`
|
|
290
|
+
: `stale reindex marker (${label}, startedAt=${new Date(startedAt).toISOString()}) ignored, removal failed: ${removalError}`;
|
|
291
|
+
return { protectedMs: [], notes: [note] };
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=agentdb-reindex-marker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentdb-reindex-marker.js","sourceRoot":"","sources":["../src/agentdb-reindex-marker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-reindex snapshot rotation (feature `reindex-snapshot-rotation`, FR-1..FR-8).
|
|
3
|
+
*
|
|
4
|
+
* `reindexAgentdbRows` copies the store to `<db>.pre-reindex-<ms>.bak` (+ `.embed-manifest.json` /
|
|
5
|
+
* `-shm` / `-wal` siblings) before every reindex, as an undo point. Nothing has ever pruned them,
|
|
6
|
+
* so they accumulate without bound — 13 snapshots / 50 MB observed on the owner's hub, and no
|
|
7
|
+
* command removed a single one.
|
|
8
|
+
*
|
|
9
|
+
* This module is the PURE planning half (NFR-2) — {@link listPreReindexSnapshots} (fs READ) and
|
|
10
|
+
* {@link planSnapshotRotation} (no fs at all) — plus the thin fs-EFFECT wrapper
|
|
11
|
+
* {@link rotatePreReindexSnapshots} that acts on the plan. A snapshot is a FAMILY of files sharing
|
|
12
|
+
* one `<ms>` timestamp (FR-2): the family is removed or kept as a unit, never partially.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
/** One file belonging to a snapshot family — always a basename, never a full path. */
|
|
17
|
+
export interface SnapshotFile {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly bytes: number;
|
|
20
|
+
}
|
|
21
|
+
/** Every file sharing one `<ms>` timestamp — the unit rotation acts on (FR-2). */
|
|
22
|
+
export interface SnapshotFamily {
|
|
23
|
+
readonly ms: number;
|
|
24
|
+
readonly files: readonly SnapshotFile[];
|
|
25
|
+
readonly bytes: number;
|
|
26
|
+
}
|
|
27
|
+
/** A rotation decision: which families survive, which are slated for removal. */
|
|
28
|
+
export interface SnapshotRotationPlan {
|
|
29
|
+
readonly kept: readonly SnapshotFamily[];
|
|
30
|
+
readonly removed: readonly SnapshotFamily[];
|
|
31
|
+
}
|
|
32
|
+
/** The fs-effect outcome (FR-4) — reported poimённо (by file name), never just a count. */
|
|
33
|
+
export interface SnapshotRotationReport {
|
|
34
|
+
readonly kept: string[];
|
|
35
|
+
readonly removed: string[];
|
|
36
|
+
readonly removedBytes: number;
|
|
37
|
+
readonly keep: number;
|
|
38
|
+
/** Per-file unlink failures (FR-5) — never fatal to the caller's own success. */
|
|
39
|
+
readonly errors?: string[];
|
|
40
|
+
/**
|
|
41
|
+
* AM-4 (fix-round after Codex review Grade D): a `readdirSync`/`lstatSync` failure other than
|
|
42
|
+
* ENOENT makes the candidate list INCOMPLETE — a family the scan never saw would look "not old"
|
|
43
|
+
* only because it was invisible, never because it truly survives review. Present ⇒ this call
|
|
44
|
+
* removed NOTHING, however conservative that looks against a nonzero `kept`.
|
|
45
|
+
*/
|
|
46
|
+
readonly scanErrors?: string[];
|
|
47
|
+
/**
|
|
48
|
+
* AM-2: `<ms>` of every family where a sibling unlink failed and the family's `.bak` — the one
|
|
49
|
+
* file that alone still proves the snapshot ever existed — was therefore left in place rather
|
|
50
|
+
* than deleted out from under a family whose removal turned out to be only partial.
|
|
51
|
+
*/
|
|
52
|
+
readonly partialFamilies?: number[];
|
|
53
|
+
/**
|
|
54
|
+
* FR-3 (agentdb-snapshot-lock): honest observations that are neither an error nor a removal —
|
|
55
|
+
* today, exactly one shape: a reindex-in-progress marker found PAST its TTL, ignored and deleted.
|
|
56
|
+
* Never populated for a marker that is still live (it simply protects its family silently) nor
|
|
57
|
+
* for the ordinary case of no marker at all.
|
|
58
|
+
*/
|
|
59
|
+
readonly notes?: string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Scan the directory next to `dbFile` for candidate families — the shared innards of
|
|
63
|
+
* {@link listPreReindexSnapshots} (which drops `scanErrors` for callers that only ever listed) and
|
|
64
|
+
* {@link rotatePreReindexSnapshots} (which must see them, per AM-4: an incomplete scan must never be
|
|
65
|
+
* read as "these are all the old ones").
|
|
66
|
+
*
|
|
67
|
+
* NFR-1: symlinks are never followed and never treated as candidates. `lstatSync` — never `stat` —
|
|
68
|
+
* decides this, so a matching NAME that resolves to a symlink is excluded entirely (AC-3), not
|
|
69
|
+
* "rotated by its link size". An orphaned sibling (e.g. a `-shm` with no `.bak`) still forms its
|
|
70
|
+
* own one-file family under its `<ms>` (AC-5) — FR-2 groups by timestamp, not by which siblings
|
|
71
|
+
* happen to exist.
|
|
72
|
+
*
|
|
73
|
+
* AM-5: a `<ms>` that is not a safe non-negative integer within `Date`'s representable range
|
|
74
|
+
* (`±8.64e15`) is never grouped into a candidate family — so it never reaches `new Date(ms)`, which
|
|
75
|
+
* throws `RangeError` past that bound, and it is never silently treated as "the oldest one".
|
|
76
|
+
*/
|
|
77
|
+
export declare function scanSnapshotDir(dbFile: string): {
|
|
78
|
+
families: SnapshotFamily[];
|
|
79
|
+
scanErrors: string[];
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* List every pre-reindex snapshot family sitting next to `dbFile`, newest `ms` first (families
|
|
83
|
+
* unsorted internally are sorted here; per-family files are name-sorted for determinism). See
|
|
84
|
+
* {@link scanSnapshotDir} for the scan rules (NFR-1, AM-5); this wrapper drops `scanErrors` — callers
|
|
85
|
+
* that only ever want to LIST (never delete) have no unsafe decision to gate on them.
|
|
86
|
+
*/
|
|
87
|
+
export declare function listPreReindexSnapshots(dbFile: string): SnapshotFamily[];
|
|
88
|
+
/**
|
|
89
|
+
* Pure decision (NFR-2, zero fs): given the families found in a directory, decide which survive.
|
|
90
|
+
* The newest `keep` families survive (`keep` clamped to ≥ 0); the freshly created backup is
|
|
91
|
+
* normally the newest family, so it is ordinarily already inside that top-`keep` slice. `protectMs`
|
|
92
|
+
* names it explicitly and is the FALLBACK that fires only when it would otherwise fall OUTSIDE that
|
|
93
|
+
* slice — the boundary case named by FR-1: at `keep=0` the top slice is empty, yet the snapshot this
|
|
94
|
+
* very call just wrote must never be deleted. `families` need not be pre-sorted.
|
|
95
|
+
*
|
|
96
|
+
* AM-3 (fix-round): a family younger than `graceMs` (default {@link DEFAULT_GRACE_MS}, 10 minutes;
|
|
97
|
+
* `ms > now - graceMs`) is rescued exactly like `protectMs`, even past `keep` — a fresh snapshot from
|
|
98
|
+
* a DIFFERENT process/run than the one calling this must never look "old" just because nobody named
|
|
99
|
+
* it via `protectMs`. There is no override: `now`/`graceMs` exist for deterministic tests, not for a
|
|
100
|
+
* CLI knob that would let someone talk the grace period down to zero.
|
|
101
|
+
*/
|
|
102
|
+
export declare function planSnapshotRotation(families: readonly SnapshotFamily[], opts: {
|
|
103
|
+
readonly keep: number;
|
|
104
|
+
readonly protectMs?: number;
|
|
105
|
+
readonly now?: number;
|
|
106
|
+
readonly graceMs?: number;
|
|
107
|
+
}): SnapshotRotationPlan;
|
|
108
|
+
/** Options shared by {@link rotatePreReindexSnapshotsUnlocked} and {@link rotatePreReindexSnapshots}. */
|
|
109
|
+
export interface RotateSnapshotsOptions {
|
|
110
|
+
readonly keep?: number;
|
|
111
|
+
readonly protectPath?: string;
|
|
112
|
+
readonly now?: number;
|
|
113
|
+
readonly graceMs?: number;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The fs-effect half: list, plan, delete. A per-file `unlink` failure lands in `errors[]` and never
|
|
117
|
+
* stops the rest of the rotation (FR-5) — by the time this runs, the caller has already produced a
|
|
118
|
+
* successful reindex, and a rotation hiccup must never be read as a reindex failure. `removedBytes`
|
|
119
|
+
* counts only bytes of files ACTUALLY unlinked, so a partial failure never overstates what was freed.
|
|
120
|
+
*
|
|
121
|
+
* AM-1 (fix-round, Codex review Grade D): `keep` is validated HERE, before any fs read or delete.
|
|
122
|
+
* The bug this closes: `Math.max(0, NaN)` is `NaN`, and `sorted.slice(0, NaN)` is `[]` — an EMPTY
|
|
123
|
+
* kept slice, so every existing family looked "older than keep" and got removed. A `keep` that is
|
|
124
|
+
* not a non-negative safe integer is refused outright; nothing is read, nothing is touched.
|
|
125
|
+
*
|
|
126
|
+
* AM-4: a scan error (readdir/lstat failing with anything but ENOENT) makes the candidate list
|
|
127
|
+
* INCOMPLETE — deciding "these are the old ones" off a partial list is exactly the class of mistake
|
|
128
|
+
* this rotation exists to avoid for the snapshots themselves, so this call removes NOTHING.
|
|
129
|
+
*
|
|
130
|
+
* AM-2: within a removed family, siblings unlink FIRST and the `.bak` LAST, and only once every
|
|
131
|
+
* sibling actually unlinked — a failed sibling leaves the `.bak` in place (the one file that alone
|
|
132
|
+
* still proves the snapshot ever existed) and names the family in `partialFamilies` rather than
|
|
133
|
+
* guessing the family is gone.
|
|
134
|
+
*
|
|
135
|
+
* FR-3 (agentdb-snapshot-lock): a family named by a LIVE reindex-in-progress marker
|
|
136
|
+
* ({@link readLiveReindexMarkers}) is rescued from `removed` exactly like `protectPath`, even past
|
|
137
|
+
* `keep` and even past `graceMs` — a reindex that is still running must never lose the undo point it
|
|
138
|
+
* is relying on. An EXPIRED marker is not a scan error: it is removed and named in `notes`.
|
|
139
|
+
*
|
|
140
|
+
* UNLOCKED by design (NFR-2): this is the internal primitive `rotatePreReindexSnapshots` (the public,
|
|
141
|
+
* locked entry point) and `reindexAgentdbRows`'s own success-path rotation both call — each under
|
|
142
|
+
* their OWN `withAgentdbSnapshotLock`, so the lock is never acquired twice in one call stack.
|
|
143
|
+
*/
|
|
144
|
+
export declare function rotatePreReindexSnapshotsUnlocked(dbFile: string, opts?: RotateSnapshotsOptions): SnapshotRotationReport;
|
|
145
|
+
/**
|
|
146
|
+
* The PUBLIC, locked rotation entry point (FR-1) — used by `dz brain snapshots --prune` and by any
|
|
147
|
+
* other caller outside `reindexAgentdbRows`'s own success path (which calls
|
|
148
|
+
* {@link rotatePreReindexSnapshotsUnlocked} directly under its own lock, per NFR-2). A
|
|
149
|
+
* `NamedLockTimeoutError` — the lock stayed held past the deadline — is reported exactly like any
|
|
150
|
+
* other rotation refusal: `{ removed: [], errors: ['lock busy: …'] }` (FR-4), never a throw.
|
|
151
|
+
*
|
|
152
|
+
* `lockTimeoutMs` is test/tuning-only: omitted, the lock uses its ordinary default deadline.
|
|
153
|
+
*
|
|
154
|
+
* AM-4 (carried forward): `withNamedLockSync` creates `<dirname(dbFile)>/.dz/locks/` before it ever
|
|
155
|
+
* runs `fn` — if `dirname(dbFile)` exists but is NOT a directory (a pathological store path, exactly
|
|
156
|
+
* the shape AM-4's own fixture uses), that `mkdirSync` throws `ENOTDIR` before any lock is even
|
|
157
|
+
* attempted. Scanning is therefore checked FIRST, outside the lock: an unreadable directory is a
|
|
158
|
+
* property of the directory, not of contention over it, and must report the SAME honest
|
|
159
|
+
* `scanErrors` (never touching, never throwing) whether or not locking is even reachable. MEASURED
|
|
160
|
+
* (fix round, this feature): omitting this pre-check turned the pre-existing
|
|
161
|
+
* `rotatePreReindexSnapshots(brokenDbFile, …)` / `dz brain snapshots --prune` "unscannable
|
|
162
|
+
* directory" fixtures from a clean `scanErrors` report into an uncaught `ENOTDIR` thrown out of the
|
|
163
|
+
* lock's own `mkdirSync` — reddening both `test/agentdb-snapshot-rotation.test.ts`'s AM-4 case and
|
|
164
|
+
* `harness-cli/test/brain-snapshots.test.ts`'s "prune mode: an unscannable store directory" case.
|
|
165
|
+
*/
|
|
166
|
+
export declare function rotatePreReindexSnapshots(dbFile: string, opts?: RotateSnapshotsOptions & {
|
|
167
|
+
readonly lockTimeoutMs?: number;
|
|
168
|
+
}): SnapshotRotationReport;
|
|
169
|
+
//# sourceMappingURL=agentdb-snapshot-rotation.d.ts.map
|