@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.
Files changed (129) hide show
  1. package/.dz-manifest.json +233 -93
  2. package/README.md +269 -0
  3. package/dist/agentdb-index.d.ts +45 -0
  4. package/dist/agentdb-index.d.ts.map +1 -1
  5. package/dist/agentdb-index.js +345 -85
  6. package/dist/agentdb-index.js.map +1 -1
  7. package/dist/agentdb-reindex-marker.d.ts +170 -0
  8. package/dist/agentdb-reindex-marker.d.ts.map +1 -0
  9. package/dist/agentdb-reindex-marker.js +293 -0
  10. package/dist/agentdb-reindex-marker.js.map +1 -0
  11. package/dist/agentdb-snapshot-rotation.d.ts +169 -0
  12. package/dist/agentdb-snapshot-rotation.d.ts.map +1 -0
  13. package/dist/agentdb-snapshot-rotation.js +322 -0
  14. package/dist/agentdb-snapshot-rotation.js.map +1 -0
  15. package/dist/agentdb-snapshot.d.ts +126 -0
  16. package/dist/agentdb-snapshot.d.ts.map +1 -0
  17. package/dist/agentdb-snapshot.js +195 -0
  18. package/dist/agentdb-snapshot.js.map +1 -0
  19. package/dist/apply-leg.d.ts +202 -0
  20. package/dist/apply-leg.d.ts.map +1 -0
  21. package/dist/apply-leg.js +975 -0
  22. package/dist/apply-leg.js.map +1 -0
  23. package/dist/book-kb.d.ts.map +1 -1
  24. package/dist/book-kb.js +57 -1
  25. package/dist/book-kb.js.map +1 -1
  26. package/dist/brain.d.ts +3 -0
  27. package/dist/brain.d.ts.map +1 -1
  28. package/dist/brain.js +1 -1
  29. package/dist/brain.js.map +1 -1
  30. package/dist/cadence.d.ts +30 -1
  31. package/dist/cadence.d.ts.map +1 -1
  32. package/dist/cadence.js +66 -2
  33. package/dist/cadence.js.map +1 -1
  34. package/dist/guard.d.ts +14 -0
  35. package/dist/guard.d.ts.map +1 -1
  36. package/dist/guard.js +36 -0
  37. package/dist/guard.js.map +1 -1
  38. package/dist/index.d.ts +23 -8
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +18 -5
  41. package/dist/index.js.map +1 -1
  42. package/dist/integration-probe-worker.d.ts.map +1 -1
  43. package/dist/integration-probe-worker.js +4 -6
  44. package/dist/integration-probe-worker.js.map +1 -1
  45. package/dist/integrations-verify.d.ts.map +1 -1
  46. package/dist/integrations-verify.js +3 -1
  47. package/dist/integrations-verify.js.map +1 -1
  48. package/dist/managed-hooks.d.ts +11 -2
  49. package/dist/managed-hooks.d.ts.map +1 -1
  50. package/dist/managed-hooks.js +33 -7
  51. package/dist/managed-hooks.js.map +1 -1
  52. package/dist/mutation-gate.d.ts +35 -1
  53. package/dist/mutation-gate.d.ts.map +1 -1
  54. package/dist/mutation-gate.js +49 -9
  55. package/dist/mutation-gate.js.map +1 -1
  56. package/dist/operations.d.ts.map +1 -1
  57. package/dist/operations.js +94 -0
  58. package/dist/operations.js.map +1 -1
  59. package/dist/patterns.d.ts.map +1 -1
  60. package/dist/patterns.js +23 -4
  61. package/dist/patterns.js.map +1 -1
  62. package/dist/repository-origin.d.ts +1 -1
  63. package/dist/repository-origin.d.ts.map +1 -1
  64. package/dist/repository-origin.js +7 -1
  65. package/dist/repository-origin.js.map +1 -1
  66. package/dist/round-exec.d.ts +41 -0
  67. package/dist/round-exec.d.ts.map +1 -0
  68. package/dist/round-exec.js +45 -0
  69. package/dist/round-exec.js.map +1 -0
  70. package/dist/round.d.ts +109 -0
  71. package/dist/round.d.ts.map +1 -0
  72. package/dist/round.js +148 -0
  73. package/dist/round.js.map +1 -0
  74. package/dist/run-records.d.ts +8 -0
  75. package/dist/run-records.d.ts.map +1 -1
  76. package/dist/run-records.js +69 -3
  77. package/dist/run-records.js.map +1 -1
  78. package/dist/setup.d.ts +8 -0
  79. package/dist/setup.d.ts.map +1 -1
  80. package/dist/setup.js +166 -3
  81. package/dist/setup.js.map +1 -1
  82. package/dist/sqlite-read-helpers.d.ts +60 -0
  83. package/dist/sqlite-read-helpers.d.ts.map +1 -0
  84. package/dist/sqlite-read-helpers.js +118 -0
  85. package/dist/sqlite-read-helpers.js.map +1 -0
  86. package/dist/statusline.d.ts +20 -3
  87. package/dist/statusline.d.ts.map +1 -1
  88. package/dist/statusline.js +40 -7
  89. package/dist/statusline.js.map +1 -1
  90. package/dist/store-counts.d.ts +30 -0
  91. package/dist/store-counts.d.ts.map +1 -1
  92. package/dist/store-counts.js +189 -0
  93. package/dist/store-counts.js.map +1 -1
  94. package/dist/usage.d.ts +55 -0
  95. package/dist/usage.d.ts.map +1 -1
  96. package/dist/usage.js +184 -33
  97. package/dist/usage.js.map +1 -1
  98. package/dist/vector-tier.d.ts +18 -1
  99. package/dist/vector-tier.d.ts.map +1 -1
  100. package/dist/vector-tier.js +26 -2
  101. package/dist/vector-tier.js.map +1 -1
  102. package/package.json +6 -6
  103. package/sbom.json +442 -92
  104. package/src/agentdb-index.ts +315 -32
  105. package/src/agentdb-reindex-marker.ts +330 -0
  106. package/src/agentdb-snapshot-rotation.ts +393 -0
  107. package/src/agentdb-snapshot.ts +222 -0
  108. package/src/apply-leg.ts +1046 -0
  109. package/src/book-kb.ts +64 -1
  110. package/src/brain.ts +11 -2
  111. package/src/cadence.ts +87 -2
  112. package/src/guard.ts +46 -0
  113. package/src/index.ts +58 -5
  114. package/src/integration-probe-worker.ts +4 -6
  115. package/src/integrations-verify.ts +3 -1
  116. package/src/managed-hooks.ts +32 -7
  117. package/src/mutation-gate.ts +87 -8
  118. package/src/operations.ts +89 -0
  119. package/src/patterns.ts +23 -4
  120. package/src/repository-origin.ts +7 -1
  121. package/src/round-exec.ts +85 -0
  122. package/src/round.ts +228 -0
  123. package/src/run-records.ts +77 -2
  124. package/src/setup.ts +198 -3
  125. package/src/sqlite-read-helpers.ts +128 -0
  126. package/src/statusline.ts +57 -9
  127. package/src/store-counts.ts +245 -1
  128. package/src/usage.ts +243 -29
  129. package/src/vector-tier.ts +45 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentdb-snapshot-rotation.d.ts","sourceRoot":"","sources":["../src/agentdb-snapshot-rotation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAQH,sFAAsF;AACtF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,iFAAiF;AACjF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,SAAS,cAAc,EAAE,CAAC;IACzC,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;CAC7C;AAED,2FAA2F;AAC3F,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAoBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,cAAc,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,CA6CpG;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,EAAE,CAExE;AAaD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,SAAS,cAAc,EAAE,EACnC,IAAI,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7G,oBAAoB,CAmBtB;AAED,yGAAyG;AACzG,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAkBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,sBAA2B,GAChC,sBAAsB,CA2ExB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,sBAAsB,GAAG;IAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO,GACtE,sBAAsB,CAwBxB"}
