moflo 4.9.37 → 4.10.1

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 (76) hide show
  1. package/.claude/guidance/shipped/moflo-memory-protocol.md +5 -1
  2. package/.claude/guidance/shipped/moflo-memorydb-maintenance.md +22 -11
  3. package/.claude/guidance/shipped/moflo-root-cause-discipline.md +47 -0
  4. package/.claude/helpers/statusline.cjs +69 -33
  5. package/.claude/helpers/subagent-bootstrap.json +1 -1
  6. package/.claude/helpers/subagent-start.cjs +1 -1
  7. package/bin/build-embeddings.mjs +6 -20
  8. package/bin/cli.js +5 -0
  9. package/bin/generate-code-map.mjs +4 -24
  10. package/bin/hooks.mjs +3 -12
  11. package/bin/index-all.mjs +3 -13
  12. package/bin/index-guidance.mjs +36 -85
  13. package/bin/index-patterns.mjs +6 -24
  14. package/bin/index-tests.mjs +4 -23
  15. package/bin/lib/db-repair.mjs +358 -62
  16. package/bin/lib/get-backend.mjs +306 -0
  17. package/bin/lib/incremental-write.mjs +27 -7
  18. package/bin/lib/moflo-paths.mjs +64 -4
  19. package/bin/lib/suppress-sqlite-warning.mjs +57 -0
  20. package/bin/migrations/knowledge-purge.mjs +7 -8
  21. package/bin/migrations/knowledge-to-learnings.mjs +7 -9
  22. package/bin/migrations/purge-doc-entries.mjs +7 -8
  23. package/bin/migrations/strip-context-preambles.mjs +4 -6
  24. package/bin/run-migrations.mjs +1 -10
  25. package/bin/semantic-search.mjs +7 -18
  26. package/bin/session-start-launcher.mjs +144 -108
  27. package/bin/simplify-classify.cjs +38 -17
  28. package/dist/src/cli/commands/daemon.js +38 -11
  29. package/dist/src/cli/commands/doctor-checks-config.js +60 -0
  30. package/dist/src/cli/commands/doctor-checks-coverage-truth.js +136 -0
  31. package/dist/src/cli/commands/doctor-checks-memory-access.js +146 -86
  32. package/dist/src/cli/commands/doctor-checks-memory.js +13 -18
  33. package/dist/src/cli/commands/doctor-checks-version-skew.js +94 -0
  34. package/dist/src/cli/commands/doctor-checks-writers-audit.js +170 -0
  35. package/dist/src/cli/commands/doctor-embedding-hygiene.js +3 -15
  36. package/dist/src/cli/commands/doctor-fixes.js +87 -0
  37. package/dist/src/cli/commands/doctor-registry.js +24 -1
  38. package/dist/src/cli/commands/doctor.js +1 -1
  39. package/dist/src/cli/commands/embeddings.js +17 -22
  40. package/dist/src/cli/commands/memory.js +13 -23
  41. package/dist/src/cli/embeddings/persistent-cache.js +44 -83
  42. package/dist/src/cli/init/moflo-init.js +40 -0
  43. package/dist/src/cli/mcp-tools/memory-tools.js +10 -3
  44. package/dist/src/cli/memory/bridge-core.js +256 -30
  45. package/dist/src/cli/memory/bridge-embedder.js +84 -3
  46. package/dist/src/cli/memory/bridge-entries.js +70 -6
  47. package/dist/src/cli/memory/controller-registry.js +7 -2
  48. package/dist/src/cli/memory/controllers/batch-operations.js +5 -1
  49. package/dist/src/cli/memory/controllers/hierarchical-memory.js +7 -2
  50. package/dist/src/cli/memory/controllers/mutation-guard.js +22 -2
  51. package/dist/src/cli/memory/daemon-backend.js +400 -0
  52. package/dist/src/cli/memory/daemon-write-client.js +192 -15
  53. package/dist/src/cli/memory/database-provider.js +57 -40
  54. package/dist/src/cli/memory/hnsw-persistence.js +6 -8
  55. package/dist/src/cli/memory/index.js +0 -1
  56. package/dist/src/cli/memory/memory-bridge.js +40 -8
  57. package/dist/src/cli/memory/memory-initializer.js +271 -211
  58. package/dist/src/cli/memory/rvf-migration.js +25 -11
  59. package/dist/src/cli/memory/sqlite-backend.js +573 -0
  60. package/dist/src/cli/memory/suppress-sqlite-warning.js +49 -0
  61. package/dist/src/cli/services/cherry-pick-learnings.js +32 -21
  62. package/dist/src/cli/services/daemon-dashboard.js +13 -1
  63. package/dist/src/cli/services/daemon-lock.js +58 -1
  64. package/dist/src/cli/services/daemon-memory-rpc.js +245 -10
  65. package/dist/src/cli/services/embeddings-migration.js +9 -12
  66. package/dist/src/cli/services/ephemeral-namespace-purge.js +21 -16
  67. package/dist/src/cli/services/learning-service.js +12 -20
  68. package/dist/src/cli/services/memory-db-integrity-repair.js +119 -0
  69. package/dist/src/cli/services/project-root.js +69 -9
  70. package/dist/src/cli/services/soft-delete-purge.js +6 -11
  71. package/dist/src/cli/services/sqljs-migration-store.js +4 -1
  72. package/dist/src/cli/services/subagent-bootstrap.js +1 -1
  73. package/dist/src/cli/shared/events/event-store.js +26 -55
  74. package/dist/src/cli/version.js +1 -1
  75. package/package.json +2 -4
  76. package/dist/src/cli/memory/sqljs-backend.js +0 -643
