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
@@ -1,28 +1,31 @@
1
1
  /**
2
- * SQLite-backed Persistent Cache for Embeddings (sql.js)
2
+ * SQLite-backed Persistent Cache for Embeddings (node:sqlite)
3
3
  *
4
4
  * Features:
5
- * - Cross-platform support (pure JavaScript/WASM, no native compilation)
6
- * - Disk persistence across sessions
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, readFileSync } from 'fs';
16
+ import { existsSync, mkdirSync, statSync } from 'fs';
13
17
  import { dirname } from 'path';
14
- import { atomicWriteFileSync } from '../shared/utils/atomic-file-write.js';
18
+ import { openDaemonDatabase } from '../memory/daemon-backend.js';
15
19
  /**
16
- * SQLite-backed persistent embedding cache using sql.js (pure JS/WASM)
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
- this.autoSaveInterval = config.autoSaveInterval ?? 30000; // 30 seconds
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
- // Dynamically import sql.js
44
- const initSqlJs = (await import('sql.js')).default;
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
- if (dbExisted) {
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
- // If sql.js not available, fall back gracefully
99
- console.warn('[persistent-cache] sql.js not available, cache disabled:', error instanceof Error ? error.message : error);
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
- const buffer = readFileSync(this.dbPath);
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.saveToFile();
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
- if (this.db && this.dirty) {
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 (sql.js installed)
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
- const initSqlJs = (await import('sql.js')).default;
345
- await initSqlJs();
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 a value from memory by key. Chunk entries also return a full `navigation` objectuse it with memory_get_neighbors for traversal instead of bulk-retrieving every search hit.',
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). Results include a compact `navigation` crumb on chunk hits — traverse via memory_get_neighbors rather than retrieving every hit.',
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',