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; null
606
- // when missing, expired, or malformed. The launcher writes status='in-progress'
607
- // while upgrade work is running, then deletes the file when done — so a
608
- // 'complete' status only ever shows up here for legacy notice files left by
609
- // pre-#738 launchers.
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
- let suffix = '';
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 {
@@ -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.3';
5
+ export const VERSION = '4.9.0-rc.4';
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.3",
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.2",
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"