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.
Files changed (79) hide show
  1. package/.claude/guidance/shipped/moflo-agent-rules.md +12 -0
  2. package/.claude/guidance/shipped/moflo-memory-protocol.md +34 -0
  3. package/.claude/guidance/shipped/moflo-memorydb-maintenance.md +22 -11
  4. package/.claude/guidance/shipped/moflo-root-cause-discipline.md +47 -0
  5. package/.claude/guidance/shipped/moflo-subagents.md +4 -0
  6. package/.claude/helpers/gate.cjs +3 -3
  7. package/.claude/helpers/statusline.cjs +69 -33
  8. package/.claude/helpers/subagent-bootstrap.json +1 -1
  9. package/.claude/helpers/subagent-start.cjs +1 -1
  10. package/.claude/skills/eldar/SKILL.md +8 -0
  11. package/bin/build-embeddings.mjs +6 -20
  12. package/bin/cli.js +5 -0
  13. package/bin/gate.cjs +3 -3
  14. package/bin/generate-code-map.mjs +4 -24
  15. package/bin/hooks.mjs +3 -12
  16. package/bin/index-all.mjs +3 -13
  17. package/bin/index-guidance.mjs +59 -119
  18. package/bin/index-patterns.mjs +6 -24
  19. package/bin/index-tests.mjs +4 -23
  20. package/bin/lib/db-repair.mjs +4 -25
  21. package/bin/lib/get-backend.mjs +306 -0
  22. package/bin/lib/incremental-write.mjs +27 -7
  23. package/bin/lib/moflo-paths.mjs +64 -4
  24. package/bin/lib/suppress-sqlite-warning.mjs +57 -0
  25. package/bin/migrations/knowledge-purge.mjs +7 -8
  26. package/bin/migrations/knowledge-to-learnings.mjs +7 -9
  27. package/bin/migrations/purge-doc-entries.mjs +52 -0
  28. package/bin/migrations/strip-context-preambles.mjs +95 -0
  29. package/bin/run-migrations.mjs +1 -10
  30. package/bin/semantic-search.mjs +11 -19
  31. package/bin/session-start-launcher.mjs +102 -100
  32. package/bin/simplify-classify.cjs +38 -17
  33. package/dist/src/cli/commands/daemon.js +38 -11
  34. package/dist/src/cli/commands/doctor-checks-coverage-truth.js +136 -0
  35. package/dist/src/cli/commands/doctor-checks-memory-access.js +244 -5
  36. package/dist/src/cli/commands/doctor-checks-memory.js +13 -18
  37. package/dist/src/cli/commands/doctor-checks-version-skew.js +94 -0
  38. package/dist/src/cli/commands/doctor-checks-writers-audit.js +170 -0
  39. package/dist/src/cli/commands/doctor-embedding-hygiene.js +3 -15
  40. package/dist/src/cli/commands/doctor-fixes.js +30 -0
  41. package/dist/src/cli/commands/doctor-registry.js +14 -0
  42. package/dist/src/cli/commands/doctor.js +1 -1
  43. package/dist/src/cli/commands/embeddings.js +17 -22
  44. package/dist/src/cli/commands/memory.js +54 -75
  45. package/dist/src/cli/embeddings/persistent-cache.js +44 -83
  46. package/dist/src/cli/init/claudemd-generator.js +4 -0
  47. package/dist/src/cli/init/moflo-init.js +40 -0
  48. package/dist/src/cli/mcp-tools/memory-tools.js +177 -32
  49. package/dist/src/cli/memory/bridge-core.js +256 -30
  50. package/dist/src/cli/memory/bridge-entries.js +76 -8
  51. package/dist/src/cli/memory/controller-registry.js +7 -2
  52. package/dist/src/cli/memory/controllers/batch-operations.js +5 -1
  53. package/dist/src/cli/memory/controllers/hierarchical-memory.js +7 -2
  54. package/dist/src/cli/memory/controllers/mutation-guard.js +22 -2
  55. package/dist/src/cli/memory/daemon-backend.js +400 -0
  56. package/dist/src/cli/memory/daemon-write-client.js +192 -15
  57. package/dist/src/cli/memory/database-provider.js +57 -40
  58. package/dist/src/cli/memory/hnsw-persistence.js +6 -8
  59. package/dist/src/cli/memory/index.js +0 -1
  60. package/dist/src/cli/memory/memory-bridge.js +40 -8
  61. package/dist/src/cli/memory/memory-initializer.js +286 -220
  62. package/dist/src/cli/memory/rvf-migration.js +25 -11
  63. package/dist/src/cli/memory/sqlite-backend.js +573 -0
  64. package/dist/src/cli/memory/suppress-sqlite-warning.js +49 -0
  65. package/dist/src/cli/services/cherry-pick-learnings.js +32 -21
  66. package/dist/src/cli/services/daemon-dashboard.js +13 -1
  67. package/dist/src/cli/services/daemon-lock.js +58 -1
  68. package/dist/src/cli/services/daemon-memory-rpc.js +245 -10
  69. package/dist/src/cli/services/embeddings-migration.js +9 -12
  70. package/dist/src/cli/services/ephemeral-namespace-purge.js +6 -11
  71. package/dist/src/cli/services/learning-service.js +12 -20
  72. package/dist/src/cli/services/project-root.js +69 -9
  73. package/dist/src/cli/services/soft-delete-purge.js +6 -11
  74. package/dist/src/cli/services/sqljs-migration-store.js +4 -1
  75. package/dist/src/cli/services/subagent-bootstrap.js +1 -1
  76. package/dist/src/cli/shared/events/event-store.js +26 -55
  77. package/dist/src/cli/version.js +1 -1
  78. package/package.json +2 -4
  79. package/dist/src/cli/memory/sqljs-backend.js +0 -643
