moflo 4.9.36 → 4.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/guidance/shipped/moflo-agent-rules.md +12 -0
- package/.claude/guidance/shipped/moflo-memory-protocol.md +34 -0
- package/.claude/guidance/shipped/moflo-memorydb-maintenance.md +22 -11
- package/.claude/guidance/shipped/moflo-root-cause-discipline.md +47 -0
- package/.claude/guidance/shipped/moflo-subagents.md +4 -0
- package/.claude/helpers/gate.cjs +3 -3
- package/.claude/helpers/statusline.cjs +69 -33
- package/.claude/helpers/subagent-bootstrap.json +1 -1
- package/.claude/helpers/subagent-start.cjs +1 -1
- package/.claude/skills/eldar/SKILL.md +8 -0
- package/bin/build-embeddings.mjs +6 -20
- package/bin/cli.js +5 -0
- package/bin/gate.cjs +3 -3
- 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 +59 -119
- package/bin/index-patterns.mjs +6 -24
- package/bin/index-tests.mjs +4 -23
- package/bin/lib/db-repair.mjs +4 -25
- 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 +52 -0
- package/bin/migrations/strip-context-preambles.mjs +95 -0
- package/bin/run-migrations.mjs +1 -10
- package/bin/semantic-search.mjs +11 -19
- package/bin/session-start-launcher.mjs +102 -100
- package/bin/simplify-classify.cjs +38 -17
- package/dist/src/cli/commands/daemon.js +38 -11
- package/dist/src/cli/commands/doctor-checks-coverage-truth.js +136 -0
- package/dist/src/cli/commands/doctor-checks-memory-access.js +244 -5
- 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 +30 -0
- package/dist/src/cli/commands/doctor-registry.js +14 -0
- 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 +54 -75
- package/dist/src/cli/embeddings/persistent-cache.js +44 -83
- package/dist/src/cli/init/claudemd-generator.js +4 -0
- package/dist/src/cli/init/moflo-init.js +40 -0
- package/dist/src/cli/mcp-tools/memory-tools.js +177 -32
- package/dist/src/cli/memory/bridge-core.js +256 -30
- package/dist/src/cli/memory/bridge-entries.js +76 -8
- 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 +286 -220
- 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 +6 -11
- package/dist/src/cli/services/learning-service.js +12 -20
- 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
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-JS factory for moflo.db low-level SQL handles — JS twin of the
|
|
3
|
+
* `openDaemonDatabase` factory in `src/cli/memory/daemon-backend.ts`. Every
|
|
4
|
+
* `bin/` script that opens `.moflo/moflo.db` MUST go through {@link openBackend}
|
|
5
|
+
* so the engine choice stays consistent with the rest of the runtime.
|
|
6
|
+
*
|
|
7
|
+
* Backend selection: always `node:sqlite` (Phase 5 / #1084 — sql.js has been
|
|
8
|
+
* deleted from the package). The `resolveBackend()` shim is retained because
|
|
9
|
+
* a handful of tests still pass an explicit `backend` option; it now validates
|
|
10
|
+
* the value but only honours `'node-sqlite'`.
|
|
11
|
+
*
|
|
12
|
+
* Engine surface — the handle exposes the **sql.js low-level Statement API**
|
|
13
|
+
* because every existing bin/ caller was written against it (db.prepare/
|
|
14
|
+
* stmt.bind/step/getAsObject/free/run, db.run/exec, db.export-via-save,
|
|
15
|
+
* db.close). For `node:sqlite`, the adapter emulates `stmt.bind()/step()/
|
|
16
|
+
* getAsObject()` via `StatementSync.iterate()` so callers don't refactor
|
|
17
|
+
* their loops.
|
|
18
|
+
*
|
|
19
|
+
* Persistence semantics:
|
|
20
|
+
* - node:sqlite — writes through the OS file handle under WAL; `save()` is
|
|
21
|
+
* a no-op kept for API parity. WAL pragmas (`journal_mode=WAL`,
|
|
22
|
+
* `synchronous=NORMAL`, `busy_timeout=15000`) are set on first open per
|
|
23
|
+
* Phase 0 spike (#1079) and Phase 1 backend (#1080).
|
|
24
|
+
*
|
|
25
|
+
* @module bin/lib/get-backend
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// MUST come before any direct/transitive `node:sqlite` import below — the
|
|
29
|
+
// node:sqlite module fires ExperimentalWarning exactly once per process on
|
|
30
|
+
// first load, and once it fires there's no way to scrub it from stderr.
|
|
31
|
+
import './suppress-sqlite-warning.mjs';
|
|
32
|
+
|
|
33
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
34
|
+
import { dirname } from 'node:path';
|
|
35
|
+
import { memoryDbPath } from './moflo-paths.mjs';
|
|
36
|
+
|
|
37
|
+
export const BACKEND_NODE_SQLITE = 'node-sqlite';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the configured backend. Phase 5 (#1084) deleted the sql.js path,
|
|
41
|
+
* so this always returns `node-sqlite`. The `opts.backend` parameter is kept
|
|
42
|
+
* for API compatibility — anything else throws so a stale caller asking for
|
|
43
|
+
* sql.js surfaces a clear error rather than silently dropping to the wrong
|
|
44
|
+
* engine.
|
|
45
|
+
*
|
|
46
|
+
* @param {{ backend?: string }} [opts]
|
|
47
|
+
* @returns {'node-sqlite'}
|
|
48
|
+
*/
|
|
49
|
+
export function resolveBackend(opts = {}) {
|
|
50
|
+
if (opts.backend && opts.backend !== BACKEND_NODE_SQLITE) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Unknown backend "${opts.backend}". moflo only supports "node-sqlite"; ` +
|
|
53
|
+
`sql.js was retired in Phase 5 (#1084).`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return BACKEND_NODE_SQLITE;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ensureDir(filePath) {
|
|
60
|
+
const dir = dirname(filePath);
|
|
61
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Open a low-level SQL backend handle. Defaults to `.moflo/moflo.db` under
|
|
66
|
+
* `projectRoot`; pass `opts.dbPath` to point at a different file (used by
|
|
67
|
+
* migrations that touch sibling DBs).
|
|
68
|
+
*
|
|
69
|
+
* @param {string} projectRoot
|
|
70
|
+
* @param {{
|
|
71
|
+
* backend?: 'node-sqlite',
|
|
72
|
+
* create?: boolean,
|
|
73
|
+
* readOnly?: boolean,
|
|
74
|
+
* dbPath?: string,
|
|
75
|
+
* }} [opts]
|
|
76
|
+
* @returns {Promise<object>} backend handle (see module doc)
|
|
77
|
+
*/
|
|
78
|
+
export async function openBackend(projectRoot, opts = {}) {
|
|
79
|
+
const dbPath = opts.dbPath || memoryDbPath(projectRoot);
|
|
80
|
+
resolveBackend(opts); // throws on stale sql.js callers
|
|
81
|
+
ensureDir(dbPath);
|
|
82
|
+
return openNodeSqlite(dbPath, opts);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// node:sqlite adapter — the only backend as of Phase 5 (#1084)
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
// Module-scope guard so we only fire the network-FS warning once per path
|
|
90
|
+
// per process — the indexer + daemon + bin/ scripts all open the same DB and
|
|
91
|
+
// we don't want N copies of the same message in one session.
|
|
92
|
+
const _networkFsWarnedPaths = new Set();
|
|
93
|
+
|
|
94
|
+
async function openNodeSqlite(dbPath, opts) {
|
|
95
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
96
|
+
const readOnly = opts.readOnly === true;
|
|
97
|
+
const db = new DatabaseSync(dbPath, { readOnly });
|
|
98
|
+
if (!readOnly) {
|
|
99
|
+
// Close the handle on any PRAGMA failure — node:sqlite opens forgivingly
|
|
100
|
+
// (even non-SQLite files succeed in the constructor) and a PRAGMA that
|
|
101
|
+
// throws later would otherwise leak the file handle across processes
|
|
102
|
+
// (visible on Windows as EPERM on subsequent rmdir of the parent).
|
|
103
|
+
try {
|
|
104
|
+
// WAL trinity validated by Phase 0 spike (#1079) and Phase 1 backend.
|
|
105
|
+
// busy_timeout MUST be set BEFORE journal_mode=WAL — the WAL pragma
|
|
106
|
+
// briefly takes an EXCLUSIVE lock, and concurrent openers (parallel
|
|
107
|
+
// doctor probes, indexer subprocess, daemon bridge init) otherwise hit
|
|
108
|
+
// "database is locked" with no retry budget. See #1097.
|
|
109
|
+
// 15000ms — sized for the consumer-smoke worst case where a
|
|
110
|
+
// background indexer holds a write lock for 5–8s during its first
|
|
111
|
+
// full-tree pass after `npm install`. See daemon-backend.ts twin for
|
|
112
|
+
// the full rationale (#1098).
|
|
113
|
+
db.exec('PRAGMA busy_timeout = 15000');
|
|
114
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
115
|
+
db.exec('PRAGMA synchronous = NORMAL');
|
|
116
|
+
// Phase 4 / #1083 — network-FS detection. SQLite's POSIX advisory locks
|
|
117
|
+
// and WAL shared-memory both fail silently on NFS/SMB; the engine falls
|
|
118
|
+
// back to a non-WAL journal mode rather than erroring. Read journal_mode
|
|
119
|
+
// back and warn if it isn't `wal`.
|
|
120
|
+
if (dbPath !== ':memory:') warnIfNotWal(db, dbPath);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
try { db.close(); } catch { /* already-dead handle */ }
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return wrapNodeSqlite(db, dbPath);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Read `journal_mode` back after we requested WAL. If the engine returned a
|
|
131
|
+
* different mode (`delete`, `truncate`, `persist`, `memory`, `off`), the
|
|
132
|
+
* underlying filesystem doesn't support WAL's shared-memory sidecar — a
|
|
133
|
+
* strong signal that POSIX advisory locks are also unreliable. Surface a
|
|
134
|
+
* one-line stderr warning naming the path so the user knows to move the
|
|
135
|
+
* project off the network mount. Deduped per (path, process).
|
|
136
|
+
*
|
|
137
|
+
* Exported so the test in `tests/bin/get-backend.test.ts` can drive a real
|
|
138
|
+
* non-WAL handle through the same probe (a local-disk DB will always come
|
|
139
|
+
* back as WAL, so we can't trigger the warning by simply opening a DB).
|
|
140
|
+
*
|
|
141
|
+
* @param {object} db node:sqlite DatabaseSync handle
|
|
142
|
+
* @param {string} dbPath
|
|
143
|
+
*/
|
|
144
|
+
export function warnIfNotWal(db, dbPath) {
|
|
145
|
+
if (_networkFsWarnedPaths.has(dbPath)) return;
|
|
146
|
+
let mode;
|
|
147
|
+
try {
|
|
148
|
+
const stmt = db.prepare('PRAGMA journal_mode');
|
|
149
|
+
const row = stmt.get();
|
|
150
|
+
mode = String(row?.journal_mode ?? '').toLowerCase();
|
|
151
|
+
} catch {
|
|
152
|
+
// Probe must never break the open path — silent failure is acceptable
|
|
153
|
+
// because the WAL pragma above already either took effect or didn't.
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (mode && mode !== 'wal') {
|
|
157
|
+
_networkFsWarnedPaths.add(dbPath);
|
|
158
|
+
process.stderr.write(
|
|
159
|
+
`[moflo] WARNING: SQLite journal_mode=${mode} on ${dbPath} (WAL not active). ` +
|
|
160
|
+
`If this directory is on NFS/SMB or another network filesystem, POSIX ` +
|
|
161
|
+
`advisory locks are unreliable and concurrent moflo processes can corrupt ` +
|
|
162
|
+
`the database. Move the project to a local disk to restore multi-process safety.\n`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** @internal — test hook only (resets the dedupe set). */
|
|
168
|
+
export function _resetNetworkFsWarnings() {
|
|
169
|
+
_networkFsWarnedPaths.clear();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function wrapNodeSqlite(db, dbPath) {
|
|
173
|
+
// node:sqlite has no `db.changes` field, so the rowsModified probe is a
|
|
174
|
+
// tiny prepared statement reused across calls — preparing on every probe
|
|
175
|
+
// would dominate the indexer's tight write loops.
|
|
176
|
+
let changesStmt = null;
|
|
177
|
+
const getChanges = () => {
|
|
178
|
+
if (!changesStmt) changesStmt = db.prepare('SELECT changes() AS c');
|
|
179
|
+
const row = changesStmt.get();
|
|
180
|
+
return Number(row?.c ?? 0);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Per-connection prepare cache for `db.run(sql, params)` calls — without
|
|
184
|
+
// this the indexer's bulk-DELETE loop (index-guidance:698,699,717) allocates
|
|
185
|
+
// a fresh StatementSync per row, churning the engine's compile cache.
|
|
186
|
+
const runStmtCache = new Map();
|
|
187
|
+
const runWithParams = (sql, params) => {
|
|
188
|
+
let s = runStmtCache.get(sql);
|
|
189
|
+
if (!s) {
|
|
190
|
+
s = db.prepare(sql);
|
|
191
|
+
runStmtCache.set(sql, s);
|
|
192
|
+
}
|
|
193
|
+
s.run(...params);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
kind: BACKEND_NODE_SQLITE,
|
|
198
|
+
prepare: (sql) => wrapNodeSqliteStmt(db.prepare(sql)),
|
|
199
|
+
run: (sql, params) => {
|
|
200
|
+
if (params && params.length > 0) runWithParams(sql, params);
|
|
201
|
+
else db.exec(sql);
|
|
202
|
+
},
|
|
203
|
+
exec: (sql) => execAsRowsNodeSqlite(db, sql),
|
|
204
|
+
getRowsModified: getChanges,
|
|
205
|
+
save: () => {
|
|
206
|
+
// node:sqlite persists incrementally via WAL — explicit save is a no-op.
|
|
207
|
+
// Callers can still invoke `save()` unconditionally; the API parity
|
|
208
|
+
// matters more than micro-optimising one call away.
|
|
209
|
+
},
|
|
210
|
+
close: () => {
|
|
211
|
+
changesStmt = null;
|
|
212
|
+
runStmtCache.clear();
|
|
213
|
+
db.close();
|
|
214
|
+
},
|
|
215
|
+
_raw: db,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Heuristic match for multi-statement SQL: `;` followed by anything
|
|
221
|
+
* substantive. node:sqlite's `db.prepare()` does NOT throw on multi-statement
|
|
222
|
+
* input — it silently parses the first statement only and drops the rest,
|
|
223
|
+
* which is the worst possible failure mode for DDL batches like
|
|
224
|
+
* `CREATE TABLE a; CREATE INDEX i; CREATE TABLE b;`. Detect and route
|
|
225
|
+
* multi-stmt SQL to `db.exec()` (which runs every statement).
|
|
226
|
+
*/
|
|
227
|
+
function isMultiStatement(sql) {
|
|
228
|
+
const trimmed = sql.trimEnd();
|
|
229
|
+
const semi = trimmed.indexOf(';');
|
|
230
|
+
if (semi === -1) return false;
|
|
231
|
+
return /\S/.test(trimmed.slice(semi + 1));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Adapt node:sqlite to sql.js's `db.exec(sql)` return shape:
|
|
236
|
+
* `[{ columns: string[], values: any[][] }]`. The bin scripts use this for
|
|
237
|
+
* single-statement queries that return rows (`PRAGMA integrity_check` in
|
|
238
|
+
* `db-repair.mjs`, `SELECT COUNT(*)` in `index-guidance.mjs`) and for
|
|
239
|
+
* multi-statement DDL batches (controller `ensureSchema`).
|
|
240
|
+
*/
|
|
241
|
+
function execAsRowsNodeSqlite(db, sql) {
|
|
242
|
+
// Multi-statement DDL: db.exec() runs every statement. Matches sql.js's
|
|
243
|
+
// exec() contract — DDL batches return `[]` (no row results to surface).
|
|
244
|
+
if (isMultiStatement(sql)) {
|
|
245
|
+
db.exec(sql);
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
let stmt;
|
|
249
|
+
try {
|
|
250
|
+
stmt = db.prepare(sql);
|
|
251
|
+
} catch {
|
|
252
|
+
db.exec(sql);
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
const rows = stmt.all();
|
|
256
|
+
if (rows.length === 0) return [];
|
|
257
|
+
const columns = Object.keys(rows[0]);
|
|
258
|
+
const values = rows.map((r) => columns.map((c) => r[c]));
|
|
259
|
+
return [{ columns, values }];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function wrapNodeSqliteStmt(stmt) {
|
|
263
|
+
// sql.js statements are stateful (bind → step* → free); node:sqlite's
|
|
264
|
+
// StatementSync is stateless (each call takes its own params). The shim
|
|
265
|
+
// captures the pending params and lazily opens an iterator on first
|
|
266
|
+
// `step()`, releasing the iterator on `free()` so the next `bind()`+
|
|
267
|
+
// `step()` cycle starts cleanly.
|
|
268
|
+
let pendingParams = null;
|
|
269
|
+
let iter = null;
|
|
270
|
+
let currentRow = null;
|
|
271
|
+
return {
|
|
272
|
+
bind: (params) => {
|
|
273
|
+
pendingParams = params && params.length > 0 ? params : null;
|
|
274
|
+
iter = null;
|
|
275
|
+
currentRow = null;
|
|
276
|
+
},
|
|
277
|
+
step: () => {
|
|
278
|
+
if (!iter) {
|
|
279
|
+
iter = pendingParams ? stmt.iterate(...pendingParams) : stmt.iterate();
|
|
280
|
+
}
|
|
281
|
+
const next = iter.next();
|
|
282
|
+
if (next.done) {
|
|
283
|
+
currentRow = null;
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
currentRow = next.value;
|
|
287
|
+
return true;
|
|
288
|
+
},
|
|
289
|
+
getAsObject: () => currentRow || {},
|
|
290
|
+
run: (params) => {
|
|
291
|
+
if (params && params.length > 0) stmt.run(...params);
|
|
292
|
+
else stmt.run();
|
|
293
|
+
},
|
|
294
|
+
free: () => {
|
|
295
|
+
// sql.js's `Statement.free()` finalises the underlying statement;
|
|
296
|
+
// node:sqlite has no per-statement finalize (StatementSync is GC'd
|
|
297
|
+
// when the Database closes). The wrapper's `free()` instead resets
|
|
298
|
+
// the iteration state so the next `bind()`+`step()` cycle starts
|
|
299
|
+
// cleanly. Functional parity with sql.js callers despite the
|
|
300
|
+
// different underlying lifecycle.
|
|
301
|
+
iter = null;
|
|
302
|
+
currentRow = null;
|
|
303
|
+
pendingParams = null;
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
@@ -66,16 +66,30 @@ export function computeContentListHash(files) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* Load `key → content` for every active row in the namespace
|
|
69
|
+
* Load `key → content` for every active row in the namespace, optionally
|
|
70
|
+
* scoped to keys starting with `keyPrefix` (one doc's chunks at a time —
|
|
71
|
+
* lets per-file indexers like `index-guidance.mjs` content-diff without
|
|
72
|
+
* loading every chunk across every file).
|
|
73
|
+
*
|
|
70
74
|
* @param {object} db - sql.js Database
|
|
71
75
|
* @param {string} namespace
|
|
76
|
+
* @param {string} [keyPrefix] — when set, restricts the scan to `key LIKE '<prefix>%'`.
|
|
77
|
+
* The same prefix scopes the orphan sweep in {@link applyIncrementalChunks}.
|
|
72
78
|
* @returns {Map<string,string>}
|
|
73
79
|
*/
|
|
74
|
-
export function loadExistingContent(db, namespace) {
|
|
75
|
-
const stmt =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
export function loadExistingContent(db, namespace, keyPrefix) {
|
|
81
|
+
const stmt = keyPrefix
|
|
82
|
+
? db.prepare(
|
|
83
|
+
`SELECT key, content FROM memory_entries WHERE namespace = ? AND key LIKE ? AND status = 'active'`,
|
|
84
|
+
)
|
|
85
|
+
: db.prepare(
|
|
86
|
+
`SELECT key, content FROM memory_entries WHERE namespace = ? AND status = 'active'`,
|
|
87
|
+
);
|
|
88
|
+
if (keyPrefix) {
|
|
89
|
+
stmt.bind([namespace, `${keyPrefix}%`]);
|
|
90
|
+
} else {
|
|
91
|
+
stmt.bind([namespace]);
|
|
92
|
+
}
|
|
79
93
|
const map = new Map();
|
|
80
94
|
while (stmt.step()) {
|
|
81
95
|
const row = stmt.getAsObject();
|
|
@@ -95,11 +109,17 @@ export function loadExistingContent(db, namespace) {
|
|
|
95
109
|
* @param {object} [opts]
|
|
96
110
|
* @param {boolean} [opts.serialize=true] - JSON.stringify metadata/tags before
|
|
97
111
|
* writing. Set false when callers already pass strings.
|
|
112
|
+
* @param {string} [opts.keyPrefix] — when set, the existing-content load AND
|
|
113
|
+
* the orphan sweep are restricted to keys matching `<prefix>%`. Use this
|
|
114
|
+
* when processing a single file's chunks at a time (e.g. index-guidance.mjs
|
|
115
|
+
* iterates files independently) — without it the sweep would delete every
|
|
116
|
+
* chunk from every OTHER file as an orphan on each call.
|
|
98
117
|
* @returns {{inserted:number, updated:number, unchanged:number, removed:number}}
|
|
99
118
|
*/
|
|
100
119
|
export function applyIncrementalChunks(db, namespace, chunks, opts = {}) {
|
|
101
120
|
const serialize = opts.serialize !== false;
|
|
102
|
-
const
|
|
121
|
+
const keyPrefix = opts.keyPrefix;
|
|
122
|
+
const existing = loadExistingContent(db, namespace, keyPrefix);
|
|
103
123
|
const newKeys = new Set();
|
|
104
124
|
let inserted = 0;
|
|
105
125
|
let updated = 0;
|
package/bin/lib/moflo-paths.mjs
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure-JS counterpart to src/cli/services/moflo-paths.ts
|
|
2
|
+
* Pure-JS counterpart to src/cli/services/moflo-paths.ts and the
|
|
3
|
+
* findProjectRoot helper in src/cli/services/project-root.ts.
|
|
3
4
|
*
|
|
4
5
|
* Lives in bin/lib because session-start-launcher.mjs and other bin/ scripts
|
|
5
6
|
* run before any TS compilation has happened — they can't import the .ts
|
|
6
|
-
* source. The TS
|
|
7
|
-
* exposes the same path constants + helpers
|
|
7
|
+
* source. The TS versions are the canonical programmatic API; this file
|
|
8
|
+
* exposes the same path constants + helpers and MUST stay algorithmically
|
|
9
|
+
* identical (see tests/system/project-root-twin.test.ts).
|
|
8
10
|
*
|
|
9
11
|
* Per #851, the legacy `.claude-flow/` rename + `.swarm/memory.db` byte-copy
|
|
10
12
|
* helpers no longer ship: the version-bump-gated cherry-pick lives entirely
|
|
11
13
|
* in the launcher and the TS service `cli/services/cherry-pick-learnings.ts`.
|
|
12
14
|
*/
|
|
13
|
-
import {
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { basename, dirname, join, parse, resolve } from 'node:path';
|
|
14
17
|
|
|
15
18
|
export const MOFLO_DIR = '.moflo';
|
|
16
19
|
export const MEMORY_DB_FILE = 'moflo.db';
|
|
@@ -57,3 +60,60 @@ export function memoryDbCandidatePaths(projectRoot) {
|
|
|
57
60
|
join(projectRoot, '.claude', LEGACY_MEMORY_DB_FILE),
|
|
58
61
|
];
|
|
59
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the project root the same way the TS bridge does. Every bin/
|
|
66
|
+
* script that touches `.moflo/moflo.db` (or any sibling state under
|
|
67
|
+
* `.moflo/`) MUST go through this so its writes land on the SAME file the
|
|
68
|
+
* bridge reads from.
|
|
69
|
+
*
|
|
70
|
+
* Algorithmic twin of `src/cli/services/project-root.ts:findProjectRoot()`
|
|
71
|
+
* and `src/cli/memory/bridge-core.ts:getProjectRoot()`. See those files for
|
|
72
|
+
* the canonical algorithm comment.
|
|
73
|
+
*
|
|
74
|
+
* @param {{ cwd?: string; honorEnv?: boolean }} [opts]
|
|
75
|
+
* @returns {string} absolute project root
|
|
76
|
+
*/
|
|
77
|
+
export function findProjectRoot(opts) {
|
|
78
|
+
const honorEnv = opts?.honorEnv !== false;
|
|
79
|
+
if (honorEnv && process.env.CLAUDE_PROJECT_DIR) {
|
|
80
|
+
return process.env.CLAUDE_PROJECT_DIR;
|
|
81
|
+
}
|
|
82
|
+
const startDir = opts?.cwd ?? process.cwd();
|
|
83
|
+
const start = resolve(startDir);
|
|
84
|
+
const fsRoot = parse(start).root;
|
|
85
|
+
|
|
86
|
+
// High-priority pass: memory markers + CLAUDE.md/package.json pair.
|
|
87
|
+
let dir = start;
|
|
88
|
+
while (dir !== fsRoot) {
|
|
89
|
+
if (basename(dir) === 'node_modules') {
|
|
90
|
+
dir = dirname(dir);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (existsSync(join(dir, '.moflo', 'moflo.db'))) return dir;
|
|
94
|
+
if (existsSync(join(dir, '.swarm', 'memory.db'))) return dir;
|
|
95
|
+
if (existsSync(join(dir, 'CLAUDE.md')) && existsSync(join(dir, 'package.json'))) {
|
|
96
|
+
return dir;
|
|
97
|
+
}
|
|
98
|
+
const parent = dirname(dir);
|
|
99
|
+
if (parent === dir) break;
|
|
100
|
+
dir = parent;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Low-priority pass: bare package.json or .git.
|
|
104
|
+
dir = start;
|
|
105
|
+
while (dir !== fsRoot) {
|
|
106
|
+
if (basename(dir) === 'node_modules') {
|
|
107
|
+
dir = dirname(dir);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (existsSync(join(dir, 'package.json')) || existsSync(join(dir, '.git'))) {
|
|
111
|
+
return dir;
|
|
112
|
+
}
|
|
113
|
+
const parent = dirname(dir);
|
|
114
|
+
if (parent === dir) break;
|
|
115
|
+
dir = parent;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return startDir;
|
|
119
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter out Node's `(node:NNN) ExperimentalWarning: SQLite is an experimental
|
|
3
|
+
* feature and might change at any time` line that fires once per process the
|
|
4
|
+
* first time `node:sqlite` is loaded.
|
|
5
|
+
*
|
|
6
|
+
* Why we suppress: moflo committed to node:sqlite as the only backend in
|
|
7
|
+
* Phase 5 (#1084). The warning is informational from Node's perspective but
|
|
8
|
+
* actively harmful for us because:
|
|
9
|
+
* 1. It lands on stderr → the consumer-smoke harness's 200-char stderr
|
|
10
|
+
* tails (`recordExit`) get filled with the warning prefix, hiding the
|
|
11
|
+
* real failure message behind it (#1098 — `Memory Access Functional`
|
|
12
|
+
* failures looked like ExperimentalWarning failures in CI logs).
|
|
13
|
+
* 2. It appears in every consumer's `flo doctor` / `flo memory init` /
|
|
14
|
+
* `flo daemon start` invocation, polluting their terminal output for
|
|
15
|
+
* something they can't act on.
|
|
16
|
+
*
|
|
17
|
+
* Implementation: replace `process.emitWarning` with a thin filter ONLY for
|
|
18
|
+
* the SQLite warning. Every other warning passes through unchanged — we
|
|
19
|
+
* specifically don't want to broadly mute `--no-warnings`-style suppression
|
|
20
|
+
* because that would also hide e.g. import-attributes ExperimentalWarning
|
|
21
|
+
* which IS something we'd want to know about.
|
|
22
|
+
*
|
|
23
|
+
* The filter is idempotent: re-importing the module is a no-op. Must run
|
|
24
|
+
* BEFORE the first `import 'node:sqlite'` anywhere in the process tree;
|
|
25
|
+
* called from `bin/cli.js`, `bin/lib/get-backend.mjs`, and the daemon-
|
|
26
|
+
* backend module loader.
|
|
27
|
+
*
|
|
28
|
+
* @module bin/lib/suppress-sqlite-warning
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const INSTALLED = Symbol.for('moflo.suppressSqliteWarning.installed');
|
|
32
|
+
|
|
33
|
+
function shouldSuppress(message) {
|
|
34
|
+
if (typeof message === 'string') {
|
|
35
|
+
return message.includes('SQLite is an experimental feature');
|
|
36
|
+
}
|
|
37
|
+
if (message && typeof message === 'object') {
|
|
38
|
+
return typeof message.message === 'string' && message.message.includes('SQLite is an experimental feature');
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!globalThis[INSTALLED]) {
|
|
44
|
+
globalThis[INSTALLED] = true;
|
|
45
|
+
const originalEmitWarning = process.emitWarning;
|
|
46
|
+
process.emitWarning = function (warning, ...args) {
|
|
47
|
+
// Two emit signatures: (message, type, code, ctor) and (message, options).
|
|
48
|
+
// Inspect both `warning` and `args[0]` (which is `type` in the legacy
|
|
49
|
+
// form or `options.type` in the new form) before deciding.
|
|
50
|
+
if (shouldSuppress(warning)) return;
|
|
51
|
+
const typeArg = typeof args[0] === 'string'
|
|
52
|
+
? args[0]
|
|
53
|
+
: (args[0] && typeof args[0] === 'object' ? args[0].type : undefined);
|
|
54
|
+
if (typeArg === 'ExperimentalWarning' && typeof warning === 'string' && warning.includes('SQLite')) return;
|
|
55
|
+
return originalEmitWarning.apply(this, [warning, ...args]);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
* @module bin/migrations/knowledge-purge
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { existsSync
|
|
19
|
-
import { mofloResolveURL } from '../lib/moflo-resolve.mjs';
|
|
18
|
+
import { existsSync } from 'fs';
|
|
20
19
|
import { memoryDbPath } from '../lib/moflo-paths.mjs';
|
|
20
|
+
import { openBackend } from '../lib/get-backend.mjs';
|
|
21
21
|
import { hasMigrationRun } from '../lib/migrations.mjs';
|
|
22
22
|
import { MIGRATED_FROM_KNOWLEDGE } from './lib/markers.mjs';
|
|
23
23
|
|
|
@@ -44,11 +44,10 @@ export async function run(projectRoot) {
|
|
|
44
44
|
const dbPath = memoryDbPath(projectRoot);
|
|
45
45
|
if (!existsSync(dbPath)) return { purged: 0, skipped: 0 };
|
|
46
46
|
|
|
47
|
-
// Lazy-load
|
|
48
|
-
// init cost (~30ms cold).
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const db = new SQL.Database(readFileSync(dbPath));
|
|
47
|
+
// Lazy-load via the backend factory — keeps the manifest-stamped no-op
|
|
48
|
+
// path off the WASM init cost (~30ms cold). Engine selection lives in
|
|
49
|
+
// openBackend() (default: node:sqlite as of #1083 Phase 4).
|
|
50
|
+
const db = await openBackend(projectRoot, { create: false });
|
|
52
51
|
|
|
53
52
|
const knowledgeStmt = db.prepare(
|
|
54
53
|
`SELECT id, key, status FROM memory_entries
|
|
@@ -101,7 +100,7 @@ export async function run(projectRoot) {
|
|
|
101
100
|
deleteStmt.free();
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
if (purged > 0)
|
|
103
|
+
if (purged > 0) db.save();
|
|
105
104
|
db.close();
|
|
106
105
|
return { purged, skipped };
|
|
107
106
|
}
|
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
* @module bin/migrations/knowledge-to-learnings
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync
|
|
14
|
-
import { writeFileSync } from 'fs';
|
|
13
|
+
import { existsSync } from 'fs';
|
|
15
14
|
import { randomBytes } from 'crypto';
|
|
16
|
-
import { mofloResolveURL } from '../lib/moflo-resolve.mjs';
|
|
17
15
|
import { memoryDbPath } from '../lib/moflo-paths.mjs';
|
|
16
|
+
import { openBackend } from '../lib/get-backend.mjs';
|
|
18
17
|
import { MIGRATED_FROM_KNOWLEDGE } from './lib/markers.mjs';
|
|
19
18
|
|
|
20
19
|
export const name = 'knowledge-to-learnings';
|
|
@@ -45,11 +44,10 @@ export async function run(projectRoot) {
|
|
|
45
44
|
const dbPath = memoryDbPath(projectRoot);
|
|
46
45
|
if (!existsSync(dbPath)) return { rowsMigrated: 0, rowsSkipped: 0 };
|
|
47
46
|
|
|
48
|
-
// Lazy-load
|
|
49
|
-
// no-op fast-path where the manifest already records
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const db = new SQL.Database(readFileSync(dbPath));
|
|
47
|
+
// Lazy-load via the backend factory — top-level await would pay the engine
|
|
48
|
+
// init cost even on the no-op fast-path where the manifest already records
|
|
49
|
+
// this migration as done.
|
|
50
|
+
const db = await openBackend(projectRoot, { create: false });
|
|
53
51
|
|
|
54
52
|
const sourceStmt = db.prepare(
|
|
55
53
|
`SELECT id, key, content, type, metadata, tags, embedding, embedding_dimensions,
|
|
@@ -119,7 +117,7 @@ export async function run(projectRoot) {
|
|
|
119
117
|
insertStmt.free();
|
|
120
118
|
}
|
|
121
119
|
|
|
122
|
-
if (migrated > 0)
|
|
120
|
+
if (migrated > 0) db.save();
|
|
123
121
|
db.close();
|
|
124
122
|
return { rowsMigrated: migrated, rowsSkipped: skipped };
|
|
125
123
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: hard-delete every legacy `doc-*` whole-document entry from the
|
|
3
|
+
* guidance namespace. The chunker no longer writes these (#1053 S4) — audit
|
|
4
|
+
* found zero production readers, they duplicated chunk semantic territory,
|
|
5
|
+
* and they ate ~13% of search slots on every query without unique signal.
|
|
6
|
+
*
|
|
7
|
+
* Idempotent: re-runs are no-ops because there will be no `doc-*` rows left.
|
|
8
|
+
*
|
|
9
|
+
* @module bin/migrations/purge-doc-entries
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync } from 'fs';
|
|
13
|
+
import { memoryDbPath } from '../lib/moflo-paths.mjs';
|
|
14
|
+
import { openBackend } from '../lib/get-backend.mjs';
|
|
15
|
+
|
|
16
|
+
export const name = 'purge-doc-entries';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} projectRoot
|
|
20
|
+
* @returns {Promise<{purged:number}>}
|
|
21
|
+
*/
|
|
22
|
+
export async function run(projectRoot) {
|
|
23
|
+
const dbPath = memoryDbPath(projectRoot);
|
|
24
|
+
if (!existsSync(dbPath)) return { purged: 0 };
|
|
25
|
+
|
|
26
|
+
// Lazy-load via the backend factory — keeps the manifest-stamped no-op
|
|
27
|
+
// path off the WASM init cost (~30ms cold). Engine selection lives in
|
|
28
|
+
// openBackend() (default: node:sqlite as of #1083 Phase 4).
|
|
29
|
+
const db = await openBackend(projectRoot, { create: false });
|
|
30
|
+
|
|
31
|
+
// Scope: every namespace, since both `flo memory index-guidance` and
|
|
32
|
+
// `bin/index-guidance.mjs` historically wrote doc-* across whatever
|
|
33
|
+
// namespace the entry was scoped to (default for guidance: `guidance`).
|
|
34
|
+
// Conservative — match the prefix only, never sweep user-stored keys
|
|
35
|
+
// that happen to start with "doc".
|
|
36
|
+
const countStmt = db.prepare(`SELECT COUNT(*) AS cnt FROM memory_entries WHERE key LIKE 'doc-%'`);
|
|
37
|
+
countStmt.step();
|
|
38
|
+
const beforeCount = Number(countStmt.getAsObject().cnt ?? 0);
|
|
39
|
+
countStmt.free();
|
|
40
|
+
|
|
41
|
+
if (beforeCount === 0) {
|
|
42
|
+
db.close();
|
|
43
|
+
return { purged: 0 };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
db.run(`DELETE FROM memory_entries WHERE key LIKE 'doc-%'`);
|
|
47
|
+
const purged = db.getRowsModified?.() ?? beforeCount;
|
|
48
|
+
|
|
49
|
+
if (purged > 0) db.save();
|
|
50
|
+
db.close();
|
|
51
|
+
return { purged };
|
|
52
|
+
}
|