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.
Files changed (73) 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 +4 -25
  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 +102 -102
  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-coverage-truth.js +136 -0
  30. package/dist/src/cli/commands/doctor-checks-memory-access.js +146 -86
  31. package/dist/src/cli/commands/doctor-checks-memory.js +13 -18
  32. package/dist/src/cli/commands/doctor-checks-version-skew.js +94 -0
  33. package/dist/src/cli/commands/doctor-checks-writers-audit.js +170 -0
  34. package/dist/src/cli/commands/doctor-embedding-hygiene.js +3 -15
  35. package/dist/src/cli/commands/doctor-fixes.js +30 -0
  36. package/dist/src/cli/commands/doctor-registry.js +14 -0
  37. package/dist/src/cli/commands/doctor.js +1 -1
  38. package/dist/src/cli/commands/embeddings.js +17 -22
  39. package/dist/src/cli/commands/memory.js +13 -23
  40. package/dist/src/cli/embeddings/persistent-cache.js +44 -83
  41. package/dist/src/cli/init/moflo-init.js +40 -0
  42. package/dist/src/cli/mcp-tools/memory-tools.js +10 -3
  43. package/dist/src/cli/memory/bridge-core.js +256 -30
  44. package/dist/src/cli/memory/bridge-entries.js +70 -6
  45. package/dist/src/cli/memory/controller-registry.js +7 -2
  46. package/dist/src/cli/memory/controllers/batch-operations.js +5 -1
  47. package/dist/src/cli/memory/controllers/hierarchical-memory.js +7 -2
  48. package/dist/src/cli/memory/controllers/mutation-guard.js +22 -2
  49. package/dist/src/cli/memory/daemon-backend.js +400 -0
  50. package/dist/src/cli/memory/daemon-write-client.js +192 -15
  51. package/dist/src/cli/memory/database-provider.js +57 -40
  52. package/dist/src/cli/memory/hnsw-persistence.js +6 -8
  53. package/dist/src/cli/memory/index.js +0 -1
  54. package/dist/src/cli/memory/memory-bridge.js +40 -8
  55. package/dist/src/cli/memory/memory-initializer.js +269 -209
  56. package/dist/src/cli/memory/rvf-migration.js +25 -11
  57. package/dist/src/cli/memory/sqlite-backend.js +573 -0
  58. package/dist/src/cli/memory/suppress-sqlite-warning.js +49 -0
  59. package/dist/src/cli/services/cherry-pick-learnings.js +32 -21
  60. package/dist/src/cli/services/daemon-dashboard.js +13 -1
  61. package/dist/src/cli/services/daemon-lock.js +58 -1
  62. package/dist/src/cli/services/daemon-memory-rpc.js +245 -10
  63. package/dist/src/cli/services/embeddings-migration.js +9 -12
  64. package/dist/src/cli/services/ephemeral-namespace-purge.js +6 -11
  65. package/dist/src/cli/services/learning-service.js +12 -20
  66. package/dist/src/cli/services/project-root.js +69 -9
  67. package/dist/src/cli/services/soft-delete-purge.js +6 -11
  68. package/dist/src/cli/services/sqljs-migration-store.js +4 -1
  69. package/dist/src/cli/services/subagent-bootstrap.js +1 -1
  70. package/dist/src/cli/shared/events/event-store.js +26 -55
  71. package/dist/src/cli/version.js +1 -1
  72. package/package.json +2 -4
  73. package/dist/src/cli/memory/sqljs-backend.js +0 -643
@@ -8,24 +8,19 @@ import { existsSync, readFileSync, statSync } from 'fs';
8
8
  import { join } from 'path';
9
9
  import { memoryDbCandidatePaths } from '../services/moflo-paths.js';
10
10
  import { errorDetail } from '../shared/utils/error-detail.js';
11
+ import { openDaemonDatabase } from '../memory/daemon-backend.js';
11
12
  /** Skew (cached / live count delta) above which the cache is treated as stale. */
12
13
  const VECTOR_STATS_SKEW_WARN_THRESHOLD = 0.2;