@@ -1,16 +1,16 @@
1
1
  /**
2
- * DatabaseProvider - Platform-aware database selection
2
+ * DatabaseProvider moflo's IMemoryBackend factory.
3
3
  *
4
- * Automatically selects best backend:
5
- * - All platforms: sql.js (WASM, no native deps)
6
- * - Windows: sql.js (WASM, universal) when native fails
7
- * - Fallback: JSON file storage
4
+ * Phase 5 (#1084) removed the sql.js backend. Selection now collapses to:
5
+ * - `node-sqlite` (Node 22+ built-in, the only SQLite backend)
6
+ * - `rvf` (pure-TS fallback when node:sqlite is somehow unavailable)
7
+ * - `json` (last-resort file storage when nothing else works)
8
8
  *
9
9
  * @module v3/memory/database-provider
10
10
  */
11
11
  import { platform } from 'node:os';
12
12
  import { existsSync } from 'node:fs';
13
- import { SqlJsBackend } from './sqljs-backend.js';
13
+ import { SqliteBackend } from './sqlite-backend.js';
14
14
  /**
15
15
  * Detect platform and recommend provider
16
16
  */
@@ -19,8 +19,9 @@ function detectPlatform() {
19
19
  const isWindows = os === 'win32';
20
20
  const isMacOS = os === 'darwin';
21
21
  const isLinux = os === 'linux';
22
- // Recommend better-sqlite3 for Unix-like systems, sql.js for Windows
23
- const recommendedProvider = 'sql.js';
22
+ // Phase 4 (#1083) flipped the default to node:sqlite (built into Node 22+,
23
+ // moflo's minimum). Phase 5 (#1084) deletes the sql.js backend + dep.
24
+ const recommendedProvider = 'node-sqlite';
24
25
  return {
25
26
  os,
26
27
  isWindows,
@@ -35,55 +36,60 @@ function detectPlatform() {
35
36
  async function testRvf() {
36
37
  return true;
37
38
  }
38
- /** better-sqlite3 removed — sql.js is the only SQLite backend */
39
39
  /**
40
- * Test if sql.js is available and working
40
+ * Test if the built-in node:sqlite engine is available (Node 22+).
41
+ *
42
+ * Loads the suppress-sqlite-warning side-effect BEFORE the probe import so
43
+ * the once-per-process ExperimentalWarning never fires (#1098). A probe
44
+ * that prints the warning to stderr defeats every consumer's "clean
45
+ * startup" expectation even when the rest of the run is healthy.
41
46
  */
42
- async function testSqlJs() {
47
+ async function testNodeSqlite() {
43
48
  try {
44
- const { initSqlJsForNode } = await import('./sqljs-backend.js');
45
- const SQL = await initSqlJsForNode();
46
- const testDb = new SQL.Database();
47
- testDb.close();
49
+ await import('./suppress-sqlite-warning.js');
50
+ await import('node:sqlite');
48
51
  return true;
49
52
  }
50
- catch (error) {
53
+ catch {
51
54
  return false;
52
55
  }
53
56
  }
54
57
  /**
55
- * Select best available provider
58
+ * Select best available provider.
59
+ *
60
+ * Phase 5 (#1084) collapsed the chain — sql.js is gone, so the order is
61
+ * just: explicit override → node:sqlite → RVF → JSON. Passing `'sql.js'`
62
+ * explicitly is a hard error.
56
63
  */
57
64
  async function selectProvider(preferred, verbose = false) {
65
+ if (preferred === 'sql.js') {
66
+ throw new Error(`DatabaseProvider: sql.js was removed in Phase 5 (#1084). ` +
67
+ `Use 'node-sqlite' (the new default) or omit the provider entirely.`);
68
+ }
58
69
  if (preferred && preferred !== 'auto') {
59
70
  if (verbose) {
60
71
  console.log(`[DatabaseProvider] Using explicitly specified provider: ${preferred}`);
61
72
  }
62
73
  return preferred;
63
74
  }
64
- const platformInfo = detectPlatform();
75
+ if (await testNodeSqlite()) {
76
+ if (verbose) {
77
+ console.log('[DatabaseProvider] node:sqlite available — using new default');
78
+ }
79
+ return 'node-sqlite';
80
+ }
81
+ // node:sqlite missing is the "broken install" signal — surface it whenever
82
+ // verbose is on so the fallback chain doesn't silently regress consumers
83
+ // to a slower backend without anyone noticing.
65
84
  if (verbose) {
66
- console.log(`[DatabaseProvider] Platform detected: ${platformInfo.os}`);
67
- console.log(`[DatabaseProvider] Recommended provider: ${platformInfo.recommendedProvider}`);
85
+ console.warn('[DatabaseProvider] node:sqlite unavailable — check Node version (22+ required); falling back to RVF');
68
86
  }
69
- // Try RVF first (always available via pure-TS fallback)
70
87
  if (await testRvf()) {
71
- if (verbose) {
72
- console.log('[DatabaseProvider] RVF backend available');
73
- }
74
88
  return 'rvf';
75
89
  }
76
- // Try sql.js (moflo: sql.js is the only SQLite backend)
77
- if (await testSqlJs()) {
78
- if (verbose) {
79
- console.log('[DatabaseProvider] sql.js available and working');
80
- }
81
- return 'sql.js';
82
- }
83
- else if (verbose) {
84
- console.log('[DatabaseProvider] sql.js not available, using JSON fallback');
90
+ if (verbose) {
91
+ console.warn('[DatabaseProvider] node:sqlite + RVF unavailable — falling back to JSON');
85
92
  }
86
- // Final fallback to JSON
87
93
  return 'json';
88
94
  }
89
95
  /**
@@ -112,7 +118,7 @@ async function selectProvider(preferred, verbose = false) {
112
118
  * ```
113
119
  */
114
120
  export async function createDatabase(path, options = {}) {
115
- const { provider = 'auto', verbose = false, walMode = true, optimize = true, defaultNamespace = 'default', maxEntries = 1000000, autoPersistInterval = 5000, wasmPath, } = options;
121
+ const { provider = 'auto', verbose = false, walMode: _walMode = true, optimize = true, defaultNamespace = 'default', maxEntries = 1000000, autoPersistInterval = 5000, wasmPath: _wasmPath, } = options;
116
122
  // Select provider
117
123
  const selectedProvider = await selectProvider(provider, verbose);
118
124
  if (verbose) {
@@ -122,6 +128,13 @@ export async function createDatabase(path, options = {}) {
122
128
  let backend;
123
129
  switch (selectedProvider) {
124
130
  case 'sql.js': {
131
+ // selectProvider() guards against 'sql.js' as an explicit preference,
132
+ // but a stale caller could still land here if `auto` resolution drifted
133
+ // (it can't — left only for exhaustive-check safety).
134
+ throw new Error(`DatabaseProvider: sql.js was removed in Phase 5 (#1084). ` +
135
+ `This case is unreachable; if you see this error, file a bug.`);
136
+ }
137
+ case 'node-sqlite': {
125
138
  const config = {
126
139
  databasePath: path,
127
140
  optimize,
@@ -129,9 +142,8 @@ export async function createDatabase(path, options = {}) {
129
142
  maxEntries,
130
143
  verbose,
131
144
  autoPersistInterval,
132
- wasmPath,
133
145
  };
134
- backend = new SqlJsBackend(config);
146
+ backend = new SqliteBackend(config);
135
147
  break;
136
148
  }
137
149
  case 'rvf': {
@@ -167,13 +179,18 @@ export function getPlatformInfo() {
167
179
  return detectPlatform();
168
180
  }
169
181
  /**
170
- * Check which providers are available
182
+ * Check which providers are available.
183
+ *
184
+ * `sqlJs` / `betterSqlite3` are retained for API stability but always
185
+ * report `false` — Phase 5 (#1084) deleted the sql.js backend and
186
+ * better-sqlite3 was never wired.
171
187
  */
172
188
  export async function getAvailableProviders() {
173
189
  return {
174
190
  rvf: true,
175
- betterSqlite3: false, // Removed — sql.js is the only SQLite backend
176
- sqlJs: await testSqlJs(),
191
+ betterSqlite3: false,
192
+ sqlJs: false,
193
+ nodeSqlite: await testNodeSqlite(),
177
194
  json: true,
178
195
  };
179
196
  }
@@ -16,11 +16,11 @@
16
16
  */
17
17
  import * as fs from 'node:fs';
18
18
  import * as path from 'node:path';
19
- import { mofloImport } from '../services/moflo-require.js';
20
19
  import { atomicWriteFileSync } from '../services/atomic-file-write.js';
21
20
  import { HnswLite } from './hnsw-lite.js';
22
21
  import { parseEmbeddingJson } from './controllers/_shared.js';
23
22
  import { hnswIndexPath } from '../services/moflo-paths.js';
23
+ import { openDaemonDatabase } from './daemon-backend.js';
24
24
  /**
25
25
  * Build an HnswLite from every active row in `dbPath` that has an embedding,
26
26
  * then atomically write the sidecar to `<projectRoot>/.moflo/hnsw.index`.
@@ -37,13 +37,11 @@ export async function buildAndWriteHnswSidecar(dbPath, projectRoot, options = {}
37
37
  if (!fs.existsSync(dbPath)) {
38
38
  throw new Error(`buildAndWriteHnswSidecar: db not found at ${dbPath}`);
39
39
  }
40
- const sqlJsModule = await mofloImport('sql.js');
41
- if (!sqlJsModule) {
42
- throw new Error(`buildAndWriteHnswSidecar: sql.js not available`);
43
- }
44
- const SQL = await sqlJsModule.default();
45
- const buf = fs.readFileSync(dbPath);
46
- const db = new SQL.Database(buf);
40
+ // node:sqlite via the unified factory — Phase 5 (#1084) replaced the
41
+ // sql.js readFileSync + new SQL.Database round-trip with a direct open
42
+ // through openDaemonDatabase. WAL writes incrementally so there's nothing
43
+ // to flush back here; the sidecar persistence below is unaffected.
44
+ const db = openDaemonDatabase(dbPath);
47
45
  const hnsw = new HnswLite(dimensions, m, efConstruction, metric);
48
46
  let skipped = 0;
49
47
  try {
@@ -61,7 +61,6 @@ export { ControllerRegistry, INIT_LEVELS } from './controller-registry.js';
61
61
  export { CONTROLLER_SPECS } from './controller-specs.js';
62
62
  // ===== Core Components =====
63
63
  export { MofloDbAdapter } from './moflo-db-adapter.js';
64
- export { SqlJsBackend } from './sqljs-backend.js';
65
64
  export { RvfBackend } from './rvf-backend.js';
66
65
  export { HnswLite, cosineSimilarity } from './hnsw-lite.js';
67
66
  export { buildAndWriteHnswSidecar, tryLoadHnswSidecar } from './hnsw-persistence.js';
@@ -119,17 +119,49 @@ export async function bridgeAddToHNSW(id, embedding, entry, dbPath) {
119
119
  // Bridge-produced vectors come from getBridgeEmbedder() (fastembed, 384-dim).
120
120
  // Pre-#648 this column was hardcoded to 'Xenova/all-MiniLM-L6-v2' which
121
121
  // misrepresented the producing model — fixed to the canonical bridge label.
122
- ctx.db.prepare(`
123
- INSERT OR REPLACE INTO memory_entries (
124
- id, key, namespace, content, type,
125
- embedding, embedding_dimensions, embedding_model,
126
- created_at, updated_at, status
127
- ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, 'active')
122
+ //
123
+ // Pre-#1067: this issued `INSERT OR REPLACE INTO memory_entries (...)` with
124
+ // only the embedding-related columns. When `storeEntry`'s direct-write
125
+ // fallback first inserted a row carrying `metadata`/`tags`/`expires_at`
126
+ // and then called `addToHNSWIndex(...)` for the same id, REPLACE clobbered
127
+ // the row wiping the metadata column to NULL and re-introducing the
128
+ // "first chunk has no navigation" smoke failure even though serialiseMetadata
129
+ // produced the right JSON one statement earlier. UPDATE-by-id preserves the
130
+ // pre-existing row's metadata/tags/expires_at/access_count while still
131
+ // back-filling embedding columns for callers that supplied them.
132
+ const updated = ctx.db.prepare(`
133
+ UPDATE memory_entries
134
+ SET embedding = ?,
135
+ embedding_dimensions = ?,
136
+ embedding_model = ?,
137
+ updated_at = ?
138
+ WHERE id = ?
128
139
  `).run([
129
- id, entry.key, entry.namespace, entry.content,
130
140
  embeddingJson, embedding.length, BRIDGE_EMBEDDING_MODEL,
131
- now, now,
141
+ now, id,
132
142
  ]);
143
+ // If no row was matched by id (rare: a rebuild path passing an embedding
144
+ // for a key that never went through storeEntry first) fall back to an
145
+ // INSERT carrying every column the bridge can populate, so the row still
146
+ // becomes searchable. Metadata/tags/expires_at stay NULL because the
147
+ // caller didn't supply them — same shape `bridgeStoreEntry` would write
148
+ // for a no-metadata caller.
149
+ const rowCount = typeof updated.changes === 'number'
150
+ ? updated.changes
151
+ : (ctx.db.getRowsModified?.() ?? 0);
152
+ if (rowCount === 0) {
153
+ ctx.db.prepare(`
154
+ INSERT INTO memory_entries (
155
+ id, key, namespace, content, type,
156
+ embedding, embedding_dimensions, embedding_model,
157
+ created_at, updated_at, status
158
+ ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, 'active')
159
+ `).run([
160
+ id, entry.key, entry.namespace, entry.content,
161
+ embeddingJson, embedding.length, BRIDGE_EMBEDDING_MODEL,
162
+ now, now,
163
+ ]);
164
+ }
133
165
  persistBridgeDb(ctx.db, dbPath);
134
166
  return true;
135
167
  });