moflo 4.9.37 → 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-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 +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 +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 +102 -102
- 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 +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 +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 +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-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 +269 -209
- 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
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQLite-backed Persistent Cache for Embeddings (
|
|
2
|
+
* SQLite-backed Persistent Cache for Embeddings (node:sqlite)
|
|
3
3
|
*
|
|
4
4
|
* Features:
|
|
5
|
-
* -
|
|
6
|
-
* - Disk persistence
|
|
5
|
+
* - Built-in node:sqlite (Node 22+) — no native compile, no WASM
|
|
6
|
+
* - Disk persistence via WAL — writes are incremental, no whole-file dumps
|
|
7
7
|
* - LRU eviction with configurable max size
|
|
8
8
|
* - Automatic schema creation
|
|
9
9
|
* - TTL support for cache entries
|
|
10
10
|
* - Lazy initialization (no startup cost if not used)
|
|
11
|
+
*
|
|
12
|
+
* Phase 5 (#1084) migrated this from sql.js to node:sqlite via the unified
|
|
13
|
+
* `openDaemonDatabase` factory. The sql.js whole-file-export pattern was the
|
|
14
|
+
* source of the multi-writer clobber class fixed in epic #1078.
|
|
11
15
|
*/
|
|
12
|
-
import { existsSync, mkdirSync,
|
|
16
|
+
import { existsSync, mkdirSync, statSync } from 'fs';
|
|
13
17
|
import { dirname } from 'path';
|
|
14
|
-
import {
|
|
18
|
+
import { openDaemonDatabase } from '../memory/daemon-backend.js';
|
|
15
19
|
/**
|
|
16
|
-
* SQLite-backed persistent embedding cache using
|
|
20
|
+
* SQLite-backed persistent embedding cache using node:sqlite via the
|
|
21
|
+
* unified daemon-backend factory.
|
|
17
22
|
*/
|
|
18
23
|
export class PersistentEmbeddingCache {
|
|
19
24
|
db = null;
|
|
20
|
-
SQL = null;
|
|
21
25
|
initialized = false;
|
|
22
26
|
dirty = false;
|
|
23
27
|
hits = 0;
|
|
24
28
|
misses = 0;
|
|
25
|
-
autoSaveTimer = null;
|
|
26
29
|
dbPath;
|
|
27
30
|
maxSize;
|
|
28
31
|
ttlMs;
|
|
@@ -31,33 +34,29 @@ export class PersistentEmbeddingCache {
|
|
|
31
34
|
this.dbPath = config.dbPath;
|
|
32
35
|
this.maxSize = config.maxSize ?? 10000;
|
|
33
36
|
this.ttlMs = config.ttlMs ?? 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
34
|
-
|
|
37
|
+
// Kept for API compatibility; node:sqlite WAL persists incrementally so
|
|
38
|
+
// there's no auto-save timer to drive any more.
|
|
39
|
+
this.autoSaveInterval = config.autoSaveInterval ?? 30000;
|
|
35
40
|
}
|
|
36
41
|
/**
|
|
37
|
-
* Lazily initialize database connection
|
|
42
|
+
* Lazily initialize database connection.
|
|
43
|
+
*
|
|
44
|
+
* Phase 5 (#1084): swapped the sql.js readFileSync + new SQL.Database
|
|
45
|
+
* round-trip for openDaemonDatabase(dbPath). WAL writes incrementally so
|
|
46
|
+
* the auto-save timer + saveToFile() that used to live here are gone.
|
|
38
47
|
*/
|
|
39
48
|
async ensureInitialized() {
|
|
40
49
|
if (this.initialized)
|
|
41
50
|
return;
|
|
42
51
|
try {
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
// Initialize sql.js (loads WASM)
|
|
46
|
-
this.SQL = await initSqlJs();
|
|
47
|
-
// Ensure directory exists
|
|
52
|
+
// Ensure directory exists (openDaemonDatabase also does this, but the
|
|
53
|
+
// dbExisted probe below needs the path to be stable first).
|
|
48
54
|
const dir = dirname(this.dbPath);
|
|
49
55
|
if (!existsSync(dir)) {
|
|
50
56
|
mkdirSync(dir, { recursive: true });
|
|
51
57
|
}
|
|
52
|
-
// Load existing database or create new
|
|
53
58
|
const dbExisted = existsSync(this.dbPath);
|
|
54
|
-
|
|
55
|
-
const fileBuffer = readFileSync(this.dbPath);
|
|
56
|
-
this.db = new this.SQL.Database(fileBuffer);
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
this.db = new this.SQL.Database();
|
|
60
|
-
}
|
|
59
|
+
this.db = openDaemonDatabase(this.dbPath);
|
|
61
60
|
// Create schema
|
|
62
61
|
this.db.run(`
|
|
63
62
|
CREATE TABLE IF NOT EXISTS embeddings (
|
|
@@ -88,53 +87,16 @@ export class PersistentEmbeddingCache {
|
|
|
88
87
|
}
|
|
89
88
|
// Clean expired entries on startup
|
|
90
89
|
this.cleanExpired();
|
|
91
|
-
// Save after initialization to persist schema
|
|
92
|
-
this.saveToFile();
|
|
93
|
-
// Start auto-save timer
|
|
94
|
-
this.startAutoSave();
|
|
95
90
|
this.initialized = true;
|
|
96
91
|
}
|
|
97
92
|
catch (error) {
|
|
98
|
-
//
|
|
99
|
-
|
|
93
|
+
// node:sqlite is built into Node 22+, so failure here is a real fault
|
|
94
|
+
// (corrupt DB, permission error, etc.) rather than missing dep. Surface
|
|
95
|
+
// and disable the cache so the embedding pipeline keeps working.
|
|
96
|
+
console.warn('[persistent-cache] disabled:', error instanceof Error ? error.message : error);
|
|
100
97
|
this.initialized = true; // Mark as initialized to prevent retry
|
|
101
98
|
}
|
|
102
99
|
}
|
|
103
|
-
/**
|
|
104
|
-
* Start auto-save timer
|
|
105
|
-
*/
|
|
106
|
-
startAutoSave() {
|
|
107
|
-
if (this.autoSaveTimer)
|
|
108
|
-
return;
|
|
109
|
-
this.autoSaveTimer = setInterval(() => {
|
|
110
|
-
if (this.dirty && this.db) {
|
|
111
|
-
this.saveToFile();
|
|
112
|
-
}
|
|
113
|
-
}, this.autoSaveInterval);
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Stop auto-save timer
|
|
117
|
-
*/
|
|
118
|
-
stopAutoSave() {
|
|
119
|
-
if (this.autoSaveTimer) {
|
|
120
|
-
clearInterval(this.autoSaveTimer);
|
|
121
|
-
this.autoSaveTimer = null;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Save database to file
|
|
126
|
-
*/
|
|
127
|
-
saveToFile() {
|
|
128
|
-
if (!this.db)
|
|
129
|
-
return;
|
|
130
|
-
try {
|
|
131
|
-
atomicWriteFileSync(this.dbPath, this.db.export());
|
|
132
|
-
this.dirty = false;
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
console.error('[persistent-cache] Save error:', error);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
100
|
/**
|
|
139
101
|
* Generate cache key from text
|
|
140
102
|
*/
|
|
@@ -284,11 +246,12 @@ export class PersistentEmbeddingCache {
|
|
|
284
246
|
if (this.db) {
|
|
285
247
|
const result = this.db.exec('SELECT COUNT(*) as count FROM embeddings');
|
|
286
248
|
stats.size = result[0]?.values[0]?.[0] ?? 0;
|
|
287
|
-
// Get file size if exists
|
|
249
|
+
// Get file size if exists. node:sqlite leaves the file on disk via WAL
|
|
250
|
+
// so statSync is enough — no whole-file read needed (sql.js used to
|
|
251
|
+
// readFileSync the entire DB to compute size).
|
|
288
252
|
if (existsSync(this.dbPath)) {
|
|
289
253
|
try {
|
|
290
|
-
|
|
291
|
-
stats.dbSizeBytes = buffer.length;
|
|
254
|
+
stats.dbSizeBytes = statSync(this.dbPath).size;
|
|
292
255
|
}
|
|
293
256
|
catch {
|
|
294
257
|
// Ignore
|
|
@@ -298,51 +261,49 @@ export class PersistentEmbeddingCache {
|
|
|
298
261
|
return stats;
|
|
299
262
|
}
|
|
300
263
|
/**
|
|
301
|
-
* Clear all cached entries
|
|
264
|
+
* Clear all cached entries. WAL persists the DELETE incrementally so
|
|
265
|
+
* there's no explicit flush — Phase 5 (#1084) removed the sql.js
|
|
266
|
+
* whole-file save here.
|
|
302
267
|
*/
|
|
303
268
|
async clear() {
|
|
304
269
|
await this.ensureInitialized();
|
|
305
270
|
if (!this.db)
|
|
306
271
|
return;
|
|
307
272
|
this.db.run('DELETE FROM embeddings');
|
|
308
|
-
this.dirty = true;
|
|
309
273
|
this.hits = 0;
|
|
310
274
|
this.misses = 0;
|
|
311
|
-
this.
|
|
275
|
+
this.dirty = false;
|
|
312
276
|
}
|
|
313
277
|
/**
|
|
314
|
-
* Force save to disk
|
|
278
|
+
* Force save to disk. node:sqlite + WAL persists each `db.run` immediately,
|
|
279
|
+
* so flush is a no-op kept for API compatibility.
|
|
315
280
|
*/
|
|
316
281
|
async flush() {
|
|
317
282
|
await this.ensureInitialized();
|
|
318
|
-
|
|
319
|
-
this.saveToFile();
|
|
320
|
-
}
|
|
283
|
+
this.dirty = false;
|
|
321
284
|
}
|
|
322
285
|
/**
|
|
323
286
|
* Close database connection
|
|
324
287
|
*/
|
|
325
288
|
async close() {
|
|
326
|
-
this.stopAutoSave();
|
|
327
289
|
if (this.db) {
|
|
328
|
-
// Save before closing
|
|
329
|
-
if (this.dirty) {
|
|
330
|
-
this.saveToFile();
|
|
331
|
-
}
|
|
332
290
|
this.db.close();
|
|
333
291
|
this.db = null;
|
|
334
|
-
this.SQL = null;
|
|
335
292
|
this.initialized = false;
|
|
336
293
|
}
|
|
337
294
|
}
|
|
338
295
|
}
|
|
339
296
|
/**
|
|
340
|
-
* Check if persistent cache is available
|
|
297
|
+
* Check if persistent cache is available. node:sqlite is built into Node 22+
|
|
298
|
+
* (moflo's minimum) so this always succeeds; kept for API compatibility.
|
|
299
|
+
*
|
|
300
|
+
* Loads the warning-suppression side-effect BEFORE the probe import so the
|
|
301
|
+
* once-per-process ExperimentalWarning doesn't leak to stderr (#1098).
|
|
341
302
|
*/
|
|
342
303
|
export async function isPersistentCacheAvailable() {
|
|
343
304
|
try {
|
|
344
|
-
|
|
345
|
-
await
|
|
305
|
+
await import('../memory/suppress-sqlite-warning.js');
|
|
306
|
+
await import('node:sqlite');
|
|
346
307
|
return true;
|
|
347
308
|
}
|
|
348
309
|
catch {
|
|
@@ -480,11 +480,51 @@ function syncScripts(root, force) {
|
|
|
480
480
|
copied++;
|
|
481
481
|
}
|
|
482
482
|
}
|
|
483
|
+
// Sync bin/lib/ and bin/migrations/ recursively. The top-level scripts
|
|
484
|
+
// import `./lib/moflo-resolve.mjs` etc., so omitting these subtrees leaves
|
|
485
|
+
// every synced script unable to load (#1090). The upgrade path in
|
|
486
|
+
// executor.ts and the post-install bootstrap both sync these trees — init
|
|
487
|
+
// had drifted out of step.
|
|
488
|
+
copied += syncTree(path.join(binDir, 'lib'), path.join(scriptsDir, 'lib'), force);
|
|
489
|
+
copied += syncTree(path.join(binDir, 'migrations'), path.join(scriptsDir, 'migrations'), force);
|
|
483
490
|
if (copied === 0) {
|
|
484
491
|
return { name: '.claude/scripts/', status: 'skipped', detail: 'Scripts already up to date' };
|
|
485
492
|
}
|
|
486
493
|
return { name: '.claude/scripts/', status: 'updated', detail: `${copied} scripts synced from moflo` };
|
|
487
494
|
}
|
|
495
|
+
function syncTree(srcRoot, destRoot, force) {
|
|
496
|
+
if (!fs.existsSync(srcRoot))
|
|
497
|
+
return 0;
|
|
498
|
+
let entries;
|
|
499
|
+
try {
|
|
500
|
+
entries = fs.readdirSync(srcRoot, { recursive: true, withFileTypes: true });
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return 0;
|
|
504
|
+
}
|
|
505
|
+
let copied = 0;
|
|
506
|
+
for (const entry of entries) {
|
|
507
|
+
if (!entry.isFile())
|
|
508
|
+
continue;
|
|
509
|
+
const parent = entry.parentPath
|
|
510
|
+
?? entry.path
|
|
511
|
+
?? srcRoot;
|
|
512
|
+
const absSrc = path.join(parent, entry.name);
|
|
513
|
+
const rel = path.relative(srcRoot, absSrc).split(path.sep).join('/');
|
|
514
|
+
const absDest = path.join(destRoot, rel);
|
|
515
|
+
try {
|
|
516
|
+
fs.mkdirSync(path.dirname(absDest), { recursive: true });
|
|
517
|
+
if (!fs.existsSync(absDest) || force || isStale(absSrc, absDest)) {
|
|
518
|
+
fs.copyFileSync(absSrc, absDest);
|
|
519
|
+
copied++;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// Non-fatal — skip individual file on error
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return copied;
|
|
527
|
+
}
|
|
488
528
|
function isStale(srcPath, destPath) {
|
|
489
529
|
try {
|
|
490
530
|
return fs.statSync(srcPath).mtimeMs > fs.statSync(destPath).mtimeMs;
|
|
@@ -211,7 +211,7 @@ async function ensureInitialized() {
|
|
|
211
211
|
export const memoryTools = [
|
|
212
212
|
{
|
|
213
213
|
name: 'memory_store',
|
|
214
|
-
description: 'Store a value in memory with vector embedding for semantic search (sql.js + HNSW backend). Upserts by default — pass upsert:false to fail on duplicate keys.',
|
|
214
|
+
description: 'Store a value in memory with vector embedding for semantic search (sql.js + HNSW backend). Upserts by default — pass upsert:false to fail on duplicate keys. Optional `metadata` lets chunk-row producers set the navigation fields (parentDoc, prevChunk, nextChunk, siblings, …) that `memory_get_neighbors` reads.',
|
|
215
215
|
category: 'memory',
|
|
216
216
|
inputSchema: {
|
|
217
217
|
type: 'object',
|
|
@@ -226,6 +226,11 @@ export const memoryTools = [
|
|
|
226
226
|
},
|
|
227
227
|
ttl: { type: 'number', description: 'Time-to-live in seconds (optional)' },
|
|
228
228
|
upsert: { type: 'boolean', description: 'If false, fail on duplicate keys instead of replacing (default: true)' },
|
|
229
|
+
metadata: {
|
|
230
|
+
type: 'object',
|
|
231
|
+
additionalProperties: true,
|
|
232
|
+
description: 'Optional per-row metadata persisted to the `metadata` TEXT column. For chunk entries, include `type: "chunk"` plus the navigation fields (parentDoc, parentPath, chunkIndex, totalChunks, prevChunk, nextChunk, siblings, hierarchicalParent, hierarchicalChildren, chunkTitle, headerLevel) so `memory_get_neighbors` can traverse. Capped at 64KB serialised.',
|
|
233
|
+
},
|
|
229
234
|
},
|
|
230
235
|
required: ['key', 'value'],
|
|
231
236
|
},
|
|
@@ -237,6 +242,7 @@ export const memoryTools = [
|
|
|
237
242
|
const value = typeof input.value === 'string' ? input.value : JSON.stringify(input.value);
|
|
238
243
|
const tags = input.tags || [];
|
|
239
244
|
const ttl = input.ttl;
|
|
245
|
+
const metadata = input.metadata;
|
|
240
246
|
// #962: default upsert=true — silent UNIQUE-constraint failures on update
|
|
241
247
|
// were dropping schedule cancels and similar updates on the floor.
|
|
242
248
|
const upsert = input.upsert === false ? false : true;
|
|
@@ -250,6 +256,7 @@ export const memoryTools = [
|
|
|
250
256
|
generateEmbeddingFlag: true,
|
|
251
257
|
tags,
|
|
252
258
|
ttl,
|
|
259
|
+
metadata,
|
|
253
260
|
upsert,
|
|
254
261
|
});
|
|
255
262
|
const duration = performance.now() - startTime;
|
|
@@ -277,7 +284,7 @@ export const memoryTools = [
|
|
|
277
284
|
},
|
|
278
285
|
{
|
|
279
286
|
name: 'memory_retrieve',
|
|
280
|
-
description: 'Retrieve
|
|
287
|
+
description: 'Retrieve the full value for a SPECIFIC key. For chunk entries, prefer `memory_get_neighbors` for traversal — bulk-retrieving search hits is a protocol violation. The returned `navigation` object lets you keep traversing. See `.claude/guidance/moflo-memory-protocol.md`.',
|
|
281
288
|
category: 'memory',
|
|
282
289
|
inputSchema: {
|
|
283
290
|
type: 'object',
|
|
@@ -319,7 +326,7 @@ export const memoryTools = [
|
|
|
319
326
|
},
|
|
320
327
|
{
|
|
321
328
|
name: 'memory_search',
|
|
322
|
-
description: 'Semantic vector search using HNSW index (150x-12,500x faster than keyword search).
|
|
329
|
+
description: 'Semantic vector search using HNSW index (150x-12,500x faster than keyword search). When a result has a non-null `navigation` crumb, you MUST traverse via `memory_get_neighbors` — bulk `memory_retrieve` per hit is a protocol violation. See `.claude/guidance/moflo-memory-protocol.md`.',
|
|
323
330
|
category: 'memory',
|
|
324
331
|
inputSchema: {
|
|
325
332
|
type: 'object',
|