claude-flow 3.21.0 → 3.22.0
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/package.json +1 -1
- package/v3/@claude-flow/cli/dist/src/commands/daemon.js +23 -2
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +56 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory-distill.d.ts +27 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory-distill.js +374 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +4 -2
- package/v3/@claude-flow/cli/dist/src/index.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/index.js +22 -1
- package/v3/@claude-flow/cli/dist/src/init/executor.js +37 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +19 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +175 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-signing.d.ts +30 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-signing.js +60 -0
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.d.ts +16 -0
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +89 -8
- package/v3/@claude-flow/cli/dist/src/init/memory-package-resolver.d.ts +53 -0
- package/v3/@claude-flow/cli/dist/src/init/memory-package-resolver.js +118 -0
- package/v3/@claude-flow/cli/dist/src/init/statusline-generator.js +18 -7
- package/v3/@claude-flow/cli/dist/src/mcp-client.js +13 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.js +9 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.d.ts +162 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.js +548 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.d.ts +9 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.js +4 -1
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +115 -54
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +71 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +330 -2
- package/v3/@claude-flow/cli/dist/src/services/distill-tuning.d.ts +111 -0
- package/v3/@claude-flow/cli/dist/src/services/distill-tuning.js +510 -0
- package/v3/@claude-flow/cli/dist/src/services/memory-distillation.d.ts +41 -0
- package/v3/@claude-flow/cli/dist/src/services/memory-distillation.js +277 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +22 -1
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +117 -6
- package/v3/@claude-flow/cli/package.json +5 -2
|
@@ -1127,6 +1127,329 @@ async function activateControllerRegistry(dbPath, verbose) {
|
|
|
1127
1127
|
}
|
|
1128
1128
|
return { activated, failed, initTimeMs: performance.now() - startTime };
|
|
1129
1129
|
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Self-heal an EXISTING memory database that is missing the `vector_indexes`
|
|
1132
|
+
* table or per-namespace rows.
|
|
1133
|
+
*
|
|
1134
|
+
* Why this exists: fresh installs create `vector_indexes` + seed rows, but a
|
|
1135
|
+
* DB written by an older CLI or by agentdb directly may have thousands of
|
|
1136
|
+
* embedded rows in `memory_entries` and NO `vector_indexes` table at all.
|
|
1137
|
+
* Two things break as a result:
|
|
1138
|
+
* 1. The statusline's vector count read collapsed to `0` (the count query
|
|
1139
|
+
* referenced the missing table and failed whole — now split, but the
|
|
1140
|
+
* HNSW flag still needs the table).
|
|
1141
|
+
* 2. #1941 — `memory_search` routes per namespace via `vector_indexes`; a
|
|
1142
|
+
* namespace with no row returns 0 results even when entries exist.
|
|
1143
|
+
*
|
|
1144
|
+
* This is idempotent and conservative:
|
|
1145
|
+
* - Does NOTHING (no writes) when the table already exists and every embedded
|
|
1146
|
+
* namespace already has a row — the common already-healed path, hit on
|
|
1147
|
+
* every MCP start, must not write to the live DB unnecessarily.
|
|
1148
|
+
* - Before ANY write, runs `PRAGMA quick_check`; if the DB reports structural
|
|
1149
|
+
* corruption it SKIPS the repair entirely (returns `corrupt:true`) rather
|
|
1150
|
+
* than writing into a malformed btree and risking making it worse. The
|
|
1151
|
+
* caller/user should recover via `sqlite3 old.db .recover | sqlite3 new.db`.
|
|
1152
|
+
* - Does NOT checkpoint. mode=ro readers already see committed WAL frames, and
|
|
1153
|
+
* forcing a checkpoint on a DB with a torn WAL could persist latent damage.
|
|
1154
|
+
*
|
|
1155
|
+
* When a repair IS needed and the DB is healthy: creates the table if absent,
|
|
1156
|
+
* seeds the fresh-install default rows, and backfills an accurate
|
|
1157
|
+
* `total_vectors` per namespace. Runs on the existing-DB path of
|
|
1158
|
+
* `initializeMemoryDatabase` (MCP start / `memory init`) and from `ruflo init`.
|
|
1159
|
+
*
|
|
1160
|
+
* Uses better-sqlite3 (WAL-safe, native). If the native module is unavailable
|
|
1161
|
+
* it is a silent no-op — the split statusline query already prevents the count
|
|
1162
|
+
* from zeroing; only the HNSW flag and namespace routing stay degraded.
|
|
1163
|
+
*/
|
|
1164
|
+
/**
|
|
1165
|
+
* Auto-recover a structurally-corrupt memory DB into a clean one, universally
|
|
1166
|
+
* (better-sqlite3 only — no dependency on the external `sqlite3` CLI, which is
|
|
1167
|
+
* absent on many npx hosts). Safe by construction:
|
|
1168
|
+
* 1. Confirms corruption (quick_check) — no-op on a healthy DB.
|
|
1169
|
+
* 2. Acquires an EXCLUSIVE lock (BEGIN IMMEDIATE). If another process is
|
|
1170
|
+
* writing, it SKIPS (returns reason:'writer-active') rather than racing a
|
|
1171
|
+
* writer and losing its in-flight writes — the mistake that must not recur.
|
|
1172
|
+
* 3. Rebuilds a fresh DB table-by-table (schema + rows), skipping any single
|
|
1173
|
+
* table whose pages won't scan so one bad table can't abort the whole
|
|
1174
|
+
* rebuild.
|
|
1175
|
+
* 4. VERIFIES the rebuild (integrity_check == ok AND recovered
|
|
1176
|
+
* memory_entries count >= the readable source count) BEFORE touching the
|
|
1177
|
+
* original.
|
|
1178
|
+
* 5. Backs up the corrupt DB to `<db>.corrupt-<ts>.bak`, then atomically
|
|
1179
|
+
* renames the verified rebuild into place and drops stale -wal/-shm.
|
|
1180
|
+
* On any failure the original + backup are left intact — never destructive.
|
|
1181
|
+
*/
|
|
1182
|
+
export async function recoverMemoryDatabase(dbPath, opts = {}) {
|
|
1183
|
+
if (!dbPath || !fs.existsSync(dbPath))
|
|
1184
|
+
return { recovered: false, reason: 'no-db' };
|
|
1185
|
+
let Database;
|
|
1186
|
+
try {
|
|
1187
|
+
// Module name behind a variable so TS does not statically resolve the
|
|
1188
|
+
// optional native dep's types at build time (CI may not install them).
|
|
1189
|
+
const mod = 'better-sqlite3';
|
|
1190
|
+
Database = (await import(mod)).default;
|
|
1191
|
+
}
|
|
1192
|
+
catch {
|
|
1193
|
+
return { recovered: false, reason: 'no-native' };
|
|
1194
|
+
}
|
|
1195
|
+
const ts = Date.now();
|
|
1196
|
+
const tmpPath = `${dbPath}.recovering-${ts}`;
|
|
1197
|
+
const bakPath = `${dbPath}.corrupt-${ts}.bak`;
|
|
1198
|
+
let src;
|
|
1199
|
+
let dst;
|
|
1200
|
+
try {
|
|
1201
|
+
src = new Database(dbPath, { timeout: 1500 });
|
|
1202
|
+
// Confirm corruption — never rewrite a healthy DB.
|
|
1203
|
+
const qc = src.prepare('PRAGMA quick_check(1)').get();
|
|
1204
|
+
const qcVal = qc ? String(Object.values(qc)[0] ?? '') : '';
|
|
1205
|
+
if (qcVal.toLowerCase() === 'ok') {
|
|
1206
|
+
src.close();
|
|
1207
|
+
return { recovered: false, reason: 'not-corrupt' };
|
|
1208
|
+
}
|
|
1209
|
+
// Exclusive-writer guard: acquire the write lock. If busy, another process
|
|
1210
|
+
// is writing — do NOT race it. Reading within this txn is still allowed.
|
|
1211
|
+
try {
|
|
1212
|
+
src.exec('BEGIN IMMEDIATE');
|
|
1213
|
+
}
|
|
1214
|
+
catch {
|
|
1215
|
+
src.close();
|
|
1216
|
+
return { recovered: false, reason: 'writer-active' };
|
|
1217
|
+
}
|
|
1218
|
+
let srcRows = 0;
|
|
1219
|
+
try {
|
|
1220
|
+
srcRows = src.prepare('SELECT COUNT(*) AS c FROM memory_entries').get()?.c ?? 0;
|
|
1221
|
+
}
|
|
1222
|
+
catch { /* unreadable */ }
|
|
1223
|
+
try {
|
|
1224
|
+
fs.rmSync(tmpPath, { force: true });
|
|
1225
|
+
}
|
|
1226
|
+
catch { /* fresh */ }
|
|
1227
|
+
dst = new Database(tmpPath);
|
|
1228
|
+
// Copy schema: tables first (so data can be inserted), then indexes/triggers.
|
|
1229
|
+
const objects = src
|
|
1230
|
+
.prepare("SELECT type, name, sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'")
|
|
1231
|
+
.all();
|
|
1232
|
+
const tables = objects.filter(o => o.type === 'table');
|
|
1233
|
+
const others = objects.filter(o => o.type !== 'table');
|
|
1234
|
+
for (const t of tables) {
|
|
1235
|
+
try {
|
|
1236
|
+
dst.exec(t.sql);
|
|
1237
|
+
}
|
|
1238
|
+
catch { /* skip an untranslatable table def */ }
|
|
1239
|
+
}
|
|
1240
|
+
// Copy rows table-by-table; a table whose pages won't scan is skipped whole.
|
|
1241
|
+
let copiedEntries = 0;
|
|
1242
|
+
for (const t of tables) {
|
|
1243
|
+
try {
|
|
1244
|
+
const cols = dst.prepare(`PRAGMA table_info("${t.name}")`).all().map(c => c.name);
|
|
1245
|
+
if (!cols.length)
|
|
1246
|
+
continue;
|
|
1247
|
+
const colList = cols.map(c => `"${c}"`).join(',');
|
|
1248
|
+
const placeholders = cols.map(() => '?').join(',');
|
|
1249
|
+
const insert = dst.prepare(`INSERT OR IGNORE INTO "${t.name}" (${colList}) VALUES (${placeholders})`);
|
|
1250
|
+
const rows = src.prepare(`SELECT ${colList} FROM "${t.name}"`).all();
|
|
1251
|
+
const runAll = dst.transaction((rs) => {
|
|
1252
|
+
for (const r of rs)
|
|
1253
|
+
insert.run(cols.map(c => r[c]));
|
|
1254
|
+
});
|
|
1255
|
+
runAll(rows);
|
|
1256
|
+
if (t.name === 'memory_entries')
|
|
1257
|
+
copiedEntries = rows.length;
|
|
1258
|
+
}
|
|
1259
|
+
catch { /* skip a table whose data pages are unreadable */ }
|
|
1260
|
+
}
|
|
1261
|
+
for (const o of others) {
|
|
1262
|
+
try {
|
|
1263
|
+
dst.exec(o.sql);
|
|
1264
|
+
}
|
|
1265
|
+
catch { /* an index over corrupt data — non-fatal */ }
|
|
1266
|
+
}
|
|
1267
|
+
dst.close();
|
|
1268
|
+
dst = null;
|
|
1269
|
+
try {
|
|
1270
|
+
src.exec('ROLLBACK');
|
|
1271
|
+
}
|
|
1272
|
+
catch { /* ignore */ }
|
|
1273
|
+
src.close();
|
|
1274
|
+
src = null;
|
|
1275
|
+
// Verify BEFORE touching the original.
|
|
1276
|
+
const check = new Database(tmpPath, { readonly: true });
|
|
1277
|
+
const integ = String(check.pragma('integrity_check', { simple: true }) ?? '');
|
|
1278
|
+
let dstRows = 0;
|
|
1279
|
+
try {
|
|
1280
|
+
dstRows = check.prepare('SELECT COUNT(*) AS c FROM memory_entries').get()?.c ?? 0;
|
|
1281
|
+
}
|
|
1282
|
+
catch { /* */ }
|
|
1283
|
+
check.close();
|
|
1284
|
+
if (integ.toLowerCase() !== 'ok' || dstRows < srcRows) {
|
|
1285
|
+
try {
|
|
1286
|
+
fs.rmSync(tmpPath, { force: true });
|
|
1287
|
+
}
|
|
1288
|
+
catch { /* */ }
|
|
1289
|
+
if (opts.verbose) {
|
|
1290
|
+
console.log(`memory DB auto-recovery aborted (integrity=${integ}, rows ${dstRows}/${srcRows}) — original untouched`);
|
|
1291
|
+
}
|
|
1292
|
+
return { recovered: false, reason: 'verify-failed' };
|
|
1293
|
+
}
|
|
1294
|
+
// Back up the corrupt DB, then atomically swap in the verified rebuild.
|
|
1295
|
+
fs.copyFileSync(dbPath, bakPath);
|
|
1296
|
+
fs.renameSync(tmpPath, dbPath);
|
|
1297
|
+
for (const s of ['-wal', '-shm']) {
|
|
1298
|
+
try {
|
|
1299
|
+
fs.rmSync(`${dbPath}${s}`, { force: true });
|
|
1300
|
+
}
|
|
1301
|
+
catch { /* */ }
|
|
1302
|
+
}
|
|
1303
|
+
if (opts.verbose) {
|
|
1304
|
+
console.log(`memory DB auto-recovered: ${dstRows} rows, integrity ok. Corrupt original saved to ${bakPath}`);
|
|
1305
|
+
}
|
|
1306
|
+
return { recovered: true, backupPath: bakPath, rows: dstRows };
|
|
1307
|
+
}
|
|
1308
|
+
catch (e) {
|
|
1309
|
+
try {
|
|
1310
|
+
dst?.close();
|
|
1311
|
+
}
|
|
1312
|
+
catch { /* */ }
|
|
1313
|
+
try {
|
|
1314
|
+
src?.exec('ROLLBACK');
|
|
1315
|
+
}
|
|
1316
|
+
catch { /* */ }
|
|
1317
|
+
try {
|
|
1318
|
+
src?.close();
|
|
1319
|
+
}
|
|
1320
|
+
catch { /* */ }
|
|
1321
|
+
try {
|
|
1322
|
+
fs.rmSync(tmpPath, { force: true });
|
|
1323
|
+
}
|
|
1324
|
+
catch { /* */ }
|
|
1325
|
+
if (opts.verbose)
|
|
1326
|
+
console.log(`memory DB auto-recovery error: ${e?.message ?? e}`);
|
|
1327
|
+
return { recovered: false, reason: 'error' };
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
export async function repairVectorIndexes(dbPath, opts = {}) {
|
|
1331
|
+
const res = { repaired: false, tableCreated: false, namespaces: [], corrupt: false };
|
|
1332
|
+
if (!dbPath || !fs.existsSync(dbPath))
|
|
1333
|
+
return res;
|
|
1334
|
+
let Database;
|
|
1335
|
+
try {
|
|
1336
|
+
// Module name behind a variable so TS does not statically resolve the
|
|
1337
|
+
// optional native dep's types at build time (CI may not install them).
|
|
1338
|
+
const mod = 'better-sqlite3';
|
|
1339
|
+
Database = (await import(mod)).default;
|
|
1340
|
+
}
|
|
1341
|
+
catch {
|
|
1342
|
+
// Native module absent (e.g. WASM-only host). Statusline fix still covers
|
|
1343
|
+
// the display; nothing to repair here.
|
|
1344
|
+
return res;
|
|
1345
|
+
}
|
|
1346
|
+
let db;
|
|
1347
|
+
try {
|
|
1348
|
+
db = new Database(dbPath, { timeout: 3000 });
|
|
1349
|
+
const tableExists = (name) => (db.prepare("SELECT COUNT(*) AS c FROM sqlite_master WHERE type='table' AND name=?").get(name)?.c ?? 0) > 0;
|
|
1350
|
+
// Nothing to key off if there is no entries table.
|
|
1351
|
+
if (!tableExists('memory_entries')) {
|
|
1352
|
+
db.close();
|
|
1353
|
+
return res;
|
|
1354
|
+
}
|
|
1355
|
+
const hasVectorIndexes = tableExists('vector_indexes');
|
|
1356
|
+
// Namespaces that actually have embeddings.
|
|
1357
|
+
const nsRows = db
|
|
1358
|
+
.prepare("SELECT COALESCE(namespace, 'default') AS ns, COUNT(*) AS c " +
|
|
1359
|
+
'FROM memory_entries WHERE embedding IS NOT NULL GROUP BY ns')
|
|
1360
|
+
.all();
|
|
1361
|
+
// Which of those are already present in vector_indexes? If the table exists
|
|
1362
|
+
// and every embedded namespace already has a row, the DB is already healed
|
|
1363
|
+
// — return WITHOUT writing anything (common path on every MCP start).
|
|
1364
|
+
let needsWrite = !hasVectorIndexes;
|
|
1365
|
+
if (hasVectorIndexes) {
|
|
1366
|
+
const present = new Set(db.prepare('SELECT name FROM vector_indexes').all().map(r => r.name));
|
|
1367
|
+
needsWrite = nsRows.some(r => !present.has(String(r.ns || 'default')));
|
|
1368
|
+
}
|
|
1369
|
+
if (!needsWrite) {
|
|
1370
|
+
db.close();
|
|
1371
|
+
return res;
|
|
1372
|
+
}
|
|
1373
|
+
// A write is needed. GUARD: never write into a structurally corrupt DB —
|
|
1374
|
+
// that risks worsening the damage. quick_check is cheaper than a full
|
|
1375
|
+
// integrity_check and only runs on the rare repair path, not every start.
|
|
1376
|
+
const qc = db.prepare('PRAGMA quick_check(1)').get();
|
|
1377
|
+
const qcVal = qc ? String(Object.values(qc)[0] ?? '') : '';
|
|
1378
|
+
if (qcVal.toLowerCase() !== 'ok') {
|
|
1379
|
+
res.corrupt = true;
|
|
1380
|
+
db.close(); // release our handle before recovery may swap the file
|
|
1381
|
+
if (opts.autoRecover) {
|
|
1382
|
+
// Auto-fix: rebuild the corrupt DB (backup + verify + atomic swap), then
|
|
1383
|
+
// provision vector_indexes on the clean rebuild. This is what makes any
|
|
1384
|
+
// npx-deployed ruflo self-repair a corrupt memory DB on init / MCP start.
|
|
1385
|
+
const rec = await recoverMemoryDatabase(dbPath, { verbose: opts.verbose });
|
|
1386
|
+
if (rec.recovered) {
|
|
1387
|
+
const healed = await repairVectorIndexes(dbPath, { verbose: opts.verbose });
|
|
1388
|
+
return { ...healed, corrupt: true, recovered: true, backupPath: rec.backupPath };
|
|
1389
|
+
}
|
|
1390
|
+
if (opts.verbose) {
|
|
1391
|
+
console.log(`vector_indexes repair skipped — corruption (${qcVal}); auto-recovery not run (${rec.reason ?? 'unknown'})`);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
else if (opts.verbose) {
|
|
1395
|
+
console.log('vector_indexes repair SKIPPED — memory DB reports corruption (' + qcVal + '). ' +
|
|
1396
|
+
'Recover with: sqlite3 <db> .recover | sqlite3 <db>.recovered');
|
|
1397
|
+
}
|
|
1398
|
+
return res;
|
|
1399
|
+
}
|
|
1400
|
+
if (!hasVectorIndexes) {
|
|
1401
|
+
db.exec(`CREATE TABLE IF NOT EXISTS vector_indexes (
|
|
1402
|
+
id TEXT PRIMARY KEY,
|
|
1403
|
+
name TEXT NOT NULL UNIQUE,
|
|
1404
|
+
dimensions INTEGER NOT NULL,
|
|
1405
|
+
metric TEXT DEFAULT 'cosine' CHECK(metric IN ('cosine', 'euclidean', 'dot')),
|
|
1406
|
+
hnsw_m INTEGER DEFAULT 16,
|
|
1407
|
+
hnsw_ef_construction INTEGER DEFAULT 200,
|
|
1408
|
+
hnsw_ef_search INTEGER DEFAULT 100,
|
|
1409
|
+
quantization_type TEXT CHECK(quantization_type IN ('none', 'scalar', 'product')),
|
|
1410
|
+
quantization_bits INTEGER DEFAULT 8,
|
|
1411
|
+
total_vectors INTEGER DEFAULT 0,
|
|
1412
|
+
last_rebuild_at INTEGER,
|
|
1413
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
|
1414
|
+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
|
|
1415
|
+
)`);
|
|
1416
|
+
res.tableCreated = true;
|
|
1417
|
+
}
|
|
1418
|
+
// 384 = default ONNX model dim (Xenova/all-MiniLM-L6-v2); HNSW rejects
|
|
1419
|
+
// dim-mismatched inserts, so this must match the stored embeddings (#1947).
|
|
1420
|
+
const ensureRow = db.prepare('INSERT OR IGNORE INTO vector_indexes (id, name, dimensions) VALUES (?, ?, 384)');
|
|
1421
|
+
const setCount = db.prepare('UPDATE vector_indexes SET total_vectors = ?, updated_at = ? WHERE name = ?');
|
|
1422
|
+
const backfill = db.transaction(() => {
|
|
1423
|
+
// Parity with a fresh install's seed rows.
|
|
1424
|
+
ensureRow.run('default', 'default');
|
|
1425
|
+
ensureRow.run('patterns', 'patterns');
|
|
1426
|
+
const now = Date.now();
|
|
1427
|
+
for (const r of nsRows) {
|
|
1428
|
+
const ns = String(r.ns || 'default');
|
|
1429
|
+
ensureRow.run(ns, ns);
|
|
1430
|
+
setCount.run(r.c, now, ns);
|
|
1431
|
+
res.namespaces.push(ns);
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
backfill();
|
|
1435
|
+
res.repaired = res.tableCreated || res.namespaces.length > 0;
|
|
1436
|
+
db.close();
|
|
1437
|
+
if (opts.verbose && res.repaired) {
|
|
1438
|
+
console.log(`vector_indexes ${res.tableCreated ? 'created' : 'refreshed'} — ` +
|
|
1439
|
+
`backfilled ${res.namespaces.length} namespace(s)`);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
catch (e) {
|
|
1443
|
+
try {
|
|
1444
|
+
db?.close();
|
|
1445
|
+
}
|
|
1446
|
+
catch { /* already closed */ }
|
|
1447
|
+
if (opts.verbose) {
|
|
1448
|
+
console.log(`vector_indexes repair skipped: ${e?.message ?? e}`);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return res;
|
|
1452
|
+
}
|
|
1130
1453
|
/**
|
|
1131
1454
|
* Initialize the memory database properly using sql.js
|
|
1132
1455
|
*/
|
|
@@ -1155,19 +1478,24 @@ export async function initializeMemoryDatabase(options) {
|
|
|
1155
1478
|
// surfaced an `[ERROR]` and a "Initialization failed" spinner even when
|
|
1156
1479
|
// the existing DB was perfectly healthy.
|
|
1157
1480
|
if (fs.existsSync(dbPath) && !force) {
|
|
1481
|
+
// #2568-followup: an existing DB may predate `vector_indexes` (or was
|
|
1482
|
+
// written by agentdb directly). Self-heal it here — this branch is hit on
|
|
1483
|
+
// every MCP-server start and `memory init`, so any ruflo repairs itself.
|
|
1484
|
+
// Idempotent + best-effort; never turns a healthy re-init into a failure.
|
|
1485
|
+
const heal = await repairVectorIndexes(dbPath, { verbose, autoRecover: true });
|
|
1158
1486
|
return {
|
|
1159
1487
|
success: true,
|
|
1160
1488
|
alreadyExists: true,
|
|
1161
1489
|
backend,
|
|
1162
1490
|
dbPath,
|
|
1163
1491
|
schemaVersion: '3.0.0',
|
|
1164
|
-
tablesCreated: [],
|
|
1492
|
+
tablesCreated: heal.tableCreated ? ['vector_indexes'] : [],
|
|
1165
1493
|
indexesCreated: [],
|
|
1166
1494
|
features: {
|
|
1167
1495
|
vectorEmbeddings: false,
|
|
1168
1496
|
patternLearning: false,
|
|
1169
1497
|
temporalDecay: false,
|
|
1170
|
-
hnswIndexing:
|
|
1498
|
+
hnswIndexing: heal.repaired,
|
|
1171
1499
|
migrationTracking: false
|
|
1172
1500
|
}
|
|
1173
1501
|
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export type PromoteThreshold = 'execution-only' | 'execution+corroborated';
|
|
2
|
+
export interface TuningConfig {
|
|
3
|
+
batchSize: number;
|
|
4
|
+
dedupDistance: number;
|
|
5
|
+
promoteThreshold: PromoteThreshold;
|
|
6
|
+
}
|
|
7
|
+
export interface ParamGrid {
|
|
8
|
+
batchSize?: number[];
|
|
9
|
+
dedupDistance?: number[];
|
|
10
|
+
promoteThreshold?: PromoteThreshold[];
|
|
11
|
+
}
|
|
12
|
+
export interface TuningCandidate {
|
|
13
|
+
config: TuningConfig;
|
|
14
|
+
/** MRR@10 on the inner train-query split (never the true held-out set). */
|
|
15
|
+
trainScore: number;
|
|
16
|
+
trainRecallAt10: number;
|
|
17
|
+
trainQueryCount: number;
|
|
18
|
+
patternCount: number;
|
|
19
|
+
promotedCount: number;
|
|
20
|
+
distillMs: number;
|
|
21
|
+
/** Present iff M1 itself skipped this run (corrupt DB, missing tables, ...). */
|
|
22
|
+
skipped?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface HeldOutScore {
|
|
25
|
+
mrrAt10: number;
|
|
26
|
+
recallAt10: number;
|
|
27
|
+
queryCount: number;
|
|
28
|
+
/** Same held-out query set scored against raw (undistilled) train entries. */
|
|
29
|
+
baselineMrrAt10: number;
|
|
30
|
+
baselineRecallAt10: number;
|
|
31
|
+
}
|
|
32
|
+
export interface TuningProvenance {
|
|
33
|
+
gridSize: number;
|
|
34
|
+
corpusSize: number;
|
|
35
|
+
trainSize: number;
|
|
36
|
+
heldOutSize: number;
|
|
37
|
+
metric: 'mrr@10';
|
|
38
|
+
/** Stamped from `options.now` (or the orchestrator's own Date.now() at the
|
|
39
|
+
* I/O boundary) — never read from inside pure scoring logic. */
|
|
40
|
+
tunedAt: number;
|
|
41
|
+
sourceDbPath: string;
|
|
42
|
+
sourceChecksumSha256: string;
|
|
43
|
+
}
|
|
44
|
+
export interface TuningReport {
|
|
45
|
+
candidates: TuningCandidate[];
|
|
46
|
+
winner: TuningCandidate;
|
|
47
|
+
heldOut: HeldOutScore;
|
|
48
|
+
/** True when held-out MRR@10 is >20% worse (relative) than the winner's train score. */
|
|
49
|
+
overfit: boolean;
|
|
50
|
+
provenance: TuningProvenance;
|
|
51
|
+
}
|
|
52
|
+
export interface TuneDistillationOptions {
|
|
53
|
+
/** Source DB — read (copied + hashed) only; NEVER opened with a DB connection. */
|
|
54
|
+
dbPath: string;
|
|
55
|
+
grid?: ParamGrid;
|
|
56
|
+
/** Namespaces to distill / include in the raw baseline pool (default: all). */
|
|
57
|
+
namespaces?: string[];
|
|
58
|
+
/** Outer train/held-out split fraction (default 0.8). */
|
|
59
|
+
trainFraction?: number;
|
|
60
|
+
/** Namespaces the query set is drawn from (default: feedback + commands). */
|
|
61
|
+
queryNamespaces?: string[];
|
|
62
|
+
topK?: number;
|
|
63
|
+
/** Timestamp stamped into `provenance.tunedAt`. Caller-supplied so the core
|
|
64
|
+
* logic stays a pure function of its inputs; defaults to `Date.now()` at
|
|
65
|
+
* this I/O boundary if omitted. */
|
|
66
|
+
now?: number;
|
|
67
|
+
tmpDir?: string;
|
|
68
|
+
verbose?: boolean;
|
|
69
|
+
}
|
|
70
|
+
export interface TimeSplit {
|
|
71
|
+
totalRows: number;
|
|
72
|
+
trainBoundaryRowid: number;
|
|
73
|
+
trainRowids: number[];
|
|
74
|
+
heldOutRowids: number[];
|
|
75
|
+
}
|
|
76
|
+
export interface TunedConfigFile {
|
|
77
|
+
batchSize: number;
|
|
78
|
+
dedupDistance: number;
|
|
79
|
+
promoteThreshold: PromoteThreshold;
|
|
80
|
+
provenance: TuningProvenance & {
|
|
81
|
+
winnerTrainScore: number;
|
|
82
|
+
heldOutScore: number;
|
|
83
|
+
baselineHeldOutScore: number;
|
|
84
|
+
overfit: boolean;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export declare const DEFAULT_GRID_BATCH_SIZE: number[];
|
|
88
|
+
export declare const DEFAULT_GRID_DEDUP_DISTANCE: number[];
|
|
89
|
+
export declare const DEFAULT_GRID_PROMOTE_THRESHOLD: PromoteThreshold[];
|
|
90
|
+
/**
|
|
91
|
+
* Grid-search the distillation config against isolated copies of `dbPath`,
|
|
92
|
+
* scored on a held-out split. See module header for the full methodology.
|
|
93
|
+
*/
|
|
94
|
+
export declare function tuneDistillation(options: TuneDistillationOptions): Promise<TuningReport>;
|
|
95
|
+
/** Shape the winning config + provenance for persistence. Pure — no I/O. */
|
|
96
|
+
export declare function buildTunedConfigFile(report: TuningReport): TunedConfigFile;
|
|
97
|
+
/** Write the winning config to disk. Caller decides whether/where to call this. */
|
|
98
|
+
export declare function writeTunedConfigFile(report: TuningReport, filePath: string): void;
|
|
99
|
+
/** Where the tuned config lives by default, for daemon/CLI callers. */
|
|
100
|
+
export declare function defaultTunedConfigPath(cwd?: string): string;
|
|
101
|
+
/**
|
|
102
|
+
* Split a table's rows by rowid (a monotonic proxy for insertion time) into
|
|
103
|
+
* an earliest `trainFraction` chunk and a most-recent remainder. Pure given a
|
|
104
|
+
* fixed db snapshot. `table`/`extraWhere` are only ever called with internal
|
|
105
|
+
* literal strings (never external input) — not a SQL-injection surface.
|
|
106
|
+
*/
|
|
107
|
+
export declare function computeTimeSplit(db: unknown, trainFraction: number, opts?: {
|
|
108
|
+
table?: string;
|
|
109
|
+
extraWhere?: string;
|
|
110
|
+
}): TimeSplit;
|
|
111
|
+
//# sourceMappingURL=distill-tuning.d.ts.map
|