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
@@ -25,25 +25,15 @@
25
25
  import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync } from 'fs';
26
26
  import { resolve, relative, dirname, basename, extname } from 'path';
27
27
  import { fileURLToPath } from 'url';
28
- import { mofloResolveURL } from './lib/moflo-resolve.mjs';
29
- import { memoryDbPath } from './lib/moflo-paths.mjs';
28
+ import { memoryDbPath, findProjectRoot } from './lib/moflo-paths.mjs';
29
+ import { openBackend } from './lib/get-backend.mjs';
30
+ import { applyIncrementalChunks } from './lib/incremental-write.mjs';
30
31
  import { resolveMofloBin } from './lib/resolve-bin.mjs';
31
32
  import { createProcessManager } from './lib/process-manager.mjs';
32
- const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
33
33
 
34
34
 
35
35
  const __dirname = dirname(fileURLToPath(import.meta.url));
36
36
 
37
- function findProjectRoot() {
38
- let dir = process.cwd();
39
- const root = resolve(dir, '/');
40
- while (dir !== root) {
41
- if (existsSync(resolve(dir, 'package.json'))) return dir;
42
- dir = dirname(dir);
43
- }
44
- return process.cwd();
45
- }
46
-
47
37
  const projectRoot = findProjectRoot();
48
38
 
49
39
  // Locate the moflo package root (for bundled guidance that ships with moflo)