@@ -0,0 +1,322 @@
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
+ import { lstatSync, readdirSync, unlinkSync } from 'node:fs';
17
+ import { basename, dirname, join } from 'node:path';
18
+ import { readLiveReindexMarkers, withAgentdbSnapshotLock } from './agentdb-reindex-marker.js';
19
+ import { NamedLockTimeoutError } from './named-lock.js';
20
+ /** AM-3: a family younger than this survives rotation even beyond `keep` — no override flag exists;
21
+ * the intent is "never delete something from the last few minutes", not a tunable retention window. */
22
+ const DEFAULT_GRACE_MS = 10 * 60 * 1000;
23
+ /** Escape a string for literal use inside a `RegExp` — basenames may carry `.`, `+`, etc. */
24
+ function escapeRegExp(s) {
25
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
26
+ }
27
+ /**
28
+ * The strict per-db snapshot-family pattern (FR-3): only names literally derived from `dbBasename`
29
+ * match — `agentdb.db.bak`, `other.pre-reindex-1.bak`, or anything else in the directory is left
30
+ * alone. Group 1 is the `<ms>` timestamp; group 2 (optional) is the sibling suffix.
31
+ */
32
+ function snapshotNamePattern(dbBasename) {
33
+ return new RegExp(`^${escapeRegExp(dbBasename)}\\.pre-reindex-(\\d+)\\.bak(\\.embed-manifest\\.json|-shm|-wal)?$`);
34
+ }
35
+ /**
36
+ * Scan the directory next to `dbFile` for candidate families — the shared innards of
37
+ * {@link listPreReindexSnapshots} (which drops `scanErrors` for callers that only ever listed) and
38
+ * {@link rotatePreReindexSnapshots} (which must see them, per AM-4: an incomplete scan must never be
39
+ * read as "these are all the old ones").
40
+ *
41
+ * NFR-1: symlinks are never followed and never treated as candidates. `lstatSync` — never `stat` —
42
+ * decides this, so a matching NAME that resolves to a symlink is excluded entirely (AC-3), not
43
+ * "rotated by its link size". An orphaned sibling (e.g. a `-shm` with no `.bak`) still forms its
44
+ * own one-file family under its `<ms>` (AC-5) — FR-2 groups by timestamp, not by which siblings
45
+ * happen to exist.
46
+ *
47
+ * AM-5: a `<ms>` that is not a safe non-negative integer within `Date`'s representable range
48
+ * (`±8.64e15`) is never grouped into a candidate family — so it never reaches `new Date(ms)`, which
49
+ * throws `RangeError` past that bound, and it is never silently treated as "the oldest one".
50
+ */
51
+ export function scanSnapshotDir(dbFile) {
52
+ const dir = dirname(dbFile);
53
+ const pattern = snapshotNamePattern(basename(dbFile));
54
+ const scanErrors = [];
55
+ let entries;
56
+ try {
57
+ entries = readdirSync(dir);
58
+ }
59
+ catch (err) {
60
+ const code = err.code;
61
+ if (code !== 'ENOENT') {
62
+ scanErrors.push(`readdir ${dir}: ${err instanceof Error ? err.message : String(err)}`);
63
+ }
64
+ return { families: [], scanErrors };
65
+ }
66
+ const byMs = new Map();
67
+ for (const name of entries) {
68
+ const match = pattern.exec(name);
69
+ if (match === null)
70
+ continue;
71
+ const full = join(dir, name);
72
+ let size;
73
+ try {
74
+ const st = lstatSync(full);
75
+ if (!st.isFile())
76
+ continue; // symlinks (and anything else non-regular) are left alone (NFR-1)
77
+ size = st.size;
78
+ }
79
+ catch (err) {
80
+ const code = err.code;
81
+ if (code !== 'ENOENT') {
82
+ scanErrors.push(`lstat ${name}: ${err instanceof Error ? err.message : String(err)}`);
83
+ }
84
+ continue; // ENOENT: vanished between readdir and lstat — nothing left to rotate, not an error
85
+ }
86
+ const ms = Number(match[1]);
87
+ if (!Number.isSafeInteger(ms) || ms < 0 || ms > 8.64e15)
88
+ continue; // AM-5: not a candidate
89
+ const list = byMs.get(ms) ?? [];
90
+ list.push({ name, bytes: size });
91
+ byMs.set(ms, list);
92
+ }
93
+ const families = [...byMs.entries()]
94
+ .map(([ms, files]) => ({
95
+ ms,
96
+ files: [...files].sort((a, b) => a.name.localeCompare(b.name)),
97
+ bytes: files.reduce((sum, f) => sum + f.bytes, 0),
98
+ }))
99
+ .sort((a, b) => b.ms - a.ms);
100
+ return { families, scanErrors };
101
+ }
102
+ /**
103
+ * List every pre-reindex snapshot family sitting next to `dbFile`, newest `ms` first (families
104
+ * unsorted internally are sorted here; per-family files are name-sorted for determinism). See
105
+ * {@link scanSnapshotDir} for the scan rules (NFR-1, AM-5); this wrapper drops `scanErrors` — callers
106
+ * that only ever want to LIST (never delete) have no unsafe decision to gate on them.
107
+ */
108
+ export function listPreReindexSnapshots(dbFile) {
109
+ return scanSnapshotDir(dbFile).families;
110
+ }
111
+ /**
112
+ * Resolve a candidate path (typically the fresh `backupPath` a reindex just wrote) to the `<ms>` it
113
+ * belongs to, under the SAME strict pattern `listPreReindexSnapshots` uses. `undefined` when the
114
+ * path does not name this db's snapshot family at all — e.g. the db never existed, so no backup was
115
+ * ever written.
116
+ */
117
+ function msOf(dbFile, candidatePath) {
118
+ const match = snapshotNamePattern(basename(dbFile)).exec(basename(candidatePath));
119
+ return match === null ? undefined : Number(match[1]);
120
+ }
121
+ /**
122
+ * Pure decision (NFR-2, zero fs): given the families found in a directory, decide which survive.
123
+ * The newest `keep` families survive (`keep` clamped to ≥ 0); the freshly created backup is
124
+ * normally the newest family, so it is ordinarily already inside that top-`keep` slice. `protectMs`
125
+ * names it explicitly and is the FALLBACK that fires only when it would otherwise fall OUTSIDE that
126
+ * slice — the boundary case named by FR-1: at `keep=0` the top slice is empty, yet the snapshot this
127
+ * very call just wrote must never be deleted. `families` need not be pre-sorted.
128
+ *
129
+ * AM-3 (fix-round): a family younger than `graceMs` (default {@link DEFAULT_GRACE_MS}, 10 minutes;
130
+ * `ms > now - graceMs`) is rescued exactly like `protectMs`, even past `keep` — a fresh snapshot from
131
+ * a DIFFERENT process/run than the one calling this must never look "old" just because nobody named
132
+ * it via `protectMs`. There is no override: `now`/`graceMs` exist for deterministic tests, not for a
133
+ * CLI knob that would let someone talk the grace period down to zero.
134
+ */
135
+ export function planSnapshotRotation(families, opts) {
136
+ const sorted = [...families].sort((a, b) => b.ms - a.ms);
137
+ const keepCount = Math.max(0, opts.keep);
138
+ const topKeep = sorted.slice(0, keepCount);
139
+ const protectMs = opts.protectMs;
140
+ const alreadyKept = protectMs !== undefined && topKeep.some((f) => f.ms === protectMs);
141
+ const rescued = protectMs !== undefined && !alreadyKept ? sorted.find((f) => f.ms === protectMs) : undefined;
142
+ const kept = rescued !== undefined ? [rescued, ...topKeep].sort((a, b) => b.ms - a.ms) : topKeep;
143
+ const keptMs = new Set(kept.map((f) => f.ms));
144
+ const removed = sorted.filter((f) => !keptMs.has(f.ms));
145
+ const now = opts.now ?? Date.now();
146
+ const graceMs = opts.graceMs ?? DEFAULT_GRACE_MS;
147
+ const graced = removed.filter((f) => f.ms > now - graceMs);
148
+ if (graced.length === 0)
149
+ return { kept, removed };
150
+ const gracedMs = new Set(graced.map((f) => f.ms));
151
+ return {
152
+ kept: [...kept, ...graced].sort((a, b) => b.ms - a.ms),
153
+ removed: removed.filter((f) => !gracedMs.has(f.ms)),
154
+ };
155
+ }
156
+ /** A lock timeout is reported exactly like any other rotation refusal (FR-4): nothing removed,
157
+ * `errors: ['lock busy: …']` — never a throw out of the public rotation entry point. */
158
+ function lockBusyReport(keep, err) {
159
+ return { kept: [], removed: [], removedBytes: 0, keep, errors: [`lock busy: ${err.message}`] };
160
+ }
161
+ /** AM-1's guarantee ("an invalid `keep` reads nothing, touches nothing") checked ONCE and shared by
162
+ * both the unlocked primitive and the public locked wrapper — so an invalid `keep` short-circuits
163
+ * before the wrapper's own pre-scan `readdir`, not just before the unlocked primitive's. */
164
+ function invalidKeepReport(keep) {
165
+ if (!Number.isSafeInteger(keep) || keep < 0) {
166
+ return { kept: [], removed: [], removedBytes: 0, keep, errors: [`invalid keep: ${keep} (must be a non-negative safe integer)`] };
167
+ }
168
+ return undefined;
169
+ }
170
+ /**
171
+ * The fs-effect half: list, plan, delete. A per-file `unlink` failure lands in `errors[]` and never
172
+ * stops the rest of the rotation (FR-5) — by the time this runs, the caller has already produced a
173
+ * successful reindex, and a rotation hiccup must never be read as a reindex failure. `removedBytes`
174
+ * counts only bytes of files ACTUALLY unlinked, so a partial failure never overstates what was freed.
175
+ *
176
+ * AM-1 (fix-round, Codex review Grade D): `keep` is validated HERE, before any fs read or delete.
177
+ * The bug this closes: `Math.max(0, NaN)` is `NaN`, and `sorted.slice(0, NaN)` is `[]` — an EMPTY
178
+ * kept slice, so every existing family looked "older than keep" and got removed. A `keep` that is
179
+ * not a non-negative safe integer is refused outright; nothing is read, nothing is touched.
180
+ *
181
+ * AM-4: a scan error (readdir/lstat failing with anything but ENOENT) makes the candidate list
182
+ * INCOMPLETE — deciding "these are the old ones" off a partial list is exactly the class of mistake
183
+ * this rotation exists to avoid for the snapshots themselves, so this call removes NOTHING.
184
+ *
185
+ * AM-2: within a removed family, siblings unlink FIRST and the `.bak` LAST, and only once every
186
+ * sibling actually unlinked — a failed sibling leaves the `.bak` in place (the one file that alone
187
+ * still proves the snapshot ever existed) and names the family in `partialFamilies` rather than
188
+ * guessing the family is gone.
189
+ *
190
+ * FR-3 (agentdb-snapshot-lock): a family named by a LIVE reindex-in-progress marker
191
+ * ({@link readLiveReindexMarkers}) is rescued from `removed` exactly like `protectPath`, even past
192
+ * `keep` and even past `graceMs` — a reindex that is still running must never lose the undo point it
193
+ * is relying on. An EXPIRED marker is not a scan error: it is removed and named in `notes`.
194
+ *
195
+ * UNLOCKED by design (NFR-2): this is the internal primitive `rotatePreReindexSnapshots` (the public,
196
+ * locked entry point) and `reindexAgentdbRows`'s own success-path rotation both call — each under
197
+ * their OWN `withAgentdbSnapshotLock`, so the lock is never acquired twice in one call stack.
198
+ */
199
+ export function rotatePreReindexSnapshotsUnlocked(dbFile, opts = {}) {
200
+ const keep = opts.keep ?? 3;
201
+ const invalid = invalidKeepReport(keep);
202
+ if (invalid !== undefined)
203
+ return invalid;
204
+ const dir = dirname(dbFile);
205
+ const { families, scanErrors } = scanSnapshotDir(dbFile);
206
+ if (scanErrors.length > 0) {
207
+ return {
208
+ kept: families.flatMap((f) => f.files.map((file) => file.name)),
209
+ removed: [],
210
+ removedBytes: 0,
211
+ keep,
212
+ scanErrors,
213
+ };
214
+ }
215
+ const protectMs = opts.protectPath !== undefined ? msOf(dbFile, opts.protectPath) : undefined;
216
+ let { kept, removed } = planSnapshotRotation(families, {
217
+ keep,
218
+ ...(protectMs !== undefined ? { protectMs } : {}),
219
+ ...(opts.now !== undefined ? { now: opts.now } : {}),
220
+ ...(opts.graceMs !== undefined ? { graceMs: opts.graceMs } : {}),
221
+ });
222
+ // FR-3: rescue any family a LIVE reindex marker names, however `planSnapshotRotation` above
223
+ // decided — this protection is orthogonal to keep/grace/protectPath and must win over all of them.
224
+ const { protectedMs: markerProtectedMs, notes } = readLiveReindexMarkers(dbFile, opts.now);
225
+ if (markerProtectedMs.length > 0) {
226
+ const protect = new Set(markerProtectedMs);
227
+ const rescued = removed.filter((f) => protect.has(f.ms));
228
+ if (rescued.length > 0) {
229
+ removed = removed.filter((f) => !protect.has(f.ms));
230
+ kept = [...kept, ...rescued].sort((a, b) => b.ms - a.ms);
231
+ }
232
+ }
233
+ const errors = [];
234
+ const partialFamilies = [];
235
+ const removedNames = [];
236
+ let removedBytes = 0;
237
+ for (const family of removed) {
238
+ const bakFile = family.files.find((f) => f.name.endsWith('.bak'));
239
+ const siblings = family.files.filter((f) => f.name !== bakFile?.name);
240
+ let allOk = true;
241
+ for (const file of siblings) {
242
+ try {
243
+ unlinkSync(join(dir, file.name));
244
+ removedNames.push(file.name);
245
+ removedBytes += file.bytes;
246
+ }
247
+ catch (err) {
248
+ allOk = false;
249
+ errors.push(`${file.name}: ${err instanceof Error ? err.message : String(err)}`);
250
+ }
251
+ }
252
+ if (bakFile !== undefined && allOk) {
253
+ try {
254
+ unlinkSync(join(dir, bakFile.name));
255
+ removedNames.push(bakFile.name);
256
+ removedBytes += bakFile.bytes;
257
+ }
258
+ catch (err) {
259
+ allOk = false;
260
+ errors.push(`${bakFile.name}: ${err instanceof Error ? err.message : String(err)}`);
261
+ }
262
+ }
263
+ if (!allOk)
264
+ partialFamilies.push(family.ms);
265
+ }
266
+ const keptNames = kept.flatMap((f) => f.files.map((file) => file.name));
267
+ return {
268
+ kept: keptNames,
269
+ removed: removedNames,
270
+ removedBytes,
271
+ keep,
272
+ ...(errors.length > 0 ? { errors } : {}),
273
+ ...(partialFamilies.length > 0 ? { partialFamilies } : {}),
274
+ ...(notes.length > 0 ? { notes } : {}),
275
+ };
276
+ }
277
+ /**
278
+ * The PUBLIC, locked rotation entry point (FR-1) — used by `dz brain snapshots --prune` and by any
279
+ * other caller outside `reindexAgentdbRows`'s own success path (which calls
280
+ * {@link rotatePreReindexSnapshotsUnlocked} directly under its own lock, per NFR-2). A
281
+ * `NamedLockTimeoutError` — the lock stayed held past the deadline — is reported exactly like any
282
+ * other rotation refusal: `{ removed: [], errors: ['lock busy: …'] }` (FR-4), never a throw.
283
+ *
284
+ * `lockTimeoutMs` is test/tuning-only: omitted, the lock uses its ordinary default deadline.
285
+ *
286
+ * AM-4 (carried forward): `withNamedLockSync` creates `<dirname(dbFile)>/.dz/locks/` before it ever
287
+ * runs `fn` — if `dirname(dbFile)` exists but is NOT a directory (a pathological store path, exactly
288
+ * the shape AM-4's own fixture uses), that `mkdirSync` throws `ENOTDIR` before any lock is even
289
+ * attempted. Scanning is therefore checked FIRST, outside the lock: an unreadable directory is a
290
+ * property of the directory, not of contention over it, and must report the SAME honest
291
+ * `scanErrors` (never touching, never throwing) whether or not locking is even reachable. MEASURED
292
+ * (fix round, this feature): omitting this pre-check turned the pre-existing
293
+ * `rotatePreReindexSnapshots(brokenDbFile, …)` / `dz brain snapshots --prune` "unscannable
294
+ * directory" fixtures from a clean `scanErrors` report into an uncaught `ENOTDIR` thrown out of the
295
+ * lock's own `mkdirSync` — reddening both `test/agentdb-snapshot-rotation.test.ts`'s AM-4 case and
296
+ * `harness-cli/test/brain-snapshots.test.ts`'s "prune mode: an unscannable store directory" case.
297
+ */
298
+ export function rotatePreReindexSnapshots(dbFile, opts = {}) {
299
+ const keep = opts.keep ?? 3;
300
+ const invalid = invalidKeepReport(keep);
301
+ if (invalid !== undefined)
302
+ return invalid;
303
+ const preScan = scanSnapshotDir(dbFile);
304
+ if (preScan.scanErrors.length > 0) {
305
+ return {
306
+ kept: preScan.families.flatMap((f) => f.files.map((file) => file.name)),
307
+ removed: [],
308
+ removedBytes: 0,
309
+ keep,
310
+ scanErrors: preScan.scanErrors,
311
+ };
312
+ }
313
+ try {
314
+ return withAgentdbSnapshotLock(dbFile, () => rotatePreReindexSnapshotsUnlocked(dbFile, opts), opts.lockTimeoutMs !== undefined ? { timeoutMs: opts.lockTimeoutMs } : {});
315
+ }
316
+ catch (err) {
317
+ if (err instanceof NamedLockTimeoutError)
318
+ return lockBusyReport(keep, err);
319
+ throw err;
320
+ }
321
+ }
322
+ //# sourceMappingURL=agentdb-snapshot-rotation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentdb-snapshot-rotation.js","sourceRoot":"","sources":["../src/agentdb-snapshot-rotation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Consistent sqlite snapshot + restore for the pre-reindex undo point (feature
3
+ * `snapshot-wal-consistency`, FR-1..FR-4; fix round AM-1..AM-4).
4
+ *
5
+ * A WAL-mode sqlite database's committed data can live in TWO files: the main `dbFile` and its
6
+ * `-wal` sidecar (frames not yet checkpointed into the main file). The snapshot this module
7
+ * replaces was a bare `copyFileSync(dbFile, backupPath)` — one file, no `-wal` — so a reindex that
8
+ * ran while any writer held the WAL open (autocheckpoint disabled, or simply a live connection
9
+ * between commits) copied a "backup" that was MISSING committed rows. MEASURED (scratch repro,
10
+ * 2026-09-13, `node` + real `better-sqlite3`): a schema + one row checkpointed, then a second row
11
+ * inserted on a connection kept open (`wal_autocheckpoint = 0`) — `copyFileSync` alone yields a
12
+ * backup with 1 row; `VACUUM INTO` on a fresh read-only connection to the SAME live db yields 2.
13
+ * The live db's main file hash and `-wal` size are unchanged before/after the `VACUUM INTO` (the
14
+ * read-only connection never checkpoints or writes) — the safety property NFR-2 names.
15
+ *
16
+ * {@link snapshotSqliteDatabase} (FR-1/FR-2) takes the snapshot through `VACUUM INTO` on a fresh
17
+ * READ-ONLY connection — one self-contained output file with every committed transaction, WAL
18
+ * frames included. If `VACUUM INTO` cannot run (older sqlite, a locked/foreign file, no free disk)
19
+ * — or a caller forces it for a test — it falls back to copying `dbFile` plus its `-wal` sibling
20
+ * (present and non-empty) to `backupPath`/`backupPath-wal`; still strictly better than the old
21
+ * single-file copy, and the fallback is never silent: `method`/`note` name it honestly so a report
22
+ * downstream (`reindexAgentdbRows`'s `snapshotMethod`) never claims a guarantee it did not get.
23
+ *
24
+ * **AM-1 (fix round).** Codex review, Grade C: reusing a `backupPath` left a stale `-wal`/`-shm`
25
+ * sidecar from an EARLIER, unrelated snapshot family sitting next to the new one; `restoreSqliteSnapshot`
26
+ * used to decide whether to restore `-wal` by checking whether `backupPath-wal` merely EXISTS, so that
27
+ * stale sidecar could ride along onto a `vacuum-into` restore that never produced a matching `-wal` of
28
+ * its own. Fixed two ways: `snapshotSqliteDatabase` now clears any `backupPath-wal`/`-shm` sidecar
29
+ * BEFORE either strategy writes anything, and again right after a successful `VACUUM INTO` (whose
30
+ * output is one self-contained file and must never be shadowed); `restoreSqliteSnapshot` now takes the
31
+ * `method` the caller already knows and restores `-wal` ONLY when `method === 'copy+wal'` — never by
32
+ * re-deriving it from file presence.
33
+ *
34
+ * **AM-2 (fix round).** The old WAL-size probe was `existsSync` + best-effort `statSync` inside a
35
+ * `catch { walSize = 0 }` — ANY stat failure (`EACCES`, `EIO`, a raced deletion) was silently read as
36
+ * "no WAL", so a snapshot could report the honest-sounding `method: 'copy'` while actually having
37
+ * skipped a WAL it could not even check. {@link statSizeIfExists} now calls `statSync` directly and
38
+ * treats only a confirmed `ENOENT` as "does not exist" — every other error propagates and aborts the
39
+ * whole snapshot, so `reindexAgentdbRows` reports "snapshot failed", never a falsely-successful `copy`.
40
+ *
41
+ * **AM-4 (fix round).** `snapshotSqliteDatabase` used to happily write into (overwrite) an existing
42
+ * `backupPath`, silently discarding whatever it held — including, combined with AM-1's stale-sidecar
43
+ * bug, a backup that looked complete but carried a wrong-generation `-wal`. It now refuses up front
44
+ * (before `VACUUM INTO` or any copy) when `backupPath` already exists and is non-empty.
45
+ *
46
+ * {@link restoreSqliteSnapshot} (FR-4) is the paired rollback. MEASURED (same repro): copying the
47
+ * old main file back over `dbFile` WITHOUT removing a `-wal` left over from the aborted operation
48
+ * — reopening the "restored" db returned ZERO rows, not the restored one, because sqlite replayed
49
+ * the stale WAL frames (a since-superseded DELETE) on top of the reverted main file. Removing
50
+ * `dbFile-wal`/`dbFile-shm` as part of the restore fixed this: the reopened db then returned
51
+ * exactly the restored row. The caller MUST close its own write connection to `dbFile` before
52
+ * calling this — restoring a file sqlite still holds open is undefined behaviour.
53
+ *
54
+ * **AM-3 (fix round).** `restoreSqliteSnapshot` used to return `void` — a failing `copyFileSync`/
55
+ * `rmSync` inside it threw straight out of the best-effort `rollback()` wrapper in
56
+ * `agentdb-index.ts`, which swallowed it in an empty `catch` and reported nothing: "absence of a
57
+ * receipt is not success" applies to a rollback exactly as much as to a forward operation. It now
58
+ * returns `{ ok: true } | { ok: false, error }` so the caller can name a failed rollback as failed,
59
+ * never as restored.
60
+ *
61
+ * @packageDocumentation
62
+ */
63
+ /** How a snapshot was actually taken — always reported by the caller, never assumed. */
64
+ export type SnapshotMethod = 'vacuum-into' | 'copy+wal' | 'copy';
65
+ /** Outcome of {@link snapshotSqliteDatabase}. */
66
+ export interface SnapshotOutcome {
67
+ readonly method: SnapshotMethod;
68
+ /** Present only when a fallback method was used — the reason `vacuum-into` did not run. */
69
+ readonly note?: string;
70
+ }
71
+ /** Outcome of {@link restoreSqliteSnapshot} (AM-3) — a failed restore is reported, never assumed. */
72
+ export interface RestoreOutcome {
73
+ readonly ok: boolean;
74
+ /** Present only when `ok` is false — why the restore did not fully complete. */
75
+ readonly error?: string;
76
+ }
77
+ /** Minimal better-sqlite3 constructor surface this module needs (read-only open + exec + close). */
78
+ export interface SnapshotDbCtor {
79
+ new (path: string, opts?: {
80
+ readonly?: boolean;
81
+ }): {
82
+ exec: (sql: string) => void;
83
+ close: () => void;
84
+ };
85
+ }
86
+ /**
87
+ * Take a consistent snapshot of `dbFile` into `backupPath` (FR-1/FR-2). Default: open `dbFile`
88
+ * READ-ONLY and run `VACUUM INTO <backupPath>` — one output file holding every committed
89
+ * transaction (including `-wal` frames), with no write to the live database (NFR-2 — MEASURED:
90
+ * main-file hash and `-wal` size are unchanged across the call). On any failure — an older sqlite
91
+ * without `VACUUM INTO`, a locked file, no disk space — or when `opts.strategy` forces it (tests),
92
+ * falls back to `copyFileSync(dbFile, backupPath)` plus a copy of `dbFile-wal` to `backupPath-wal`
93
+ * when the WAL sibling exists and is non-empty; `method` is `'copy+wal'` when the sibling was
94
+ * copied, `'copy'` when there was none to copy. `note` names the fallback reason (FR-2) — set even
95
+ * for a forced-strategy test call, so a caller never has to guess why the fast path was skipped.
96
+ * The read-only connection opened for `VACUUM INTO` is always closed before returning.
97
+ *
98
+ * Throws (no snapshot taken, or an incomplete one left in a fully-cleared state) when: `backupPath`
99
+ * already names an existing, non-empty file (AM-4); or a `-wal` stat probe hits a non-ENOENT error
100
+ * (AM-2, inside the fallback path). Never silently overwrites, never mis-reports a lesser guarantee
101
+ * as a stronger one.
102
+ */
103
+ export declare function snapshotSqliteDatabase(Database: SnapshotDbCtor, dbFile: string, backupPath: string, opts?: {
104
+ strategy?: 'vacuum-into' | 'copy+wal';
105
+ }): SnapshotOutcome;
106
+ /**
107
+ * Restore `dbFile` from a snapshot taken by {@link snapshotSqliteDatabase} (FR-4). The caller MUST
108
+ * close its write connection to `dbFile` BEFORE calling this — this restore is a plain file copy,
109
+ * not a sqlite-mediated rollback, and a live handle can reintroduce exactly the frames being undone.
110
+ *
111
+ * `method` is the EXACT {@link SnapshotMethod} the paired `snapshotSqliteDatabase` call returned
112
+ * (AM-1) — never re-derived from whether `backupPath-wal` happens to exist on disk, which a stale
113
+ * sidecar from an unrelated earlier snapshot at the same path could satisfy. Copies `backupPath`
114
+ * over `dbFile`; when `method === 'copy+wal'`, also restores `backupPath-wal` to `dbFile-wal`.
115
+ * Otherwise (`'vacuum-into'` or `'copy'`, neither of which produced a matching `-wal` of its own) any
116
+ * LIVE `dbFile-wal` — left over from the operation being undone — is removed instead: MEASURED,
117
+ * leaving it in place made a reopened "restored" db replay that WAL's frames (a since-superseded
118
+ * write) on top of the reverted main file, returning zero rows instead of the restored set.
119
+ * `dbFile-shm` is always removed — its offsets are valid only for the `-wal` that no longer matches.
120
+ *
121
+ * Returns `{ ok: true }`, or `{ ok: false, error }` (AM-3) when any step throws — a caller that
122
+ * reported a failed restore as a successful one would leave the store worse than before rollback was
123
+ * attempted, with nothing on record to say so.
124
+ */
125
+ export declare function restoreSqliteSnapshot(dbFile: string, backupPath: string, method: SnapshotMethod): RestoreOutcome;
126
+ //# sourceMappingURL=agentdb-snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentdb-snapshot.d.ts","sourceRoot":"","sources":["../src/agentdb-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAIH,wFAAwF;AACxF,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,UAAU,GAAG,MAAM,CAAC;AAEjE,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,2FAA2F;IAC3F,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qGAAqG;AACrG,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,oGAAoG;AACpG,MAAM,WAAW,cAAc;IAC7B,KAAK,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;CACvG;AA6DD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,aAAa,GAAG,UAAU,CAAA;CAAO,GACnD,eAAe,CAkBjB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,cAAc,CAchH"}