moflo 4.9.0-rc.1 → 4.9.0-rc.2

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.
@@ -256,13 +256,15 @@ function formatModelName(modelId) {
256
256
  return 'Claude Code';
257
257
  }
258
258
 
259
- // Get learning stats from memory database (pure stat calls)
259
+ // Get learning stats from memory database (pure stat calls).
260
+ // Canonical first (post-#727), then legacy paths. Keep in lockstep with
261
+ // `memoryDbCandidatePaths` in src/cli/services/moflo-paths.ts.
260
262
  function getLearningStats() {
261
263
  const memoryPaths = [
264
+ path.join(CWD, '.moflo', 'moflo.db'),
262
265
  path.join(CWD, '.swarm', 'memory.db'),
263
- path.join(CWD, '.moflo', 'memory.db'),
264
- path.join(CWD, '.claude', 'memory.db'),
265
266
  path.join(CWD, 'data', 'memory.db'),
267
+ path.join(CWD, '.claude', 'memory.db'),
266
268
  path.join(CWD, '.agentdb', 'memory.db'),
267
269
  ];
268
270
 
@@ -506,12 +508,13 @@ function getAgentDBStats() {
506
508
  return { vectorCount, dbSizeKB, namespaces, hasHnsw };
507
509
  }
508
510
 
509
- // Fallback: estimate from DB file size (no subprocess)
511
+ // Fallback: estimate from DB file size (no subprocess). Same probe order
512
+ // as `getLearningStats` above — see comment there.
510
513
  const dbFiles = [
514
+ path.join(CWD, '.moflo', 'moflo.db'),
511
515
  path.join(CWD, '.swarm', 'memory.db'),
512
- path.join(CWD, '.moflo', 'memory.db'),
513
- path.join(CWD, '.claude', 'memory.db'),
514
516
  path.join(CWD, 'data', 'memory.db'),
517
+ path.join(CWD, '.claude', 'memory.db'),
515
518
  ];
516
519
  for (const f of dbFiles) {
517
520
  const stat = safeStat(f);
@@ -524,8 +527,8 @@ function getAgentDBStats() {
524
527
  }
525
528
 
526
529
  const hnswPaths = [
527
- path.join(CWD, '.swarm', 'hnsw.index'),
528
530
  path.join(CWD, '.moflo', 'hnsw.index'),
531
+ path.join(CWD, '.swarm', 'hnsw.index'), // legacy pre-#727
529
532
  ];
530
533
  for (const p of hnswPaths) {
531
534
  if (safeStat(p)) {
@@ -592,7 +595,7 @@ function getIntegrationStatus() {
592
595
  }
593
596
  }
594
597
 
595
- const hasDatabase = ['.swarm/memory.db', '.moflo/memory.db', 'data/memory.db']
598
+ const hasDatabase = ['.moflo/moflo.db', '.swarm/memory.db', 'data/memory.db']
596
599
  .some(p => fs.existsSync(path.join(CWD, p)));
597
600
  const hasApi = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY);
598
601
 
@@ -19,6 +19,7 @@
19
19
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
20
20
  import { resolve, dirname } from 'path';
21
21
  import { mofloResolveURL, mofloInternalURL } from './lib/moflo-resolve.mjs';
22
+ import { memoryDbPath, hnswIndexPath } from './lib/moflo-paths.mjs';
22
23
  const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
23
24
  const FASTEMBED_INLINE = 'dist/src/cli/embeddings/fastembed-inline/index.js';
24
25
  const BRIDGE_CORE = 'dist/src/cli/memory/bridge-core.js';
@@ -35,7 +36,7 @@ function findProjectRoot() {
35
36
  }
36
37
 
37
38
  const projectRoot = findProjectRoot();
38
- const DB_PATH = resolve(projectRoot, '.swarm/memory.db');
39
+ const DB_PATH = memoryDbPath(projectRoot);
39
40
 
40
41
  // Canonical model name emitted into `memory_entries.embedding_model`. The
41
42
  // semantic-search bin script accepts this plus a short set of legacy aliases
@@ -182,8 +183,7 @@ function getEmbeddingStats(db) {
182
183
  function writeVectorStatsCache(stats, nsStats) {
183
184
  try {
184
185
  const dbSizeKB = Math.floor(readFileSync(DB_PATH).length / 1024);
185
- const hasHnsw = existsSync(resolve(projectRoot, '.swarm', 'hnsw.index'))
186
- || existsSync(resolve(projectRoot, '.moflo', 'hnsw.index'));
186
+ const hasHnsw = existsSync(hnswIndexPath(projectRoot));
187
187
  const missing = nsStats.reduce((sum, ns) => sum + (ns.missing || 0), 0);
188
188
  writeVectorStatsJson(projectRoot, {
189
189
  vectorCount: stats.withEmbeddings,
@@ -263,8 +263,8 @@ async function main() {
263
263
  if (embedded > 0) {
264
264
  saveDb(db);
265
265
  for (const p of [
266
- resolve(projectRoot, '.swarm/hnsw.index'),
267
- resolve(projectRoot, '.swarm/hnsw.metadata.json'),
266
+ hnswIndexPath(projectRoot),
267
+ resolve(projectRoot, '.moflo', 'hnsw.metadata.json'),
268
268
  ]) {
269
269
  if (existsSync(p)) {
270
270
  unlinkSync(p);
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Generate structural code map for a monorepo or project.
4
4
  *
5
- * Produces five chunk types stored in the `code-map` namespace of .swarm/memory.db:
5
+ * Produces five chunk types stored in the `code-map` namespace of .moflo/moflo.db:
6
6
  * 1. project: — one per top-level project directory (bird's-eye overview)
7
7
  * 2. dir: — one per directory with 2+ exported types (drill-down detail)
8
8
  * 3. iface-map: — batched interface-to-implementation mappings
@@ -31,6 +31,7 @@ import { fileURLToPath } from 'url';
31
31
  import { createHash } from 'crypto';
32
32
  import { execSync, execFileSync, spawn } from 'child_process';
33
33
  import { mofloResolveURL } from './lib/moflo-resolve.mjs';
34
+ import { memoryDbPath, MOFLO_DIR } from './lib/moflo-paths.mjs';
34
35
  const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
35
36
 
36
37
 
@@ -49,8 +50,8 @@ function findProjectRoot() {
49
50
 
50
51
  const projectRoot = findProjectRoot();
51
52
  const NAMESPACE = 'code-map';
52
- const DB_PATH = resolve(projectRoot, '.swarm/memory.db');
53
- const HASH_CACHE_PATH = resolve(projectRoot, '.swarm/code-map-hash.txt');
53
+ const DB_PATH = memoryDbPath(projectRoot);
54
+ const HASH_CACHE_PATH = resolve(projectRoot, MOFLO_DIR, 'code-map-hash.txt');
54
55
 
55
56
  // Directories to exclude from indexing
56
57
  const EXCLUDE_DIRS = [
@@ -26,6 +26,7 @@ import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSy
26
26
  import { resolve, relative, dirname, basename, extname } from 'path';
27
27
  import { fileURLToPath } from 'url';
28
28
  import { mofloResolveURL } from './lib/moflo-resolve.mjs';
29
+ import { memoryDbPath } from './lib/moflo-paths.mjs';
29
30
  const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
30
31
 
31
32
 
@@ -47,7 +48,7 @@ const projectRoot = findProjectRoot();
47
48
  const mofloRoot = resolve(__dirname, '..');
48
49
 
49
50
  const NAMESPACE = 'guidance';
50
- const DB_PATH = resolve(projectRoot, '.swarm/memory.db');
51
+ const DB_PATH = memoryDbPath(projectRoot);
51
52
 
52
53
  // ============================================================================
53
54
  // Load guidance directories from moflo.yaml, falling back to defaults
@@ -883,7 +884,7 @@ if (!skipEmbeddings && needsEmbeddings) {
883
884
  const embeddingArgs = ['--namespace', NAMESPACE];
884
885
 
885
886
  // Create log file for background process output
886
- const logDir = resolve(projectRoot, '.swarm/logs');
887
+ const logDir = resolve(projectRoot, '.moflo/logs');
887
888
  if (!existsSync(logDir)) {
888
889
  mkdirSync(logDir, { recursive: true });
889
890
  }
@@ -902,7 +903,7 @@ if (!skipEmbeddings && needsEmbeddings) {
902
903
  proc.unref(); // Allow parent to exit independently
903
904
 
904
905
  log(`Background embedding started (PID: ${proc.pid})`);
905
- log(`Log file: .swarm/logs/embeddings.log`);
906
+ log(`Log file: .moflo/logs/embeddings.log`);
906
907
  } else {
907
908
  log('⚠️ Embedding script not found, skipping embedding generation');
908
909
  }
@@ -30,6 +30,7 @@ import { fileURLToPath } from 'url';
30
30
  import { createHash } from 'crypto';
31
31
  import { spawn } from 'child_process';
32
32
  import { mofloResolveURL } from './lib/moflo-resolve.mjs';
33
+ import { memoryDbPath, MOFLO_DIR } from './lib/moflo-paths.mjs';
33
34
  const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
34
35
 
35
36
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -46,8 +47,8 @@ function findProjectRoot() {
46
47
 
47
48
  const projectRoot = findProjectRoot();
48
49
  const NAMESPACE = 'patterns';
49
- const DB_PATH = resolve(projectRoot, '.swarm/memory.db');
50
- const HASH_CACHE_PATH = resolve(projectRoot, '.swarm/patterns-hash.txt');
50
+ const DB_PATH = memoryDbPath(projectRoot);
51
+ const HASH_CACHE_PATH = resolve(projectRoot, MOFLO_DIR, 'patterns-hash.txt');
51
52
 
52
53
  const args = process.argv.slice(2);
53
54
  const force = args.includes('--force');
@@ -28,6 +28,7 @@ import { fileURLToPath } from 'url';
28
28
  import { createHash } from 'crypto';
29
29
  import { execSync, execFileSync, spawn } from 'child_process';
30
30
  import { mofloResolveURL } from './lib/moflo-resolve.mjs';
31
+ import { memoryDbPath, MOFLO_DIR } from './lib/moflo-paths.mjs';
31
32
  const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
32
33
 
33
34
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -44,8 +45,8 @@ function findProjectRoot() {
44
45
 
45
46
  const projectRoot = findProjectRoot();
46
47
  const NAMESPACE = 'tests';
47
- const DB_PATH = resolve(projectRoot, '.swarm/memory.db');
48
- const HASH_CACHE_PATH = resolve(projectRoot, '.swarm/tests-hash.txt');
48
+ const DB_PATH = memoryDbPath(projectRoot);
49
+ const HASH_CACHE_PATH = resolve(projectRoot, MOFLO_DIR, 'tests-hash.txt');
49
50
 
50
51
  // Parse args
51
52
  const args = process.argv.slice(2);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Pure-JS counterpart to src/cli/services/moflo-paths.ts (#699).
2
+ * Pure-JS counterpart to src/cli/services/moflo-paths.ts (#699, #727).
3
3
  *
4
4
  * Lives in bin/lib because session-start-launcher.mjs and other bin/ scripts
5
5
  * run before any TS compilation has happened — they can't import the .ts
@@ -8,11 +8,30 @@
8
8
  * launcher path. Algorithm parity is enforced by the parity case in
9
9
  * src/cli/__tests__/services/moflo-paths-migration.test.ts.
10
10
  */
11
- import { existsSync, readdirSync, renameSync, rmdirSync } from 'node:fs';
12
- import { join } from 'node:path';
11
+ import {
12
+ closeSync,
13
+ copyFileSync,
14
+ existsSync,
15
+ mkdirSync,
16
+ openSync,
17
+ readFileSync,
18
+ readSync,
19
+ readdirSync,
20
+ renameSync,
21
+ rmdirSync,
22
+ statSync,
23
+ unlinkSync,
24
+ } from 'node:fs';
25
+ import { dirname, join } from 'node:path';
13
26
 
14
27
  export const MOFLO_DIR = '.moflo';
28
+ export const MEMORY_DB_FILE = 'moflo.db';
29
+ export const HNSW_INDEX_FILE = 'hnsw.index';
30
+
15
31
  export const LEGACY_CLAUDE_FLOW_DIR = '.claude-flow';
32
+ export const LEGACY_SWARM_DIR = '.swarm';
33
+ export const LEGACY_MEMORY_DB_FILE = 'memory.db';
34
+ export const LEGACY_MEMORY_DB_BAK_SUFFIX = '.bak';
16
35
 
17
36
  export function mofloDir(projectRoot) {
18
37
  return join(projectRoot, MOFLO_DIR);
@@ -22,6 +41,35 @@ export function legacyClaudeFlowDir(projectRoot) {
22
41
  return join(projectRoot, LEGACY_CLAUDE_FLOW_DIR);
23
42
  }
24
43
 
44
+ export function memoryDbPath(projectRoot) {
45
+ return join(projectRoot, MOFLO_DIR, MEMORY_DB_FILE);
46
+ }
47
+
48
+ export function hnswIndexPath(projectRoot) {
49
+ return join(projectRoot, MOFLO_DIR, HNSW_INDEX_FILE);
50
+ }
51
+
52
+ export function legacyMemoryDbPath(projectRoot) {
53
+ return join(projectRoot, LEGACY_SWARM_DIR, LEGACY_MEMORY_DB_FILE);
54
+ }
55
+
56
+ export function legacyHnswIndexPath(projectRoot) {
57
+ return join(projectRoot, LEGACY_SWARM_DIR, HNSW_INDEX_FILE);
58
+ }
59
+
60
+ export function legacyMemoryDbBakPath(projectRoot) {
61
+ return join(projectRoot, LEGACY_SWARM_DIR, `${LEGACY_MEMORY_DB_FILE}${LEGACY_MEMORY_DB_BAK_SUFFIX}`);
62
+ }
63
+
64
+ export function memoryDbCandidatePaths(projectRoot) {
65
+ return [
66
+ memoryDbPath(projectRoot),
67
+ legacyMemoryDbPath(projectRoot),
68
+ join(projectRoot, 'data', LEGACY_MEMORY_DB_FILE),
69
+ join(projectRoot, '.claude', LEGACY_MEMORY_DB_FILE),
70
+ ];
71
+ }
72
+
25
73
  /**
26
74
  * One-time migration of `.claude-flow/` → `.moflo/`. Idempotent — safe to call
27
75
  * on every session start. See moflo-paths.ts for the full contract.
@@ -66,3 +114,106 @@ export function migrateClaudeFlowToMoflo(projectRoot) {
66
114
 
67
115
  return moved > 0 ? { migrated: true } : { migrated: false, reason: 'merged-nothing' };
68
116
  }
117
+
118
+ const SQLITE_MAGIC_HEADER = Buffer.from('SQLite format 3\0', 'utf8');
119
+
120
+ function looksLikeSqliteFile(filePath) {
121
+ let fd = null;
122
+ try {
123
+ fd = openSync(filePath, 'r');
124
+ const buf = Buffer.alloc(SQLITE_MAGIC_HEADER.length);
125
+ const read = readSync(fd, buf, 0, buf.length, 0);
126
+ if (read < SQLITE_MAGIC_HEADER.length) return false;
127
+ return buf.equals(SQLITE_MAGIC_HEADER);
128
+ } catch {
129
+ return false;
130
+ } finally {
131
+ if (fd !== null) try { closeSync(fd); } catch { /* non-fatal */ }
132
+ }
133
+ }
134
+
135
+ function verifyByteEqual(srcPath, dstPath) {
136
+ try {
137
+ const srcStat = statSync(srcPath);
138
+ const dstStat = statSync(dstPath);
139
+ if (srcStat.size !== dstStat.size) return false;
140
+ const srcBuf = readFileSync(srcPath);
141
+ const dstBuf = readFileSync(dstPath);
142
+ return srcBuf.equals(dstBuf);
143
+ } catch {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ function tryUnlink(filePath) {
149
+ try { unlinkSync(filePath); } catch { /* non-fatal */ }
150
+ }
151
+
152
+ function migrateHnswIndex(projectRoot) {
153
+ const src = legacyHnswIndexPath(projectRoot);
154
+ const dst = hnswIndexPath(projectRoot);
155
+
156
+ if (!existsSync(src)) return false;
157
+ if (existsSync(dst)) return false;
158
+
159
+ try {
160
+ mkdirSync(dirname(dst), { recursive: true });
161
+ copyFileSync(src, dst);
162
+ } catch {
163
+ tryUnlink(dst);
164
+ return false;
165
+ }
166
+
167
+ if (!verifyByteEqual(src, dst)) {
168
+ tryUnlink(dst);
169
+ return false;
170
+ }
171
+
172
+ tryUnlink(src);
173
+ return true;
174
+ }
175
+
176
+ /**
177
+ * One-time relocation of memory DB from `.swarm/memory.db` → `.moflo/moflo.db`
178
+ * (story #727). Idempotent. See moflo-paths.ts for the full contract and the
179
+ * SQLite-header / byte-equal verification rationale.
180
+ *
181
+ * MUST run before any long-lived sql.js consumer (MCP server, daemon) opens
182
+ * the DB — sql.js dumps the whole snapshot on every flush and would clobber
183
+ * the relocated file. Launcher section 0b is the safe boundary.
184
+ */
185
+ export function migrateMemoryDbToMoflo(projectRoot) {
186
+ const target = memoryDbPath(projectRoot);
187
+ if (existsSync(target)) return { migrated: false, reason: 'target-exists' };
188
+
189
+ const source = legacyMemoryDbPath(projectRoot);
190
+ if (!existsSync(source)) return { migrated: false, reason: 'no-legacy' };
191
+
192
+ try {
193
+ mkdirSync(dirname(target), { recursive: true });
194
+ } catch {
195
+ return { migrated: false, reason: 'copy-failed' };
196
+ }
197
+
198
+ try {
199
+ copyFileSync(source, target);
200
+ } catch {
201
+ tryUnlink(target);
202
+ return { migrated: false, reason: 'copy-failed' };
203
+ }
204
+
205
+ if (!verifyByteEqual(source, target) || !looksLikeSqliteFile(target)) {
206
+ tryUnlink(target);
207
+ return { migrated: false, reason: 'verify-failed' };
208
+ }
209
+
210
+ const hnswMoved = migrateHnswIndex(projectRoot);
211
+
212
+ try {
213
+ renameSync(source, legacyMemoryDbBakPath(projectRoot));
214
+ } catch {
215
+ return { migrated: true, reason: 'rename-failed', hnswMoved };
216
+ }
217
+
218
+ return { migrated: true, hnswMoved };
219
+ }
@@ -20,6 +20,7 @@
20
20
  import { existsSync, readFileSync } from 'fs';
21
21
  import { resolve, dirname } from 'path';
22
22
  import { mofloResolveURL, mofloInternalURL } from './lib/moflo-resolve.mjs';
23
+ import { memoryDbPath } from './lib/moflo-paths.mjs';
23
24
  const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
24
25
  const FASTEMBED_INLINE = 'dist/src/cli/embeddings/fastembed-inline/index.js';
25
26
 
@@ -34,7 +35,7 @@ function findProjectRoot() {
34
35
  }
35
36
 
36
37
  const projectRoot = findProjectRoot();
37
- const DB_PATH = resolve(projectRoot, '.swarm/memory.db');
38
+ const DB_PATH = memoryDbPath(projectRoot);
38
39
 
39
40
  const EMBEDDING_MODEL = 'fast-all-MiniLM-L6-v2';
40
41
  const EMBEDDING_DIMS = 384;
@@ -11,7 +11,7 @@ import { spawn } from 'child_process';
11
11
  import { existsSync, readFileSync, writeFileSync, copyFileSync, unlinkSync, readdirSync, mkdirSync, statSync } from 'fs';
12
12
  import { resolve, dirname } from 'path';
13
13
  import { fileURLToPath } from 'url';
14
- import { migrateClaudeFlowToMoflo } from './lib/moflo-paths.mjs';
14
+ import { migrateClaudeFlowToMoflo, migrateMemoryDbToMoflo } from './lib/moflo-paths.mjs';
15
15
 
16
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
17
 
@@ -72,6 +72,25 @@ try {
72
72
  // again next session. Better to keep launching than to block on it.
73
73
  }
74
74
 
75
+ // ── 0b. LEGACY memory DB relocation (#727) ──────────────────────────────────
76
+ // Run BEFORE long-lived sql.js consumers (MCP server, daemon) — see the
77
+ // `migrateMemoryDbToMoflo` JSDoc for the copy-verify-delete contract and
78
+ // the sql.js write-back hazard.
79
+ try {
80
+ const dbMigration = migrateMemoryDbToMoflo(projectRoot);
81
+ if (dbMigration?.migrated) {
82
+ const detail = dbMigration.hnswMoved
83
+ ? '.swarm/memory.db → .moflo/moflo.db (with hnsw.index)'
84
+ : '.swarm/memory.db → .moflo/moflo.db';
85
+ emitMutation('relocated memory db', detail);
86
+ if (dbMigration.reason === 'rename-failed') {
87
+ emitMutation('legacy .swarm/memory.db remains', 'rename to .bak failed — flo doctor will warn');
88
+ }
89
+ }
90
+ } catch {
91
+ // Non-fatal — failed migration leaves both DBs in place; next session retries.
92
+ }
93
+
75
94
  // ── 1. Helper: fire-and-forget a background process ─────────────────────────
76
95
  function fireAndForget(cmd, args, label) {
77
96
  try {
@@ -23,9 +23,9 @@
23
23
  */
24
24
  /* eslint-disable @typescript-eslint/no-explicit-any */
25
25
  import { existsSync } from 'fs';
26
- import { join } from 'path';
27
26
  import { CANONICAL_EMBEDDING_MODEL } from '../embeddings/migration/types.js';
28
27
  import { mofloImport } from '../services/moflo-require.js';
28
+ import { memoryDbCandidatePaths } from '../services/moflo-paths.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
@@ -124,16 +124,7 @@ export async function checkEmbeddingHygiene() {
124
124
  };
125
125
  }
126
126
  function resolveMemoryDb() {
127
- const candidates = [
128
- join(process.cwd(), '.swarm', 'memory.db'),
129
- join(process.cwd(), '.moflo', 'memory.db'),
130
- join(process.cwd(), 'data', 'memory.db'),
131
- ];
132
- for (const p of candidates) {
133
- if (existsSync(p))
134
- return p;
135
- }
136
- return null;
127
+ return memoryDbCandidatePaths(process.cwd()).find((p) => existsSync(p)) ?? null;
137
128
  }
138
129
  async function loadModelGroups(dbPath) {
139
130
  const fs = await import('fs');
@@ -16,6 +16,7 @@ import { getDaemonLockHolder, releaseDaemonLock, isDaemonProcess } from '../serv
16
16
  import { checkSubagentHealth, checkSpellExecution, checkMcpToolInvocation, checkHookExecution, checkMcpSpellIntegration, checkGateHealth, checkMofloDbBridge, getMofloRoot, } from './doctor-checks-deep.js';
17
17
  import { checkEmbeddingHygiene } from './doctor-embedding-hygiene.js';
18
18
  import { repairHookWiring } from '../services/hook-wiring.js';
19
+ import { legacyMemoryDbPath, memoryDbCandidatePaths, memoryDbPath, } from '../services/moflo-paths.js';
19
20
  // Promisified exec with proper shell and env inheritance for cross-platform support
20
21
  const execAsync = promisify(exec);
21
22
  /**
@@ -168,22 +169,33 @@ async function checkDaemonStatus() {
168
169
  }
169
170
  // Check memory database
170
171
  async function checkMemoryDatabase() {
171
- const dbPaths = [
172
- '.moflo/memory.db',
173
- '.swarm/memory.db',
174
- 'data/memory.db'
175
- ];
176
- for (const dbPath of dbPaths) {
177
- if (existsSync(dbPath)) {
178
- try {
179
- const stats = statSync(dbPath);
180
- const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
181
- return { name: 'Memory Database', status: 'pass', message: `${dbPath} (${sizeMB} MB)` };
182
- }
183
- catch {
184
- return { name: 'Memory Database', status: 'warn', message: `${dbPath} (unable to stat)` };
172
+ const root = process.cwd();
173
+ const canonical = memoryDbPath(root);
174
+ for (const dbPath of memoryDbCandidatePaths(root)) {
175
+ let stats;
176
+ try {
177
+ stats = statSync(dbPath);
178
+ }
179
+ catch {
180
+ continue;
181
+ }
182
+ const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
183
+ if (dbPath === canonical) {
184
+ let message = `.moflo/moflo.db (${sizeMB} MB)`;
185
+ // Unfinished migration tail: source still present means the launcher's
186
+ // rename-to-.bak step failed (Windows lock most often). Flag so the user
187
+ // knows to clear the stale source.
188
+ if (existsSync(legacyMemoryDbPath(root))) {
189
+ message += ' — legacy .swarm/memory.db still present (delete it after confirming canonical is healthy)';
185
190
  }
191
+ return { name: 'Memory Database', status: 'pass', message };
186
192
  }
193
+ return {
194
+ name: 'Memory Database',
195
+ status: 'warn',
196
+ message: `${dbPath} (${sizeMB} MB) — legacy location, will migrate to .moflo/moflo.db on next session start`,
197
+ fix: 'restart claude code session',
198
+ };
187
199
  }
188
200
  return { name: 'Memory Database', status: 'warn', message: 'Not initialized', fix: 'claude-flow memory configure --backend hybrid' };
189
201
  }
@@ -474,12 +486,7 @@ const VECTOR_STATS_SKEW_WARN_THRESHOLD = 0.2;
474
486
  // Check embeddings / vector index health.
475
487
  // Exported so the #639 stale-cache regression test can invoke it directly.
476
488
  export async function checkEmbeddings() {
477
- const dbPaths = [
478
- join(process.cwd(), '.swarm', 'memory.db'),
479
- join(process.cwd(), '.moflo', 'memory.db'),
480
- join(process.cwd(), 'data', 'memory.db'),
481
- ];
482
- const liveDbPath = dbPaths.find((p) => existsSync(p));
489
+ const liveDbPath = memoryDbCandidatePaths(process.cwd()).find((p) => existsSync(p));
483
490
  // 1. Fast path: read cached vector-stats.json if available
484
491
  const statsPath = join(process.cwd(), '.moflo', 'vector-stats.json');
485
492
  try {
@@ -15,7 +15,9 @@
15
15
  import { output } from '../output.js';
16
16
  import { mofloImport } from '../services/moflo-require.js';
17
17
  import { runEmbeddingsMigrationIfNeeded } from '../services/embeddings-migration.js';
18
+ import { memoryDbPath, MEMORY_DB_FILE, MOFLO_DIR } from '../services/moflo-paths.js';
18
19
  import * as embeddings from '../embeddings/index.js';
20
+ const DEFAULT_DB_PATH_FLAG = `${MOFLO_DIR}/${MEMORY_DB_FILE}`;
19
21
  // Generate subcommand - REAL implementation
20
22
  const generateCommand = {
21
23
  name: 'generate',
@@ -96,7 +98,7 @@ const searchCommand = {
96
98
  { name: 'collection', short: 'c', type: 'string', description: 'Namespace to search', default: 'default' },
97
99
  { name: 'limit', short: 'l', type: 'number', description: 'Max results', default: '10' },
98
100
  { name: 'threshold', short: 't', type: 'number', description: 'Similarity threshold (0-1)', default: '0.5' },
99
- { name: 'db-path', type: 'string', description: 'Database path', default: '.swarm/memory.db' },
101
+ { name: 'db-path', type: 'string', description: 'Database path', default: DEFAULT_DB_PATH_FLAG },
100
102
  ],
101
103
  examples: [
102
104
  { command: 'claude-flow embeddings search -q "error handling"', description: 'Search for similar' },
@@ -107,7 +109,7 @@ const searchCommand = {
107
109
  const namespace = ctx.flags.collection || 'default';
108
110
  const limit = parseInt(ctx.flags.limit || '10', 10);
109
111
  const threshold = parseFloat(ctx.flags.threshold || '0.5');
110
- const dbPath = ctx.flags['db-path'] || '.swarm/memory.db';
112
+ const dbPath = ctx.flags['db-path'] || memoryDbPath(process.cwd());
111
113
  if (!query) {
112
114
  output.printError('Query is required');
113
115
  return { success: false, exitCode: 1 };
@@ -347,7 +349,7 @@ const collectionsCommand = {
347
349
  options: [
348
350
  { name: 'action', short: 'a', type: 'string', description: 'Action: list, stats', default: 'list' },
349
351
  { name: 'name', short: 'n', type: 'string', description: 'Namespace name' },
350
- { name: 'db-path', type: 'string', description: 'Database path', default: '.swarm/memory.db' },
352
+ { name: 'db-path', type: 'string', description: 'Database path', default: DEFAULT_DB_PATH_FLAG },
351
353
  ],
352
354
  examples: [
353
355
  { command: 'claude-flow embeddings collections', description: 'List collections' },
@@ -355,7 +357,7 @@ const collectionsCommand = {
355
357
  ],
356
358
  action: async (ctx) => {
357
359
  const action = ctx.flags.action || 'list';
358
- const dbPath = ctx.flags['db-path'] || '.swarm/memory.db';
360
+ const dbPath = ctx.flags['db-path'] || memoryDbPath(process.cwd());
359
361
  output.writeln();
360
362
  output.writeln(output.bold('Embedding Collections (Namespaces)'));
361
363
  output.writeln(output.dim('─'.repeat(60)));
@@ -1489,16 +1491,16 @@ const migrateCommand = {
1489
1491
  name: 'migrate',
1490
1492
  description: 'Re-embed existing vectors using the current neural model (runs automatically on session start if needed)',
1491
1493
  options: [
1492
- { name: 'db', short: 'd', type: 'string', description: 'Path to memory DB', default: '.swarm/memory.db' },
1494
+ { name: 'db', short: 'd', type: 'string', description: 'Path to memory DB', default: DEFAULT_DB_PATH_FLAG },
1493
1495
  { name: 'batch-size', short: 'b', type: 'number', description: 'Batch size', default: '128' },
1494
1496
  { name: 'verbose', short: 'v', type: 'boolean', description: 'Verbose output', default: 'false' },
1495
1497
  ],
1496
1498
  examples: [
1497
- { command: 'claude-flow embeddings migrate', description: 'Migrate .swarm/memory.db to current version' },
1499
+ { command: 'claude-flow embeddings migrate', description: `Migrate ${DEFAULT_DB_PATH_FLAG} to current version` },
1498
1500
  { command: 'claude-flow embeddings migrate -d .custom/mem.db', description: 'Migrate a custom DB' },
1499
1501
  ],
1500
1502
  action: async (ctx) => {
1501
- const dbPath = ctx.flags.db || '.swarm/memory.db';
1503
+ const dbPath = ctx.flags.db || memoryDbPath(process.cwd());
1502
1504
  try {
1503
1505
  const ran = await runEmbeddingsMigrationIfNeeded({ dbPath });
1504
1506
  return { success: true, data: { ran } };
@@ -7,6 +7,7 @@ import { formatStatus } from '../services/cli-formatters.js';
7
7
  import { confirm } from '../prompt.js';
8
8
  import { callMCPTool, MCPClientError } from '../mcp-client.js';
9
9
  import { storeCommand } from './transfer-store.js';
10
+ import { memoryDbCandidatePaths } from '../services/moflo-paths.js';
10
11
  // Hook types
11
12
  const HOOK_TYPES = [
12
13
  { value: 'pre-edit', label: 'Pre-Edit', hint: 'Get context before editing files' },
@@ -2544,10 +2545,7 @@ const statuslineCommand = {
2544
2545
  const { execSync } = await import('child_process');
2545
2546
  // Get learning stats from memory database
2546
2547
  function getLearningStats() {
2547
- const memoryPaths = [
2548
- path.join(process.cwd(), '.swarm', 'memory.db'),
2549
- path.join(process.cwd(), '.claude', 'memory.db'),
2550
- ];
2548
+ const memoryPaths = memoryDbCandidatePaths(process.cwd());
2551
2549
  let patterns = 0;
2552
2550
  let sessions = 0;
2553
2551
  let trajectories = 0;
@@ -2798,12 +2796,9 @@ const statuslineCommand = {
2798
2796
  }
2799
2797
  // Get AgentDB stats (matching .claude/helpers/statusline.cjs paths)
2800
2798
  const agentdbStats = { vectorCount: 0, dbSizeKB: 0, hasHnsw: false };
2801
- // Check for direct database files first
2799
+ // Check for direct database files first. Canonical first, legacies after.
2802
2800
  const dbPaths = [
2803
- path.join(process.cwd(), '.swarm', 'memory.db'),
2804
- path.join(process.cwd(), '.moflo', 'memory.db'),
2805
- path.join(process.cwd(), '.claude', 'memory.db'),
2806
- path.join(process.cwd(), 'data', 'memory.db'),
2801
+ ...memoryDbCandidatePaths(process.cwd()),
2807
2802
  path.join(process.cwd(), 'memory.db'),
2808
2803
  path.join(process.cwd(), '.agentdb', 'memory.db'),
2809
2804
  path.join(process.cwd(), '.moflo', 'memory', 'agentdb.db'),
@@ -7,14 +7,12 @@ import { select, confirm } from '../prompt.js';
7
7
  import { callMCPTool, MCPClientError } from '../mcp-client.js';
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
10
+ import { memoryDbCandidatePaths } from '../services/moflo-paths.js';
10
11
  // Get dynamic swarm status from memory/session files
11
12
  function getSwarmStatus(swarmId) {
12
13
  const swarmDir = path.join(process.cwd(), '.swarm');
13
14
  const sessionDir = path.join(process.cwd(), '.claude', 'sessions');
14
- const memoryPaths = [
15
- path.join(process.cwd(), '.swarm', 'memory.db'),
16
- path.join(process.cwd(), '.claude', 'memory.db'),
17
- ];
15
+ const memoryPaths = memoryDbCandidatePaths(process.cwd());
18
16
  // Check for active swarm state file
19
17
  const swarmStateFile = path.join(swarmDir, 'state.json');
20
18
  let swarmState = null;
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * 2. An `embeddings_migration_cursor` table that persists the resume
11
11
  * cursor per-store (so multiple stores sharing a DB file — e.g.
12
- * `.swarm/memory.db` with both `memory_entries` and `patterns` —
12
+ * `.moflo/moflo.db` with both `memory_entries` and `patterns` —
13
13
  * each get their own row).
14
14
  *
15
15
  * The helpers are framework-agnostic: they accept a minimal `SqlJsDatabase`
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { EventEmitter } from 'node:events';
16
16
  import { TestDeterministicEmbedding } from './__mocks__/test-embedding-service.js';
17
+ import { MEMORY_DB_FILE, MOFLO_DIR } from '../../services/moflo-paths.js';
17
18
  // Dynamic imports for optional dependencies
18
19
  let MofloDbAdapter = null;
19
20
  let HNSWIndex = null;
@@ -28,7 +29,7 @@ const DEFAULT_CONFIG = {
28
29
  promotionThreshold: 3,
29
30
  qualityThreshold: 0.6,
30
31
  dedupThreshold: 0.95,
31
- dbPath: '.moflo/memory.db',
32
+ dbPath: `${MOFLO_DIR}/${MEMORY_DB_FILE}`,
32
33
  useMockEmbeddings: false,
33
34
  };
34
35
  /**
@@ -8,6 +8,7 @@ import * as path from 'path';
8
8
  import * as fs from 'fs';
9
9
  import * as crypto from 'crypto';
10
10
  import { atomicWriteFileSync } from '../services/atomic-file-write.js';
11
+ import { legacyMemoryDbPath, memoryDbPath, MOFLO_DIR, } from '../services/moflo-paths.js';
11
12
  // When run via npx, CWD may be node_modules/moflo — walk up to find actual project
12
13
  let _projectRoot;
13
14
  /**
@@ -28,7 +29,10 @@ function getProjectRoot() {
28
29
  let dir = process.cwd();
29
30
  const root = path.parse(dir).root;
30
31
  while (dir !== root) {
31
- if (fs.existsSync(path.join(dir, '.swarm', 'memory.db'))) {
32
+ // `.moflo/moflo.db` is the canonical post-#727 marker. Older consumers
33
+ // mid-migration may still only have `.swarm/memory.db`; recognise both
34
+ // so the bridge can find the project root either way.
35
+ if (fs.existsSync(memoryDbPath(dir)) || fs.existsSync(legacyMemoryDbPath(dir))) {
32
36
  _projectRoot = dir;
33
37
  return _projectRoot;
34
38
  }
@@ -69,22 +73,42 @@ function logBridgeError(context, err) {
69
73
  const msg = err instanceof Error ? err.message : String(err);
70
74
  console.error(`[moflo] ${context}: ${msg}`);
71
75
  }
72
- function getDbPath(customPath) {
73
- const swarmDir = path.resolve(getProjectRoot(), '.swarm');
74
- if (!customPath)
75
- return path.join(swarmDir, 'memory.db');
76
+ /**
77
+ * Resolve the on-disk DB path the bridge should read/write.
78
+ *
79
+ * Default to `.moflo/moflo.db`, but during the post-#727 migration window —
80
+ * after a consumer upgrades but before the next session-start launcher fires
81
+ * — prefer `.swarm/memory.db` if only the legacy file exists. Without this
82
+ * preference, any CLI command (e.g. `moflo doctor`) that opens the bridge
83
+ * before the launcher runs would create an empty canonical, defeating the
84
+ * launcher's `target-exists` short-circuit and stranding the user's data in
85
+ * `.swarm/memory.db`.
86
+ *
87
+ * Exported for test access; production callers go through the no-arg
88
+ * `getDbPath()` wrapper below.
89
+ */
90
+ export function resolveBridgeDbPath(root, customPath) {
91
+ const canonical = memoryDbPath(root);
92
+ if (!customPath) {
93
+ if (!fs.existsSync(canonical) && fs.existsSync(legacyMemoryDbPath(root))) {
94
+ return legacyMemoryDbPath(root);
95
+ }
96
+ return canonical;
97
+ }
76
98
  if (customPath === ':memory:')
77
99
  return ':memory:';
78
100
  const resolved = path.resolve(customPath);
79
- const root = getProjectRoot();
80
101
  const rel = path.relative(root, resolved);
81
102
  // Reject anything that escapes the project root or is an absolute path
82
103
  // outside it (path.relative returns an absolute path on different drives).
83
104
  if (rel.startsWith('..') || path.isAbsolute(rel)) {
84
- return path.join(swarmDir, 'memory.db');
105
+ return canonical;
85
106
  }
86
107
  return resolved;
87
108
  }
109
+ function getDbPath(customPath) {
110
+ return resolveBridgeDbPath(getProjectRoot(), customPath);
111
+ }
88
112
  export function generateId(prefix) {
89
113
  return `${prefix}_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`;
90
114
  }
@@ -163,9 +187,11 @@ export function execRows(db, sql, params) {
163
187
  * when the process exits, which breaks store→retrieve across CLI commands.
164
188
  */
165
189
  export function persistBridgeDb(db, dbPath) {
166
- const target = dbPath
167
- ? path.resolve(dbPath)
168
- : path.join(getProjectRoot(), '.swarm', 'memory.db');
190
+ // Mirror the read-side resolution so writes land where reads come from.
191
+ // Important during the migration window (#727): if we read from
192
+ // `.swarm/memory.db` because the canonical doesn't exist yet, writing back
193
+ // there keeps the legacy file fresh until the launcher relocates it.
194
+ const target = dbPath ? path.resolve(dbPath) : getDbPath();
169
195
  if (target === ':memory:')
170
196
  return;
171
197
  try {
@@ -286,19 +312,15 @@ export function writeVectorStatsJson(rootDir, stats) {
286
312
  fs.mkdirSync(cacheDir, { recursive: true });
287
313
  fs.writeFileSync(path.join(cacheDir, 'vector-stats.json'), JSON.stringify({ ...stats, updatedAt: Date.now() }));
288
314
  }
289
- /** Probe both legacy locations for an HNSW index sidecar file. */
315
+ /** Probe for the HNSW index sidecar at its canonical post-#727 location. */
290
316
  function detectHnswIndex(rootDir) {
291
- for (const p of [
292
- path.join(rootDir, '.swarm', 'hnsw.index'),
293
- path.join(rootDir, '.moflo', 'hnsw.index'),
294
- ]) {
295
- try {
296
- fs.statSync(p);
297
- return true;
298
- }
299
- catch { /* nope */ }
317
+ try {
318
+ fs.statSync(path.join(rootDir, MOFLO_DIR, 'hnsw.index'));
319
+ return true;
320
+ }
321
+ catch {
322
+ return false;
300
323
  }
301
- return false;
302
324
  }
303
325
  /**
304
326
  * Read the existing on-disk vector-stats cache. Returns null when missing
@@ -17,6 +17,7 @@ import { HnswLite } from './hnsw-lite.js';
17
17
  import { EMBEDDING_MODEL_OPT_OUT, getBridgeEmbedder } from './bridge-embedder.js';
18
18
  import { toFloat32 } from './controllers/_shared.js';
19
19
  import { writeVectorStatsJson } from './bridge-core.js';
20
+ import { MOFLO_DIR, hnswIndexPath, legacyMemoryDbPath, memoryDbPath, } from '../services/moflo-paths.js';
20
21
  /**
21
22
  * Write vector-stats.json cache for the statusline (no subprocess needed).
22
23
  * Called after memory store in the raw-sql.js fallback path. The bridge path
@@ -31,19 +32,13 @@ function writeVectorStatsCache(dbPath, stats) {
31
32
  const dbSizeKB = Math.floor(fileStat.size / 1024);
32
33
  const { vectorCount, namespaces, missing = 0 } = stats;
33
34
  const dbDir = path.dirname(dbPath);
34
- const projectDir = path.dirname(dbDir); // .swarm -> project root
35
+ const projectDir = path.dirname(dbDir); // .moflo (or legacy .swarm) -> project root
35
36
  let hasHnsw = false;
36
- for (const p of [
37
- path.join(dbDir, 'hnsw.index'),
38
- path.join(projectDir, '.moflo', 'hnsw.index'),
39
- ]) {
40
- try {
41
- fs.statSync(p);
42
- hasHnsw = true;
43
- break;
44
- }
45
- catch { /* nope */ }
37
+ try {
38
+ fs.statSync(hnswIndexPath(projectDir));
39
+ hasHnsw = true;
46
40
  }
41
+ catch { /* nope */ }
47
42
  writeVectorStatsJson(projectDir, { vectorCount, missing, dbSizeKB, namespaces, hasHnsw });
48
43
  }
49
44
  catch { /* Non-fatal */ }
@@ -381,13 +376,13 @@ export async function getHNSWIndex(options) {
381
376
  hnswInitializing = true;
382
377
  try {
383
378
  // Use HnswLite pure TS implementation (no native dependencies).
384
- // Persistent storage paths
385
- const swarmDir = path.join(process.cwd(), '.swarm');
386
- if (!fs.existsSync(swarmDir)) {
387
- fs.mkdirSync(swarmDir, { recursive: true });
379
+ // Persistent storage paths — colocated with the canonical memory DB.
380
+ const dbPath = options?.dbPath || memoryDbPath(process.cwd());
381
+ const dbDir = path.dirname(dbPath);
382
+ if (!fs.existsSync(dbDir)) {
383
+ fs.mkdirSync(dbDir, { recursive: true });
388
384
  }
389
- const metadataPath = path.join(swarmDir, 'hnsw.metadata.json');
390
- const dbPath = options?.dbPath || path.join(swarmDir, 'memory.db');
385
+ const metadataPath = path.join(dbDir, 'hnsw.metadata.json');
391
386
  // Create HnswLite index and wrap it with the expected db interface
392
387
  const hnsw = new HnswLite(dimensions, 16, 200, 'cosine');
393
388
  const db = {
@@ -478,8 +473,7 @@ function saveHNSWMetadata() {
478
473
  if (!hnswIndex?.entries)
479
474
  return;
480
475
  try {
481
- const swarmDir = path.join(process.cwd(), '.swarm');
482
- const metadataPath = path.join(swarmDir, 'hnsw.metadata.json');
476
+ const metadataPath = path.join(path.dirname(memoryDbPath(process.cwd())), 'hnsw.metadata.json');
483
477
  const metadata = Array.from(hnswIndex.entries.entries());
484
478
  fs.writeFileSync(metadataPath, JSON.stringify(metadata));
485
479
  }
@@ -893,12 +887,17 @@ export async function ensureSchemaColumns(dbPath) {
893
887
  */
894
888
  export async function checkAndMigrateLegacy(options) {
895
889
  const { dbPath, verbose = false } = options;
896
- // Check for legacy locations
890
+ // Check for legacy locations. `.swarm/memory.db` is the pre-#727 layout
891
+ // primarily handled by the launcher's copy-verify-delete migration; this
892
+ // probe still catches consumers whose launcher migration deferred. The
893
+ // bare `memory.db` and `.claude/memory.db`/`data/memory.db` entries are
894
+ // older still.
897
895
  const legacyPaths = [
896
+ legacyMemoryDbPath(process.cwd()),
897
+ path.join(process.cwd(), MOFLO_DIR, 'memory.db'),
898
898
  path.join(process.cwd(), 'memory.db'),
899
- path.join(process.cwd(), '.claude/memory.db'),
900
- path.join(process.cwd(), 'data/memory.db'),
901
- path.join(process.cwd(), '.moflo/memory.db')
899
+ path.join(process.cwd(), '.claude', 'memory.db'),
900
+ path.join(process.cwd(), 'data', 'memory.db'),
902
901
  ];
903
902
  for (const legacyPath of legacyPaths) {
904
903
  if (fs.existsSync(legacyPath) && legacyPath !== dbPath) {
@@ -983,8 +982,7 @@ async function activateControllerRegistry(dbPath, verbose) {
983
982
  */
984
983
  export async function initializeMemoryDatabase(options) {
985
984
  const { backend = 'hybrid', dbPath: customPath, force = false, verbose = false, migrate = true } = options;
986
- const swarmDir = path.join(process.cwd(), '.swarm');
987
- const dbPath = customPath || path.join(swarmDir, 'memory.db');
985
+ const dbPath = customPath || memoryDbPath(process.cwd());
988
986
  const dbDir = path.dirname(dbPath);
989
987
  try {
990
988
  // Create directory if needed
@@ -1165,8 +1163,7 @@ export async function initializeMemoryDatabase(options) {
1165
1163
  * Check if memory database is properly initialized
1166
1164
  */
1167
1165
  export async function checkMemoryInitialization(dbPath) {
1168
- const swarmDir = path.join(process.cwd(), '.swarm');
1169
- const path_ = dbPath || path.join(swarmDir, 'memory.db');
1166
+ const path_ = dbPath || memoryDbPath(process.cwd());
1170
1167
  if (!fs.existsSync(path_)) {
1171
1168
  return { initialized: false };
1172
1169
  }
@@ -1214,8 +1211,7 @@ export async function checkMemoryInitialization(dbPath) {
1214
1211
  * Reduces confidence of patterns that haven't been used recently
1215
1212
  */
1216
1213
  export async function applyTemporalDecay(dbPath) {
1217
- const swarmDir = path.join(process.cwd(), '.swarm');
1218
- const path_ = dbPath || path.join(swarmDir, 'memory.db');
1214
+ const path_ = dbPath || memoryDbPath(process.cwd());
1219
1215
  try {
1220
1216
  const initSqlJs = (await mofloImport('sql.js')).default;
1221
1217
  const SQL = await initSqlJs();
@@ -1579,8 +1575,7 @@ export async function storeEntry(options) {
1579
1575
  }
1580
1576
  // Fallback: raw sql.js
1581
1577
  const { key, value, namespace = 'default', generateEmbeddingFlag = true, tags = [], ttl, dbPath: customPath, upsert = false } = options;
1582
- const swarmDir = path.join(process.cwd(), '.swarm');
1583
- const dbPath = customPath || path.join(swarmDir, 'memory.db');
1578
+ const dbPath = customPath || memoryDbPath(process.cwd());
1584
1579
  try {
1585
1580
  if (!fs.existsSync(dbPath)) {
1586
1581
  return { success: false, id: '', error: 'Database not initialized. Run: claude-flow memory init' };
@@ -1718,8 +1713,7 @@ export async function searchEntries(options) {
1718
1713
  }
1719
1714
  // Fallback: raw sql.js
1720
1715
  const { query, namespace = 'default', limit = 10, threshold = 0.3, dbPath: customPath } = options;
1721
- const swarmDir = path.join(process.cwd(), '.swarm');
1722
- const dbPath = customPath || path.join(swarmDir, 'memory.db');
1716
+ const dbPath = customPath || memoryDbPath(process.cwd());
1723
1717
  const startTime = Date.now();
1724
1718
  try {
1725
1719
  if (!fs.existsSync(dbPath)) {
@@ -1837,8 +1831,7 @@ export async function listEntries(options) {
1837
1831
  }
1838
1832
  // Fallback: raw sql.js
1839
1833
  const { namespace, limit = 20, offset = 0, dbPath: customPath } = options;
1840
- const swarmDir = path.join(process.cwd(), '.swarm');
1841
- const dbPath = customPath || path.join(swarmDir, 'memory.db');
1834
+ const dbPath = customPath || memoryDbPath(process.cwd());
1842
1835
  try {
1843
1836
  if (!fs.existsSync(dbPath)) {
1844
1837
  return { success: false, entries: [], total: 0, error: 'Database not found' };
@@ -1906,8 +1899,7 @@ export async function getEntry(options) {
1906
1899
  }
1907
1900
  // Fallback: raw sql.js
1908
1901
  const { key, namespace = 'default', dbPath: customPath } = options;
1909
- const swarmDir = path.join(process.cwd(), '.swarm');
1910
- const dbPath = customPath || path.join(swarmDir, 'memory.db');
1902
+ const dbPath = customPath || memoryDbPath(process.cwd());
1911
1903
  try {
1912
1904
  if (!fs.existsSync(dbPath)) {
1913
1905
  return { success: false, found: false, error: 'Database not found' };
@@ -1987,8 +1979,7 @@ export async function deleteEntry(options) {
1987
1979
  }
1988
1980
  // Fallback: raw sql.js
1989
1981
  const { key, namespace = 'default', dbPath: customPath } = options;
1990
- const swarmDir = path.join(process.cwd(), '.swarm');
1991
- const dbPath = customPath || path.join(swarmDir, 'memory.db');
1982
+ const dbPath = customPath || memoryDbPath(process.cwd());
1992
1983
  try {
1993
1984
  if (!fs.existsSync(dbPath)) {
1994
1985
  return {
@@ -2065,8 +2056,7 @@ export async function deleteEntry(options) {
2065
2056
  * Returns { namespaces: Record<string, number>, total: number }.
2066
2057
  */
2067
2058
  export async function getNamespaceCounts(dbPath) {
2068
- const swarmDir = path.join(process.cwd(), '.swarm');
2069
- const resolvedPath = dbPath || path.join(swarmDir, 'memory.db');
2059
+ const resolvedPath = dbPath || memoryDbPath(process.cwd());
2070
2060
  try {
2071
2061
  if (!fs.existsSync(resolvedPath)) {
2072
2062
  return { namespaces: {}, total: 0 };
@@ -197,6 +197,7 @@ export class MemoryMigrator extends EventEmitter {
197
197
  // For now, try to load from common paths
198
198
  const possiblePaths = [
199
199
  './memory/memory-store.json',
200
+ './.moflo/moflo.db',
200
201
  './.swarm/memory.db',
201
202
  './memory.json',
202
203
  ];
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Idempotent embeddings-version migration for moflo's memory.db.
2
+ * Idempotent embeddings-version migration for moflo's memory DB (`.moflo/moflo.db`).
3
3
  *
4
4
  * Runs the story-2 driver (`runUpgrade`) with the story-3 UX on any DB whose
5
5
  * stored `embeddings_version` is below the current runtime version OR whose
@@ -17,6 +17,7 @@
17
17
  /* eslint-disable @typescript-eslint/no-explicit-any */
18
18
  import { mofloImport } from './moflo-require.js';
19
19
  import { atomicWriteFileSync } from './atomic-file-write.js';
20
+ import { memoryDbPath } from './moflo-paths.js';
20
21
  // EMBEDDINGS_VERSION is a number constant in a leaf types module — pulling it
21
22
  // eagerly is cheap. The heavy imports (fastembed wrapper, upgrade renderer,
22
23
  // etc.) stay deferred behind the early returns so session-start stays fast.
@@ -33,7 +34,7 @@ import { CANONICAL_EMBEDDING_MODEL, EMBEDDINGS_VERSION, } from '../embeddings/mi
33
34
  export async function runEmbeddingsMigrationIfNeeded(options = {}) {
34
35
  const fs = await import('fs');
35
36
  const path = await import('path');
36
- const dbPath = path.resolve(options.dbPath ?? path.join(process.cwd(), '.swarm', 'memory.db'));
37
+ const dbPath = path.resolve(options.dbPath ?? memoryDbPath(process.cwd()));
37
38
  if (!fs.existsSync(dbPath))
38
39
  return false;
39
40
  const initSqlJs = (await mofloImport('sql.js'))?.default;
@@ -106,7 +107,7 @@ export async function runEmbeddingsMigrationIfNeeded(options = {}) {
106
107
  if (summary.status === 'completed') {
107
108
  // `db.export()` returns a Uint8Array; `writeFileSync` accepts it directly,
108
109
  // so no Buffer.from() copy. Atomic temp-file + rename so SIGINT mid-write
109
- // cannot truncate memory.db — see atomic-file-write.ts.
110
+ // cannot truncate moflo.db — see atomic-file-write.ts.
110
111
  atomicWriteFileSync(dbPath, db.export());
111
112
  options.onMigrationComplete?.(summary.totalItemsMigrated);
112
113
  return true;
@@ -6,16 +6,17 @@
6
6
  * promotion, deduplication, and consolidation.
7
7
  *
8
8
  * Features:
9
- * - Pattern storage/search with sql.js backend (.swarm/memory.db)
9
+ * - Pattern storage/search with sql.js backend (.moflo/moflo.db)
10
10
  * - Short-term -> long-term pattern promotion (promote after 3 uses)
11
11
  * - Quality thresholds: minimum quality 0.6, dedup threshold 0.95
12
12
  * - Consolidation: max 500 short-term, 2000 long-term, prune after 30 days
13
13
  * - HNSW indexing for fast similarity search
14
14
  */
15
15
  import { existsSync, mkdirSync, readFileSync } from 'fs';
16
- import { join } from 'path';
16
+ import { dirname } from 'path';
17
17
  import { mofloImport } from './moflo-require.js';
18
18
  import { atomicWriteFileSync } from './atomic-file-write.js';
19
+ import { memoryDbPath } from './moflo-paths.js';
19
20
  import { createNeuralEmbeddingProvider, } from './neural-embedding-provider.js';
20
21
  // ============================================================================
21
22
  // Configuration
@@ -262,12 +263,9 @@ export class LearningService {
262
263
  longTermIndex = new HNSWIndex();
263
264
  dirty = false;
264
265
  dbPath;
265
- dataDir;
266
266
  embedderPromise = null;
267
267
  constructor(projectRoot, embedder) {
268
- const root = projectRoot || process.cwd();
269
- this.dataDir = join(root, '.swarm');
270
- this.dbPath = join(this.dataDir, 'memory.db');
268
+ this.dbPath = memoryDbPath(projectRoot || process.cwd());
271
269
  if (embedder)
272
270
  this.embedderPromise = Promise.resolve(embedder);
273
271
  }
@@ -281,8 +279,9 @@ export class LearningService {
281
279
  async initialize() {
282
280
  if (this.db)
283
281
  return;
284
- if (!existsSync(this.dataDir)) {
285
- mkdirSync(this.dataDir, { recursive: true });
282
+ const dataDir = dirname(this.dbPath);
283
+ if (!existsSync(dataDir)) {
284
+ mkdirSync(dataDir, { recursive: true });
286
285
  }
287
286
  const SQL = await loadSqlJs();
288
287
  if (existsSync(this.dbPath)) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * MoFlo runtime state directory constants + legacy migration (#699).
2
+ * MoFlo runtime state directory constants + legacy migrations (#699, #727).
3
3
  *
4
4
  * MoFlo owns its state under `.moflo/` at the project root. The upstream Ruflo
5
5
  * fork used `.claude-flow/`; consumers upgrading from older moflo builds (which
@@ -16,20 +16,66 @@
16
16
  * been compiled. The parity test in moflo-paths-migration.test.ts catches
17
17
  * algorithm divergence between the two.
18
18
  */
19
- import { existsSync, readdirSync, renameSync, rmdirSync } from 'node:fs';
20
- import { join } from 'node:path';
19
+ import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmdirSync, statSync, unlinkSync, } from 'node:fs';
20
+ import { dirname, join } from 'node:path';
21
21
  export const MOFLO_DIR = '.moflo';
22
+ /** Canonical memory DB filename (post-#727). Lives at `<root>/.moflo/moflo.db`. */
23
+ export const MEMORY_DB_FILE = 'moflo.db';
24
+ /** HNSW persisted index sidecar. Lives next to the DB at `<root>/.moflo/hnsw.index`. */
25
+ export const HNSW_INDEX_FILE = 'hnsw.index';
22
26
  /**
23
27
  * Legacy runtime directory inherited from upstream Ruflo. Only referenced from
24
28
  * migration code paths — production code should use {@link MOFLO_DIR}.
25
29
  */
26
30
  export const LEGACY_CLAUDE_FLOW_DIR = '.claude-flow';
31
+ /** Legacy `.swarm/` directory used by Ruflo + pre-#727 moflo for the memory DB. */
32
+ export const LEGACY_SWARM_DIR = '.swarm';
33
+ /** Legacy memory DB filename — only ever inside `.swarm/`. Pre-#727. */
34
+ export const LEGACY_MEMORY_DB_FILE = 'memory.db';
35
+ /** Suffix appended to `.swarm/memory.db` once migrated, retained one upgrade cycle. */
36
+ export const LEGACY_MEMORY_DB_BAK_SUFFIX = '.bak';
27
37
  export function mofloDir(projectRoot) {
28
38
  return join(projectRoot, MOFLO_DIR);
29
39
  }
30
40
  export function legacyClaudeFlowDir(projectRoot) {
31
41
  return join(projectRoot, LEGACY_CLAUDE_FLOW_DIR);
32
42
  }
43
+ /** Canonical memory DB path: `<root>/.moflo/moflo.db`. */
44
+ export function memoryDbPath(projectRoot) {
45
+ return join(projectRoot, MOFLO_DIR, MEMORY_DB_FILE);
46
+ }
47
+ /** Canonical HNSW index sidecar path: `<root>/.moflo/hnsw.index`. */
48
+ export function hnswIndexPath(projectRoot) {
49
+ return join(projectRoot, MOFLO_DIR, HNSW_INDEX_FILE);
50
+ }
51
+ /** Legacy memory DB path: `<root>/.swarm/memory.db`. Migration source only. */
52
+ export function legacyMemoryDbPath(projectRoot) {
53
+ return join(projectRoot, LEGACY_SWARM_DIR, LEGACY_MEMORY_DB_FILE);
54
+ }
55
+ /** Legacy HNSW index path: `<root>/.swarm/hnsw.index`. Migration source only. */
56
+ export function legacyHnswIndexPath(projectRoot) {
57
+ return join(projectRoot, LEGACY_SWARM_DIR, HNSW_INDEX_FILE);
58
+ }
59
+ /** Backup sentinel kept for one upgrade cycle: `<root>/.swarm/memory.db.bak`. */
60
+ export function legacyMemoryDbBakPath(projectRoot) {
61
+ return join(projectRoot, LEGACY_SWARM_DIR, `${LEGACY_MEMORY_DB_FILE}${LEGACY_MEMORY_DB_BAK_SUFFIX}`);
62
+ }
63
+ /**
64
+ * Memory-DB probe order used by every reader that does best-effort detection
65
+ * (statusline, doctor, swarm status, hooks aggregator). Canonical first so
66
+ * the early-break stops at the post-#727 location; legacy paths kept so a
67
+ * partially-migrated consumer still surfaces a result.
68
+ *
69
+ * Keep in sync with the pure-JS twin in `bin/lib/moflo-paths.mjs`.
70
+ */
71
+ export function memoryDbCandidatePaths(projectRoot) {
72
+ return [
73
+ memoryDbPath(projectRoot),
74
+ legacyMemoryDbPath(projectRoot),
75
+ join(projectRoot, 'data', LEGACY_MEMORY_DB_FILE),
76
+ join(projectRoot, '.claude', LEGACY_MEMORY_DB_FILE),
77
+ ];
78
+ }
33
79
  /**
34
80
  * One-time migration of `.claude-flow/` → `.moflo/`.
35
81
  *
@@ -80,4 +126,134 @@ export function migrateClaudeFlowToMoflo(projectRoot) {
80
126
  }
81
127
  return moved > 0 ? { migrated: true } : { migrated: false, reason: 'merged-nothing' };
82
128
  }
129
+ const SQLITE_MAGIC_HEADER = Buffer.from('SQLite format 3\0', 'utf8');
130
+ function looksLikeSqliteFile(filePath) {
131
+ let fd = null;
132
+ try {
133
+ fd = openSync(filePath, 'r');
134
+ const buf = Buffer.alloc(SQLITE_MAGIC_HEADER.length);
135
+ const read = readSync(fd, buf, 0, buf.length, 0);
136
+ if (read < SQLITE_MAGIC_HEADER.length)
137
+ return false;
138
+ return buf.equals(SQLITE_MAGIC_HEADER);
139
+ }
140
+ catch {
141
+ return false;
142
+ }
143
+ finally {
144
+ if (fd !== null)
145
+ try {
146
+ closeSync(fd);
147
+ }
148
+ catch { /* non-fatal */ }
149
+ }
150
+ }
151
+ function verifyByteEqual(srcPath, dstPath) {
152
+ try {
153
+ const srcStat = statSync(srcPath);
154
+ const dstStat = statSync(dstPath);
155
+ if (srcStat.size !== dstStat.size)
156
+ return false;
157
+ const srcBuf = readFileSync(srcPath);
158
+ const dstBuf = readFileSync(dstPath);
159
+ return srcBuf.equals(dstBuf);
160
+ }
161
+ catch {
162
+ return false;
163
+ }
164
+ }
165
+ function tryUnlink(filePath) {
166
+ try {
167
+ unlinkSync(filePath);
168
+ }
169
+ catch { /* non-fatal */ }
170
+ }
171
+ /**
172
+ * Move `.swarm/hnsw.index` → `.moflo/hnsw.index` using the same
173
+ * copy-verify-delete pattern as the DB. Returns true on success, false when
174
+ * source absent or copy/verify failed (caller treats as best-effort).
175
+ */
176
+ function migrateHnswIndex(projectRoot) {
177
+ const src = legacyHnswIndexPath(projectRoot);
178
+ const dst = hnswIndexPath(projectRoot);
179
+ if (!existsSync(src))
180
+ return false;
181
+ if (existsSync(dst))
182
+ return false; // already there — leave both alone
183
+ try {
184
+ mkdirSync(dirname(dst), { recursive: true });
185
+ copyFileSync(src, dst);
186
+ }
187
+ catch {
188
+ tryUnlink(dst);
189
+ return false;
190
+ }
191
+ if (!verifyByteEqual(src, dst)) {
192
+ tryUnlink(dst);
193
+ return false;
194
+ }
195
+ tryUnlink(src); // sidecar can be regenerated; no .bak retention needed
196
+ return true;
197
+ }
198
+ /**
199
+ * One-time relocation of the memory DB from the upstream `.swarm/memory.db`
200
+ * layout to the canonical `.moflo/moflo.db` (story #727).
201
+ *
202
+ * Algorithm — copy-verify-delete, never `mv`:
203
+ * 1. If `.moflo/moflo.db` exists → no-op (`target-exists`).
204
+ * 2. If `.swarm/memory.db` absent → no-op (`no-legacy`).
205
+ * 3. Ensure `.moflo/` exists.
206
+ * 4. `copyFileSync(.swarm/memory.db, .moflo/moflo.db)`.
207
+ * 5. Verify byte-equal (size + content) AND SQLite header magic.
208
+ * 6. Move `.swarm/hnsw.index` → `.moflo/hnsw.index` (best-effort).
209
+ * 7. Rename `.swarm/memory.db` → `.swarm/memory.db.bak` (kept one upgrade cycle).
210
+ *
211
+ * Any failure between 4–7 deletes the partial target and returns failure;
212
+ * next session-start retries.
213
+ *
214
+ * MUST run BEFORE any long-lived consumer (MCP server, daemon) opens the DB —
215
+ * sql.js holds a full snapshot in RAM and would clobber the relocated file
216
+ * on its next flush. The launcher's section before the fire-and-forget block
217
+ * is the safe boundary; see feedback memory `feedback_sqljs_writeback_clobber`.
218
+ *
219
+ * Idempotent.
220
+ */
221
+ export function migrateMemoryDbToMoflo(projectRoot) {
222
+ const target = memoryDbPath(projectRoot);
223
+ if (existsSync(target))
224
+ return { migrated: false, reason: 'target-exists' };
225
+ const source = legacyMemoryDbPath(projectRoot);
226
+ if (!existsSync(source))
227
+ return { migrated: false, reason: 'no-legacy' };
228
+ try {
229
+ mkdirSync(dirname(target), { recursive: true });
230
+ }
231
+ catch {
232
+ return { migrated: false, reason: 'copy-failed' };
233
+ }
234
+ try {
235
+ copyFileSync(source, target);
236
+ }
237
+ catch {
238
+ tryUnlink(target);
239
+ return { migrated: false, reason: 'copy-failed' };
240
+ }
241
+ if (!verifyByteEqual(source, target) || !looksLikeSqliteFile(target)) {
242
+ tryUnlink(target);
243
+ return { migrated: false, reason: 'verify-failed' };
244
+ }
245
+ const hnswMoved = migrateHnswIndex(projectRoot);
246
+ // Final step: retire the source by renaming to .bak. Only after the new
247
+ // file is verified — never lose data. If this rename fails we leave the
248
+ // source in place; a stale `.swarm/memory.db` next to a healthy
249
+ // `.moflo/moflo.db` is harmless (the bridge reads only the new path) and
250
+ // surfaces as a `flo doctor` warning.
251
+ try {
252
+ renameSync(source, legacyMemoryDbBakPath(projectRoot));
253
+ }
254
+ catch {
255
+ return { migrated: true, reason: 'rename-failed', hnswMoved };
256
+ }
257
+ return { migrated: true, hnswMoved };
258
+ }
83
259
  //# sourceMappingURL=moflo-paths.js.map
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.9.0-rc.1';
5
+ export const VERSION = '4.9.0-rc.2';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.9.0-rc.1",
3
+ "version": "4.9.0-rc.2",
4
4
  "description": "MoFlo — AI agent orchestration for Claude Code. Forked from ruflo/claude-flow with patches applied to source, plus feature-level orchestration.",
5
5
  "main": "dist/src/cli/index.js",
6
6
  "type": "module",
@@ -78,7 +78,7 @@
78
78
  "@typescript-eslint/eslint-plugin": "^7.18.0",
79
79
  "@typescript-eslint/parser": "^7.18.0",
80
80
  "eslint": "^8.0.0",
81
- "moflo": "^4.8.87",
81
+ "moflo": "^4.9.0-rc.1",
82
82
  "tsx": "^4.21.0",
83
83
  "typescript": "^5.9.3",
84
84
  "vitest": "^4.0.0"