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.
- package/.claude/guidance/shipped/moflo-memory-protocol.md +5 -1
- package/.claude/guidance/shipped/moflo-memorydb-maintenance.md +22 -11
- package/.claude/guidance/shipped/moflo-root-cause-discipline.md +47 -0
- package/.claude/helpers/statusline.cjs +69 -33
- package/.claude/helpers/subagent-bootstrap.json +1 -1
- package/.claude/helpers/subagent-start.cjs +1 -1
- package/bin/build-embeddings.mjs +6 -20
- package/bin/cli.js +5 -0
- package/bin/generate-code-map.mjs +4 -24
- package/bin/hooks.mjs +3 -12
- package/bin/index-all.mjs +3 -13
- package/bin/index-guidance.mjs +36 -85
- package/bin/index-patterns.mjs +6 -24
- package/bin/index-tests.mjs +4 -23
- package/bin/lib/db-repair.mjs +358 -62
- package/bin/lib/get-backend.mjs +306 -0
- package/bin/lib/incremental-write.mjs +27 -7
- package/bin/lib/moflo-paths.mjs +64 -4
- package/bin/lib/suppress-sqlite-warning.mjs +57 -0
- package/bin/migrations/knowledge-purge.mjs +7 -8
- package/bin/migrations/knowledge-to-learnings.mjs +7 -9
- package/bin/migrations/purge-doc-entries.mjs +7 -8
- package/bin/migrations/strip-context-preambles.mjs +4 -6
- package/bin/run-migrations.mjs +1 -10
- package/bin/semantic-search.mjs +7 -18
- package/bin/session-start-launcher.mjs +144 -108
- package/bin/simplify-classify.cjs +38 -17
- package/dist/src/cli/commands/daemon.js +38 -11
- package/dist/src/cli/commands/doctor-checks-config.js +60 -0
- package/dist/src/cli/commands/doctor-checks-coverage-truth.js +136 -0
- package/dist/src/cli/commands/doctor-checks-memory-access.js +146 -86
- package/dist/src/cli/commands/doctor-checks-memory.js +13 -18
- package/dist/src/cli/commands/doctor-checks-version-skew.js +94 -0
- package/dist/src/cli/commands/doctor-checks-writers-audit.js +170 -0
- package/dist/src/cli/commands/doctor-embedding-hygiene.js +3 -15
- package/dist/src/cli/commands/doctor-fixes.js +87 -0
- package/dist/src/cli/commands/doctor-registry.js +24 -1
- package/dist/src/cli/commands/doctor.js +1 -1
- package/dist/src/cli/commands/embeddings.js +17 -22
- package/dist/src/cli/commands/memory.js +13 -23
- package/dist/src/cli/embeddings/persistent-cache.js +44 -83
- package/dist/src/cli/init/moflo-init.js +40 -0
- package/dist/src/cli/mcp-tools/memory-tools.js +10 -3
- package/dist/src/cli/memory/bridge-core.js +256 -30
- package/dist/src/cli/memory/bridge-embedder.js +84 -3
- package/dist/src/cli/memory/bridge-entries.js +70 -6
- package/dist/src/cli/memory/controller-registry.js +7 -2
- package/dist/src/cli/memory/controllers/batch-operations.js +5 -1
- package/dist/src/cli/memory/controllers/hierarchical-memory.js +7 -2
- package/dist/src/cli/memory/controllers/mutation-guard.js +22 -2
- package/dist/src/cli/memory/daemon-backend.js +400 -0
- package/dist/src/cli/memory/daemon-write-client.js +192 -15
- package/dist/src/cli/memory/database-provider.js +57 -40
- package/dist/src/cli/memory/hnsw-persistence.js +6 -8
- package/dist/src/cli/memory/index.js +0 -1
- package/dist/src/cli/memory/memory-bridge.js +40 -8
- package/dist/src/cli/memory/memory-initializer.js +271 -211
- package/dist/src/cli/memory/rvf-migration.js +25 -11
- package/dist/src/cli/memory/sqlite-backend.js +573 -0
- package/dist/src/cli/memory/suppress-sqlite-warning.js +49 -0
- package/dist/src/cli/services/cherry-pick-learnings.js +32 -21
- package/dist/src/cli/services/daemon-dashboard.js +13 -1
- package/dist/src/cli/services/daemon-lock.js +58 -1
- package/dist/src/cli/services/daemon-memory-rpc.js +245 -10
- package/dist/src/cli/services/embeddings-migration.js +9 -12
- package/dist/src/cli/services/ephemeral-namespace-purge.js +21 -16
- package/dist/src/cli/services/learning-service.js +12 -20
- package/dist/src/cli/services/memory-db-integrity-repair.js +119 -0
- package/dist/src/cli/services/project-root.js +69 -9
- package/dist/src/cli/services/soft-delete-purge.js +6 -11
- package/dist/src/cli/services/sqljs-migration-store.js +4 -1
- package/dist/src/cli/services/subagent-bootstrap.js +1 -1
- package/dist/src/cli/shared/events/event-store.js +26 -55
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -4
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|