quilltap 4.10.0-dev.149 → 4.10.0-dev.175
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/README.md +32 -12
- package/bin/quilltap.js +7 -1
- package/lib/__tests__/memories-maintain-command.test.js +276 -0
- package/lib/__tests__/server-port-resolution.test.js +122 -0
- package/lib/anchor-probe-command.js +48 -17
- package/lib/completion/bash.template +12 -8
- package/lib/completion/fish.template +7 -2
- package/lib/completion/zsh.template +8 -3
- package/lib/db-commands.js +19 -3
- package/lib/graph-integrity.js +108 -5
- package/lib/lock-helpers.js +66 -0
- package/lib/memories-commands.js +324 -79
- package/lib/memories-maintain-command.js +238 -0
- package/lib/memories-server.js +86 -0
- package/lib/memory-diff-command.js +28 -6
- package/lib/recall-replay-command.js +65 -6
- package/package.json +1 -1
- package/lib/__tests__/memories-consolidate-command.test.js +0 -66
- package/lib/memories-consolidate-command.js +0 -131
package/lib/memories-commands.js
CHANGED
|
@@ -11,8 +11,14 @@ const {
|
|
|
11
11
|
resolveChat,
|
|
12
12
|
resolveProject,
|
|
13
13
|
} = require('./db-helpers');
|
|
14
|
-
const {
|
|
15
|
-
|
|
14
|
+
const {
|
|
15
|
+
scanDanglingEdges,
|
|
16
|
+
scanDigestIntegrity,
|
|
17
|
+
loadMemoryIds,
|
|
18
|
+
memoryColumns,
|
|
19
|
+
} = require('./graph-integrity');
|
|
20
|
+
const { cmdMaintain } = require('./memories-maintain-command');
|
|
21
|
+
const { postMemoriesAction, resolveMemoriesServerPort } = require('./memories-server');
|
|
16
22
|
|
|
17
23
|
// ---------- colour & marker helpers ----------
|
|
18
24
|
|
|
@@ -89,13 +95,19 @@ function parseFlags(args) {
|
|
|
89
95
|
noRelated: false,
|
|
90
96
|
// validate
|
|
91
97
|
list: false,
|
|
98
|
+
repair: false,
|
|
99
|
+
write: false,
|
|
92
100
|
// semantic search
|
|
93
101
|
semantic: false,
|
|
94
102
|
top: 0,
|
|
95
103
|
threshold: -1,
|
|
96
|
-
port
|
|
97
|
-
//
|
|
104
|
+
// Undefined unless --port is given, so the instance lock can supply it
|
|
105
|
+
// (resolveServerPort in lock-helpers).
|
|
106
|
+
port: undefined,
|
|
107
|
+
// maintain
|
|
98
108
|
dryRun: false,
|
|
109
|
+
phases: '',
|
|
110
|
+
cap: null,
|
|
99
111
|
};
|
|
100
112
|
const positional = [];
|
|
101
113
|
let i = 0;
|
|
@@ -138,6 +150,19 @@ function parseFlags(args) {
|
|
|
138
150
|
case '--no-related': flags.noRelated = true; break;
|
|
139
151
|
|
|
140
152
|
case '--list': flags.list = true; break;
|
|
153
|
+
case '--repair': flags.repair = true; break;
|
|
154
|
+
case '--write': flags.write = true; break;
|
|
155
|
+
|
|
156
|
+
case '--phases': flags.phases = args[++i] || ''; break;
|
|
157
|
+
case '--cap': {
|
|
158
|
+
const v = Number(args[++i]);
|
|
159
|
+
if (!Number.isInteger(v)) {
|
|
160
|
+
console.error('Error: --cap must be an integer');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
flags.cap = v;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
141
166
|
|
|
142
167
|
case '--semantic': flags.semantic = true; break;
|
|
143
168
|
case '--dry-run': flags.dryRun = true; break;
|
|
@@ -151,7 +176,7 @@ function parseFlags(args) {
|
|
|
151
176
|
flags.threshold = v;
|
|
152
177
|
break;
|
|
153
178
|
}
|
|
154
|
-
case '--port': {
|
|
179
|
+
case '-p': case '--port': {
|
|
155
180
|
const p = parseInt(args[++i], 10);
|
|
156
181
|
if (isNaN(p) || p < 1 || p > 65535) {
|
|
157
182
|
console.error('Error: --port must be between 1 and 65535');
|
|
@@ -641,7 +666,7 @@ async function cmdSemanticGrep(flags, query) {
|
|
|
641
666
|
|
|
642
667
|
const top = flags.top > 0 ? flags.top : 20;
|
|
643
668
|
const threshold = flags.threshold >= 0 ? flags.threshold : 0.5;
|
|
644
|
-
const port = flags
|
|
669
|
+
const { port } = resolveMemoriesServerPort(flags);
|
|
645
670
|
const url = `http://localhost:${port}/api/v1/memories?action=search`;
|
|
646
671
|
|
|
647
672
|
const body = { characterId, query, limit: top, minScore: threshold };
|
|
@@ -1099,9 +1124,23 @@ async function cmdStatus(flags) {
|
|
|
1099
1124
|
`).all();
|
|
1100
1125
|
}
|
|
1101
1126
|
|
|
1127
|
+
// The parent's maintenance ledger (last run per phase, last demotion's
|
|
1128
|
+
// effectiveness) lives in the running server; ask it, briefly. Without a
|
|
1129
|
+
// server the job rows still say when maintenance last ran.
|
|
1130
|
+
let ledgerPort = 3000;
|
|
1131
|
+
try {
|
|
1132
|
+
ledgerPort = resolveMemoriesServerPort(flags, { quiet: true }).port;
|
|
1133
|
+
} catch { /* openDb already validated the instance; fall back to the default */ }
|
|
1134
|
+
const ledger = await fetchMaintenanceLedger(ledgerPort);
|
|
1135
|
+
|
|
1102
1136
|
const report = [];
|
|
1103
1137
|
for (const holder of holderRows) {
|
|
1104
1138
|
const stats = computeHolderStats(db, holder.id);
|
|
1139
|
+
stats.maintenance = {
|
|
1140
|
+
recentJobs: recentMaintenanceJobs(db, holder.id),
|
|
1141
|
+
ledger: ledger ? ledger.find((e) => e.characterId === holder.id) || null : null,
|
|
1142
|
+
ledgerReachable: !!ledger,
|
|
1143
|
+
};
|
|
1105
1144
|
report.push({ holder, stats });
|
|
1106
1145
|
}
|
|
1107
1146
|
|
|
@@ -1134,6 +1173,41 @@ async function cmdStatus(flags) {
|
|
|
1134
1173
|
}
|
|
1135
1174
|
}
|
|
1136
1175
|
|
|
1176
|
+
/** The server's maintenance ledger, or null when the server cannot be reached. */
|
|
1177
|
+
async function fetchMaintenanceLedger(port) {
|
|
1178
|
+
try {
|
|
1179
|
+
const res = await fetch(`http://localhost:${port}/api/v1/memories?action=maintenance-status`, {
|
|
1180
|
+
signal: AbortSignal.timeout(1500),
|
|
1181
|
+
});
|
|
1182
|
+
if (!res.ok) return null;
|
|
1183
|
+
const payload = await res.json();
|
|
1184
|
+
return Array.isArray(payload && payload.ledger) ? payload.ledger : null;
|
|
1185
|
+
} catch {
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/** The character's most recent MEMORY_MAINTENANCE job rows, newest first. */
|
|
1191
|
+
function recentMaintenanceJobs(db, characterId) {
|
|
1192
|
+
try {
|
|
1193
|
+
return db.prepare(`
|
|
1194
|
+
SELECT id, status, updatedAt, lastError,
|
|
1195
|
+
json_extract(payload, '$.reason') AS reason,
|
|
1196
|
+
json_extract(payload, '$.phases') AS phases
|
|
1197
|
+
FROM background_jobs
|
|
1198
|
+
WHERE type = 'MEMORY_MAINTENANCE'
|
|
1199
|
+
AND json_extract(payload, '$.characterId') = ?
|
|
1200
|
+
ORDER BY updatedAt DESC
|
|
1201
|
+
LIMIT 3
|
|
1202
|
+
`).all(characterId).map((row) => ({
|
|
1203
|
+
...row,
|
|
1204
|
+
phases: row.phases ? JSON.parse(row.phases) : null,
|
|
1205
|
+
}));
|
|
1206
|
+
} catch {
|
|
1207
|
+
return [];
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1137
1211
|
function computeHolderStats(db, characterId) {
|
|
1138
1212
|
const counts = db.prepare(`
|
|
1139
1213
|
SELECT
|
|
@@ -1224,6 +1298,7 @@ function renderStatusBlock(holder, stats) {
|
|
|
1224
1298
|
console.log(` max degree: ${stats.graph.maxDegree}`);
|
|
1225
1299
|
const danglingSuffix = stats.graph.danglingEdges > 0 ? ' ⚠ JSON UUIDs that no longer resolve' : '';
|
|
1226
1300
|
console.log(` dangling edges: ${stats.graph.danglingEdges}${danglingSuffix}`);
|
|
1301
|
+
if (stats.maintenance) renderMaintenance(stats.maintenance);
|
|
1227
1302
|
if (stats.top.length) {
|
|
1228
1303
|
console.log(` Top by reinforcedImportance:`);
|
|
1229
1304
|
for (const t of stats.top) {
|
|
@@ -1233,80 +1308,202 @@ function renderStatusBlock(holder, stats) {
|
|
|
1233
1308
|
}
|
|
1234
1309
|
}
|
|
1235
1310
|
|
|
1311
|
+
function renderMaintenance(m) {
|
|
1312
|
+
console.log(` Maintenance:`);
|
|
1313
|
+
if (m.ledger) {
|
|
1314
|
+
// The phases are the server's; print the ones it reports a run for.
|
|
1315
|
+
const runs = Object.entries(m.ledger.lastRun || {});
|
|
1316
|
+
if (runs.length === 0) console.log(' no phase has run since the server started');
|
|
1317
|
+
for (const [phase, run] of runs) {
|
|
1318
|
+
console.log(` ${(phase + ':').padEnd(22)}${run.at} (${run.reason})`);
|
|
1319
|
+
}
|
|
1320
|
+
const d = m.ledger.lastDemotion;
|
|
1321
|
+
if (d) {
|
|
1322
|
+
if (d.excess === 0) {
|
|
1323
|
+
console.log(` last demotion: within the cap${d.effective ? '' : '; automatic demotion looks again in an hour'}`);
|
|
1324
|
+
} else {
|
|
1325
|
+
console.log(` last demotion: ${d.demoted} of ${d.excess} over the cap${d.effective ? '' : ' ⚠ ineffective; watermark backing off'}`);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
} else if (!m.ledgerReachable) {
|
|
1329
|
+
console.log(` ledger: (server not reachable; job history only)`);
|
|
1330
|
+
}
|
|
1331
|
+
if (m.recentJobs.length === 0) {
|
|
1332
|
+
console.log(` jobs: none recorded`);
|
|
1333
|
+
}
|
|
1334
|
+
for (const job of m.recentJobs) {
|
|
1335
|
+
const phases = Array.isArray(job.phases) ? job.phases.join(',') : 'all';
|
|
1336
|
+
const err = job.lastError ? ` ⚠ ${truncate(job.lastError, 60)}` : '';
|
|
1337
|
+
console.log(` job ${shortId(job.id)} ${job.status.padEnd(10)} ${job.updatedAt} ${job.reason || '?'} [${phases}]${err}`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1236
1341
|
// ---------- validate ----------
|
|
1237
1342
|
|
|
1238
1343
|
async function cmdValidate(flags) {
|
|
1344
|
+
let characterId = null;
|
|
1345
|
+
let local;
|
|
1239
1346
|
const { db, openMounts } = await openDb(flags);
|
|
1240
1347
|
try {
|
|
1241
|
-
let holderIds = null;
|
|
1242
1348
|
let holderRows;
|
|
1243
1349
|
if (flags.character && flags.character !== 'all') {
|
|
1244
1350
|
const c = resolveCharacter(db, flags.character, openMounts);
|
|
1245
1351
|
holderRows = [{ id: c.id, name: c.name }];
|
|
1246
|
-
|
|
1352
|
+
characterId = c.id;
|
|
1247
1353
|
} else {
|
|
1354
|
+
// Every holder in the memories table — including one whose character
|
|
1355
|
+
// row is gone — so the local scan covers the same rows the server's
|
|
1356
|
+
// unscoped repair scans and scrubs (review of PR 98).
|
|
1248
1357
|
holderRows = db.prepare(`
|
|
1249
|
-
SELECT
|
|
1250
|
-
|
|
1251
|
-
|
|
1358
|
+
SELECT h.characterId AS id, COALESCE(c.name, '(missing character ' || substr(h.characterId, 1, 8) || ')') AS name
|
|
1359
|
+
FROM (SELECT DISTINCT characterId FROM memories) h
|
|
1360
|
+
LEFT JOIN characters c ON c.id = h.characterId
|
|
1361
|
+
ORDER BY c.name IS NULL, c.name
|
|
1252
1362
|
`).all();
|
|
1253
1363
|
}
|
|
1364
|
+
local = scanForValidate(db, holderRows, Boolean(flags.list));
|
|
1365
|
+
} finally {
|
|
1366
|
+
db.close();
|
|
1367
|
+
}
|
|
1254
1368
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
}
|
|
1369
|
+
// --repair hands the fix (or its rehearsal) to the running server, which is
|
|
1370
|
+
// the only database writer; --write is the explicit opt-in to the real thing.
|
|
1371
|
+
let repair = null;
|
|
1372
|
+
if (flags.repair) {
|
|
1373
|
+
const body = { dryRun: !flags.write };
|
|
1374
|
+
if (characterId) body.characterId = characterId;
|
|
1375
|
+
const payload = await postMemoriesAction(flags, 'repair', body, {
|
|
1376
|
+
why: 'The repair runs inside the server (it is the only database writer, and it queues the embedding jobs).',
|
|
1377
|
+
announce: flags.json ? undefined : (url) => process.stderr.write(
|
|
1378
|
+
`${body.dryRun ? 'Rehearsing' : 'Running'} graph repair via ${url}\n`
|
|
1379
|
+
),
|
|
1380
|
+
});
|
|
1381
|
+
repair = (payload && payload.report) || null;
|
|
1382
|
+
}
|
|
1270
1383
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1384
|
+
if (flags.json) {
|
|
1385
|
+
renderJson({
|
|
1386
|
+
scanned: characterId ? 'single' : 'all',
|
|
1387
|
+
characterIds: local.perHolder.map(({ holder }) => holder.id),
|
|
1388
|
+
totals: local.totals,
|
|
1389
|
+
holders: local.perHolder.map(({ holder, stats, digests }) => ({
|
|
1390
|
+
holder: { id: holder.id, name: holder.name },
|
|
1391
|
+
...stats,
|
|
1392
|
+
...digests,
|
|
1393
|
+
})),
|
|
1394
|
+
...(flags.repair ? { repair } : {}),
|
|
1395
|
+
});
|
|
1396
|
+
} else {
|
|
1397
|
+
renderValidateText(local, characterId, flags);
|
|
1398
|
+
if (flags.repair) renderRepairText(repair);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// Exit 1 while dangling links remain: always after a read-only validate or
|
|
1402
|
+
// a rehearsal; after a --write repair only if the server reported nothing
|
|
1403
|
+
// repaired (a missing embedding alone is a warning, never a failure).
|
|
1404
|
+
const t = local.totals;
|
|
1405
|
+
const dangling = t.danglingEdges + t.danglingConsolidatedFrom + t.danglingSupersededBy;
|
|
1406
|
+
const repaired = Boolean(flags.repair && flags.write && repair && repair.repaired);
|
|
1407
|
+
if (dangling > 0 && !repaired) {
|
|
1408
|
+
process.exitCode = 1;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/** Run every read-only integrity scan over the holders. */
|
|
1413
|
+
function scanForValidate(db, holderRows, includePairs) {
|
|
1414
|
+
const allIds = loadMemoryIds(db);
|
|
1415
|
+
const columns = memoryColumns(db);
|
|
1416
|
+
const totals = {
|
|
1417
|
+
nodes: 0,
|
|
1418
|
+
edges: 0,
|
|
1419
|
+
danglingEdges: 0,
|
|
1420
|
+
danglingConsolidatedFrom: 0,
|
|
1421
|
+
digestsWithDanglingMembers: 0,
|
|
1422
|
+
danglingSupersededBy: 0,
|
|
1423
|
+
missingEmbeddings: 0,
|
|
1424
|
+
};
|
|
1425
|
+
const perHolder = [];
|
|
1426
|
+
for (const holder of holderRows) {
|
|
1427
|
+
const stats = scanDanglingEdges(db, { characterId: holder.id, includePairs, allIds });
|
|
1428
|
+
const digests = scanDigestIntegrity(db, { characterId: holder.id, includePairs, allIds, columns });
|
|
1429
|
+
totals.nodes += stats.nodes;
|
|
1430
|
+
totals.edges += stats.totalEdges;
|
|
1431
|
+
totals.danglingEdges += stats.danglingEdges;
|
|
1432
|
+
totals.danglingConsolidatedFrom += digests.consolidatedFrom.links;
|
|
1433
|
+
totals.digestsWithDanglingMembers += digests.consolidatedFrom.digests;
|
|
1434
|
+
totals.danglingSupersededBy += digests.supersededBy;
|
|
1435
|
+
totals.missingEmbeddings += digests.missingEmbeddings;
|
|
1436
|
+
perHolder.push({ holder, stats, digests });
|
|
1437
|
+
}
|
|
1438
|
+
return { totals, perHolder };
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function renderValidateText(local, characterId, flags) {
|
|
1442
|
+
const { totals: t, perHolder } = local;
|
|
1443
|
+
const scope = characterId ? `holder ${perHolder[0].holder.name}` : `${perHolder.length} holders`;
|
|
1444
|
+
const drift = t.edges > 0 ? ((t.danglingEdges / t.edges) * 100).toFixed(2) : '0.00';
|
|
1445
|
+
console.log(`Scanned ${scope}: ${t.nodes} memories, ${t.edges} edges, ${t.danglingEdges} dangling (${drift}%).`);
|
|
1446
|
+
console.log(`Digests: ${t.danglingConsolidatedFrom} dangling consolidatedFrom member(s) across ` +
|
|
1447
|
+
`${t.digestsWithDanglingMembers} digest(s); ${t.danglingSupersededBy} dangling supersededById.`);
|
|
1448
|
+
if (t.missingEmbeddings > 0) {
|
|
1449
|
+
console.log(colorize(`Warning: ${t.missingEmbeddings} memories have no embedding and cannot be recalled by meaning.`, YELLOW));
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const dangling = t.danglingEdges + t.danglingConsolidatedFrom + t.danglingSupersededBy;
|
|
1453
|
+
if (dangling === 0) {
|
|
1454
|
+
console.log(colorize('Graph is clean.', GREEN));
|
|
1455
|
+
} else if (flags.list) {
|
|
1456
|
+
console.log('');
|
|
1457
|
+
for (const { holder, stats, digests } of perHolder) {
|
|
1458
|
+
const related = stats.danglingPairs || [];
|
|
1459
|
+
const members = digests.consolidatedFromPairs || [];
|
|
1460
|
+
const superseded = digests.supersededByPairs || [];
|
|
1461
|
+
if (related.length + members.length + superseded.length === 0) continue;
|
|
1462
|
+
console.log(colorize(`${holder.name} (${shortId(holder.id)})`, BOLD));
|
|
1463
|
+
for (const pair of related) {
|
|
1464
|
+
console.log(` related ${shortId(pair.sourceId)} → ${pair.targetIds.map(shortId).join(', ')}`);
|
|
1465
|
+
}
|
|
1466
|
+
for (const pair of members) {
|
|
1467
|
+
console.log(` digest ${shortId(pair.digestId)} ⊃ ${pair.memberIds.map(shortId).join(', ')}`);
|
|
1468
|
+
}
|
|
1469
|
+
for (const pair of superseded) {
|
|
1470
|
+
console.log(` superseded ${shortId(pair.memberId)} → ${shortId(pair.digestId)}`);
|
|
1302
1471
|
}
|
|
1303
1472
|
}
|
|
1473
|
+
} else {
|
|
1474
|
+
console.log(colorize('Run with --list to see the offending memory IDs and dangling targets.', YELLOW));
|
|
1475
|
+
}
|
|
1476
|
+
if ((dangling > 0 || t.missingEmbeddings > 0) && !flags.repair) {
|
|
1477
|
+
console.log(colorize('Run with --repair to rehearse the fix on the running server (add --write to apply it).', YELLOW));
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1304
1480
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1481
|
+
function renderRepairText(report) {
|
|
1482
|
+
if (!report) {
|
|
1483
|
+
console.log(colorize('The server returned no repair report.', YELLOW));
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
const f = report.found || {};
|
|
1487
|
+
const related = f.related || { links: 0, rows: 0 };
|
|
1488
|
+
const cf = f.consolidatedFrom || { links: 0, digests: 0 };
|
|
1489
|
+
console.log('');
|
|
1490
|
+
console.log(colorize(`Server graph repair (${report.dryRun ? 'dry run' : 'applied'}${report.characterId ? '' : ', every memory'}):`, BOLD));
|
|
1491
|
+
console.log(` found: ${related.links} dangling related link(s) on ${related.rows} row(s); ` +
|
|
1492
|
+
`${cf.links} dangling consolidatedFrom member(s) on ${cf.digests} digest(s); ` +
|
|
1493
|
+
`${f.supersededBy || 0} dangling supersededById; ${f.missingEmbeddings || 0} row(s) without an embedding.`);
|
|
1494
|
+
if (report.dryRun) {
|
|
1495
|
+
const anything = related.links + cf.links + (f.supersededBy || 0) + (f.missingEmbeddings || 0);
|
|
1496
|
+
console.log(anything > 0
|
|
1497
|
+
? colorize(' Nothing was changed. Add --write to scrub the links and queue the embedding jobs.', YELLOW)
|
|
1498
|
+
: colorize(' Nothing to repair.', GREEN));
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
const r = report.repaired || {};
|
|
1502
|
+
console.log(` repaired: ${r.relatedRowsTouched || 0} row(s) relinked, ${r.digestsTouched || 0} digest(s) touched, ` +
|
|
1503
|
+
`${r.membersCut || 0} member(s) cut loose, ${r.charactersAffected || 0} character(s) affected; ` +
|
|
1504
|
+
`${r.embeddingJobsQueued || 0} embedding job(s) queued.`);
|
|
1505
|
+
if (r.embeddingSkippedReason) {
|
|
1506
|
+
console.log(colorize(` Embedding jobs not queued: ${r.embeddingSkippedReason} (set a default embedding profile, then run again).`, YELLOW));
|
|
1310
1507
|
}
|
|
1311
1508
|
}
|
|
1312
1509
|
|
|
@@ -1339,18 +1536,50 @@ Subcommands:
|
|
|
1339
1536
|
tree <id|prefix> [--depth N] [--max-nodes N] [--json]
|
|
1340
1537
|
ASCII walk of the
|
|
1341
1538
|
related-memory graph.
|
|
1342
|
-
status [--character <name|id>] [--json]
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1539
|
+
status [--character <name|id>] [--json] [--port <n>]
|
|
1540
|
+
Per-holder rollup, maintenance
|
|
1541
|
+
ledger + dangling-edge check.
|
|
1542
|
+
validate [--character <name|id>] [--list] [--json]
|
|
1543
|
+
Read-only memory-graph health
|
|
1544
|
+
check: dangling relatedMemoryIds,
|
|
1545
|
+
consolidatedFrom members and
|
|
1546
|
+
supersededById links, plus rows
|
|
1547
|
+
with no embedding (a warning).
|
|
1548
|
+
Exits 1 if any dangling link
|
|
1549
|
+
remains.
|
|
1550
|
+
validate --repair [--write] [--character <name|id>] [--port N]
|
|
1551
|
+
Ask the running server to repair
|
|
1552
|
+
the graph. Without --write it is
|
|
1553
|
+
a dry run: it reports what would
|
|
1554
|
+
be repaired and changes nothing.
|
|
1555
|
+
With --write the server scrubs
|
|
1556
|
+
the dangling links and queues
|
|
1557
|
+
embedding jobs for unembedded
|
|
1558
|
+
rows. The write runs inside the
|
|
1559
|
+
server (the only database
|
|
1560
|
+
writer, holder of the instance
|
|
1561
|
+
lock), so the server must be up.
|
|
1562
|
+
maintain --character <name|id|all> [--phases a,b,…] [--dry-run]
|
|
1563
|
+
[--max N] [--threshold 0..1] [--cap N] [--port N] [--json]
|
|
1564
|
+
Run memory maintenance by hand on
|
|
1565
|
+
the running server. Phases, in
|
|
1566
|
+
run order: fold-near-duplicates,
|
|
1567
|
+
consolidate, demote, retire,
|
|
1568
|
+
mirror (default: all; naming
|
|
1569
|
+
consolidate brings mirror).
|
|
1570
|
+
--dry-run prints what each phase
|
|
1571
|
+
would do and writes nothing;
|
|
1572
|
+
otherwise one background job per
|
|
1573
|
+
character is queued. 'all' runs
|
|
1574
|
+
every character (a dry run over
|
|
1575
|
+
'all' must leave out consolidate).
|
|
1576
|
+
--max caps clusters consolidated,
|
|
1577
|
+
--threshold sets the cluster
|
|
1578
|
+
cosine (neural embedding
|
|
1579
|
+
profiles), --cap the hot-row cap
|
|
1580
|
+
for demotion (100-100000).
|
|
1581
|
+
To consolidate only:
|
|
1582
|
+
maintain --phases consolidate.
|
|
1354
1583
|
|
|
1355
1584
|
Shared filter flags:
|
|
1356
1585
|
--character <name|id|all> Holder. Default: all.
|
|
@@ -1377,6 +1606,10 @@ Global flags (may appear before or after the subcommand):
|
|
|
1377
1606
|
-d, --data-dir <path> Override data directory.
|
|
1378
1607
|
--instance <name> Use a registered instance.
|
|
1379
1608
|
--passphrase <pass> Decrypt .dbkey if peppered.
|
|
1609
|
+
-p, --port <n> Server port for the verbs that call the running
|
|
1610
|
+
server (default: the port in the instance's
|
|
1611
|
+
lock when --instance/--data-dir is given, else
|
|
1612
|
+
3000).
|
|
1380
1613
|
--json Machine-readable output.
|
|
1381
1614
|
-h, --help Show this help.
|
|
1382
1615
|
|
|
@@ -1384,8 +1617,9 @@ Note: -i is reserved here for 'grep --ignore-case'. Use the long --instance
|
|
|
1384
1617
|
form to target a registered instance.
|
|
1385
1618
|
|
|
1386
1619
|
All memories verbs are read-only. They open the main encrypted database
|
|
1387
|
-
(quilltap.db) and never write to it
|
|
1388
|
-
running server
|
|
1620
|
+
(quilltap.db) and never write to it. 'maintain' and 'validate --repair' leave
|
|
1621
|
+
the writing to the running server; 'validate --repair' changes data only with
|
|
1622
|
+
--write.
|
|
1389
1623
|
|
|
1390
1624
|
Examples:
|
|
1391
1625
|
quilltap memories ls
|
|
@@ -1398,8 +1632,12 @@ Examples:
|
|
|
1398
1632
|
quilltap memories status --character Ariadne
|
|
1399
1633
|
quilltap memories validate
|
|
1400
1634
|
quilltap memories validate --list
|
|
1401
|
-
quilltap memories
|
|
1402
|
-
quilltap memories
|
|
1635
|
+
quilltap memories validate --repair # rehearse the repair
|
|
1636
|
+
quilltap memories validate --repair --write # apply it
|
|
1637
|
+
quilltap memories maintain --instance Friday --character Friday --dry-run
|
|
1638
|
+
quilltap memories maintain --instance Friday --character Friday --phases consolidate --max 100
|
|
1639
|
+
quilltap memories maintain --character all --phases demote,retire --dry-run
|
|
1640
|
+
quilltap memories maintain --character all
|
|
1403
1641
|
`);
|
|
1404
1642
|
}
|
|
1405
1643
|
|
|
@@ -1430,6 +1668,12 @@ async function memoriesCommand(args) {
|
|
|
1430
1668
|
printMemoriesHelp();
|
|
1431
1669
|
return;
|
|
1432
1670
|
}
|
|
1671
|
+
if (flags.repair && verb !== 'validate') {
|
|
1672
|
+
throw new Error('--repair applies only to memories validate.');
|
|
1673
|
+
}
|
|
1674
|
+
if (flags.write && !(verb === 'validate' && flags.repair)) {
|
|
1675
|
+
throw new Error('--write applies only to memories validate --repair.');
|
|
1676
|
+
}
|
|
1433
1677
|
switch (verb) {
|
|
1434
1678
|
case 'ls': await cmdLs(flags); break;
|
|
1435
1679
|
case 'find': await cmdFind(flags, positional); break;
|
|
@@ -1438,7 +1682,7 @@ async function memoriesCommand(args) {
|
|
|
1438
1682
|
case 'tree': await cmdTree(flags, positional); break;
|
|
1439
1683
|
case 'status': await cmdStatus(flags); break;
|
|
1440
1684
|
case 'validate': await cmdValidate(flags); break;
|
|
1441
|
-
case '
|
|
1685
|
+
case 'maintain': await cmdMaintain(flags, resolveCharacterIdForServer); break;
|
|
1442
1686
|
default:
|
|
1443
1687
|
console.error(`Unknown memories subcommand: ${verb}`);
|
|
1444
1688
|
console.error("Run 'quilltap memories --help' for a list.");
|
|
@@ -1451,6 +1695,7 @@ module.exports = {
|
|
|
1451
1695
|
// Exported for unit tests:
|
|
1452
1696
|
parseFlags,
|
|
1453
1697
|
resolveCharacterIdForServer,
|
|
1698
|
+
renderRepairText,
|
|
1454
1699
|
buildWhereClause,
|
|
1455
1700
|
buildOrderBy,
|
|
1456
1701
|
traverseMemoryGraph,
|