13
14
  /**
14
- * Open `dbPath` via moflo's bundled sql.js and return the count of memory_entries
15
- * rows that have an embedding. Returns null if sql.js can't be loaded, the file
16
- * isn't a v3 schema, or the query fails — every error is treated as "unknown
17
- * truth", letting the caller fall back to the cached stats rather than masking
18
- * a healthy DB as broken.
15
+ * Open `dbPath` via the unified node:sqlite factory and return the count of
16
+ * memory_entries rows that have an embedding. Returns null on any error
17
+ * (corrupt DB, missing column, schema mismatch) — every error is treated as
18
+ * "unknown truth", letting the caller fall back to the cached stats rather
19
+ * than masking a healthy DB as broken.
19
20
  */
20
21
  async function countEmbeddedRowsFromDb(dbPath) {
21
22
  try {
22
- const { mofloImport } = await import('../services/moflo-require.js');
23
- const initSqlJs = (await mofloImport('sql.js'))?.default;
24
- if (!initSqlJs)
25
- return null;
26
- const SQL = await initSqlJs();
27
- const buffer = readFileSync(dbPath);
28
- const db = new SQL.Database(buffer);
23
+ const db = openDaemonDatabase(dbPath);
29
24
  try {
30
25
  const res = db.exec("SELECT COUNT(*) FROM memory_entries WHERE embedding IS NOT NULL AND embedding != ''");
31
26
  const cell = res?.[0]?.values?.[0]?.[0];
@@ -137,21 +132,21 @@ export async function checkEmbeddings() {
137
132
  message: `Memory DB initialized (v${info.version}, vectors enabled)`,
138
133
  };
139
134
  }
140
- catch (sqlJsError) {
141
- // sql.js not available — fall back to file-size heuristic
142
- const sqlDetail = errorDetail(sqlJsError);
135
+ catch (sqliteError) {
136
+ // node:sqlite open / introspection failed — fall back to file-size heuristic.
137
+ const sqlDetail = errorDetail(sqliteError);
143
138
  try {
144
139
  const stats = statSync(foundDbPath);
145
140
  const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
146
141
  return {
147
142
  name: 'Embeddings',
148
143
  status: 'warn',
149
- message: `Memory DB exists (${sizeMB} MB) — cannot verify vectors (sql.js not available: ${sqlDetail})`,
150
- fix: 'npm install sql.js && npx moflo embeddings init',
144
+ message: `Memory DB exists (${sizeMB} MB) — cannot verify vectors (DB unreadable: ${sqlDetail})`,
145
+ fix: 'npx moflo embeddings init',
151
146
  };
152
147
  }
153
148
  catch (statError) {
154
- return { name: 'Embeddings', status: 'warn', message: `Unable to check: sql.js failed (${sqlDetail}), stat failed (${errorDetail(statError)})` };
149
+ return { name: 'Embeddings', status: 'warn', message: `Unable to check: DB read failed (${sqlDetail}), stat failed (${errorDetail(statError)})` };
155
150
  }
156
151
  }
157
152
  }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Daemon Version Skew doctor check (epic #1054.S5 / #1059).
3
+ *
4
+ * Surfaces the failure mode that silently masked the #1054 bug class for two
5
+ * version bumps: a long-lived daemon that survived `npm install moflo@<new>`
6
+ * keeps running pre-upgrade code while the on-disk package.json reads `<new>`.
7
+ *
8
+ * Distinct failure mode — NOT buried in "stale cache". Consumes the same
9
+ * signal the launcher acts on (`bin/session-start-launcher.mjs` section 3a-pre)
10
+ * via `getDaemonLockPayload` so the diagnosis and the auto-recycle path share
11
+ * one source of truth.
12
+ *
13
+ * @module cli/commands/doctor-checks-version-skew
14
+ */
15
+ import { existsSync, readFileSync } from 'fs';
16
+ import { join } from 'path';
17
+ import { getDaemonLockPayload, readOwnMofloVersion } from '../services/daemon-lock.js';
18
+ import { errorDetail } from '../shared/utils/error-detail.js';
19
+ /**
20
+ * Resolve the installed package version from `node_modules/moflo/package.json`.
21
+ * Falls back to the daemon's own version (same package on consumers; same
22
+ * dogfood layout in this repo).
23
+ */
24
+ function readInstalledVersion(cwd) {
25
+ const pkgPath = join(cwd, 'node_modules', 'moflo', 'package.json');
26
+ if (existsSync(pkgPath)) {
27
+ try {
28
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
29
+ if (typeof pkg.version === 'string')
30
+ return pkg.version;
31
+ }
32
+ catch {
33
+ // unreadable / malformed — fall through
34
+ }
35
+ }
36
+ // Dogfood path (this repo): no `node_modules/moflo`; read the root package
37
+ // via the same walker the daemon uses.
38
+ return readOwnMofloVersion() ?? null;
39
+ }
40
+ /**
41
+ * Distinct doctor entry — fails when the running daemon's `version` (recorded
42
+ * in `.moflo/daemon.lock` by S2) does not match the installed package version.
43
+ *
44
+ * Pre-#1054 daemons have no `version` in their lock — treated as a mismatch
45
+ * because by construction they were launched before version publishing
46
+ * existed.
47
+ *
48
+ * No daemon running → pass with a neutral message (the daemon-status check
49
+ * already owns the "not running" diagnosis).
50
+ */
51
+ export async function checkDaemonVersionSkew(cwd = process.cwd()) {
52
+ const name = 'Daemon Version Skew';
53
+ try {
54
+ const installed = readInstalledVersion(cwd);
55
+ if (!installed) {
56
+ return {
57
+ name,
58
+ status: 'warn',
59
+ message: 'Cannot resolve installed moflo version (no node_modules/moflo/package.json)',
60
+ };
61
+ }
62
+ const payload = getDaemonLockPayload(cwd);
63
+ if (!payload) {
64
+ return {
65
+ name,
66
+ status: 'pass',
67
+ message: `No daemon running — installed v${installed}`,
68
+ };
69
+ }
70
+ const observed = payload.version ?? '<pre-1054 / unknown>';
71
+ if (payload.version === installed) {
72
+ return {
73
+ name,
74
+ status: 'pass',
75
+ message: `Daemon v${observed} matches installed v${installed} (PID ${payload.pid})`,
76
+ };
77
+ }
78
+ return {
79
+ name,
80
+ status: 'fail',
81
+ message: `Daemon (PID ${payload.pid}) running v${observed} but installed package is v${installed}. ` +
82
+ `Stale pre-upgrade daemon — every write it makes is against pre-upgrade code paths (#1054 bug class).`,
83
+ fix: 'npx moflo daemon stop && npx moflo daemon start',
84
+ };
85
+ }
86
+ catch (e) {
87
+ return {
88
+ name,
89
+ status: 'warn',
90
+ message: `Unable to check version skew: ${errorDetail(e, { firstLineOnly: true })}`,
91
+ };
92
+ }
93
+ }
94
+ //# sourceMappingURL=doctor-checks-version-skew.js.map
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Writers Audit runtime doctor check (epic #1054.S5 / #1059).
3
+ *
4
+ * Runtime sibling of S1's static lint (`tests/system/moflo-db-writer-audit.test.ts`).
5
+ * Enumerates running node processes whose command line invokes one of the
6
+ * known cross-process writers (build-embeddings, migrations) and fails if any
7
+ * are alive while the daemon owns the lock.
8
+ *
9
+ * Why not lsof / handle.exe?
10
+ * - `lsof` not installed on every Linux distro and not on Windows.
11
+ * - `handle.exe` (Sysinternals) requires manual install.
12
+ * - `openfiles.exe` is disabled by default on Windows and requires a reboot to
13
+ * enable.
14
+ *
15
+ * Command-line signature scan is cross-platform, dependency-free, and matches
16
+ * the writers we actually care about — S3 ported them all to the daemon-offline
17
+ * pattern, so any of them being alive concurrently with the daemon is a
18
+ * regression of S3's wrapper logic.
19
+ *
20
+ * @module cli/commands/doctor-checks-writers-audit
21
+ */
22
+ import { execSync } from 'child_process';
23
+ import { existsSync, readFileSync } from 'fs';
24
+ import { join } from 'path';
25
+ import { getDaemonLockHolder } from '../services/daemon-lock.js';
26
+ import { errorDetail } from '../shared/utils/error-detail.js';
27
+ const SCAN_TIMEOUT_MS_WIN = 10_000;
28
+ const SCAN_TIMEOUT_MS_POSIX = 5_000;
29
+ const CMDLINE_CAPTURE_LEN = 300;
30
+ /**
31
+ * Command-line fragments that identify a CROSS-PROCESS moflo.db writer.
32
+ * These should never run while the daemon owns the lock (S3 wraps every
33
+ * invocation in an explicit daemon-stop). Indexer scripts (index-guidance,
34
+ * index-tests, etc.) are daemon-spawned children — they appear in
35
+ * `background-pids.json` and are filtered out below.
36
+ */
37
+ const FOREIGN_WRITER_PATTERNS = [
38
+ /build-embeddings\.mjs/i,
39
+ /bin[\\\/]migrations[\\\/][^\s"']+\.mjs/i,
40
+ /lib[\\\/]db-repair\.mjs/i,
41
+ ];
42
+ function readTrackedBackgroundPids(cwd) {
43
+ const result = new Set();
44
+ const registryFile = join(cwd, '.moflo', 'background-pids.json');
45
+ try {
46
+ if (!existsSync(registryFile))
47
+ return result;
48
+ const entries = JSON.parse(readFileSync(registryFile, 'utf-8'));
49
+ if (!Array.isArray(entries))
50
+ return result;
51
+ for (const entry of entries) {
52
+ if (entry && typeof entry.pid === 'number' && entry.pid > 0) {
53
+ result.add(entry.pid);
54
+ }
55
+ }
56
+ }
57
+ catch { /* unreadable — treat as empty */ }
58
+ return result;
59
+ }
60
+ function enumerateNodeProcesses() {
61
+ try {
62
+ if (process.platform === 'win32') {
63
+ const csv = execSync('powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"Name=\'node.exe\'\\" | Select-Object ProcessId,CommandLine | ConvertTo-Csv -NoTypeInformation"', { encoding: 'utf-8', timeout: SCAN_TIMEOUT_MS_WIN, windowsHide: true });
64
+ const out = [];
65
+ for (const line of csv.split(/\r?\n/)) {
66
+ const m = line.match(/^"(\d+)","?(.*?)"?$/);
67
+ if (!m)
68
+ continue;
69
+ const pid = parseInt(m[1], 10);
70
+ if (!Number.isFinite(pid) || pid <= 0)
71
+ continue;
72
+ out.push({ pid, cmdline: m[2].replace(/""/g, '"').slice(0, CMDLINE_CAPTURE_LEN) });
73
+ }
74
+ return out;
75
+ }
76
+ // POSIX
77
+ const ps = execSync('ps -ww -eo pid,command', { encoding: 'utf-8', timeout: SCAN_TIMEOUT_MS_POSIX });
78
+ const out = [];
79
+ for (const line of ps.split(/\r?\n/)) {
80
+ const m = line.trim().match(/^(\d+)\s+(.*)$/);
81
+ if (!m)
82
+ continue;
83
+ const cmd = m[2];
84
+ if (!/\bnode\b/.test(cmd))
85
+ continue;
86
+ const pid = parseInt(m[1], 10);
87
+ if (!Number.isFinite(pid) || pid <= 0)
88
+ continue;
89
+ out.push({ pid, cmdline: cmd.slice(0, CMDLINE_CAPTURE_LEN) });
90
+ }
91
+ return out;
92
+ }
93
+ catch {
94
+ return [];
95
+ }
96
+ }
97
+ function formatCmdline(raw, max = 80) {
98
+ const cleaned = raw.replace(/^"?[^"\s]*node(?:\.exe)?"?\s+/i, '').trim();
99
+ return cleaned.length > max ? cleaned.slice(0, max - 1) + '…' : cleaned;
100
+ }
101
+ export function findForeignWriters(procs, daemonPid, trackedPids, selfPid) {
102
+ const out = [];
103
+ for (const p of procs) {
104
+ if (p.pid === selfPid)
105
+ continue;
106
+ if (p.pid === daemonPid)
107
+ continue;
108
+ if (trackedPids.has(p.pid))
109
+ continue;
110
+ for (const re of FOREIGN_WRITER_PATTERNS) {
111
+ const m = p.cmdline.match(re);
112
+ if (m) {
113
+ out.push({ ...p, matchedPattern: m[0] });
114
+ break;
115
+ }
116
+ }
117
+ }
118
+ return out;
119
+ }
120
+ /**
121
+ * Pass: daemon down (single-writer invariant has nothing to enforce) OR no
122
+ * foreign writers detected.
123
+ *
124
+ * Fail: daemon owns the lock AND a non-daemon, non-tracked node process is
125
+ * running a known cross-process writer. Lists every offender so the user can
126
+ * SIGKILL them by PID.
127
+ */
128
+ export async function checkWritersAudit(cwd = process.cwd()) {
129
+ const name = 'Writers Audit';
130
+ try {
131
+ const daemonPid = getDaemonLockHolder(cwd);
132
+ if (daemonPid === null) {
133
+ return {
134
+ name,
135
+ status: 'pass',
136
+ message: 'Daemon not running — single-writer invariant trivially satisfied',
137
+ };
138
+ }
139
+ const procs = enumerateNodeProcesses();
140
+ const trackedPids = readTrackedBackgroundPids(cwd);
141
+ const foreign = findForeignWriters(procs, daemonPid, trackedPids, process.pid);
142
+ if (foreign.length === 0) {
143
+ return {
144
+ name,
145
+ status: 'pass',
146
+ message: `Daemon PID ${daemonPid} is sole writer; no foreign writers detected`,
147
+ };
148
+ }
149
+ const detail = foreign
150
+ .map((p) => `pid=${p.pid} (${p.matchedPattern}): ${formatCmdline(p.cmdline)}`)
151
+ .join(' | ');
152
+ return {
153
+ name,
154
+ status: 'fail',
155
+ message: `${foreign.length} non-daemon writer(s) running concurrently with daemon (PID ${daemonPid}): ${detail}. ` +
156
+ `Each will clobber the daemon's sql.js snapshot on flush — #1054 bug class.`,
157
+ fix: process.platform === 'win32'
158
+ ? `taskkill /F /PID ${foreign.map((p) => p.pid).join(' /PID ')}`
159
+ : `kill ${foreign.map((p) => p.pid).join(' ')}`,
160
+ };
161
+ }
162
+ catch (e) {
163
+ return {
164
+ name,
165
+ status: 'warn',
166
+ message: `Unable to audit writers: ${errorDetail(e, { firstLineOnly: true })}`,
167
+ };
168
+ }
169
+ }
170
+ //# sourceMappingURL=doctor-checks-writers-audit.js.map
@@ -24,8 +24,8 @@
24
24
  /* eslint-disable @typescript-eslint/no-explicit-any */
25
25
  import { existsSync } from 'fs';
26
26
  import { CANONICAL_EMBEDDING_MODEL } from '../embeddings/migration/types.js';
27
- import { mofloImport } from '../services/moflo-require.js';
28
27
  import { memoryDbCandidatePaths } from '../services/moflo-paths.js';
28
+ import { openDaemonDatabase } from '../memory/daemon-backend.js';
29
29
  /**
30
30
  * Known neural-model labels that all share the all-MiniLM-L6-v2 384-dim
31
31
  * vector space. The Story-2 migration retags any of these to the
@@ -59,7 +59,7 @@ export async function checkEmbeddingHygiene() {
59
59
  return {
60
60
  name: 'Embedding hygiene',
61
61
  status: 'pass',
62
- message: 'Cannot inspect memory DB (sql.js not available)',
62
+ message: 'Cannot inspect memory DB (open failed)',
63
63
  };
64
64
  }
65
65
  if (groups.length === 0) {
@@ -127,21 +127,9 @@ function resolveMemoryDb() {
127
127
  return memoryDbCandidatePaths(process.cwd()).find((p) => existsSync(p)) ?? null;
128
128
  }
129
129
  async function loadModelGroups(dbPath) {
130
- const fs = await import('fs');
131
- let initSqlJs;
132
- try {
133
- initSqlJs = (await mofloImport('sql.js'))?.default;
134
- }
135
- catch {
136
- return null;
137
- }
138
- if (!initSqlJs)
139
- return null;
140
130
  let db;
141
131
  try {
142
- const SQL = await initSqlJs();
143
- const buffer = fs.readFileSync(dbPath);
144
- db = new SQL.Database(buffer);
132
+ db = openDaemonDatabase(dbPath);
145
133
  }
146
134
  catch {
147
135
  return null;
@@ -169,6 +169,36 @@ export async function autoFixCheck(check) {
169
169
  catch { /* best effort */ }
170
170
  return runFixCommand('npx moflo daemon start');
171
171
  },
172
+ // Epic #1054.S5 / #1059 — SIGTERM the stale daemon and let the launcher's
173
+ // existing respawn path (mirrored as `npx moflo daemon start`) pick up the
174
+ // installed-version code. Mirrors `recycleDaemon` in
175
+ // bin/session-start-launcher.mjs so the auto-fix matches the launcher's
176
+ // behavior exactly.
177
+ 'Daemon Version Skew': async () => {
178
+ const cwd = process.cwd();
179
+ const { getDaemonLockPayload } = await import('../services/daemon-lock.js');
180
+ const payload = getDaemonLockPayload(cwd);
181
+ if (payload?.pid && payload.pid > 0) {
182
+ try {
183
+ process.kill(payload.pid, 'SIGTERM');
184
+ }
185
+ catch { /* already dead */ }
186
+ }
187
+ const lockFile = join(cwd, '.moflo', 'daemon.lock');
188
+ try {
189
+ if (existsSync(lockFile))
190
+ unlinkSync(lockFile);
191
+ }
192
+ catch { /* ok */ }
193
+ return runFixCommand('npx moflo daemon start');
194
+ },
195
+ 'Embedding Coverage Truth': async () => {
196
+ // Same as the existing Embeddings fix — rebuild the cache by re-running
197
+ // the embeddings pipeline. Routes through `npx moflo` so the consumer
198
+ // CLI resolution stays consistent across platforms (see
199
+ // feedback_cross_platform_mandatory).
200
+ return runFixCommand('npx moflo embeddings init --force');
201
+ },
172
202
  'MCP Servers': async () => {
173
203
  return runFixCommand('claude mcp add moflo -- npx -y moflo mcp start');
174
204
  },
@@ -6,6 +6,9 @@
6
6
  */
7
7
  import { checkSubagentHealth, checkSpellExecution, checkMcpToolInvocation, checkHookExecution, checkMcpSpellIntegration, checkGateHealth, checkHookBlockDrift, checkMofloDbBridge, } from './doctor-checks-deep.js';
8
8
  import { checkEmbeddingHygiene } from './doctor-embedding-hygiene.js';
9
+ import { checkDaemonVersionSkew } from './doctor-checks-version-skew.js';
10
+ import { checkEmbeddingCoverageTruth } from './doctor-checks-coverage-truth.js';
11
+ import { checkWritersAudit } from './doctor-checks-writers-audit.js';
9
12
  import { checkSwarmFunctional, checkHiveMindFunctional, } from './doctor-checks-swarm.js';
10
13
  import { checkMemoryAccessFunctional } from './doctor-checks-memory-access.js';
11
14
  import { checkBuildTools, checkClaudeCode, checkDiskSpace, checkGit, checkGitRepo, checkNodeVersion, checkNpmVersion, } from './doctor-checks-runtime.js';
@@ -33,10 +36,13 @@ export const allChecks = [
33
36
  checkMofloYamlCompliance,
34
37
  checkStatusLine,
35
38
  checkDaemonStatus,
39
+ checkDaemonVersionSkew,
36
40
  checkDaemonWriteRouting,
41
+ checkWritersAudit,
37
42
  checkMemoryDatabase,
38
43
  checkEmbeddings,
39
44
  checkEmbeddingHygiene,
45
+ checkEmbeddingCoverageTruth,
40
46
  checkTestDirs,
41
47
  checkMcpServers,
42
48
  checkDiskSpace,
@@ -77,11 +83,19 @@ export const componentMap = {
77
83
  'statusline': checkStatusLine,
78
84
  'status-line': checkStatusLine,
79
85
  'daemon': checkDaemonStatus,
86
+ 'daemon-version-skew': checkDaemonVersionSkew,
87
+ 'version-skew': checkDaemonVersionSkew,
88
+ 'skew': checkDaemonVersionSkew,
80
89
  'daemon-write-routing': checkDaemonWriteRouting,
81
90
  'write-routing': checkDaemonWriteRouting,
91
+ 'writers-audit': checkWritersAudit,
92
+ 'writers': checkWritersAudit,
82
93
  'memory': checkMemoryDatabase,
83
94
  'embeddings': checkEmbeddings,
84
95
  'embedding-hygiene': checkEmbeddingHygiene,
96
+ 'embedding-coverage': checkEmbeddingCoverageTruth,
97
+ 'coverage': checkEmbeddingCoverageTruth,
98
+ 'coverage-truth': checkEmbeddingCoverageTruth,
85
99
  'hygiene': checkEmbeddingHygiene,
86
100
  'git': checkGit,
87
101
  'mcp': checkMcpServers,
@@ -40,7 +40,7 @@ export const doctorCommand = {
40
40
  {
41
41
  name: 'component',
42
42
  short: 'c',
43
- description: 'Check specific component (version, node, npm, config, daemon, memory, embeddings, git, mcp, claude, disk, typescript, semantic, intelligence, swarm, hive-mind)',
43
+ description: 'Check specific component (version, version-skew, node, npm, config, daemon, writers-audit, memory, embeddings, coverage-truth, git, mcp, claude, disk, typescript, semantic, intelligence, swarm, hive-mind)',
44
44
  type: 'string',
45
45
  },
46
46
  {
@@ -13,10 +13,10 @@
13
13
  * Created with ❤️ by motailz.com
14
14
  */
15
15
  import { output } from '../output.js';
16
- import { mofloImport } from '../services/moflo-require.js';
17
16
  import { runEmbeddingsMigrationIfNeeded } from '../services/embeddings-migration.js';
18
17
  import { memoryDbPath, MEMORY_DB_FILE, MOFLO_DIR } from '../services/moflo-paths.js';
19
18
  import * as embeddings from '../embeddings/index.js';
19
+ import { openDaemonDatabase } from '../memory/daemon-backend.js';
20
20
  import { errorDetail } from '../shared/utils/error-detail.js';
21
21
  const DEFAULT_DB_PATH_FLAG = `${MOFLO_DIR}/${MEMORY_DB_FILE}`;
22
22
  // Generate subcommand - REAL implementation
@@ -90,7 +90,7 @@ const generateCommand = {
90
90
  }
91
91
  },
92
92
  };
93
- // Search subcommand - REAL implementation using sql.js
93
+ // Search subcommand - REAL implementation using node:sqlite via openDaemonDatabase
94
94
  const searchCommand = {
95
95
  name: 'search',
96
96
  description: 'Semantic similarity search',
@@ -131,11 +131,8 @@ const searchCommand = {
131
131
  output.printInfo('Run: claude-flow memory init');
132
132
  return { success: false, exitCode: 1 };
133
133
  }
134
- // Load sql.js
135
- const initSqlJs = (await mofloImport('sql.js')).default;
136
- const SQL = await initSqlJs();
137
- const fileBuffer = fs.readFileSync(fullDbPath);
138
- const db = new SQL.Database(fileBuffer);
134
+ // node:sqlite via the unified factory (Phase 5 / #1084).
135
+ const db = openDaemonDatabase(fullDbPath);
139
136
  const startTime = Date.now();
140
137
  // Generate embedding for query
141
138
  const { generateEmbedding } = await import('../memory/memory-initializer.js');
@@ -343,7 +340,7 @@ const compareCommand = {
343
340
  }
344
341
  },
345
342
  };
346
- // Collections subcommand - REAL implementation using sql.js
343
+ // Collections subcommand - REAL implementation using node:sqlite via openDaemonDatabase
347
344
  const collectionsCommand = {
348
345
  name: 'collections',
349
346
  description: 'Manage embedding collections (namespaces)',
@@ -374,11 +371,8 @@ const collectionsCommand = {
374
371
  output.writeln(output.dim('No collections yet - initialize memory first'));
375
372
  return { success: true, data: [] };
376
373
  }
377
- // Load sql.js and query real data
378
- const initSqlJs = (await mofloImport('sql.js')).default;
379
- const SQL = await initSqlJs();
380
- const fileBuffer = fs.readFileSync(fullDbPath);
381
- const db = new SQL.Database(fileBuffer);
374
+ // node:sqlite via the unified factory (Phase 5 / #1084).
375
+ const db = openDaemonDatabase(fullDbPath);
382
376
  // Get collection stats from database
383
377
  const statsQuery = db.exec(`
384
378
  SELECT
@@ -1159,17 +1153,18 @@ const cacheCommand = {
1159
1153
  else {
1160
1154
  sqliteSize = `${sizeBytes} B`;
1161
1155
  }
1162
- // Try to count real entries via sql.js
1156
+ // Try to count real entries via node:sqlite
1163
1157
  try {
1164
- const initSqlJs = (await mofloImport('sql.js')).default;
1165
- const SQL = await initSqlJs();
1166
- const fileBuffer = fs.readFileSync(resolvedDbPath);
1167
- const db = new SQL.Database(fileBuffer);
1168
- const result = db.exec('SELECT COUNT(*) as count FROM embeddings');
1169
- if (result.length > 0 && result[0].values.length > 0) {
1170
- sqliteEntries = result[0].values[0][0];
1158
+ const db = openDaemonDatabase(resolvedDbPath);
1159
+ try {
1160
+ const result = db.exec('SELECT COUNT(*) as count FROM embeddings');
1161
+ if (result.length > 0 && result[0].values.length > 0) {
1162
+ sqliteEntries = result[0].values[0][0];
1163
+ }
1164
+ }
1165
+ finally {
1166
+ db.close();
1171
1167
  }
1172
- db.close();
1173
1168
  }
1174
1169
  catch {
1175
1170
  // Estimate entries from file size (~1600 bytes per entry for 384-dim embeddings)
@@ -7,8 +7,7 @@ import * as pathModule from 'path';
7
7
  import { output } from '../output.js';
8
8
  import { select, confirm, input } from '../prompt.js';
9
9
  import { callMCPTool, MCPClientError } from '../mcp-client.js';
10
- import { mofloImport } from '../services/moflo-require.js';
11
- import { atomicWriteFileSync } from '../services/atomic-file-write.js';
10
+ import { openDaemonDatabase } from '../memory/daemon-backend.js';
12
11
  import { errorDetail } from '../shared/utils/error-detail.js';
13
12
  // Memory backends
14
13
  const BACKENDS = [
@@ -1265,23 +1264,10 @@ const initMemoryCommand = {
1265
1264
  const DB_FILENAME = 'memory.db';
1266
1265
  const SWARM_DIR = '.swarm';
1267
1266
  async function openDb(cwd) {
1268
- const fs = await import('fs');
1269
1267
  const path = await import('path');
1270
- const initSqlJs = (await mofloImport('sql.js')).default;
1271
- const SQL = await initSqlJs();
1272
1268
  const dbPath = path.join(cwd, SWARM_DIR, DB_FILENAME);
1273
- const dir = path.dirname(dbPath);
1274
- if (!fs.existsSync(dir)) {
1275
- fs.mkdirSync(dir, { recursive: true });
1276
- }
1277
- let db;
1278
- if (fs.existsSync(dbPath)) {
1279
- const buffer = fs.readFileSync(dbPath);
1280
- db = new SQL.Database(buffer);
1281
- }
1282
- else {
1283
- db = new SQL.Database();
1284
- }
1269
+ // openDaemonDatabase ensures the parent directory exists and applies WAL.
1270
+ const db = openDaemonDatabase(dbPath);
1285
1271
  // Ensure table exists
1286
1272
  db.run(`
1287
1273
  CREATE TABLE IF NOT EXISTS memory_entries (
@@ -1307,10 +1293,14 @@ async function openDb(cwd) {
1307
1293
  `);
1308
1294
  db.run(`CREATE INDEX IF NOT EXISTS idx_memory_key_ns ON memory_entries(key, namespace)`);
1309
1295
  db.run(`CREATE INDEX IF NOT EXISTS idx_memory_namespace ON memory_entries(namespace)`);
1310
- return { db, dbPath, SQL };
1296
+ return { db, dbPath };
1311
1297
  }
1312
- function saveAndCloseDb(db, dbPath) {
1313
- atomicWriteFileSync(dbPath, db.export());
1298
+ /**
1299
+ * Close the DB handle. node:sqlite + WAL has already persisted every prior
1300
+ * `db.run` incrementally — the explicit atomicWriteFileSync sql.js used to
1301
+ * need is gone (Phase 5 / #1084).
1302
+ */
1303
+ function saveAndCloseDb(db, _dbPath) {
1314
1304
  db.close();
1315
1305
  }
1316
1306
  function batchGenerateId() {
@@ -1937,9 +1927,9 @@ const rebuildIndexCommand = {
1937
1927
  }
1938
1928
  failed++;
1939
1929
  }
1940
- if ((i + 1) % BATCH_SIZE === 0) {
1941
- atomicWriteFileSync(dbPath, db.export());
1942
- }
1930
+ // node:sqlite + WAL persists each db.run incrementally — the
1931
+ // periodic batch flush sql.js needed here was the export-+-rewrite
1932
+ // pattern Phase 5 (#1084) killed. No flush needed.
1943
1933
  }
1944
1934
  const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
1945
1935
  // Final stats