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.
- package/.claude/guidance/shipped/moflo-error-handling.md +25 -0
- package/.claude/guidance/shipped/moflo-source-hygiene.md +16 -0
- package/bin/build-embeddings.mjs +31 -14
- package/bin/lib/moflo-paths.mjs +7 -216
- package/bin/lib/process-manager.mjs +7 -2
- package/bin/session-start-launcher.mjs +310 -128
- package/dist/src/cli/commands/memory.js +60 -2
- package/dist/src/cli/services/cherry-pick-learnings.js +209 -0
- package/dist/src/cli/services/moflo-paths.js +12 -256
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
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/`;
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
15
|
-
* `bin/session-start-launcher.mjs` can
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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 {
|
|
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
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.9.
|
|
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.
|
|
82
|
+
"moflo": "^4.9.2",
|
|
83
83
|
"tsx": "^4.21.0",
|
|
84
84
|
"typescript": "^5.9.3",
|
|
85
85
|
"vitest": "^4.0.0"
|