@@ -181,14 +171,7 @@ function ensureDbDir() {
181
171
 
182
172
  async function getDb() {
183
173
  ensureDbDir();
184
- const SQL = await initSqlJs();
185
- let db;
186
- if (existsSync(DB_PATH)) {
187
- const buffer = readFileSync(DB_PATH);
188
- db = new SQL.Database(buffer);
189
- } else {
190
- db = new SQL.Database();
191
- }
174
+ const db = await openBackend(projectRoot, { create: true });
192
175
 
193
176
  // Ensure table exists with unique constraint
194
177
  db.run(`
@@ -221,12 +204,7 @@ async function getDb() {
221
204
  }
222
205
 
223
206
  function saveDb(db) {
224
- const data = db.export();
225
- writeFileSync(DB_PATH, Buffer.from(data));
226
- }
227
-
228
- function generateId() {
229
- return `mem_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
207
+ db.save();
230
208
  }
231
209
 
232
210
  function hashContent(content) {
@@ -239,39 +217,6 @@ function hashContent(content) {
239
217
  return hash.toString(16);
240
218
  }
241
219
 
242
- function storeEntry(db, key, content, metadata = {}, tags = []) {
243
- const now = Date.now();
244
- const id = generateId();
245
- const metaJson = JSON.stringify(metadata);
246
- const tagsJson = JSON.stringify(tags);
247
-
248
- db.run(`
249
- INSERT OR REPLACE INTO memory_entries
250
- (id, key, namespace, content, metadata, tags, created_at, updated_at, status)
251
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'active')
252
- `, [id, key, NAMESPACE, content, metaJson, tagsJson, now, now]);
253
-
254
- return true;
255
- }
256
-
257
- function deleteByPrefix(db, prefix) {
258
- db.run(`DELETE FROM memory_entries WHERE namespace = ? AND key LIKE ?`, [NAMESPACE, `${prefix}%`]);
259
- }
260
-
261
- function getEntryHash(db, key) {
262
- const stmt = db.prepare('SELECT metadata FROM memory_entries WHERE key = ? AND namespace = ?');
263
- stmt.bind([key, NAMESPACE]);
264
- const entry = stmt.step() ? stmt.getAsObject() : null;
265
- stmt.free();
266
- if (entry?.metadata) {
267
- try {
268
- const meta = JSON.parse(entry.metadata);
269
- return meta.contentHash;
270
- } catch { /* ignore */ }
271
- }
272
- return null;
273
- }
274
-
275
220
  // #1053 S4: doc-* entries retired. Doc-level skip check now reads
276
221
  // docContentHash off chunk-0 (every chunk carries it).
277
222
  function getDocHashFromChunkZero(db, chunkPrefix) {
@@ -564,10 +509,9 @@ function indexFile(db, filePath, keyPrefix, options = {}) {
564
509
  const stats = statSync(filePath);
565
510
  const relativePath = '/' + relative(projectRoot, filePath).replace(/\\/g, '/');
566
511
 
567
- // Delete old chunks for this file before re-indexing.
568
- // #1053 S4: also delete any legacy doc-* row (one-time cleanup if a
569
- // pre-S4 install left one behind for this prefix).
570
- deleteByPrefix(db, chunkPrefix);
512
+ // #1053 S4: drop any legacy doc-* row left over from a pre-S4 install.
513
+ // The chunker stopped emitting these and the audit found zero production
514
+ // readers; safe one-time cleanup as part of the per-doc re-index.
571
515
  db.run(`DELETE FROM memory_entries WHERE namespace = ? AND key = ?`, [NAMESPACE, docKey]);
572
516
 
573
517
  // #1053 S4: Chunker no longer writes doc-* entries. Audit found zero
@@ -580,6 +524,8 @@ function indexFile(db, filePath, keyPrefix, options = {}) {
580
524
  const chunks = chunkMarkdown(content, fileName);
581
525
 
582
526
  if (chunks.length === 0) {
527
+ // No chunks generated → sweep any stragglers from a prior run and exit.
528
+ db.run(`DELETE FROM memory_entries WHERE namespace = ? AND key LIKE ?`, [NAMESPACE, `${chunkPrefix}-%`]);
583
529
  return { docKey, status: 'indexed', chunks: 0 };
584
530
  }
585
531
 
@@ -587,21 +533,17 @@ function indexFile(db, filePath, keyPrefix, options = {}) {
587
533
  const hierarchy = buildHierarchy(chunks, chunkPrefix);
588
534
  const siblings = chunks.map((_, i) => `${chunkPrefix}-${i}`);
589
535
 
590
- for (let i = 0; i < chunks.length; i++) {
591
- const chunk = chunks[i];
536
+ // #1057: route chunk writes through applyIncrementalChunks so an
537
+ // unchanged chunk (~95% of the time when only one section of a doc
538
+ // edited) keeps its embedding column. Pre-#1057 this loop used raw
539
+ // INSERT OR REPLACE after a blanket deleteByPrefix — every re-index
540
+ // wiped every chunk's embedding and forced build-embeddings to
541
+ // re-vectorise the whole doc on every save. See
542
+ // feedback_indexer_preserve_embeddings.md.
543
+ const chunkRows = chunks.map((chunk, i) => {
592
544
  const chunkKey = `${chunkPrefix}-${i}`;
593
-
594
- // Build prev/next links
595
545
  const prevChunk = i > 0 ? `${chunkPrefix}-${i - 1}` : null;
596
546
  const nextChunk = i < chunks.length - 1 ? `${chunkPrefix}-${i + 1}` : null;
597
-
598
- // #1053 S5: dropped extractOverlapContext + preamble wrapping. The
599
- // preambles were a workaround for missing traversal — once memory_get_neighbors
600
- // is wired (S2), prevChunk/nextChunk metadata + a real call is the
601
- // alternative path. Saved ~25-30% bloat per chunk on disk and in
602
- // embeddings.
603
-
604
- // Get hierarchical relationships
605
547
  const hierInfo = hierarchy[chunkKey];
606
548
 
607
549
  const chunkMetadata = {
@@ -646,16 +588,25 @@ function indexFile(db, filePath, keyPrefix, options = {}) {
646
588
  // #1053 S5: title heading + chunk body. No prev/next preamble.
647
589
  const searchableContent = `# ${chunk.title}\n\n${chunk.content}`;
648
590
 
649
- // Store chunk with full metadata
650
- storeEntry(
651
- db,
652
- chunkKey,
653
- searchableContent,
654
- chunkMetadata,
655
- [keyPrefix, 'chunk', `level-${chunk.level}`, chunk.title.toLowerCase().replace(/[^a-z0-9]+/g, '-'), ...extraTags]
656
- );
591
+ return {
592
+ key: chunkKey,
593
+ content: searchableContent,
594
+ metadata: chunkMetadata,
595
+ tags: [
596
+ keyPrefix,
597
+ 'chunk',
598
+ `level-${chunk.level}`,
599
+ chunk.title.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
600
+ ...extraTags,
601
+ ],
602
+ };
603
+ });
657
604
 
658
- debug(` Stored chunk ${i}: ${chunk.title} (${chunk.content.length} chars, prev=${!!prevChunk}, next=${!!nextChunk})`);
605
+ const counts = applyIncrementalChunks(db, NAMESPACE, chunkRows, {
606
+ keyPrefix: `${chunkPrefix}-`,
607
+ });
608
+ if (verbose) {
609
+ debug(` Doc ${docKey}: inserted=${counts.inserted} updated=${counts.updated} unchanged=${counts.unchanged} removed=${counts.removed}`);
659
610
  }
660
611
 
661
612
  return { docKey, status: 'indexed', chunks: chunks.length };
@@ -28,23 +28,13 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from
28
28
  import { resolve, dirname, relative, basename, extname } from 'path';
29
29
  import { fileURLToPath } from 'url';
30
30
  import { resolveMofloBin } from './lib/resolve-bin.mjs';
31
- import { mofloResolveURL } from './lib/moflo-resolve.mjs';
32
- import { memoryDbPath, MOFLO_DIR } from './lib/moflo-paths.mjs';
31
+ import { memoryDbPath, MOFLO_DIR, findProjectRoot } from './lib/moflo-paths.mjs';
32
+ import { openBackend } from './lib/get-backend.mjs';
33
33
  import { applyIncrementalChunks, computeContentListHash } from './lib/incremental-write.mjs';
34
34
  import { createProcessManager } from './lib/process-manager.mjs';
35
35
 
36
36
  const __dirname = dirname(fileURLToPath(import.meta.url));
37
37
 
38
- function findProjectRoot() {
39
- let dir = process.cwd();
40
- const root = resolve(dir, '/');
41
- while (dir !== root) {
42
- if (existsSync(resolve(dir, 'package.json'))) return dir;
43
- dir = dirname(dir);
44
- }
45
- return process.cwd();
46
- }
47
-
48
38
  const projectRoot = findProjectRoot();
49
39
  const NAMESPACE = 'patterns';
50
40
  const DB_PATH = memoryDbPath(projectRoot);
@@ -76,16 +66,9 @@ function ensureDbDir() {
76
66
  async function getDb() {
77
67
  ensureDbDir();
78
68
  // Lazy: hash-cache-match and no-source-files early-exits in main() never
79
- // reach this, and the sql.js wasm cold-load is ~400ms otherwise wasted.
80
- const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
81
- const SQL = await initSqlJs();
82
- let db;
83
- if (existsSync(DB_PATH)) {
84
- const buffer = readFileSync(DB_PATH);
85
- db = new SQL.Database(buffer);
86
- } else {
87
- db = new SQL.Database();
88
- }
69
+ // reach this, and the backend cold-load (sql.js wasm ~400ms, node:sqlite
70
+ // WAL init ~20ms) is otherwise wasted on the no-op path.
71
+ const db = await openBackend(projectRoot, { create: true });
89
72
  db.run(`
90
73
  CREATE TABLE IF NOT EXISTS memory_entries (
91
74
  id TEXT PRIMARY KEY,
@@ -114,8 +97,7 @@ async function getDb() {
114
97
  }
115
98
 
116
99
  function saveDb(db) {
117
- const data = db.export();
118
- writeFileSync(DB_PATH, Buffer.from(data));
100
+ db.save();
119
101
  }
120
102
 
121
103
  function countNamespace(db) {
@@ -26,24 +26,13 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from
26
26
  import { resolve, dirname, relative, basename, extname, join } from 'path';
27
27
  import { fileURLToPath } from 'url';
28
28
  import { execSync, execFileSync, spawn } from 'child_process';
29
- import { mofloResolveURL } from './lib/moflo-resolve.mjs';
30
- import { memoryDbPath, MOFLO_DIR } from './lib/moflo-paths.mjs';
29
+ import { memoryDbPath, MOFLO_DIR, findProjectRoot } from './lib/moflo-paths.mjs';
30
+ import { openBackend } from './lib/get-backend.mjs';
31
31
  import { resolveMofloBin } from './lib/resolve-bin.mjs';
32
32
  import { applyIncrementalChunks, computeContentListHash } from './lib/incremental-write.mjs';
33
- const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
34
33
 
35
34
  const __dirname = dirname(fileURLToPath(import.meta.url));
36
35
 
37
- function findProjectRoot() {
38
- let dir = process.cwd();
39
- const root = resolve(dir, '/');
40
- while (dir !== root) {
41
- if (existsSync(resolve(dir, 'package.json'))) return dir;
42
- dir = dirname(dir);
43
- }
44
- return process.cwd();
45
- }
46
-
47
36
  const projectRoot = findProjectRoot();
48
37
  const NAMESPACE = 'tests';
49
38
  const DB_PATH = memoryDbPath(projectRoot);
@@ -87,14 +76,7 @@ function ensureDbDir() {
87
76
 
88
77
  async function getDb() {
89
78
  ensureDbDir();
90
- const SQL = await initSqlJs();
91
- let db;
92
- if (existsSync(DB_PATH)) {
93
- const buffer = readFileSync(DB_PATH);
94
- db = new SQL.Database(buffer);
95
- } else {
96
- db = new SQL.Database();
97
- }
79
+ const db = await openBackend(projectRoot, { create: true });
98
80
 
99
81
  db.run(`
100
82
  CREATE TABLE IF NOT EXISTS memory_entries (
@@ -124,8 +106,7 @@ async function getDb() {
124
106
  }
125
107
 
126
108
  function saveDb(db) {
127
- const data = db.export();
128
- writeFileSync(DB_PATH, Buffer.from(data));
109
+ db.save();
129
110
  }
130
111
 
131
112
  function countNamespace(db) {