@@ -108,7 +108,10 @@ export class HierarchicalMemory {
108
108
  * transaction. Rolls back on exception and rethrows.
109
109
  */
110
110
  transaction(fn) {
111
- this.db.run('BEGIN TRANSACTION');
111
+ // BEGIN IMMEDIATE — busy_handler engages on lock acquisition. Plain
112
+ // `BEGIN` (= deferred) would fail-fast if the inner fn reads-then-
113
+ // writes while another process holds RESERVED (#1099 / #1098 trap).
114
+ this.db.run('BEGIN IMMEDIATE');
112
115
  return Promise.resolve()
113
116
  .then(() => fn())
114
117
  .then((result) => {
@@ -187,7 +190,9 @@ export class HierarchicalMemory {
187
190
  if (ids.length === 0)
188
191
  return;
189
192
  const now = Date.now();
190
- this.db.run('BEGIN TRANSACTION');
193
+ // BEGIN IMMEDIATE — pure write batch. See transaction() above for the
194
+ // #1099 rationale; converting consistently keeps the policy uniform.
195
+ this.db.run('BEGIN IMMEDIATE');
191
196
  try {
192
197
  for (const id of ids) {
193
198
  this.db.run(`UPDATE ${TABLE} SET access_count = access_count + 1, accessed_at = ? WHERE id = ?`, [now, id]);
@@ -49,8 +49,28 @@ export class MutationGuard {
49
49
  if (!input.bypassDedupe && entries.some((e) => e.hash === hash)) {
50
50
  return { allowed: false, reason: 'duplicate mutation within dedupe window' };
51
51
  }
52
- this.record(op, entries, hash, ts);
53
- return { allowed: true };
52
+ // #1098: defer the recording — return a token instead of fire-and-
53
+ // forget recording at validate-time. Callers commit() after the
54
+ // write succeeds; failed writes leave the buffer clean so retries
55
+ // can re-validate without seeing themselves as duplicates.
56
+ return { allowed: true, token: { op, hash, ts } };
57
+ }
58
+ /**
59
+ * Record a previously-validated mutation in the dedupe buffer. Call
60
+ * exactly once per token, only after the corresponding write has
61
+ * succeeded. No-op for tokens that don't match a watched op — those
62
+ * never produce a real recording in the first place.
63
+ *
64
+ * Silent no-op if `token` is null/undefined so callers can do
65
+ * `guard.commit(result.token)` without a null check.
66
+ */
67
+ commit(token) {
68
+ if (!token)
69
+ return;
70
+ if (!this.isWatched(token.op))
71
+ return;
72
+ const entries = this.pruneAndFetch(token.op, token.ts);
73
+ this.record(token.op, entries, token.hash, token.ts);
54
74
  }
55
75
  reset() {
56
76
  this.recent.clear();
@@ -0,0 +1,400 @@
1
+ /**
2
+ * Daemon-side memory DB factory — TS twin of `bin/lib/get-backend.mjs`.
3
+ *
4
+ * Returns a handle whose surface matches the sql.js `Database` type that the
5
+ * controller registry + bridge code expect (prepare → Statement, run, exec,
6
+ * close, plus a `save()` that maps to the engine's preferred persistence).
7
+ * Currently always returns a node:sqlite-backed adapter — Phase 4 (#1083)
8
+ * flipped the SQLite default; Phase 5 (#1084) deletes the remaining sql.js
9
+ * paths in the bridge + memory-initializer.
10
+ *
11
+ * The sql.js Statement API the bridge code relies on:
12
+ * - db.prepare(sql) → stmt
13
+ * - db.run(sql, params?)
14
+ * - db.exec(sql) → [{ columns, values }]
15
+ * - db.close()
16
+ * - stmt.bind(params)
17
+ * - stmt.step() → boolean
18
+ * - stmt.getAsObject() → row object
19
+ * - stmt.run(params?) → boolean
20
+ * - stmt.free()
21
+ *
22
+ * node:sqlite's `StatementSync` is stateless (each call takes params), so we
23
+ * shim a stateful wrapper via `stmt.iterate(...)` opened on first `step()`.
24
+ * This is the same shape implemented in bin/lib/get-backend.mjs — keep the
25
+ * two in lockstep until Phase 5 collapses them.
26
+ *
27
+ * @module v3/memory/daemon-backend
28
+ */
29
+ // MUST come before `import 'node:sqlite'` below — that module fires
30
+ // `ExperimentalWarning: SQLite is an experimental feature` exactly once per
31
+ // process on first load. Once it fires, there's no way to scrub it from
32
+ // stderr, and consumer-smoke's 200-char stderr tails get filled with it
33
+ // (hiding the real error message; #1098).
34
+ import './suppress-sqlite-warning.js';
35
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
36
+ import { dirname } from 'node:path';
37
+ import { DatabaseSync } from 'node:sqlite';
38
+ /**
39
+ * Coerce a caller-supplied parameter set into node:sqlite's `SQLInputValue[]`
40
+ * shape. Callers pass `any` (sql.js parity) so we just pass through after
41
+ * normalising null/undefined/array.
42
+ */
43
+ function toParamsArray(params) {
44
+ if (params === undefined || params === null)
45
+ return [];
46
+ if (Array.isArray(params))
47
+ return params;
48
+ // sql.js's `Statement.bind(obj)` named-param shape isn't reachable from
49
+ // moflo's bridge code today — every caller passes an array. Tolerate
50
+ // anyway by wrapping the lone value.
51
+ return [params];
52
+ }
53
+ function wrapStatement(stmt) {
54
+ let pendingParams = [];
55
+ let iter = null;
56
+ let currentRow = null;
57
+ let columnNamesCache = null;
58
+ const ensureIter = () => {
59
+ if (!iter) {
60
+ iter = (pendingParams.length > 0
61
+ ? stmt.iterate(...pendingParams)
62
+ : stmt.iterate());
63
+ }
64
+ };
65
+ return {
66
+ bind(params) {
67
+ pendingParams = toParamsArray(params);
68
+ iter = null;
69
+ currentRow = null;
70
+ return true;
71
+ },
72
+ step() {
73
+ ensureIter();
74
+ const next = iter.next();
75
+ if (next.done) {
76
+ currentRow = null;
77
+ return false;
78
+ }
79
+ currentRow = next.value;
80
+ return true;
81
+ },
82
+ getAsObject(params) {
83
+ // sql.js semantics: with params it's a one-shot bind+step+return;
84
+ // without params it returns whatever the last step() materialised. The
85
+ // bridge uses both shapes (one-shot in bridge-entries.ts:bridgeGetEntry,
86
+ // iterator in list/search). Returning {} from the one-shot form when
87
+ // there's no row is correct (caller checks for nullish primary key).
88
+ if (params !== undefined) {
89
+ pendingParams = toParamsArray(params);
90
+ iter = null;
91
+ ensureIter();
92
+ const next = iter.next();
93
+ if (next.done) {
94
+ currentRow = null;
95
+ return {};
96
+ }
97
+ currentRow = next.value;
98
+ return currentRow;
99
+ }
100
+ return currentRow ?? {};
101
+ },
102
+ get(params) {
103
+ // sql.js `Statement.get()` semantics:
104
+ // - With params: one-shot bind+step, returns positional values of the
105
+ // first row (or [] if no rows).
106
+ // - Without params: returns positional values of the CURRENT row —
107
+ // the row that the most-recent `step()` landed on. Callers use it
108
+ // in a `while (stmt.step()) { const row = stmt.get(); ... }` loop;
109
+ // calling iter.next() again here would skip every other row.
110
+ if (params !== undefined) {
111
+ pendingParams = toParamsArray(params);
112
+ iter = null;
113
+ ensureIter();
114
+ const next = iter.next();
115
+ if (next.done) {
116
+ currentRow = null;
117
+ return [];
118
+ }
119
+ currentRow = next.value;
120
+ }
121
+ if (!currentRow)
122
+ return [];
123
+ const cols = columnNamesCache ?? Object.keys(currentRow);
124
+ columnNamesCache = cols;
125
+ return cols.map((c) => currentRow[c]);
126
+ },
127
+ getColumnNames() {
128
+ if (columnNamesCache)
129
+ return columnNamesCache;
130
+ // Force one step to materialise a row so column names are knowable.
131
+ ensureIter();
132
+ const next = iter.next();
133
+ if (next.done) {
134
+ columnNamesCache = [];
135
+ currentRow = null;
136
+ return [];
137
+ }
138
+ currentRow = next.value;
139
+ columnNamesCache = Object.keys(currentRow);
140
+ return columnNamesCache;
141
+ },
142
+ run(params) {
143
+ const arr = toParamsArray(params);
144
+ if (arr.length > 0)
145
+ stmt.run(...arr);
146
+ else
147
+ stmt.run();
148
+ return true;
149
+ },
150
+ reset() {
151
+ iter = null;
152
+ currentRow = null;
153
+ },
154
+ free() {
155
+ iter = null;
156
+ currentRow = null;
157
+ pendingParams = [];
158
+ columnNamesCache = null;
159
+ },
160
+ };
161
+ }
162
+ /**
163
+ * Per-process dedupe of network-FS warnings — emit once per (dbPath, process).
164
+ * Matches the JS twin's `_networkFsWarnedPaths` set so a session that opens
165
+ * both the daemon adapter and a bin/ writer on the same path only logs once.
166
+ */
167
+ const _networkFsWarnedPaths = new Set();
168
+ /**
169
+ * Read `journal_mode` back after we requested WAL. If the engine returned a
170
+ * different mode (`delete`, `truncate`, `persist`, `memory`, `off`), the
171
+ * underlying filesystem doesn't support WAL's shared-memory sidecar — a
172
+ * strong signal that POSIX advisory locks are also unreliable. Surface a
173
+ * one-line stderr warning naming the path so the user knows to move the
174
+ * project off the network mount. Deduped per (path, process).
175
+ *
176
+ * Twin: `bin/lib/get-backend.mjs:warnIfNotWal`. Must stay in lockstep until
177
+ * Phase 5 (#1084) extracts a shared module.
178
+ */
179
+ function warnIfNotWal(db, dbPath) {
180
+ if (_networkFsWarnedPaths.has(dbPath))
181
+ return;
182
+ let mode;
183
+ try {
184
+ const stmt = db.prepare('PRAGMA journal_mode');
185
+ const row = stmt.get();
186
+ mode = String(row?.journal_mode ?? '').toLowerCase();
187
+ }
188
+ catch {
189
+ return;
190
+ }
191
+ if (mode && mode !== 'wal') {
192
+ _networkFsWarnedPaths.add(dbPath);
193
+ process.stderr.write(`[moflo] WARNING: SQLite journal_mode=${mode} on ${dbPath} (WAL not active). ` +
194
+ `If this directory is on NFS/SMB or another network filesystem, POSIX ` +
195
+ `advisory locks are unreliable and concurrent moflo processes can corrupt ` +
196
+ `the database. Move the project to a local disk to restore multi-process safety.\n`);
197
+ }
198
+ }
199
+ /** @internal — test hook only (resets the dedupe set). */
200
+ export function _resetDaemonNetworkFsWarnings() {
201
+ _networkFsWarnedPaths.clear();
202
+ }
203
+ /**
204
+ * Heuristic: SQL is multi-statement if there's a `;` followed by anything
205
+ * substantive. node:sqlite's `db.prepare()` does NOT throw on multi-statement
206
+ * input — it silently parses only the first statement and discards the rest,
207
+ * which is the worst possible failure mode for DDL batches like
208
+ * `CREATE TABLE a; CREATE INDEX i; CREATE TABLE b;`. Detect explicitly and
209
+ * route multi-stmt SQL to `db.exec()` (which runs every statement).
210
+ *
211
+ * The bridge + controller schema strings don't embed literal `;` inside
212
+ * string literals, so the naive index-of check is sound. If that ever
213
+ * changes, this needs a real tokeniser.
214
+ */
215
+ function isMultiStatement(sql) {
216
+ const trimmed = sql.trimEnd();
217
+ const semi = trimmed.indexOf(';');
218
+ if (semi === -1)
219
+ return false;
220
+ return /\S/.test(trimmed.slice(semi + 1));
221
+ }
222
+ function execAsRowsNodeSqlite(db, sql, params) {
223
+ // Multi-statement DDL: route to db.exec() so every statement runs.
224
+ // (sql.js's exec() runs every statement and returns row sets from any
225
+ // that produce rows; the bridge code only reads [0]?.values, so DDL
226
+ // batches correctly return []. Match that contract.)
227
+ if (isMultiStatement(sql)) {
228
+ db.exec(sql);
229
+ return [];
230
+ }
231
+ let stmt;
232
+ try {
233
+ stmt = db.prepare(sql);
234
+ }
235
+ catch {
236
+ // Last resort for any single-statement SQL that prepare rejects.
237
+ db.exec(sql);
238
+ return [];
239
+ }
240
+ const args = toParamsArray(params);
241
+ const rows = (args.length > 0 ? stmt.all(...args) : stmt.all());
242
+ if (rows.length === 0)
243
+ return [];
244
+ const columns = Object.keys(rows[0]);
245
+ const values = rows.map((r) => columns.map((c) => r[c]));
246
+ return [{ columns, values }];
247
+ }
248
+ /**
249
+ * Open the daemon's memory DB handle. Always returns a node:sqlite-backed
250
+ * adapter shaped like sql.js's Database so the existing bridge + controller
251
+ * surface keeps working.
252
+ *
253
+ * @param dbPath disk path or `:memory:`
254
+ */
255
+ export function openDaemonDatabase(dbPath) {
256
+ // node:sqlite opens forgivingly even on non-SQLite files. Keep parity with
257
+ // openSqlJsDatabase's "create if missing" semantic — DatabaseSync handles
258
+ // file creation for us, BUT does NOT auto-create parent directories. The
259
+ // bridge's first-init path commonly lands on a path whose parent .moflo/
260
+ // doesn't exist yet (fresh consumer install, test fixtures with temp
261
+ // project roots) — without the mkdir below, DatabaseSync throws ENOENT,
262
+ // the controller-registry sets mofloDb=null, and the bridge silently
263
+ // falls back to a raw-sql.js write rooted at process.cwd() (catastrophic
264
+ // path drift bug; #1057 was about exactly this class of issue).
265
+ if (dbPath !== ':memory:') {
266
+ try {
267
+ mkdirSync(dirname(dbPath), { recursive: true });
268
+ }
269
+ catch { /* tolerate — DatabaseSync's ENOENT below is the real signal */ }
270
+ }
271
+ const db = new DatabaseSync(dbPath);
272
+ if (dbPath !== ':memory:') {
273
+ try {
274
+ // busy_timeout MUST be set BEFORE journal_mode=WAL, because the WAL
275
+ // pragma briefly takes an EXCLUSIVE lock and concurrent openers race
276
+ // on it. Without busy_timeout in place, parallel doctor probes /
277
+ // bridge initializations / indexer subprocess opens hit "database is
278
+ // locked" and the bridge tears down (CI #1097). Order matters:
279
+ // 1. busy_timeout — gives every subsequent pragma a retry budget
280
+ // 2. journal_mode = WAL — needs the budget on contention
281
+ // 3. synchronous — purely advisory, can come anytime
282
+ //
283
+ // Budget: 15000ms. The consumer-smoke harness exposes the realistic
284
+ // worst case — a background indexer subprocess opens its own write
285
+ // connection right after `npm install` and walks the entire consumer
286
+ // tree (hundreds of guidance/skill files). The whole-tree first-pass
287
+ // can hold a RESERVED/EXCLUSIVE lock for 5–8s while the doctor
288
+ // foreground probe races against it. 5000ms was the original Phase 4
289
+ // value and ran the budget out under that exact load on Windows CI
290
+ // (#1098); 15000ms gives the indexer's full first-pass time to finish
291
+ // before doctor's probe gives up. The price of being wrong-high here
292
+ // is one slow probe per session, not lost data.
293
+ db.exec('PRAGMA busy_timeout = 15000');
294
+ db.exec('PRAGMA journal_mode = WAL');
295
+ db.exec('PRAGMA synchronous = NORMAL');
296
+ // The daemon is the process most exposed to network-FS edge cases
297
+ // (long-lived MCP server, ~30s of writes per indexer pass). NFS/SMB
298
+ // mounts silently fall back from WAL to a rollback journal — surface
299
+ // a one-line warning so the user knows to move the project off the
300
+ // network mount. Mirrors `bin/lib/get-backend.mjs:warnIfNotWal`.
301
+ warnIfNotWal(db, dbPath);
302
+ }
303
+ catch (err) {
304
+ try {
305
+ db.close();
306
+ }
307
+ catch {
308
+ /* handle already dead */
309
+ }
310
+ throw err;
311
+ }
312
+ }
313
+ // sql.js's `db.run(sql, params?)` and `prepare/run` share state; node:sqlite
314
+ // requires fresh statements per call. Cache prepared statements keyed by SQL
315
+ // text so the indexer-equivalent tight write loops don't churn the compiler.
316
+ const runStmtCache = new Map();
317
+ let changesStmt = null;
318
+ return {
319
+ kind: 'node-sqlite',
320
+ _raw: db,
321
+ prepare(sql) {
322
+ return wrapStatement(db.prepare(sql));
323
+ },
324
+ run(sql, params) {
325
+ const arr = toParamsArray(params);
326
+ if (arr.length > 0) {
327
+ let s = runStmtCache.get(sql);
328
+ if (!s) {
329
+ s = db.prepare(sql);
330
+ runStmtCache.set(sql, s);
331
+ }
332
+ s.run(...arr);
333
+ }
334
+ else {
335
+ db.exec(sql);
336
+ }
337
+ return undefined;
338
+ },
339
+ exec(sql, params) {
340
+ return execAsRowsNodeSqlite(db, sql, params);
341
+ },
342
+ getRowsModified() {
343
+ if (!changesStmt)
344
+ changesStmt = db.prepare('SELECT changes() AS c');
345
+ const row = changesStmt.get();
346
+ const c = row?.c ?? 0;
347
+ return typeof c === 'bigint' ? Number(c) : c;
348
+ },
349
+ save() {
350
+ // node:sqlite persists incrementally via WAL — no-op. The shape exists
351
+ // so bridge-core's persistBridgeDb can call it unconditionally during
352
+ // the Phase 4/5 transition window. Once everything routes through this
353
+ // adapter, the explicit persist call becomes dead code (Phase 5).
354
+ },
355
+ export() {
356
+ // Bridge-core's old persist path used `db.export()` + atomicWriteFileSync.
357
+ // node:sqlite ships a `serialize()` that returns the same shape so the
358
+ // few callers that still need a buffer (e.g. tests, backup tooling) work.
359
+ // The bridge-core persist call itself is being switched to `save()` so
360
+ // this exists only as a safety net during the migration.
361
+ const buf = db.prepare('SELECT 1').all();
362
+ void buf;
363
+ // Real serialize. node:sqlite added `DatabaseSync.prototype.serialize()`
364
+ // in Node 22; the TS type for it landed later, so cast through the
365
+ // engine's runtime API.
366
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
367
+ const ser = db.serialize?.();
368
+ if (ser instanceof Uint8Array)
369
+ return ser;
370
+ if (ser && typeof ser === 'object' && 'buffer' in ser)
371
+ return new Uint8Array(ser);
372
+ // Last resort: read the file off disk. The caller knows the path; we
373
+ // don't, so this branch should never fire under normal flow.
374
+ return new Uint8Array();
375
+ },
376
+ close() {
377
+ runStmtCache.clear();
378
+ changesStmt = null;
379
+ db.close();
380
+ },
381
+ };
382
+ }
383
+ /**
384
+ * Seed an empty daemon DB from an existing file on disk. Equivalent to
385
+ * sql.js's `new SQL.Database(readFileSync(path))` round-trip — node:sqlite
386
+ * opens the path directly so this is just a wrapper that errors when the
387
+ * file doesn't exist (existing callers expected the sql.js behaviour).
388
+ */
389
+ export function openDaemonDatabaseFromFile(dbPath) {
390
+ if (dbPath !== ':memory:' && !existsSync(dbPath)) {
391
+ throw new Error(`Database file not found: ${dbPath}`);
392
+ }
393
+ // Touch readFileSync so callers that previously expected eager I/O still
394
+ // observe the same failure shape (e.g. EACCES errors fire here, not
395
+ // lazily on first query). node:sqlite would lazy-error otherwise.
396
+ if (dbPath !== ':memory:')
397
+ readFileSync(dbPath, { flag: 'r' });
398
+ return openDaemonDatabase(dbPath);
399
+ }
400
+ //# sourceMappingURL=daemon-backend.js.map