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
@@ -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
  },
@@ -213,6 +243,63 @@ export async function autoFixCheck(check) {
213
243
  return false;
214
244
  }
215
245
  },
246
+ // Tiered recovery for `.moflo/moflo.db` corruption (REINDEX → VACUUM
247
+ // INTO → row-level salvage). The TS service stops the daemon
248
+ // automatically (cross-platform via `process.kill('SIGTERM')`) so the
249
+ // atomic swap doesn't race a live writer; we restart it via the
250
+ // existing `npx moflo daemon start` shorthand after. The MCP server,
251
+ // started by Claude Code outside our process tree, isn't stopped here —
252
+ // explicit user guidance covers that case at the end.
253
+ 'Memory DB Integrity': async () => {
254
+ try {
255
+ const { repairMemoryDbIntegrity } = await import('../services/memory-db-integrity-repair.js');
256
+ const result = await repairMemoryDbIntegrity(process.cwd());
257
+ if (result.repaired) {
258
+ const tierLabel = result.tier === 'reindex' ? 'REINDEX (index rebuild)'
259
+ : result.tier === 'vacuum' ? 'VACUUM INTO (fresh-file rebuild)'
260
+ : result.tier === 'salvage' ? 'row-level salvage'
261
+ : 'repaired';
262
+ output.writeln(output.dim(` Recovered via ${tierLabel}.`));
263
+ if (result.corruptBackup) {
264
+ output.writeln(output.dim(` Pre-repair backup retained: ${result.corruptBackup}`));
265
+ }
266
+ if (result.lossStats) {
267
+ for (const [tbl, s] of Object.entries(result.lossStats)) {
268
+ if (s.read > 0) {
269
+ const lost = Math.max(0, s.read - s.written);
270
+ if (lost > 0) {
271
+ output.writeln(output.warning(` ${tbl}: ${s.written}/${s.read} rows preserved (lost ${lost} across ${s.errors} unreadable chunk(s))`));
272
+ }
273
+ }
274
+ }
275
+ output.writeln(output.dim(' Embeddings for lost rows will be regenerated on next index pass — run `npx moflo embeddings init` to force.'));
276
+ }
277
+ // Restart the daemon if we stopped it. The launcher's own
278
+ // section-4 spawn handles this on next session-start, but a
279
+ // mid-session healer call shouldn't leave the daemon down.
280
+ if (result.daemonStopped) {
281
+ output.writeln(output.dim(' Restarting daemon...'));
282
+ await runFixCommand('npx moflo daemon start');
283
+ }
284
+ // Cross-platform note for the MCP server (out-of-tree, can't
285
+ // SIGTERM). On Windows the swap would have failed if MCP was
286
+ // holding the file; on POSIX the swap succeeds but MCP keeps
287
+ // reading the stale inode until restart. Either way: restart
288
+ // Claude Code to fully apply.
289
+ output.writeln(output.dim(' Restart Claude Code so the MCP server re-opens the recovered DB.'));
290
+ return true;
291
+ }
292
+ if (result.persistent) {
293
+ output.writeln(output.warning(' Corruption survived every recovery tier. Manual options: ' +
294
+ '`npx moflo memory rebuild-index` (destructive) or restore from a known-good backup.'));
295
+ }
296
+ return false;
297
+ }
298
+ catch (e) {
299
+ output.writeln(output.warning(` Repair failed: ${errorDetail(e)}`));
300
+ return false;
301
+ }
302
+ },
216
303
  'Status Line': async () => {
217
304
  const settingsPath = join(process.cwd(), '.claude', 'settings.json');
218
305
  if (!existsSync(settingsPath))
@@ -6,10 +6,13 @@
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';
12
- import { checkConfigFile, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMofloYamlCompliance, checkStatusLine, checkTestDirs, } from './doctor-checks-config.js';
15
+ import { checkConfigFile, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMofloYamlCompliance, checkStatusLine, checkTestDirs, } from './doctor-checks-config.js';
13
16
  import { checkSpellEngine, checkSandboxTier } from './doctor-checks-platform.js';
14
17
  import { checkEmbeddings, checkSemanticQuality, } from './doctor-checks-memory.js';
15
18
  import { checkIntelligence } from './doctor-checks-intelligence.js';
@@ -33,10 +36,19 @@ export const allChecks = [
33
36
  checkMofloYamlCompliance,
34
37
  checkStatusLine,
35
38
  checkDaemonStatus,
39
+ checkDaemonVersionSkew,
36
40
  checkDaemonWriteRouting,
41
+ checkWritersAudit,
37
42
  checkMemoryDatabase,
43
+ // Owns the corruption signal so downstream checks (Embeddings, Semantic
44
+ // Quality, Memory Access Functional) don't surface it as the synthetic
45
+ // "Check" failure (doctor.ts:214). MUST run after checkMemoryDatabase
46
+ // (which confirms the file exists) and before any check that opens the
47
+ // DB via openBackend.
48
+ checkMemoryDbIntegrity,
38
49
  checkEmbeddings,
39
50
  checkEmbeddingHygiene,
51
+ checkEmbeddingCoverageTruth,
40
52
  checkTestDirs,
41
53
  checkMcpServers,
42
54
  checkDiskSpace,
@@ -77,11 +89,22 @@ export const componentMap = {
77
89
  'statusline': checkStatusLine,
78
90
  'status-line': checkStatusLine,
79
91
  'daemon': checkDaemonStatus,
92
+ 'daemon-version-skew': checkDaemonVersionSkew,
93
+ 'version-skew': checkDaemonVersionSkew,
94
+ 'skew': checkDaemonVersionSkew,
80
95
  'daemon-write-routing': checkDaemonWriteRouting,
81
96
  'write-routing': checkDaemonWriteRouting,
97
+ 'writers-audit': checkWritersAudit,
98
+ 'writers': checkWritersAudit,
82
99
  'memory': checkMemoryDatabase,
100
+ 'memory-db-integrity': checkMemoryDbIntegrity,
101
+ 'integrity': checkMemoryDbIntegrity,
102
+ 'memory-integrity': checkMemoryDbIntegrity,
83
103
  'embeddings': checkEmbeddings,
84
104
  'embedding-hygiene': checkEmbeddingHygiene,
105
+ 'embedding-coverage': checkEmbeddingCoverageTruth,
106
+ 'coverage': checkEmbeddingCoverageTruth,
107
+ 'coverage-truth': checkEmbeddingCoverageTruth,
85
108
  'hygiene': checkEmbeddingHygiene,
86
109
  'git': checkGit,
87
110
  '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