quilltap 4.10.0-dev.149 → 4.10.0-dev.173
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 +320 -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,198 @@ 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
|
+
console.log(` last demotion: ${d.demoted} of ${d.excess} over the cap${d.effective ? '' : ' ⚠ ineffective; watermark backing off'}`);
|
|
1323
|
+
}
|
|
1324
|
+
} else if (!m.ledgerReachable) {
|
|
1325
|
+
console.log(` ledger: (server not reachable; job history only)`);
|
|
1326
|
+
}
|
|
1327
|
+
if (m.recentJobs.length === 0) {
|
|
1328
|
+
console.log(` jobs: none recorded`);
|
|
1329
|
+
}
|
|
1330
|
+
for (const job of m.recentJobs) {
|
|
1331
|
+
const phases = Array.isArray(job.phases) ? job.phases.join(',') : 'all';
|
|
1332
|
+
const err = job.lastError ? ` ⚠ ${truncate(job.lastError, 60)}` : '';
|
|
1333
|
+
console.log(` job ${shortId(job.id)} ${job.status.padEnd(10)} ${job.updatedAt} ${job.reason || '?'} [${phases}]${err}`);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1236
1337
|
// ---------- validate ----------
|
|
1237
1338
|
|
|
1238
1339
|
async function cmdValidate(flags) {
|
|
1340
|
+
let characterId = null;
|
|
1341
|
+
let local;
|
|
1239
1342
|
const { db, openMounts } = await openDb(flags);
|
|
1240
1343
|
try {
|
|
1241
|
-
let holderIds = null;
|
|
1242
1344
|
let holderRows;
|
|
1243
1345
|
if (flags.character && flags.character !== 'all') {
|
|
1244
1346
|
const c = resolveCharacter(db, flags.character, openMounts);
|
|
1245
1347
|
holderRows = [{ id: c.id, name: c.name }];
|
|
1246
|
-
|
|
1348
|
+
characterId = c.id;
|
|
1247
1349
|
} else {
|
|
1350
|
+
// Every holder in the memories table — including one whose character
|
|
1351
|
+
// row is gone — so the local scan covers the same rows the server's
|
|
1352
|
+
// unscoped repair scans and scrubs (review of PR 98).
|
|
1248
1353
|
holderRows = db.prepare(`
|
|
1249
|
-
SELECT
|
|
1250
|
-
|
|
1251
|
-
|
|
1354
|
+
SELECT h.characterId AS id, COALESCE(c.name, '(missing character ' || substr(h.characterId, 1, 8) || ')') AS name
|
|
1355
|
+
FROM (SELECT DISTINCT characterId FROM memories) h
|
|
1356
|
+
LEFT JOIN characters c ON c.id = h.characterId
|
|
1357
|
+
ORDER BY c.name IS NULL, c.name
|
|
1252
1358
|
`).all();
|
|
1253
1359
|
}
|
|
1360
|
+
local = scanForValidate(db, holderRows, Boolean(flags.list));
|
|
1361
|
+
} finally {
|
|
1362
|
+
db.close();
|
|
1363
|
+
}
|
|
1254
1364
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
}
|
|
1365
|
+
// --repair hands the fix (or its rehearsal) to the running server, which is
|
|
1366
|
+
// the only database writer; --write is the explicit opt-in to the real thing.
|
|
1367
|
+
let repair = null;
|
|
1368
|
+
if (flags.repair) {
|
|
1369
|
+
const body = { dryRun: !flags.write };
|
|
1370
|
+
if (characterId) body.characterId = characterId;
|
|
1371
|
+
const payload = await postMemoriesAction(flags, 'repair', body, {
|
|
1372
|
+
why: 'The repair runs inside the server (it is the only database writer, and it queues the embedding jobs).',
|
|
1373
|
+
announce: flags.json ? undefined : (url) => process.stderr.write(
|
|
1374
|
+
`${body.dryRun ? 'Rehearsing' : 'Running'} graph repair via ${url}\n`
|
|
1375
|
+
),
|
|
1376
|
+
});
|
|
1377
|
+
repair = (payload && payload.report) || null;
|
|
1378
|
+
}
|
|
1270
1379
|
|
|
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
|
-
|
|
1380
|
+
if (flags.json) {
|
|
1381
|
+
renderJson({
|
|
1382
|
+
scanned: characterId ? 'single' : 'all',
|
|
1383
|
+
characterIds: local.perHolder.map(({ holder }) => holder.id),
|
|
1384
|
+
totals: local.totals,
|
|
1385
|
+
holders: local.perHolder.map(({ holder, stats, digests }) => ({
|
|
1386
|
+
holder: { id: holder.id, name: holder.name },
|
|
1387
|
+
...stats,
|
|
1388
|
+
...digests,
|
|
1389
|
+
})),
|
|
1390
|
+
...(flags.repair ? { repair } : {}),
|
|
1391
|
+
});
|
|
1392
|
+
} else {
|
|
1393
|
+
renderValidateText(local, characterId, flags);
|
|
1394
|
+
if (flags.repair) renderRepairText(repair);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// Exit 1 while dangling links remain: always after a read-only validate or
|
|
1398
|
+
// a rehearsal; after a --write repair only if the server reported nothing
|
|
1399
|
+
// repaired (a missing embedding alone is a warning, never a failure).
|
|
1400
|
+
const t = local.totals;
|
|
1401
|
+
const dangling = t.danglingEdges + t.danglingConsolidatedFrom + t.danglingSupersededBy;
|
|
1402
|
+
const repaired = Boolean(flags.repair && flags.write && repair && repair.repaired);
|
|
1403
|
+
if (dangling > 0 && !repaired) {
|
|
1404
|
+
process.exitCode = 1;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/** Run every read-only integrity scan over the holders. */
|
|
1409
|
+
function scanForValidate(db, holderRows, includePairs) {
|
|
1410
|
+
const allIds = loadMemoryIds(db);
|
|
1411
|
+
const columns = memoryColumns(db);
|
|
1412
|
+
const totals = {
|
|
1413
|
+
nodes: 0,
|
|
1414
|
+
edges: 0,
|
|
1415
|
+
danglingEdges: 0,
|
|
1416
|
+
danglingConsolidatedFrom: 0,
|
|
1417
|
+
digestsWithDanglingMembers: 0,
|
|
1418
|
+
danglingSupersededBy: 0,
|
|
1419
|
+
missingEmbeddings: 0,
|
|
1420
|
+
};
|
|
1421
|
+
const perHolder = [];
|
|
1422
|
+
for (const holder of holderRows) {
|
|
1423
|
+
const stats = scanDanglingEdges(db, { characterId: holder.id, includePairs, allIds });
|
|
1424
|
+
const digests = scanDigestIntegrity(db, { characterId: holder.id, includePairs, allIds, columns });
|
|
1425
|
+
totals.nodes += stats.nodes;
|
|
1426
|
+
totals.edges += stats.totalEdges;
|
|
1427
|
+
totals.danglingEdges += stats.danglingEdges;
|
|
1428
|
+
totals.danglingConsolidatedFrom += digests.consolidatedFrom.links;
|
|
1429
|
+
totals.digestsWithDanglingMembers += digests.consolidatedFrom.digests;
|
|
1430
|
+
totals.danglingSupersededBy += digests.supersededBy;
|
|
1431
|
+
totals.missingEmbeddings += digests.missingEmbeddings;
|
|
1432
|
+
perHolder.push({ holder, stats, digests });
|
|
1433
|
+
}
|
|
1434
|
+
return { totals, perHolder };
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function renderValidateText(local, characterId, flags) {
|
|
1438
|
+
const { totals: t, perHolder } = local;
|
|
1439
|
+
const scope = characterId ? `holder ${perHolder[0].holder.name}` : `${perHolder.length} holders`;
|
|
1440
|
+
const drift = t.edges > 0 ? ((t.danglingEdges / t.edges) * 100).toFixed(2) : '0.00';
|
|
1441
|
+
console.log(`Scanned ${scope}: ${t.nodes} memories, ${t.edges} edges, ${t.danglingEdges} dangling (${drift}%).`);
|
|
1442
|
+
console.log(`Digests: ${t.danglingConsolidatedFrom} dangling consolidatedFrom member(s) across ` +
|
|
1443
|
+
`${t.digestsWithDanglingMembers} digest(s); ${t.danglingSupersededBy} dangling supersededById.`);
|
|
1444
|
+
if (t.missingEmbeddings > 0) {
|
|
1445
|
+
console.log(colorize(`Warning: ${t.missingEmbeddings} memories have no embedding and cannot be recalled by meaning.`, YELLOW));
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const dangling = t.danglingEdges + t.danglingConsolidatedFrom + t.danglingSupersededBy;
|
|
1449
|
+
if (dangling === 0) {
|
|
1450
|
+
console.log(colorize('Graph is clean.', GREEN));
|
|
1451
|
+
} else if (flags.list) {
|
|
1452
|
+
console.log('');
|
|
1453
|
+
for (const { holder, stats, digests } of perHolder) {
|
|
1454
|
+
const related = stats.danglingPairs || [];
|
|
1455
|
+
const members = digests.consolidatedFromPairs || [];
|
|
1456
|
+
const superseded = digests.supersededByPairs || [];
|
|
1457
|
+
if (related.length + members.length + superseded.length === 0) continue;
|
|
1458
|
+
console.log(colorize(`${holder.name} (${shortId(holder.id)})`, BOLD));
|
|
1459
|
+
for (const pair of related) {
|
|
1460
|
+
console.log(` related ${shortId(pair.sourceId)} → ${pair.targetIds.map(shortId).join(', ')}`);
|
|
1461
|
+
}
|
|
1462
|
+
for (const pair of members) {
|
|
1463
|
+
console.log(` digest ${shortId(pair.digestId)} ⊃ ${pair.memberIds.map(shortId).join(', ')}`);
|
|
1464
|
+
}
|
|
1465
|
+
for (const pair of superseded) {
|
|
1466
|
+
console.log(` superseded ${shortId(pair.memberId)} → ${shortId(pair.digestId)}`);
|
|
1302
1467
|
}
|
|
1303
1468
|
}
|
|
1469
|
+
} else {
|
|
1470
|
+
console.log(colorize('Run with --list to see the offending memory IDs and dangling targets.', YELLOW));
|
|
1471
|
+
}
|
|
1472
|
+
if ((dangling > 0 || t.missingEmbeddings > 0) && !flags.repair) {
|
|
1473
|
+
console.log(colorize('Run with --repair to rehearse the fix on the running server (add --write to apply it).', YELLOW));
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1304
1476
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1477
|
+
function renderRepairText(report) {
|
|
1478
|
+
if (!report) {
|
|
1479
|
+
console.log(colorize('The server returned no repair report.', YELLOW));
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const f = report.found || {};
|
|
1483
|
+
const related = f.related || { links: 0, rows: 0 };
|
|
1484
|
+
const cf = f.consolidatedFrom || { links: 0, digests: 0 };
|
|
1485
|
+
console.log('');
|
|
1486
|
+
console.log(colorize(`Server graph repair (${report.dryRun ? 'dry run' : 'applied'}${report.characterId ? '' : ', every memory'}):`, BOLD));
|
|
1487
|
+
console.log(` found: ${related.links} dangling related link(s) on ${related.rows} row(s); ` +
|
|
1488
|
+
`${cf.links} dangling consolidatedFrom member(s) on ${cf.digests} digest(s); ` +
|
|
1489
|
+
`${f.supersededBy || 0} dangling supersededById; ${f.missingEmbeddings || 0} row(s) without an embedding.`);
|
|
1490
|
+
if (report.dryRun) {
|
|
1491
|
+
const anything = related.links + cf.links + (f.supersededBy || 0) + (f.missingEmbeddings || 0);
|
|
1492
|
+
console.log(anything > 0
|
|
1493
|
+
? colorize(' Nothing was changed. Add --write to scrub the links and queue the embedding jobs.', YELLOW)
|
|
1494
|
+
: colorize(' Nothing to repair.', GREEN));
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
const r = report.repaired || {};
|
|
1498
|
+
console.log(` repaired: ${r.relatedRowsTouched || 0} row(s) relinked, ${r.digestsTouched || 0} digest(s) touched, ` +
|
|
1499
|
+
`${r.membersCut || 0} member(s) cut loose, ${r.charactersAffected || 0} character(s) affected; ` +
|
|
1500
|
+
`${r.embeddingJobsQueued || 0} embedding job(s) queued.`);
|
|
1501
|
+
if (r.embeddingSkippedReason) {
|
|
1502
|
+
console.log(colorize(` Embedding jobs not queued: ${r.embeddingSkippedReason} (set a default embedding profile, then run again).`, YELLOW));
|
|
1310
1503
|
}
|
|
1311
1504
|
}
|
|
1312
1505
|
|
|
@@ -1339,18 +1532,50 @@ Subcommands:
|
|
|
1339
1532
|
tree <id|prefix> [--depth N] [--max-nodes N] [--json]
|
|
1340
1533
|
ASCII walk of the
|
|
1341
1534
|
related-memory graph.
|
|
1342
|
-
status [--character <name|id>] [--json]
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1535
|
+
status [--character <name|id>] [--json] [--port <n>]
|
|
1536
|
+
Per-holder rollup, maintenance
|
|
1537
|
+
ledger + dangling-edge check.
|
|
1538
|
+
validate [--character <name|id>] [--list] [--json]
|
|
1539
|
+
Read-only memory-graph health
|
|
1540
|
+
check: dangling relatedMemoryIds,
|
|
1541
|
+
consolidatedFrom members and
|
|
1542
|
+
supersededById links, plus rows
|
|
1543
|
+
with no embedding (a warning).
|
|
1544
|
+
Exits 1 if any dangling link
|
|
1545
|
+
remains.
|
|
1546
|
+
validate --repair [--write] [--character <name|id>] [--port N]
|
|
1547
|
+
Ask the running server to repair
|
|
1548
|
+
the graph. Without --write it is
|
|
1549
|
+
a dry run: it reports what would
|
|
1550
|
+
be repaired and changes nothing.
|
|
1551
|
+
With --write the server scrubs
|
|
1552
|
+
the dangling links and queues
|
|
1553
|
+
embedding jobs for unembedded
|
|
1554
|
+
rows. The write runs inside the
|
|
1555
|
+
server (the only database
|
|
1556
|
+
writer, holder of the instance
|
|
1557
|
+
lock), so the server must be up.
|
|
1558
|
+
maintain --character <name|id|all> [--phases a,b,…] [--dry-run]
|
|
1559
|
+
[--max N] [--threshold 0..1] [--cap N] [--port N] [--json]
|
|
1560
|
+
Run memory maintenance by hand on
|
|
1561
|
+
the running server. Phases, in
|
|
1562
|
+
run order: fold-near-duplicates,
|
|
1563
|
+
consolidate, demote, retire,
|
|
1564
|
+
mirror (default: all; naming
|
|
1565
|
+
consolidate brings mirror).
|
|
1566
|
+
--dry-run prints what each phase
|
|
1567
|
+
would do and writes nothing;
|
|
1568
|
+
otherwise one background job per
|
|
1569
|
+
character is queued. 'all' runs
|
|
1570
|
+
every character (a dry run over
|
|
1571
|
+
'all' must leave out consolidate).
|
|
1572
|
+
--max caps clusters consolidated,
|
|
1573
|
+
--threshold sets the cluster
|
|
1574
|
+
cosine (neural embedding
|
|
1575
|
+
profiles), --cap the hot-row cap
|
|
1576
|
+
for demotion (100-100000).
|
|
1577
|
+
To consolidate only:
|
|
1578
|
+
maintain --phases consolidate.
|
|
1354
1579
|
|
|
1355
1580
|
Shared filter flags:
|
|
1356
1581
|
--character <name|id|all> Holder. Default: all.
|
|
@@ -1377,6 +1602,10 @@ Global flags (may appear before or after the subcommand):
|
|
|
1377
1602
|
-d, --data-dir <path> Override data directory.
|
|
1378
1603
|
--instance <name> Use a registered instance.
|
|
1379
1604
|
--passphrase <pass> Decrypt .dbkey if peppered.
|
|
1605
|
+
-p, --port <n> Server port for the verbs that call the running
|
|
1606
|
+
server (default: the port in the instance's
|
|
1607
|
+
lock when --instance/--data-dir is given, else
|
|
1608
|
+
3000).
|
|
1380
1609
|
--json Machine-readable output.
|
|
1381
1610
|
-h, --help Show this help.
|
|
1382
1611
|
|
|
@@ -1384,8 +1613,9 @@ Note: -i is reserved here for 'grep --ignore-case'. Use the long --instance
|
|
|
1384
1613
|
form to target a registered instance.
|
|
1385
1614
|
|
|
1386
1615
|
All memories verbs are read-only. They open the main encrypted database
|
|
1387
|
-
(quilltap.db) and never write to it
|
|
1388
|
-
running server
|
|
1616
|
+
(quilltap.db) and never write to it. 'maintain' and 'validate --repair' leave
|
|
1617
|
+
the writing to the running server; 'validate --repair' changes data only with
|
|
1618
|
+
--write.
|
|
1389
1619
|
|
|
1390
1620
|
Examples:
|
|
1391
1621
|
quilltap memories ls
|
|
@@ -1398,8 +1628,12 @@ Examples:
|
|
|
1398
1628
|
quilltap memories status --character Ariadne
|
|
1399
1629
|
quilltap memories validate
|
|
1400
1630
|
quilltap memories validate --list
|
|
1401
|
-
quilltap memories
|
|
1402
|
-
quilltap memories
|
|
1631
|
+
quilltap memories validate --repair # rehearse the repair
|
|
1632
|
+
quilltap memories validate --repair --write # apply it
|
|
1633
|
+
quilltap memories maintain --instance Friday --character Friday --dry-run
|
|
1634
|
+
quilltap memories maintain --instance Friday --character Friday --phases consolidate --max 100
|
|
1635
|
+
quilltap memories maintain --character all --phases demote,retire --dry-run
|
|
1636
|
+
quilltap memories maintain --character all
|
|
1403
1637
|
`);
|
|
1404
1638
|
}
|
|
1405
1639
|
|
|
@@ -1430,6 +1664,12 @@ async function memoriesCommand(args) {
|
|
|
1430
1664
|
printMemoriesHelp();
|
|
1431
1665
|
return;
|
|
1432
1666
|
}
|
|
1667
|
+
if (flags.repair && verb !== 'validate') {
|
|
1668
|
+
throw new Error('--repair applies only to memories validate.');
|
|
1669
|
+
}
|
|
1670
|
+
if (flags.write && !(verb === 'validate' && flags.repair)) {
|
|
1671
|
+
throw new Error('--write applies only to memories validate --repair.');
|
|
1672
|
+
}
|
|
1433
1673
|
switch (verb) {
|
|
1434
1674
|
case 'ls': await cmdLs(flags); break;
|
|
1435
1675
|
case 'find': await cmdFind(flags, positional); break;
|
|
@@ -1438,7 +1678,7 @@ async function memoriesCommand(args) {
|
|
|
1438
1678
|
case 'tree': await cmdTree(flags, positional); break;
|
|
1439
1679
|
case 'status': await cmdStatus(flags); break;
|
|
1440
1680
|
case 'validate': await cmdValidate(flags); break;
|
|
1441
|
-
case '
|
|
1681
|
+
case 'maintain': await cmdMaintain(flags, resolveCharacterIdForServer); break;
|
|
1442
1682
|
default:
|
|
1443
1683
|
console.error(`Unknown memories subcommand: ${verb}`);
|
|
1444
1684
|
console.error("Run 'quilltap memories --help' for a list.");
|
|
@@ -1451,6 +1691,7 @@ module.exports = {
|
|
|
1451
1691
|
// Exported for unit tests:
|
|
1452
1692
|
parseFlags,
|
|
1453
1693
|
resolveCharacterIdForServer,
|
|
1694
|
+
renderRepairText,
|
|
1454
1695
|
buildWhereClause,
|
|
1455
1696
|
buildOrderBy,
|
|
1456
1697
|
traverseMemoryGraph,
|