moflo 4.9.0-rc.3 → 4.9.0-rc.4
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.
|
@@ -602,33 +602,29 @@ function getIntegrationStatus() {
|
|
|
602
602
|
return { mcpServers, hasDatabase, hasApi };
|
|
603
603
|
}
|
|
604
604
|
|
|
605
|
-
// Upgrade notice (#636, #738) — written by the session-start launcher
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
605
|
+
// Upgrade notice (#636, #738, #743) — written by the session-start launcher
|
|
606
|
+
// ONLY while upgrade work is in flight; the launcher deletes the file when
|
|
607
|
+
// work completes. We render it strictly for status='in-progress' so a stale
|
|
608
|
+
// notice (legacy "complete" file from pre-#738 launchers, zombie write from
|
|
609
|
+
// an aborted launcher, future writer mistakes) cannot turn the statusline
|
|
610
|
+
// segment into a permanent column. The launcher's section 0-pre also drops
|
|
611
|
+
// any leftover file at session start as a second line of defence.
|
|
610
612
|
function getUpgradeNotice() {
|
|
611
613
|
const data = readJSON(path.join(CWD, '.moflo', 'upgrade-notice.json'));
|
|
612
614
|
if (!data || typeof data !== 'object') return null;
|
|
615
|
+
if (data.status !== 'in-progress') return null;
|
|
613
616
|
const expiresAt = data.expiresAt ? new Date(data.expiresAt).getTime() : 0;
|
|
614
617
|
if (!expiresAt || Date.now() > expiresAt) return null;
|
|
615
618
|
return {
|
|
616
|
-
status: data.status === 'in-progress' ? 'in-progress' : 'complete',
|
|
617
619
|
kind: data.kind === 'repair' ? 'repair' : 'upgrade',
|
|
618
620
|
from: typeof data.from === 'string' ? data.from : '',
|
|
619
621
|
to: typeof data.to === 'string' ? data.to : '',
|
|
620
|
-
changes: typeof data.changes === 'number' && data.changes > 0 ? data.changes : 0,
|
|
621
622
|
};
|
|
622
623
|
}
|
|
623
624
|
|
|
624
625
|
function formatUpgradeNoticeSegment(notice) {
|
|
625
626
|
if (!notice) return '';
|
|
626
|
-
|
|
627
|
-
if (notice.status === 'in-progress') {
|
|
628
|
-
suffix = ` ${c.dim}(updating…)${c.reset}`;
|
|
629
|
-
} else if (notice.changes > 0) {
|
|
630
|
-
suffix = ` ${c.dim}(${notice.changes} ${notice.changes === 1 ? 'change' : 'changes'})${c.reset}`;
|
|
631
|
-
}
|
|
627
|
+
const suffix = ` ${c.dim}(updating…)${c.reset}`;
|
|
632
628
|
if (notice.kind === 'repair') {
|
|
633
629
|
return `${c.brightYellow}📦 install repaired${c.reset}${suffix}`;
|
|
634
630
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory-DB integrity check + auto-REINDEX (story #743).
|
|
3
|
+
*
|
|
4
|
+
* The `.moflo/moflo.db` SQLite file routinely accumulates index corruption of
|
|
5
|
+
* the form `row N missing from index sqlite_autoindex_memory_entries_1` —
|
|
6
|
+
* the row data is intact, only the unique-key index has drifted. The most
|
|
7
|
+
* common trigger is sql.js's whole-file dump-on-flush behaviour racing with
|
|
8
|
+
* concurrent writes (see `feedback_sqljs_writeback_clobber.md` and #714).
|
|
9
|
+
*
|
|
10
|
+
* Symptoms when uncorrected:
|
|
11
|
+
* - `index-guidance.mjs` and `index-patterns.mjs` fail mid-write with
|
|
12
|
+
* `database disk image is malformed`, leaving partial state.
|
|
13
|
+
* - The ephemeral-namespace purge (#729) fails silently, so hive-mind /
|
|
14
|
+
* tasklist / epic-state / test-bridge-fix rows accumulate.
|
|
15
|
+
* - Vector counts in the statusline stay inflated (observed: 4415 with
|
|
16
|
+
* 1025 unpurged ephemeral rows).
|
|
17
|
+
*
|
|
18
|
+
* Fix shape: REINDEX rebuilds indexes from the canonical row data — much less
|
|
19
|
+
* destructive than a full rebuild and works for the typical drift mode. If
|
|
20
|
+
* REINDEX itself fails to restore integrity we leave the file alone and
|
|
21
|
+
* report; manual `flo memory rebuild-index` is the fallback.
|
|
22
|
+
*
|
|
23
|
+
* MUST run BEFORE any long-lived sql.js consumer (MCP server, daemon) opens
|
|
24
|
+
* the DB and BEFORE the embeddings migration / soft-delete purge / ephemeral
|
|
25
|
+
* purge — those all swallow corruption errors and silently no-op.
|
|
26
|
+
*/
|
|
27
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
28
|
+
import { memoryDbPath } from './moflo-paths.mjs';
|
|
29
|
+
|
|
30
|
+
let _initSqlJs = null;
|
|
31
|
+
|
|
32
|
+
async function loadSqlJs() {
|
|
33
|
+
if (_initSqlJs) return _initSqlJs;
|
|
34
|
+
// sql.js is a hard dependency of moflo (see top-level package.json);
|
|
35
|
+
// resolving it from the consumer's node_modules works because the launcher
|
|
36
|
+
// runs from the consumer cwd.
|
|
37
|
+
const mod = await import('sql.js');
|
|
38
|
+
_initSqlJs = mod.default || mod;
|
|
39
|
+
return _initSqlJs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isOk(execResult) {
|
|
43
|
+
const rows = execResult?.[0]?.values ?? [];
|
|
44
|
+
return rows.length === 1 && rows[0]?.[0] === 'ok';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function corruptionCount(execResult) {
|
|
48
|
+
return execResult?.[0]?.values?.length ?? 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Probe the memory DB for index corruption and run REINDEX in place if
|
|
53
|
+
* found. Returns `{ repaired, errors, persistent }`:
|
|
54
|
+
* - `repaired: true` and `errors > 0` when REINDEX restored integrity.
|
|
55
|
+
* - `repaired: false, errors: 0` when the DB is healthy or absent.
|
|
56
|
+
* - `repaired: false, errors > 0, persistent: true` when corruption survives
|
|
57
|
+
* REINDEX (caller should surface to the user — manual rebuild needed).
|
|
58
|
+
*
|
|
59
|
+
* Never throws; any internal failure becomes `{ repaired: false, errors: 0 }`
|
|
60
|
+
* so a probe failure cannot block session start.
|
|
61
|
+
*/
|
|
62
|
+
export async function repairMemoryDbIfCorrupt(projectRoot) {
|
|
63
|
+
const dbPath = memoryDbPath(projectRoot);
|
|
64
|
+
if (!existsSync(dbPath)) return { repaired: false, errors: 0 };
|
|
65
|
+
|
|
66
|
+
let initSql;
|
|
67
|
+
try {
|
|
68
|
+
initSql = await loadSqlJs();
|
|
69
|
+
} catch {
|
|
70
|
+
return { repaired: false, errors: 0 };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let db = null;
|
|
74
|
+
try {
|
|
75
|
+
const SQL = await initSql();
|
|
76
|
+
const data = readFileSync(dbPath);
|
|
77
|
+
db = new SQL.Database(data);
|
|
78
|
+
|
|
79
|
+
const before = db.exec('PRAGMA integrity_check');
|
|
80
|
+
if (isOk(before)) {
|
|
81
|
+
return { repaired: false, errors: 0 };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const errors = corruptionCount(before);
|
|
85
|
+
db.run('REINDEX');
|
|
86
|
+
|
|
87
|
+
const after = db.exec('PRAGMA integrity_check');
|
|
88
|
+
if (!isOk(after)) {
|
|
89
|
+
return { repaired: false, errors, persistent: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const out = Buffer.from(db.export());
|
|
93
|
+
writeFileSync(dbPath, out);
|
|
94
|
+
return { repaired: true, errors };
|
|
95
|
+
} catch {
|
|
96
|
+
return { repaired: false, errors: 0 };
|
|
97
|
+
} finally {
|
|
98
|
+
if (db) try { db.close(); } catch { /* non-fatal */ }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -12,6 +12,7 @@ import { existsSync, readFileSync, writeFileSync, copyFileSync, unlinkSync, read
|
|
|
12
12
|
import { resolve, dirname, join } from 'path';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
14
|
import { migrateClaudeFlowToMoflo, migrateMemoryDbToMoflo, mofloDir } from './lib/moflo-paths.mjs';
|
|
15
|
+
import { repairMemoryDbIfCorrupt } from './lib/db-repair.mjs';
|
|
15
16
|
|
|
16
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
18
|
|
|
@@ -91,6 +92,20 @@ function clearUpgradeNotice() {
|
|
|
91
92
|
} catch { /* non-fatal — already gone or never existed */ }
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
// ── 0-pre. Drop any stale upgrade notice (#738, #743) ───────────────────────
|
|
96
|
+
// `upgrade-notice.json` is a transient handshake between launcher and
|
|
97
|
+
// statusline — it should never survive past the launcher run that wrote it.
|
|
98
|
+
// Pre-#738 launchers wrote a 1-hour-TTL "complete" notice after upgrade work
|
|
99
|
+
// finished; with the #738 contract that file can only be a leftover, but the
|
|
100
|
+
// statusline still rendered it for the rest of the hour. Unconditionally
|
|
101
|
+
// removing it here makes the contract self-healing — any future zombie
|
|
102
|
+
// notice (legacy file, aborted launcher, future writer mistake) gets dropped
|
|
103
|
+
// before the statusline can see it. The in-progress notice for THIS session,
|
|
104
|
+
// if any, is written later in section 3 and cleared in section 3f.
|
|
105
|
+
try {
|
|
106
|
+
unlinkSync(join(mofloDir(projectRoot), 'upgrade-notice.json'));
|
|
107
|
+
} catch { /* non-fatal — file usually doesn't exist */ }
|
|
108
|
+
|
|
94
109
|
// ── 0. LEGACY state migration (#699) ─────────────────────────────────────────
|
|
95
110
|
// Consumers upgrading from older moflo builds (inherited from upstream Ruflo)
|
|
96
111
|
// get a one-time auto-migration of LEGACY `.claude-flow/` → `.moflo/` so claim
|
|
@@ -126,6 +141,38 @@ try {
|
|
|
126
141
|
// Non-fatal — failed migration leaves both DBs in place; next session retries.
|
|
127
142
|
}
|
|
128
143
|
|
|
144
|
+
// ── 0c. Memory DB index repair (#743) ───────────────────────────────────────
|
|
145
|
+
// The .moflo/moflo.db SQLite file accumulates index corruption ("row N missing
|
|
146
|
+
// from sqlite_autoindex_memory_entries_1") when sql.js's whole-file flush
|
|
147
|
+
// races with concurrent writes. Symptom is silent: indexers fail mid-write,
|
|
148
|
+
// the ephemeral-namespace purge (#729) silently no-ops, vector counts inflate.
|
|
149
|
+
//
|
|
150
|
+
// Probe + REINDEX in place. Must run BEFORE any sql.js consumer (the
|
|
151
|
+
// embeddings migration in 3e, the soft-delete + ephemeral purges in 3e-728/
|
|
152
|
+
// 3e-729, and the long-lived MCP server / daemon spawned in section 4) — all
|
|
153
|
+
// of those swallow corruption errors and silently drop work on the floor.
|
|
154
|
+
//
|
|
155
|
+
// Awaited because every downstream sql.js touch this session depends on a
|
|
156
|
+
// healthy index. Cost on the happy path is one PRAGMA check (~10ms).
|
|
157
|
+
try {
|
|
158
|
+
const repair = await repairMemoryDbIfCorrupt(projectRoot);
|
|
159
|
+
if (repair?.repaired) {
|
|
160
|
+
emitMutation(
|
|
161
|
+
'repaired memory db index',
|
|
162
|
+
`${plural(repair.errors, 'index error')} fixed via REINDEX`,
|
|
163
|
+
);
|
|
164
|
+
} else if (repair?.persistent) {
|
|
165
|
+
// Surface to stderr — Claude additionalContext + the user both see this.
|
|
166
|
+
// Manual `flo memory rebuild-index` is the next step.
|
|
167
|
+
process.stderr.write(
|
|
168
|
+
`moflo: memory db has ${plural(repair.errors, 'index error')} REINDEX could not fix — run 'flo memory rebuild-index'\n`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
// Non-fatal — repair is best-effort; downstream code paths report their
|
|
173
|
+
// own errors if the DB is still broken.
|
|
174
|
+
}
|
|
175
|
+
|
|
129
176
|
// ── 1. Helper: fire-and-forget a background process ─────────────────────────
|
|
130
177
|
function fireAndForget(cmd, args, label) {
|
|
131
178
|
try {
|
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.0-rc.
|
|
3
|
+
"version": "4.9.0-rc.4",
|
|
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.9.0-rc.
|
|
81
|
+
"moflo": "^4.9.0-rc.3",
|
|
82
82
|
"tsx": "^4.21.0",
|
|
83
83
|
"typescript": "^5.9.3",
|
|
84
84
|
"vitest": "^4.0.0"
|