moflo 4.9.37 → 4.10.1
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-memory-protocol.md +5 -1
- package/.claude/guidance/shipped/moflo-memorydb-maintenance.md +22 -11
- package/.claude/guidance/shipped/moflo-root-cause-discipline.md +47 -0
- package/.claude/helpers/statusline.cjs +69 -33
- package/.claude/helpers/subagent-bootstrap.json +1 -1
- package/.claude/helpers/subagent-start.cjs +1 -1
- package/bin/build-embeddings.mjs +6 -20
- package/bin/cli.js +5 -0
- package/bin/generate-code-map.mjs +4 -24
- package/bin/hooks.mjs +3 -12
- package/bin/index-all.mjs +3 -13
- package/bin/index-guidance.mjs +36 -85
- package/bin/index-patterns.mjs +6 -24
- package/bin/index-tests.mjs +4 -23
- package/bin/lib/db-repair.mjs +358 -62
- package/bin/lib/get-backend.mjs +306 -0
- package/bin/lib/incremental-write.mjs +27 -7
- package/bin/lib/moflo-paths.mjs +64 -4
- package/bin/lib/suppress-sqlite-warning.mjs +57 -0
- package/bin/migrations/knowledge-purge.mjs +7 -8
- package/bin/migrations/knowledge-to-learnings.mjs +7 -9
- package/bin/migrations/purge-doc-entries.mjs +7 -8
- package/bin/migrations/strip-context-preambles.mjs +4 -6
- package/bin/run-migrations.mjs +1 -10
- package/bin/semantic-search.mjs +7 -18
- package/bin/session-start-launcher.mjs +144 -108
- package/bin/simplify-classify.cjs +38 -17
- package/dist/src/cli/commands/daemon.js +38 -11
- package/dist/src/cli/commands/doctor-checks-config.js +60 -0
- package/dist/src/cli/commands/doctor-checks-coverage-truth.js +136 -0
- package/dist/src/cli/commands/doctor-checks-memory-access.js +146 -86
- package/dist/src/cli/commands/doctor-checks-memory.js +13 -18
- package/dist/src/cli/commands/doctor-checks-version-skew.js +94 -0
- package/dist/src/cli/commands/doctor-checks-writers-audit.js +170 -0
- package/dist/src/cli/commands/doctor-embedding-hygiene.js +3 -15
- package/dist/src/cli/commands/doctor-fixes.js +87 -0
- package/dist/src/cli/commands/doctor-registry.js +24 -1
- package/dist/src/cli/commands/doctor.js +1 -1
- package/dist/src/cli/commands/embeddings.js +17 -22
- package/dist/src/cli/commands/memory.js +13 -23
- package/dist/src/cli/embeddings/persistent-cache.js +44 -83
- package/dist/src/cli/init/moflo-init.js +40 -0
- package/dist/src/cli/mcp-tools/memory-tools.js +10 -3
- package/dist/src/cli/memory/bridge-core.js +256 -30
- package/dist/src/cli/memory/bridge-embedder.js +84 -3
- package/dist/src/cli/memory/bridge-entries.js +70 -6
- package/dist/src/cli/memory/controller-registry.js +7 -2
- package/dist/src/cli/memory/controllers/batch-operations.js +5 -1
- package/dist/src/cli/memory/controllers/hierarchical-memory.js +7 -2
- package/dist/src/cli/memory/controllers/mutation-guard.js +22 -2
- package/dist/src/cli/memory/daemon-backend.js +400 -0
- package/dist/src/cli/memory/daemon-write-client.js +192 -15
- package/dist/src/cli/memory/database-provider.js +57 -40
- package/dist/src/cli/memory/hnsw-persistence.js +6 -8
- package/dist/src/cli/memory/index.js +0 -1
- package/dist/src/cli/memory/memory-bridge.js +40 -8
- package/dist/src/cli/memory/memory-initializer.js +271 -211
- package/dist/src/cli/memory/rvf-migration.js +25 -11
- package/dist/src/cli/memory/sqlite-backend.js +573 -0
- package/dist/src/cli/memory/suppress-sqlite-warning.js +49 -0
- package/dist/src/cli/services/cherry-pick-learnings.js +32 -21
- package/dist/src/cli/services/daemon-dashboard.js +13 -1
- package/dist/src/cli/services/daemon-lock.js +58 -1
- package/dist/src/cli/services/daemon-memory-rpc.js +245 -10
- package/dist/src/cli/services/embeddings-migration.js +9 -12
- package/dist/src/cli/services/ephemeral-namespace-purge.js +21 -16
- package/dist/src/cli/services/learning-service.js +12 -20
- package/dist/src/cli/services/memory-db-integrity-repair.js +119 -0
- package/dist/src/cli/services/project-root.js +69 -9
- package/dist/src/cli/services/soft-delete-purge.js +6 -11
- package/dist/src/cli/services/sqljs-migration-store.js +4 -1
- package/dist/src/cli/services/subagent-bootstrap.js +1 -1
- package/dist/src/cli/shared/events/event-store.js +26 -55
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -4
- package/dist/src/cli/memory/sqljs-backend.js +0 -643
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writers Audit runtime doctor check (epic #1054.S5 / #1059).
|
|
3
|
+
*
|
|
4
|
+
* Runtime sibling of S1's static lint (`tests/system/moflo-db-writer-audit.test.ts`).
|
|
5
|
+
* Enumerates running node processes whose command line invokes one of the
|
|
6
|
+
* known cross-process writers (build-embeddings, migrations) and fails if any
|
|
7
|
+
* are alive while the daemon owns the lock.
|
|
8
|
+
*
|
|
9
|
+
* Why not lsof / handle.exe?
|
|
10
|
+
* - `lsof` not installed on every Linux distro and not on Windows.
|
|
11
|
+
* - `handle.exe` (Sysinternals) requires manual install.
|
|
12
|
+
* - `openfiles.exe` is disabled by default on Windows and requires a reboot to
|
|
13
|
+
* enable.
|
|
14
|
+
*
|
|
15
|
+
* Command-line signature scan is cross-platform, dependency-free, and matches
|
|
16
|
+
* the writers we actually care about — S3 ported them all to the daemon-offline
|
|
17
|
+
* pattern, so any of them being alive concurrently with the daemon is a
|
|
18
|
+
* regression of S3's wrapper logic.
|
|
19
|
+
*
|
|
20
|
+
* @module cli/commands/doctor-checks-writers-audit
|
|
21
|
+
*/
|
|
22
|
+
import { execSync } from 'child_process';
|
|
23
|
+
import { existsSync, readFileSync } from 'fs';
|
|
24
|
+
import { join } from 'path';
|
|
25
|
+
import { getDaemonLockHolder } from '../services/daemon-lock.js';
|
|
26
|
+
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
27
|
+
const SCAN_TIMEOUT_MS_WIN = 10_000;
|
|
28
|
+
const SCAN_TIMEOUT_MS_POSIX = 5_000;
|
|
29
|
+
const CMDLINE_CAPTURE_LEN = 300;
|
|
30
|
+
/**
|
|
31
|
+
* Command-line fragments that identify a CROSS-PROCESS moflo.db writer.
|
|
32
|
+
* These should never run while the daemon owns the lock (S3 wraps every
|
|
33
|
+
* invocation in an explicit daemon-stop). Indexer scripts (index-guidance,
|
|
34
|
+
* index-tests, etc.) are daemon-spawned children — they appear in
|
|
35
|
+
* `background-pids.json` and are filtered out below.
|
|
36
|
+
*/
|
|
37
|
+
const FOREIGN_WRITER_PATTERNS = [
|
|
38
|
+
/build-embeddings\.mjs/i,
|
|
39
|
+
/bin[\\\/]migrations[\\\/][^\s"']+\.mjs/i,
|
|
40
|
+
/lib[\\\/]db-repair\.mjs/i,
|
|
41
|
+
];
|
|
42
|
+
function readTrackedBackgroundPids(cwd) {
|
|
43
|
+
const result = new Set();
|
|
44
|
+
const registryFile = join(cwd, '.moflo', 'background-pids.json');
|
|
45
|
+
try {
|
|
46
|
+
if (!existsSync(registryFile))
|
|
47
|
+
return result;
|
|
48
|
+
const entries = JSON.parse(readFileSync(registryFile, 'utf-8'));
|
|
49
|
+
if (!Array.isArray(entries))
|
|
50
|
+
return result;
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (entry && typeof entry.pid === 'number' && entry.pid > 0) {
|
|
53
|
+
result.add(entry.pid);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch { /* unreadable — treat as empty */ }
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
function enumerateNodeProcesses() {
|
|
61
|
+
try {
|
|
62
|
+
if (process.platform === 'win32') {
|
|
63
|
+
const csv = execSync('powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"Name=\'node.exe\'\\" | Select-Object ProcessId,CommandLine | ConvertTo-Csv -NoTypeInformation"', { encoding: 'utf-8', timeout: SCAN_TIMEOUT_MS_WIN, windowsHide: true });
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const line of csv.split(/\r?\n/)) {
|
|
66
|
+
const m = line.match(/^"(\d+)","?(.*?)"?$/);
|
|
67
|
+
if (!m)
|
|
68
|
+
continue;
|
|
69
|
+
const pid = parseInt(m[1], 10);
|
|
70
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
71
|
+
continue;
|
|
72
|
+
out.push({ pid, cmdline: m[2].replace(/""/g, '"').slice(0, CMDLINE_CAPTURE_LEN) });
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
// POSIX
|
|
77
|
+
const ps = execSync('ps -ww -eo pid,command', { encoding: 'utf-8', timeout: SCAN_TIMEOUT_MS_POSIX });
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const line of ps.split(/\r?\n/)) {
|
|
80
|
+
const m = line.trim().match(/^(\d+)\s+(.*)$/);
|
|
81
|
+
if (!m)
|
|
82
|
+
continue;
|
|
83
|
+
const cmd = m[2];
|
|
84
|
+
if (!/\bnode\b/.test(cmd))
|
|
85
|
+
continue;
|
|
86
|
+
const pid = parseInt(m[1], 10);
|
|
87
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
88
|
+
continue;
|
|
89
|
+
out.push({ pid, cmdline: cmd.slice(0, CMDLINE_CAPTURE_LEN) });
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function formatCmdline(raw, max = 80) {
|
|
98
|
+
const cleaned = raw.replace(/^"?[^"\s]*node(?:\.exe)?"?\s+/i, '').trim();
|
|
99
|
+
return cleaned.length > max ? cleaned.slice(0, max - 1) + '…' : cleaned;
|
|
100
|
+
}
|
|
101
|
+
export function findForeignWriters(procs, daemonPid, trackedPids, selfPid) {
|
|
102
|
+
const out = [];
|
|
103
|
+
for (const p of procs) {
|
|
104
|
+
if (p.pid === selfPid)
|
|
105
|
+
continue;
|
|
106
|
+
if (p.pid === daemonPid)
|
|
107
|
+
continue;
|
|
108
|
+
if (trackedPids.has(p.pid))
|
|
109
|
+
continue;
|
|
110
|
+
for (const re of FOREIGN_WRITER_PATTERNS) {
|
|
111
|
+
const m = p.cmdline.match(re);
|
|
112
|
+
if (m) {
|
|
113
|
+
out.push({ ...p, matchedPattern: m[0] });
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Pass: daemon down (single-writer invariant has nothing to enforce) OR no
|
|
122
|
+
* foreign writers detected.
|
|
123
|
+
*
|
|
124
|
+
* Fail: daemon owns the lock AND a non-daemon, non-tracked node process is
|
|
125
|
+
* running a known cross-process writer. Lists every offender so the user can
|
|
126
|
+
* SIGKILL them by PID.
|
|
127
|
+
*/
|
|
128
|
+
export async function checkWritersAudit(cwd = process.cwd()) {
|
|
129
|
+
const name = 'Writers Audit';
|
|
130
|
+
try {
|
|
131
|
+
const daemonPid = getDaemonLockHolder(cwd);
|
|
132
|
+
if (daemonPid === null) {
|
|
133
|
+
return {
|
|
134
|
+
name,
|
|
135
|
+
status: 'pass',
|
|
136
|
+
message: 'Daemon not running — single-writer invariant trivially satisfied',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const procs = enumerateNodeProcesses();
|
|
140
|
+
const trackedPids = readTrackedBackgroundPids(cwd);
|
|
141
|
+
const foreign = findForeignWriters(procs, daemonPid, trackedPids, process.pid);
|
|
142
|
+
if (foreign.length === 0) {
|
|
143
|
+
return {
|
|
144
|
+
name,
|
|
145
|
+
status: 'pass',
|
|
146
|
+
message: `Daemon PID ${daemonPid} is sole writer; no foreign writers detected`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const detail = foreign
|
|
150
|
+
.map((p) => `pid=${p.pid} (${p.matchedPattern}): ${formatCmdline(p.cmdline)}`)
|
|
151
|
+
.join(' | ');
|
|
152
|
+
return {
|
|
153
|
+
name,
|
|
154
|
+
status: 'fail',
|
|
155
|
+
message: `${foreign.length} non-daemon writer(s) running concurrently with daemon (PID ${daemonPid}): ${detail}. ` +
|
|
156
|
+
`Each will clobber the daemon's sql.js snapshot on flush — #1054 bug class.`,
|
|
157
|
+
fix: process.platform === 'win32'
|
|
158
|
+
? `taskkill /F /PID ${foreign.map((p) => p.pid).join(' /PID ')}`
|
|
159
|
+
: `kill ${foreign.map((p) => p.pid).join(' ')}`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
return {
|
|
164
|
+
name,
|
|
165
|
+
status: 'warn',
|
|
166
|
+
message: `Unable to audit writers: ${errorDetail(e, { firstLineOnly: true })}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=doctor-checks-writers-audit.js.map
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
25
25
|
import { existsSync } from 'fs';
|
|
26
26
|
import { CANONICAL_EMBEDDING_MODEL } from '../embeddings/migration/types.js';
|
|
27
|
-
import { mofloImport } from '../services/moflo-require.js';
|
|
28
27
|
import { memoryDbCandidatePaths } from '../services/moflo-paths.js';
|
|
28
|
+
import { openDaemonDatabase } from '../memory/daemon-backend.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
|
|
@@ -59,7 +59,7 @@ export async function checkEmbeddingHygiene() {
|
|
|
59
59
|
return {
|
|
60
60
|
name: 'Embedding hygiene',
|
|
61
61
|
status: 'pass',
|
|
62
|
-
message: 'Cannot inspect memory DB (
|
|
62
|
+
message: 'Cannot inspect memory DB (open failed)',
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
if (groups.length === 0) {
|
|
@@ -127,21 +127,9 @@ function resolveMemoryDb() {
|
|
|
127
127
|
return memoryDbCandidatePaths(process.cwd()).find((p) => existsSync(p)) ?? null;
|
|
128
128
|
}
|
|
129
129
|
async function loadModelGroups(dbPath) {
|
|
130
|
-
const fs = await import('fs');
|
|
131
|
-
let initSqlJs;
|
|
132
|
-
try {
|
|
133
|
-
initSqlJs = (await mofloImport('sql.js'))?.default;
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
if (!initSqlJs)
|
|
139
|
-
return null;
|
|
140
130
|
let db;
|
|
141
131
|
try {
|
|
142
|
-
|
|
143
|
-
const buffer = fs.readFileSync(dbPath);
|
|
144
|
-
db = new SQL.Database(buffer);
|
|
132
|
+
db = openDaemonDatabase(dbPath);
|
|
145
133
|
}
|
|
146
134
|
catch {
|
|
147
135
|
return null;
|
|
@@ -169,6 +169,36 @@ export async function autoFixCheck(check) {
|
|
|
169
169
|
catch { /* best effort */ }
|
|
170
170
|
return runFixCommand('npx moflo daemon start');
|
|
171
171
|
},
|
|
172
|
+
// Epic #1054.S5 / #1059 — SIGTERM the stale daemon and let the launcher's
|
|
173
|
+
// existing respawn path (mirrored as `npx moflo daemon start`) pick up the
|
|
174
|
+
// installed-version code. Mirrors `recycleDaemon` in
|
|
175
|
+
// bin/session-start-launcher.mjs so the auto-fix matches the launcher's
|
|
176
|
+
// behavior exactly.
|
|
177
|
+
'Daemon Version Skew': async () => {
|
|
178
|
+
const cwd = process.cwd();
|
|
179
|
+
const { getDaemonLockPayload } = await import('../services/daemon-lock.js');
|
|
180
|
+
const payload = getDaemonLockPayload(cwd);
|
|
181
|
+
if (payload?.pid && payload.pid > 0) {
|
|
182
|
+
try {
|
|
183
|
+
process.kill(payload.pid, 'SIGTERM');
|
|
184
|
+
}
|
|
185
|
+
catch { /* already dead */ }
|
|
186
|
+
}
|
|
187
|
+
const lockFile = join(cwd, '.moflo', 'daemon.lock');
|
|
188
|
+
try {
|
|
189
|
+
if (existsSync(lockFile))
|
|
190
|
+
unlinkSync(lockFile);
|
|
191
|
+
}
|
|
192
|
+
catch { /* ok */ }
|
|
193
|
+
return runFixCommand('npx moflo daemon start');
|
|
194
|
+
},
|
|
195
|
+
'Embedding Coverage Truth': async () => {
|
|
196
|
+
// Same as the existing Embeddings fix — rebuild the cache by re-running
|
|
197
|
+
// the embeddings pipeline. Routes through `npx moflo` so the consumer
|
|
198
|
+
// CLI resolution stays consistent across platforms (see
|
|
199
|
+
// feedback_cross_platform_mandatory).
|
|
200
|
+
return runFixCommand('npx moflo embeddings init --force');
|
|
201
|
+
},
|
|
172
202
|
'MCP Servers': async () => {
|
|
173
203
|
return runFixCommand('claude mcp add moflo -- npx -y moflo mcp start');
|
|
174
204
|
},
|
|
@@ -213,6 +243,63 @@ export async function autoFixCheck(check) {
|
|
|
213
243
|
return false;
|
|
214
244
|
}
|
|
215
245
|
},
|
|
246
|
+
// Tiered recovery for `.moflo/moflo.db` corruption (REINDEX → VACUUM
|
|
247
|
+
// INTO → row-level salvage). The TS service stops the daemon
|
|
248
|
+
// automatically (cross-platform via `process.kill('SIGTERM')`) so the
|
|
249
|
+
// atomic swap doesn't race a live writer; we restart it via the
|
|
250
|
+
// existing `npx moflo daemon start` shorthand after. The MCP server,
|
|
251
|
+
// started by Claude Code outside our process tree, isn't stopped here —
|
|
252
|
+
// explicit user guidance covers that case at the end.
|
|
253
|
+
'Memory DB Integrity': async () => {
|
|
254
|
+
try {
|
|
255
|
+
const { repairMemoryDbIntegrity } = await import('../services/memory-db-integrity-repair.js');
|
|
256
|
+
const result = await repairMemoryDbIntegrity(process.cwd());
|
|
257
|
+
if (result.repaired) {
|
|
258
|
+
const tierLabel = result.tier === 'reindex' ? 'REINDEX (index rebuild)'
|
|
259
|
+
: result.tier === 'vacuum' ? 'VACUUM INTO (fresh-file rebuild)'
|
|
260
|
+
: result.tier === 'salvage' ? 'row-level salvage'
|
|
261
|
+
: 'repaired';
|
|
262
|
+
output.writeln(output.dim(` Recovered via ${tierLabel}.`));
|
|
263
|
+
if (result.corruptBackup) {
|
|
264
|
+
output.writeln(output.dim(` Pre-repair backup retained: ${result.corruptBackup}`));
|
|
265
|
+
}
|
|
266
|
+
if (result.lossStats) {
|
|
267
|
+
for (const [tbl, s] of Object.entries(result.lossStats)) {
|
|
268
|
+
if (s.read > 0) {
|
|
269
|
+
const lost = Math.max(0, s.read - s.written);
|
|
270
|
+
if (lost > 0) {
|
|
271
|
+
output.writeln(output.warning(` ${tbl}: ${s.written}/${s.read} rows preserved (lost ${lost} across ${s.errors} unreadable chunk(s))`));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
output.writeln(output.dim(' Embeddings for lost rows will be regenerated on next index pass — run `npx moflo embeddings init` to force.'));
|
|
276
|
+
}
|
|
277
|
+
// Restart the daemon if we stopped it. The launcher's own
|
|
278
|
+
// section-4 spawn handles this on next session-start, but a
|
|
279
|
+
// mid-session healer call shouldn't leave the daemon down.
|
|
280
|
+
if (result.daemonStopped) {
|
|
281
|
+
output.writeln(output.dim(' Restarting daemon...'));
|
|
282
|
+
await runFixCommand('npx moflo daemon start');
|
|
283
|
+
}
|
|
284
|
+
// Cross-platform note for the MCP server (out-of-tree, can't
|
|
285
|
+
// SIGTERM). On Windows the swap would have failed if MCP was
|
|
286
|
+
// holding the file; on POSIX the swap succeeds but MCP keeps
|
|
287
|
+
// reading the stale inode until restart. Either way: restart
|
|
288
|
+
// Claude Code to fully apply.
|
|
289
|
+
output.writeln(output.dim(' Restart Claude Code so the MCP server re-opens the recovered DB.'));
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
if (result.persistent) {
|
|
293
|
+
output.writeln(output.warning(' Corruption survived every recovery tier. Manual options: ' +
|
|
294
|
+
'`npx moflo memory rebuild-index` (destructive) or restore from a known-good backup.'));
|
|
295
|
+
}
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
catch (e) {
|
|
299
|
+
output.writeln(output.warning(` Repair failed: ${errorDetail(e)}`));
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
},
|
|
216
303
|
'Status Line': async () => {
|
|
217
304
|
const settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
|
218
305
|
if (!existsSync(settingsPath))
|
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { checkSubagentHealth, checkSpellExecution, checkMcpToolInvocation, checkHookExecution, checkMcpSpellIntegration, checkGateHealth, checkHookBlockDrift, checkMofloDbBridge, } from './doctor-checks-deep.js';
|
|
8
8
|
import { checkEmbeddingHygiene } from './doctor-embedding-hygiene.js';
|
|
9
|
+
import { checkDaemonVersionSkew } from './doctor-checks-version-skew.js';
|
|
10
|
+
import { checkEmbeddingCoverageTruth } from './doctor-checks-coverage-truth.js';
|
|
11
|
+
import { checkWritersAudit } from './doctor-checks-writers-audit.js';
|
|
9
12
|
import { checkSwarmFunctional, checkHiveMindFunctional, } from './doctor-checks-swarm.js';
|
|
10
13
|
import { checkMemoryAccessFunctional } from './doctor-checks-memory-access.js';
|
|
11
14
|
import { checkBuildTools, checkClaudeCode, checkDiskSpace, checkGit, checkGitRepo, checkNodeVersion, checkNpmVersion, } from './doctor-checks-runtime.js';
|
|
12
|
-
import { checkConfigFile, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMofloYamlCompliance, checkStatusLine, checkTestDirs, } from './doctor-checks-config.js';
|
|
15
|
+
import { checkConfigFile, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMofloYamlCompliance, checkStatusLine, checkTestDirs, } from './doctor-checks-config.js';
|
|
13
16
|
import { checkSpellEngine, checkSandboxTier } from './doctor-checks-platform.js';
|
|
14
17
|
import { checkEmbeddings, checkSemanticQuality, } from './doctor-checks-memory.js';
|
|
15
18
|
import { checkIntelligence } from './doctor-checks-intelligence.js';
|
|
@@ -33,10 +36,19 @@ export const allChecks = [
|
|
|
33
36
|
checkMofloYamlCompliance,
|
|
34
37
|
checkStatusLine,
|
|
35
38
|
checkDaemonStatus,
|
|
39
|
+
checkDaemonVersionSkew,
|
|
36
40
|
checkDaemonWriteRouting,
|
|
41
|
+
checkWritersAudit,
|
|
37
42
|
checkMemoryDatabase,
|
|
43
|
+
// Owns the corruption signal so downstream checks (Embeddings, Semantic
|
|
44
|
+
// Quality, Memory Access Functional) don't surface it as the synthetic
|
|
45
|
+
// "Check" failure (doctor.ts:214). MUST run after checkMemoryDatabase
|
|
46
|
+
// (which confirms the file exists) and before any check that opens the
|
|
47
|
+
// DB via openBackend.
|
|
48
|
+
checkMemoryDbIntegrity,
|
|
38
49
|
checkEmbeddings,
|
|
39
50
|
checkEmbeddingHygiene,
|
|
51
|
+
checkEmbeddingCoverageTruth,
|
|
40
52
|
checkTestDirs,
|
|
41
53
|
checkMcpServers,
|
|
42
54
|
checkDiskSpace,
|
|
@@ -77,11 +89,22 @@ export const componentMap = {
|
|
|
77
89
|
'statusline': checkStatusLine,
|
|
78
90
|
'status-line': checkStatusLine,
|
|
79
91
|
'daemon': checkDaemonStatus,
|
|
92
|
+
'daemon-version-skew': checkDaemonVersionSkew,
|
|
93
|
+
'version-skew': checkDaemonVersionSkew,
|
|
94
|
+
'skew': checkDaemonVersionSkew,
|
|
80
95
|
'daemon-write-routing': checkDaemonWriteRouting,
|
|
81
96
|
'write-routing': checkDaemonWriteRouting,
|
|
97
|
+
'writers-audit': checkWritersAudit,
|
|
98
|
+
'writers': checkWritersAudit,
|
|
82
99
|
'memory': checkMemoryDatabase,
|
|
100
|
+
'memory-db-integrity': checkMemoryDbIntegrity,
|
|
101
|
+
'integrity': checkMemoryDbIntegrity,
|
|
102
|
+
'memory-integrity': checkMemoryDbIntegrity,
|
|
83
103
|
'embeddings': checkEmbeddings,
|
|
84
104
|
'embedding-hygiene': checkEmbeddingHygiene,
|
|
105
|
+
'embedding-coverage': checkEmbeddingCoverageTruth,
|
|
106
|
+
'coverage': checkEmbeddingCoverageTruth,
|
|
107
|
+
'coverage-truth': checkEmbeddingCoverageTruth,
|
|
85
108
|
'hygiene': checkEmbeddingHygiene,
|
|
86
109
|
'git': checkGit,
|
|
87
110
|
'mcp': checkMcpServers,
|
|
@@ -40,7 +40,7 @@ export const doctorCommand = {
|
|
|
40
40
|
{
|
|
41
41
|
name: 'component',
|
|
42
42
|
short: 'c',
|
|
43
|
-
description: 'Check specific component (version, node, npm, config, daemon, memory, embeddings, git, mcp, claude, disk, typescript, semantic, intelligence, swarm, hive-mind)',
|
|
43
|
+
description: 'Check specific component (version, version-skew, node, npm, config, daemon, writers-audit, memory, embeddings, coverage-truth, git, mcp, claude, disk, typescript, semantic, intelligence, swarm, hive-mind)',
|
|
44
44
|
type: 'string',
|
|
45
45
|
},
|
|
46
46
|
{
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
* Created with ❤️ by motailz.com
|
|
14
14
|
*/
|
|
15
15
|
import { output } from '../output.js';
|
|
16
|
-
import { mofloImport } from '../services/moflo-require.js';
|
|
17
16
|
import { runEmbeddingsMigrationIfNeeded } from '../services/embeddings-migration.js';
|
|
18
17
|
import { memoryDbPath, MEMORY_DB_FILE, MOFLO_DIR } from '../services/moflo-paths.js';
|
|
19
18
|
import * as embeddings from '../embeddings/index.js';
|
|
19
|
+
import { openDaemonDatabase } from '../memory/daemon-backend.js';
|
|
20
20
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
21
21
|
const DEFAULT_DB_PATH_FLAG = `${MOFLO_DIR}/${MEMORY_DB_FILE}`;
|
|
22
22
|
// Generate subcommand - REAL implementation
|
|
@@ -90,7 +90,7 @@ const generateCommand = {
|
|
|
90
90
|
}
|
|
91
91
|
},
|
|
92
92
|
};
|
|
93
|
-
// Search subcommand - REAL implementation using
|
|
93
|
+
// Search subcommand - REAL implementation using node:sqlite via openDaemonDatabase
|
|
94
94
|
const searchCommand = {
|
|
95
95
|
name: 'search',
|
|
96
96
|
description: 'Semantic similarity search',
|
|
@@ -131,11 +131,8 @@ const searchCommand = {
|
|
|
131
131
|
output.printInfo('Run: claude-flow memory init');
|
|
132
132
|
return { success: false, exitCode: 1 };
|
|
133
133
|
}
|
|
134
|
-
//
|
|
135
|
-
const
|
|
136
|
-
const SQL = await initSqlJs();
|
|
137
|
-
const fileBuffer = fs.readFileSync(fullDbPath);
|
|
138
|
-
const db = new SQL.Database(fileBuffer);
|
|
134
|
+
// node:sqlite via the unified factory (Phase 5 / #1084).
|
|
135
|
+
const db = openDaemonDatabase(fullDbPath);
|
|
139
136
|
const startTime = Date.now();
|
|
140
137
|
// Generate embedding for query
|
|
141
138
|
const { generateEmbedding } = await import('../memory/memory-initializer.js');
|
|
@@ -343,7 +340,7 @@ const compareCommand = {
|
|
|
343
340
|
}
|
|
344
341
|
},
|
|
345
342
|
};
|
|
346
|
-
// Collections subcommand - REAL implementation using
|
|
343
|
+
// Collections subcommand - REAL implementation using node:sqlite via openDaemonDatabase
|
|
347
344
|
const collectionsCommand = {
|
|
348
345
|
name: 'collections',
|
|
349
346
|
description: 'Manage embedding collections (namespaces)',
|
|
@@ -374,11 +371,8 @@ const collectionsCommand = {
|
|
|
374
371
|
output.writeln(output.dim('No collections yet - initialize memory first'));
|
|
375
372
|
return { success: true, data: [] };
|
|
376
373
|
}
|
|
377
|
-
//
|
|
378
|
-
const
|
|
379
|
-
const SQL = await initSqlJs();
|
|
380
|
-
const fileBuffer = fs.readFileSync(fullDbPath);
|
|
381
|
-
const db = new SQL.Database(fileBuffer);
|
|
374
|
+
// node:sqlite via the unified factory (Phase 5 / #1084).
|
|
375
|
+
const db = openDaemonDatabase(fullDbPath);
|
|
382
376
|
// Get collection stats from database
|
|
383
377
|
const statsQuery = db.exec(`
|
|
384
378
|
SELECT
|
|
@@ -1159,17 +1153,18 @@ const cacheCommand = {
|
|
|
1159
1153
|
else {
|
|
1160
1154
|
sqliteSize = `${sizeBytes} B`;
|
|
1161
1155
|
}
|
|
1162
|
-
// Try to count real entries via
|
|
1156
|
+
// Try to count real entries via node:sqlite
|
|
1163
1157
|
try {
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1158
|
+
const db = openDaemonDatabase(resolvedDbPath);
|
|
1159
|
+
try {
|
|
1160
|
+
const result = db.exec('SELECT COUNT(*) as count FROM embeddings');
|
|
1161
|
+
if (result.length > 0 && result[0].values.length > 0) {
|
|
1162
|
+
sqliteEntries = result[0].values[0][0];
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
finally {
|
|
1166
|
+
db.close();
|
|
1171
1167
|
}
|
|
1172
|
-
db.close();
|
|
1173
1168
|
}
|
|
1174
1169
|
catch {
|
|
1175
1170
|
// Estimate entries from file size (~1600 bytes per entry for 384-dim embeddings)
|
|
@@ -7,8 +7,7 @@ import * as pathModule from 'path';
|
|
|
7
7
|
import { output } from '../output.js';
|
|
8
8
|
import { select, confirm, input } from '../prompt.js';
|
|
9
9
|
import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
10
|
-
import {
|
|
11
|
-
import { atomicWriteFileSync } from '../services/atomic-file-write.js';
|
|
10
|
+
import { openDaemonDatabase } from '../memory/daemon-backend.js';
|
|
12
11
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
13
12
|
// Memory backends
|
|
14
13
|
const BACKENDS = [
|
|
@@ -1265,23 +1264,10 @@ const initMemoryCommand = {
|
|
|
1265
1264
|
const DB_FILENAME = 'memory.db';
|
|
1266
1265
|
const SWARM_DIR = '.swarm';
|
|
1267
1266
|
async function openDb(cwd) {
|
|
1268
|
-
const fs = await import('fs');
|
|
1269
1267
|
const path = await import('path');
|
|
1270
|
-
const initSqlJs = (await mofloImport('sql.js')).default;
|
|
1271
|
-
const SQL = await initSqlJs();
|
|
1272
1268
|
const dbPath = path.join(cwd, SWARM_DIR, DB_FILENAME);
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1276
|
-
}
|
|
1277
|
-
let db;
|
|
1278
|
-
if (fs.existsSync(dbPath)) {
|
|
1279
|
-
const buffer = fs.readFileSync(dbPath);
|
|
1280
|
-
db = new SQL.Database(buffer);
|
|
1281
|
-
}
|
|
1282
|
-
else {
|
|
1283
|
-
db = new SQL.Database();
|
|
1284
|
-
}
|
|
1269
|
+
// openDaemonDatabase ensures the parent directory exists and applies WAL.
|
|
1270
|
+
const db = openDaemonDatabase(dbPath);
|
|
1285
1271
|
// Ensure table exists
|
|
1286
1272
|
db.run(`
|
|
1287
1273
|
CREATE TABLE IF NOT EXISTS memory_entries (
|
|
@@ -1307,10 +1293,14 @@ async function openDb(cwd) {
|
|
|
1307
1293
|
`);
|
|
1308
1294
|
db.run(`CREATE INDEX IF NOT EXISTS idx_memory_key_ns ON memory_entries(key, namespace)`);
|
|
1309
1295
|
db.run(`CREATE INDEX IF NOT EXISTS idx_memory_namespace ON memory_entries(namespace)`);
|
|
1310
|
-
return { db, dbPath
|
|
1296
|
+
return { db, dbPath };
|
|
1311
1297
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1298
|
+
/**
|
|
1299
|
+
* Close the DB handle. node:sqlite + WAL has already persisted every prior
|
|
1300
|
+
* `db.run` incrementally — the explicit atomicWriteFileSync sql.js used to
|
|
1301
|
+
* need is gone (Phase 5 / #1084).
|
|
1302
|
+
*/
|
|
1303
|
+
function saveAndCloseDb(db, _dbPath) {
|
|
1314
1304
|
db.close();
|
|
1315
1305
|
}
|
|
1316
1306
|
function batchGenerateId() {
|
|
@@ -1937,9 +1927,9 @@ const rebuildIndexCommand = {
|
|
|
1937
1927
|
}
|
|
1938
1928
|
failed++;
|
|
1939
1929
|
}
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1930
|
+
// node:sqlite + WAL persists each db.run incrementally — the
|
|
1931
|
+
// periodic batch flush sql.js needed here was the export-+-rewrite
|
|
1932
|
+
// pattern Phase 5 (#1084) killed. No flush needed.
|
|
1943
1933
|
}
|
|
1944
1934
|
const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
1945
1935
|
// Final stats
|