moflo 4.9.1 → 4.9.3

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.
@@ -2582,11 +2582,68 @@ const refreshCommand = {
2582
2582
  return { success: true };
2583
2583
  },
2584
2584
  };
2585
+ // Manual recovery for legacy DBs the launcher's auto cherry-pick can't reach
2586
+ // — schema mismatches that made the auto-run skip, an even older legacy DB
2587
+ // the candidate list doesn't include, or a friend's exported DB.
2588
+ const restoreLearningsCommand = {
2589
+ name: 'restore-learnings',
2590
+ description: 'Cherry-pick learnings/knowledge entries from a legacy DB into .moflo/moflo.db',
2591
+ options: [
2592
+ {
2593
+ name: 'from',
2594
+ description: 'Path to the legacy DB to read from (e.g. .swarm/memory.db)',
2595
+ type: 'string',
2596
+ required: true,
2597
+ },
2598
+ ],
2599
+ examples: [
2600
+ { command: 'flo memory restore-learnings --from .swarm/memory.db', description: 'Recover from a legacy memory DB' },
2601
+ { command: 'flo memory restore-learnings --from .swarm/memory.db.bak', description: 'Recover from a post-upgrade .bak' },
2602
+ ],
2603
+ action: async (ctx) => {
2604
+ const from = ctx.flags.from;
2605
+ if (!from) {
2606
+ output.printError('Source DB path is required. Use --from <path>');
2607
+ return { success: false, exitCode: 1 };
2608
+ }
2609
+ const sourcePath = pathModule.resolve(from);
2610
+ if (!fs.existsSync(sourcePath)) {
2611
+ output.printError(`Source DB not found: ${sourcePath}`);
2612
+ return { success: false, exitCode: 1 };
2613
+ }
2614
+ try {
2615
+ const { cherryPickLearningsFromLegacy, CHERRY_PICK_SKIP_REASONS } = await import('../services/cherry-pick-learnings.js');
2616
+ const result = await cherryPickLearningsFromLegacy({
2617
+ projectRoot: process.cwd(),
2618
+ legacyPaths: [sourcePath],
2619
+ });
2620
+ const report = result.sources[0];
2621
+ if (report?.reason === CHERRY_PICK_SKIP_REASONS.SCHEMA_MISMATCH) {
2622
+ output.printWarning(`Source DB has no memory_entries table — nothing to copy: ${sourcePath}`);
2623
+ return { success: true, data: result };
2624
+ }
2625
+ if (report?.reason === CHERRY_PICK_SKIP_REASONS.OPEN_FAILED) {
2626
+ output.printError(`Could not open source DB: ${sourcePath}`);
2627
+ return { success: false, exitCode: 1, data: result };
2628
+ }
2629
+ output.printSuccess(`Cherry-picked ${result.copied} of ${result.considered} learning/knowledge entries from ${sourcePath}`);
2630
+ if (result.copied < result.considered) {
2631
+ output.printInfo(`${result.considered - result.copied} duplicate row${result.considered - result.copied === 1 ? '' : 's'} skipped (INSERT OR IGNORE)`);
2632
+ }
2633
+ output.printInfo(`Target: ${result.target}`);
2634
+ return { success: true, data: result };
2635
+ }
2636
+ catch (error) {
2637
+ output.printError(`restore-learnings failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
2638
+ return { success: false, exitCode: 1 };
2639
+ }
2640
+ },
2641
+ };
2585
2642
  // Main memory command
2586
2643
  export const memoryCommand = {
2587
2644
  name: 'memory',
2588
2645
  description: 'Memory management commands',
2589
- subcommands: [initMemoryCommand, storeCommand, retrieveCommand, searchCommand, listCommand, deleteCommand, statsCommand, configureCommand, cleanupCommand, compressCommand, exportCommand, importCommand, indexGuidanceCommand, rebuildIndexCommand, codeMapCommand, refreshCommand],
2646
+ subcommands: [initMemoryCommand, storeCommand, retrieveCommand, searchCommand, listCommand, deleteCommand, statsCommand, configureCommand, cleanupCommand, compressCommand, exportCommand, importCommand, indexGuidanceCommand, rebuildIndexCommand, codeMapCommand, refreshCommand, restoreLearningsCommand],
2590
2647
  options: [],
2591
2648
  examples: [
2592
2649
  { command: 'claude-flow memory store -k "key" -v "value"', description: 'Store data' },
@@ -2616,7 +2673,8 @@ export const memoryCommand = {
2616
2673
  `${output.highlight('index-guidance')} - Index .claude/guidance/ files with RAG segments`,
2617
2674
  `${output.highlight('rebuild-index')} - Regenerate embeddings for memory entries`,
2618
2675
  `${output.highlight('code-map')} - Generate structural code map`,
2619
- `${output.highlight('refresh')} - Reindex all content, rebuild embeddings, cleanup, and vacuum`
2676
+ `${output.highlight('refresh')} - Reindex all content, rebuild embeddings, cleanup, and vacuum`,
2677
+ `${output.highlight('restore-learnings')} - Cherry-pick learnings/knowledge from a legacy DB`
2620
2678
  ]);
2621
2679
  return { success: true };
2622
2680
  }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Selective cherry-pick of durable memory rows on upgrade (#851).
3
+ *
4
+ * Replaces the full-DB byte-copy migration that epic #726 / story #727
5
+ * shipped (`migrateMemoryDbToMoflo`). That approach moved the entire 50+ MB
6
+ * of accumulated memory state across paths, then left the daemon writing to
7
+ * stale paths from its in-memory module cache. The user-visible result was
8
+ * "git went haywire" — see docs/moflo-4.9.1-upgrade-experience-2026-05-02.md.
9
+ *
10
+ * Almost all DB content is derived (code-map, embeddings, patterns, guidance
11
+ * chunks) and rebuilds on demand from the indexers. Only the `learnings`
12
+ * and `knowledge` namespaces are user-authored and worth carrying forward
13
+ * across upgrades. Everything else is regenerated cheaply.
14
+ *
15
+ * Algorithm:
16
+ * 1. Probe legacy DB candidates (`.swarm/memory.db.bak`, `.swarm/memory.db`,
17
+ * etc.) read-only — sources are NEVER mutated.
18
+ * 2. Ensure the target `.moflo/moflo.db` exists with V3 schema (idempotent
19
+ * `CREATE TABLE IF NOT EXISTS`).
20
+ * 3. `SELECT … WHERE namespace IN ('learnings', 'knowledge')` from each
21
+ * legacy source.
22
+ * 4. `INSERT OR IGNORE INTO memory_entries` keyed on the existing
23
+ * `UNIQUE(namespace, key)` constraint — duplicates skip silently, which
24
+ * is what makes a re-run of an interrupted migration safe.
25
+ * 5. `atomicWriteFileSync` the target so a SIGINT mid-flush can't truncate.
26
+ *
27
+ * Caller (the launcher) is responsible for stopping the daemon before this
28
+ * runs — sql.js holds a full snapshot in memory and a concurrent flush from
29
+ * a stale daemon would clobber the cherry-picked rows.
30
+ *
31
+ * @module cli/services/cherry-pick-learnings
32
+ */
33
+ /* eslint-disable @typescript-eslint/no-explicit-any */
34
+ import * as fs from 'fs';
35
+ import * as path from 'path';
36
+ import { mofloImport } from './moflo-require.js';
37
+ import { atomicWriteFileSync } from './atomic-file-write.js';
38
+ import { legacyMemoryDbBakPath, memoryDbCandidatePaths, memoryDbPath, } from './moflo-paths.js';
39
+ import { MEMORY_SCHEMA_V3 } from '../memory/memory-initializer.js';
40
+ /** Namespaces preserved across upgrades. Everything else is derived. */
41
+ export const DURABLE_NAMESPACES = ['learnings', 'knowledge'];
42
+ /**
43
+ * Reasons a single source contributed zero rows. Exported so callers can
44
+ * branch on the cause without duplicating string literals.
45
+ */
46
+ export const CHERRY_PICK_SKIP_REASONS = {
47
+ OPEN_FAILED: 'open-failed',
48
+ SCHEMA_MISMATCH: 'schema-mismatch',
49
+ NO_ROWS: 'no-rows',
50
+ SELF_REFERENCE: 'self-reference',
51
+ };
52
+ /**
53
+ * Composed off `memoryDbCandidatePaths` so any future addition there
54
+ * (statusline, doctor, etc. all share that probe order) automatically
55
+ * extends the cherry-pick. The `.bak` path is added at highest priority
56
+ * because it's the post-#727 migration backup and ranks above the live
57
+ * legacy file. The canonical `.moflo/moflo.db` is dropped — the
58
+ * self-reference guard would skip it anyway, and including it would just
59
+ * confuse the report.
60
+ */
61
+ function defaultLegacyCandidates(projectRoot) {
62
+ const canonical = memoryDbPath(projectRoot);
63
+ const tail = memoryDbCandidatePaths(projectRoot).filter((p) => p !== canonical);
64
+ return [legacyMemoryDbBakPath(projectRoot), ...tail];
65
+ }
66
+ /**
67
+ * Cherry-pick durable memory rows from any legacy DBs into `.moflo/moflo.db`.
68
+ *
69
+ * Returns a report — never throws on per-source failures. The caller (launcher)
70
+ * surfaces the count via `emitMutation`; a missing source / schema mismatch /
71
+ * locked file is recorded in `sources[].reason` and the upgrade continues.
72
+ *
73
+ * Hard failures (sql.js unavailable, target write failure) propagate so the
74
+ * launcher can choose to swallow + retry next session-start.
75
+ */
76
+ export async function cherryPickLearningsFromLegacy(options = {}) {
77
+ const projectRoot = options.projectRoot ?? process.cwd();
78
+ const target = path.resolve(options.toPath ?? memoryDbPath(projectRoot));
79
+ const namespaces = options.namespaces ?? DURABLE_NAMESPACES;
80
+ const legacyPaths = (options.legacyPaths ?? defaultLegacyCandidates(projectRoot)).map((p) => path.resolve(p));
81
+ const result = {
82
+ copied: 0,
83
+ considered: 0,
84
+ sources: [],
85
+ target,
86
+ };
87
+ const initSqlJs = (await mofloImport('sql.js'))?.default;
88
+ if (!initSqlJs)
89
+ return result;
90
+ const SQL = (await initSqlJs());
91
+ fs.mkdirSync(path.dirname(target), { recursive: true });
92
+ const targetExists = fs.existsSync(target);
93
+ const targetDb = targetExists
94
+ ? new SQL.Database(fs.readFileSync(target))
95
+ : new SQL.Database();
96
+ let insertStmt = null;
97
+ try {
98
+ targetDb.run(MEMORY_SCHEMA_V3);
99
+ const placeholders = namespaces.map(() => '?').join(',');
100
+ const selectSql = `SELECT id, key, namespace, content, type, embedding, embedding_model, ` +
101
+ `embedding_dimensions, tags, metadata, owner_id, created_at, updated_at, status ` +
102
+ `FROM memory_entries WHERE namespace IN (${placeholders})`;
103
+ // Hoisted prepare — avoids re-parsing the SQL inside sql.js for every
104
+ // INSERT. Matters for legacy DBs with hundreds of learnings rows.
105
+ insertStmt = targetDb.prepare(`INSERT OR IGNORE INTO memory_entries ` +
106
+ `(id, key, namespace, content, type, embedding, embedding_model, ` +
107
+ ` embedding_dimensions, tags, metadata, owner_id, created_at, updated_at, status) ` +
108
+ `VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
109
+ for (const sourcePath of legacyPaths) {
110
+ if (sourcePath === target) {
111
+ result.sources.push({
112
+ path: sourcePath,
113
+ rowsRead: 0,
114
+ rowsInserted: 0,
115
+ reason: CHERRY_PICK_SKIP_REASONS.SELF_REFERENCE,
116
+ });
117
+ continue;
118
+ }
119
+ if (!fs.existsSync(sourcePath))
120
+ continue;
121
+ const report = readAndInsert(SQL, sourcePath, targetDb, insertStmt, selectSql, namespaces);
122
+ result.sources.push(report);
123
+ result.copied += report.rowsInserted;
124
+ result.considered += report.rowsRead;
125
+ }
126
+ // Skip the atomic write when there's nothing to persist:
127
+ // - copied=0 + target didn't exist → don't materialize an empty DB
128
+ // (the regular initializer creates it on first real write).
129
+ // - copied=0 + target already existed → no diff, nothing to flush.
130
+ if (result.copied > 0) {
131
+ atomicWriteFileSync(target, Buffer.from(targetDb.export()));
132
+ }
133
+ }
134
+ finally {
135
+ if (insertStmt) {
136
+ try {
137
+ insertStmt.free();
138
+ }
139
+ catch { /* best-effort cleanup */ }
140
+ }
141
+ targetDb.close();
142
+ }
143
+ return result;
144
+ }
145
+ function readAndInsert(SQL, sourcePath, targetDb, insertStmt, selectSql, namespaces) {
146
+ let sourceDb;
147
+ try {
148
+ sourceDb = new SQL.Database(fs.readFileSync(sourcePath));
149
+ }
150
+ catch {
151
+ return {
152
+ path: sourcePath,
153
+ rowsRead: 0,
154
+ rowsInserted: 0,
155
+ reason: CHERRY_PICK_SKIP_REASONS.OPEN_FAILED,
156
+ };
157
+ }
158
+ try {
159
+ // Older / unrelated DBs may not have memory_entries.
160
+ const probe = sourceDb.exec(`SELECT name FROM sqlite_master WHERE type='table' AND name='memory_entries' LIMIT 1`);
161
+ if (!probe[0]?.values?.[0]) {
162
+ return {
163
+ path: sourcePath,
164
+ rowsRead: 0,
165
+ rowsInserted: 0,
166
+ reason: CHERRY_PICK_SKIP_REASONS.SCHEMA_MISMATCH,
167
+ };
168
+ }
169
+ let rowsRead = 0;
170
+ let rowsInserted = 0;
171
+ const selectStmt = sourceDb.prepare(selectSql);
172
+ selectStmt.bind(namespaces.slice());
173
+ while (selectStmt.step()) {
174
+ rowsRead++;
175
+ const row = selectStmt.getAsObject();
176
+ insertStmt.bind([
177
+ row.id,
178
+ row.key,
179
+ row.namespace,
180
+ row.content,
181
+ row.type ?? 'semantic',
182
+ row.embedding ?? null,
183
+ row.embedding_model ?? null,
184
+ row.embedding_dimensions ?? null,
185
+ row.tags ?? null,
186
+ row.metadata ?? null,
187
+ row.owner_id ?? null,
188
+ row.created_at ?? null,
189
+ row.updated_at ?? null,
190
+ row.status ?? 'active',
191
+ ]);
192
+ insertStmt.step();
193
+ if (targetDb.getRowsModified() > 0)
194
+ rowsInserted++;
195
+ insertStmt.reset();
196
+ }
197
+ selectStmt.free();
198
+ return {
199
+ path: sourcePath,
200
+ rowsRead,
201
+ rowsInserted,
202
+ reason: rowsRead === 0 ? CHERRY_PICK_SKIP_REASONS.NO_ROWS : undefined,
203
+ };
204
+ }
205
+ finally {
206
+ sourceDb.close();
207
+ }
208
+ }
209
+ //# sourceMappingURL=cherry-pick-learnings.js.map
@@ -1,24 +1,25 @@
1
1
  /**
2
- * MoFlo runtime state directory constants + legacy migrations (#699, #727).
2
+ * MoFlo runtime state directory constants.
3
3
  *
4
4
  * MoFlo owns its state under `.moflo/` at the project root. The upstream Ruflo
5
- * fork used `.claude-flow/`; consumers upgrading from older moflo builds (which
6
- * inherited that path) get a one-time auto-migration so they don't lose claim
7
- * files, daemon state, metrics, etc.
5
+ * fork used `.claude-flow/`; both legacy locations are still recognized as
6
+ * read-only sources for the version-bump-gated cherry-pick (#851) but are
7
+ * never relocated or renamed automatically — leaving them in place gives
8
+ * consumers a recovery source and avoids the failure modes that motivated
9
+ * the issue (silent migrations, daemon-held stale paths, .gitignore deletion).
8
10
  *
9
11
  * Anything that touches a runtime state path under the project root must
10
12
  * compose it from `MOFLO_DIR`. Plain string literals like `'.moflo'` are
11
13
  * tolerated where a constant import is awkward (shell templates, tests) but
12
14
  * production code is checked by `published-package-drift-guard.test.ts`.
13
15
  *
14
- * The pure-JS twin at `bin/lib/moflo-paths.mjs` mirrors the algorithm so
15
- * `bin/session-start-launcher.mjs` can run the migration before any TS has
16
- * been compiled. The parity test in moflo-paths-migration.test.ts catches
17
- * algorithm divergence between the two.
16
+ * The pure-JS twin at `bin/lib/moflo-paths.mjs` mirrors these constants so
17
+ * `bin/session-start-launcher.mjs` can resolve paths before any TS is
18
+ * compiled. The cherry-pick logic itself lives in
19
+ * `cli/services/cherry-pick-learnings.ts` and is dynamically imported from
20
+ * the compiled `dist/` by the launcher.
18
21
  */
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
- import { atomicWriteFileSync } from '../shared/utils/atomic-file-write.js';
22
+ import { join } from 'node:path';
22
23
  export const MOFLO_DIR = '.moflo';
23
24
  /** Canonical memory DB filename (post-#727). Lives at `<root>/.moflo/moflo.db`. */
24
25
  export const MEMORY_DB_FILE = 'moflo.db';
@@ -77,249 +78,4 @@ export function memoryDbCandidatePaths(projectRoot) {
77
78
  join(projectRoot, '.claude', LEGACY_MEMORY_DB_FILE),
78
79
  ];
79
80
  }
80
- /**
81
- * One-time migration of `.claude-flow/` → `.moflo/`.
82
- *
83
- * - Legacy missing → no-op (the steady state after first run).
84
- * - Legacy present + target missing → atomic rename (preserves mtimes).
85
- * - Both present → merge: target wins on collision, leaving the colliding
86
- * entry behind in legacy/ so the launcher can warn and a future run can
87
- * retry. Drops the legacy dir if everything moved cleanly.
88
- *
89
- * When the models cache moves (legacy `.claude-flow/models/` lands at
90
- * `.moflo/models/`), `.moflo/embeddings.json:modelPath` is rewritten in the
91
- * same call so the next embedder run finds the relocated ONNX bytes instead
92
- * of re-downloading multi-MB binaries (#735).
93
- *
94
- * Idempotent and safe to call from session start.
95
- */
96
- export function migrateClaudeFlowToMoflo(projectRoot) {
97
- const legacy = legacyClaudeFlowDir(projectRoot);
98
- const target = mofloDir(projectRoot);
99
- if (!existsSync(legacy))
100
- return { migrated: false, reason: 'no-legacy' };
101
- if (!existsSync(target)) {
102
- // Wholesale rename — count what's about to move so the launcher can
103
- // report `migrated N files` instead of opaque "migrated runtime state".
104
- let movedCount = 0;
105
- try {
106
- movedCount = readdirSync(legacy).length;
107
- }
108
- catch { /* count is cosmetic */ }
109
- renameSync(legacy, target);
110
- rewriteEmbeddingsModelPath(projectRoot);
111
- return { migrated: true, movedCount };
112
- }
113
- let entries;
114
- try {
115
- entries = readdirSync(legacy);
116
- }
117
- catch {
118
- return { migrated: false, reason: 'legacy-unreadable' };
119
- }
120
- let moved = 0;
121
- let modelsMoved = false;
122
- const collisions = [];
123
- for (const name of entries) {
124
- const dst = join(target, name);
125
- if (existsSync(dst)) {
126
- collisions.push(name); // target wins — but the launcher must warn
127
- continue;
128
- }
129
- try {
130
- renameSync(join(legacy, name), dst);
131
- moved++;
132
- if (name === 'models')
133
- modelsMoved = true;
134
- }
135
- catch {
136
- // Best-effort merge — a failed move on one entry shouldn't abort the rest.
137
- }
138
- }
139
- // Drop empty legacy dir so future runs short-circuit at existsSync(legacy).
140
- try {
141
- if (readdirSync(legacy).length === 0)
142
- rmdirSync(legacy);
143
- }
144
- catch {
145
- // Non-fatal — leftover legacy dir just means migration runs next time.
146
- }
147
- if (modelsMoved)
148
- rewriteEmbeddingsModelPath(projectRoot);
149
- if (moved === 0) {
150
- return { migrated: false, reason: 'merged-nothing', movedCount: 0, collisions };
151
- }
152
- return { migrated: true, movedCount: moved, collisions };
153
- }
154
- /**
155
- * Rewrite `.moflo/embeddings.json:modelPath` if it still references the
156
- * legacy `.claude-flow/` location. Called after a models/ relocation so the
157
- * stale path doesn't force a multi-MB re-download (#735).
158
- *
159
- * Best-effort: file-not-present, malformed JSON, missing `modelPath`, or
160
- * already-correct path are all silent no-ops. Returns true only when the
161
- * file was actually rewritten. Uses atomicWriteFileSync so a SIGINT mid-flush
162
- * can't truncate embeddings.json and break every subsequent embedder run.
163
- */
164
- export function rewriteEmbeddingsModelPath(projectRoot) {
165
- const cfgPath = join(projectRoot, MOFLO_DIR, 'embeddings.json');
166
- let raw;
167
- try {
168
- raw = readFileSync(cfgPath, 'utf8');
169
- }
170
- catch {
171
- return false;
172
- }
173
- let cfg;
174
- try {
175
- cfg = JSON.parse(raw);
176
- }
177
- catch {
178
- return false;
179
- }
180
- if (typeof cfg.modelPath !== 'string')
181
- return false;
182
- if (!cfg.modelPath.includes(LEGACY_CLAUDE_FLOW_DIR))
183
- return false;
184
- // split-join handles every occurrence and works with both `/` and `\\`
185
- // (escaped) forms in JSON-encoded Windows paths.
186
- cfg.modelPath = cfg.modelPath.split(LEGACY_CLAUDE_FLOW_DIR).join(MOFLO_DIR);
187
- try {
188
- atomicWriteFileSync(cfgPath, JSON.stringify(cfg, null, 2));
189
- return true;
190
- }
191
- catch {
192
- return false;
193
- }
194
- }
195
- const SQLITE_MAGIC_HEADER = Buffer.from('SQLite format 3\0', 'utf8');
196
- function looksLikeSqliteFile(filePath) {
197
- let fd = null;
198
- try {
199
- fd = openSync(filePath, 'r');
200
- const buf = Buffer.alloc(SQLITE_MAGIC_HEADER.length);
201
- const read = readSync(fd, buf, 0, buf.length, 0);
202
- if (read < SQLITE_MAGIC_HEADER.length)
203
- return false;
204
- return buf.equals(SQLITE_MAGIC_HEADER);
205
- }
206
- catch {
207
- return false;
208
- }
209
- finally {
210
- if (fd !== null)
211
- try {
212
- closeSync(fd);
213
- }
214
- catch { /* non-fatal */ }
215
- }
216
- }
217
- function verifyByteEqual(srcPath, dstPath) {
218
- try {
219
- const srcStat = statSync(srcPath);
220
- const dstStat = statSync(dstPath);
221
- if (srcStat.size !== dstStat.size)
222
- return false;
223
- const srcBuf = readFileSync(srcPath);
224
- const dstBuf = readFileSync(dstPath);
225
- return srcBuf.equals(dstBuf);
226
- }
227
- catch {
228
- return false;
229
- }
230
- }
231
- function tryUnlink(filePath) {
232
- try {
233
- unlinkSync(filePath);
234
- }
235
- catch { /* non-fatal */ }
236
- }
237
- /**
238
- * Move `.swarm/hnsw.index` → `.moflo/hnsw.index` using the same
239
- * copy-verify-delete pattern as the DB. Returns true on success, false when
240
- * source absent or copy/verify failed (caller treats as best-effort).
241
- */
242
- function migrateHnswIndex(projectRoot) {
243
- const src = legacyHnswIndexPath(projectRoot);
244
- const dst = hnswIndexPath(projectRoot);
245
- if (!existsSync(src))
246
- return false;
247
- if (existsSync(dst))
248
- return false; // already there — leave both alone
249
- try {
250
- mkdirSync(dirname(dst), { recursive: true });
251
- copyFileSync(src, dst);
252
- }
253
- catch {
254
- tryUnlink(dst);
255
- return false;
256
- }
257
- if (!verifyByteEqual(src, dst)) {
258
- tryUnlink(dst);
259
- return false;
260
- }
261
- tryUnlink(src); // sidecar can be regenerated; no .bak retention needed
262
- return true;
263
- }
264
- /**
265
- * One-time relocation of the memory DB from the upstream `.swarm/memory.db`
266
- * layout to the canonical `.moflo/moflo.db` (story #727).
267
- *
268
- * Algorithm — copy-verify-delete, never `mv`:
269
- * 1. If `.moflo/moflo.db` exists → no-op (`target-exists`).
270
- * 2. If `.swarm/memory.db` absent → no-op (`no-legacy`).
271
- * 3. Ensure `.moflo/` exists.
272
- * 4. `copyFileSync(.swarm/memory.db, .moflo/moflo.db)`.
273
- * 5. Verify byte-equal (size + content) AND SQLite header magic.
274
- * 6. Move `.swarm/hnsw.index` → `.moflo/hnsw.index` (best-effort).
275
- * 7. Rename `.swarm/memory.db` → `.swarm/memory.db.bak` (kept one upgrade cycle).
276
- *
277
- * Any failure between 4–7 deletes the partial target and returns failure;
278
- * next session-start retries.
279
- *
280
- * MUST run BEFORE any long-lived consumer (MCP server, daemon) opens the DB —
281
- * sql.js holds a full snapshot in RAM and would clobber the relocated file
282
- * on its next flush. The launcher's section before the fire-and-forget block
283
- * is the safe boundary; see feedback memory `feedback_sqljs_writeback_clobber`.
284
- *
285
- * Idempotent.
286
- */
287
- export function migrateMemoryDbToMoflo(projectRoot) {
288
- const target = memoryDbPath(projectRoot);
289
- if (existsSync(target))
290
- return { migrated: false, reason: 'target-exists' };
291
- const source = legacyMemoryDbPath(projectRoot);
292
- if (!existsSync(source))
293
- return { migrated: false, reason: 'no-legacy' };
294
- try {
295
- mkdirSync(dirname(target), { recursive: true });
296
- }
297
- catch {
298
- return { migrated: false, reason: 'copy-failed' };
299
- }
300
- try {
301
- copyFileSync(source, target);
302
- }
303
- catch {
304
- tryUnlink(target);
305
- return { migrated: false, reason: 'copy-failed' };
306
- }
307
- if (!verifyByteEqual(source, target) || !looksLikeSqliteFile(target)) {
308
- tryUnlink(target);
309
- return { migrated: false, reason: 'verify-failed' };
310
- }
311
- const hnswMoved = migrateHnswIndex(projectRoot);
312
- // Final step: retire the source by renaming to .bak. Only after the new
313
- // file is verified — never lose data. If this rename fails we leave the
314
- // source in place; a stale `.swarm/memory.db` next to a healthy
315
- // `.moflo/moflo.db` is harmless (the bridge reads only the new path) and
316
- // surfaces as a `flo doctor` warning.
317
- try {
318
- renameSync(source, legacyMemoryDbBakPath(projectRoot));
319
- }
320
- catch {
321
- return { migrated: true, reason: 'rename-failed', hnswMoved };
322
- }
323
- return { migrated: true, hnswMoved };
324
- }
325
81
  //# 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.1';
5
+ export const VERSION = '4.9.3';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.9.1",
3
+ "version": "4.9.3",
4
4
  "description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
5
5
  "main": "dist/src/cli/index.js",
6
6
  "type": "module",
@@ -79,7 +79,7 @@
79
79
  "@typescript-eslint/eslint-plugin": "^7.18.0",
80
80
  "@typescript-eslint/parser": "^7.18.0",
81
81
  "eslint": "^8.0.0",
82
- "moflo": "^4.9.0",
82
+ "moflo": "^4.9.2",
83
83
  "tsx": "^4.21.0",
84
84
  "typescript": "^5.9.3",
85
85
  "vitest": "^4.0.0"