quilltap 4.10.0-dev.99 → 4.11.0-dev.106
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 +70 -11
- package/bin/quilltap.js +20 -3
- package/lib/__tests__/completion-coverage.test.js +1 -0
- package/lib/__tests__/memories-maintain-command.test.js +276 -0
- package/lib/__tests__/migrations-source-ids.test.js +54 -0
- package/lib/__tests__/server-port-resolution.test.js +122 -0
- package/lib/anchor-probe-command.js +222 -0
- package/lib/completion/bash.template +25 -11
- package/lib/completion/fish.template +26 -4
- package/lib/completion/zsh.template +35 -3
- package/lib/db-commands.js +20 -3
- package/lib/graph-integrity.js +108 -5
- package/lib/lock-helpers.js +66 -0
- package/lib/memories-commands.js +474 -75
- package/lib/memories-maintain-command.js +238 -0
- package/lib/memories-server.js +105 -0
- package/lib/memory-diff-command.js +28 -6
- package/lib/migrations-commands.js +18 -4
- package/lib/recall-replay-command.js +195 -11
- package/lib/text-codec.js +30 -1
- package/package.json +2 -2
package/lib/memories-commands.js
CHANGED
|
@@ -11,7 +11,14 @@ const {
|
|
|
11
11
|
resolveChat,
|
|
12
12
|
resolveProject,
|
|
13
13
|
} = require('./db-helpers');
|
|
14
|
-
const {
|
|
14
|
+
const {
|
|
15
|
+
scanDanglingEdges,
|
|
16
|
+
scanDigestIntegrity,
|
|
17
|
+
loadMemoryIds,
|
|
18
|
+
memoryColumns,
|
|
19
|
+
} = require('./graph-integrity');
|
|
20
|
+
const { cmdMaintain } = require('./memories-maintain-command');
|
|
21
|
+
const { postMemoriesAction, putMemory, resolveMemoriesServerPort } = require('./memories-server');
|
|
15
22
|
|
|
16
23
|
// ---------- colour & marker helpers ----------
|
|
17
24
|
|
|
@@ -88,11 +95,19 @@ function parseFlags(args) {
|
|
|
88
95
|
noRelated: false,
|
|
89
96
|
// validate
|
|
90
97
|
list: false,
|
|
98
|
+
repair: false,
|
|
99
|
+
write: false,
|
|
91
100
|
// semantic search
|
|
92
101
|
semantic: false,
|
|
93
102
|
top: 0,
|
|
94
103
|
threshold: -1,
|
|
95
|
-
port
|
|
104
|
+
// Undefined unless --port is given, so the instance lock can supply it
|
|
105
|
+
// (resolveServerPort in lock-helpers).
|
|
106
|
+
port: undefined,
|
|
107
|
+
// maintain
|
|
108
|
+
dryRun: false,
|
|
109
|
+
phases: '',
|
|
110
|
+
cap: null,
|
|
96
111
|
};
|
|
97
112
|
const positional = [];
|
|
98
113
|
let i = 0;
|
|
@@ -108,6 +123,7 @@ function parseFlags(args) {
|
|
|
108
123
|
case '--character': flags.character = args[++i]; break;
|
|
109
124
|
case '--about': flags.about = args[++i]; break;
|
|
110
125
|
case '--source': flags.source = args[++i]; break;
|
|
126
|
+
case '--tier': flags.tier = args[++i]; break;
|
|
111
127
|
case '--chat': flags.chat = args[++i]; break;
|
|
112
128
|
case '--project': flags.project = args[++i]; break;
|
|
113
129
|
case '--since': flags.since = args[++i]; break;
|
|
@@ -134,8 +150,24 @@ function parseFlags(args) {
|
|
|
134
150
|
case '--no-related': flags.noRelated = true; break;
|
|
135
151
|
|
|
136
152
|
case '--list': flags.list = true; break;
|
|
153
|
+
case '--repair': flags.repair = true; break;
|
|
154
|
+
case '--write': flags.write = true; break;
|
|
155
|
+
case '--keystone': flags.keystone = true; break;
|
|
156
|
+
case '--off': flags.off = true; break;
|
|
157
|
+
|
|
158
|
+
case '--phases': flags.phases = args[++i] || ''; break;
|
|
159
|
+
case '--cap': {
|
|
160
|
+
const v = Number(args[++i]);
|
|
161
|
+
if (!Number.isInteger(v)) {
|
|
162
|
+
console.error('Error: --cap must be an integer');
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
flags.cap = v;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
137
168
|
|
|
138
169
|
case '--semantic': flags.semantic = true; break;
|
|
170
|
+
case '--dry-run': flags.dryRun = true; break;
|
|
139
171
|
case '--top': flags.top = parseInt(args[++i], 10) || 0; break;
|
|
140
172
|
case '--threshold': {
|
|
141
173
|
const v = parseFloat(args[++i]);
|
|
@@ -146,7 +178,7 @@ function parseFlags(args) {
|
|
|
146
178
|
flags.threshold = v;
|
|
147
179
|
break;
|
|
148
180
|
}
|
|
149
|
-
case '--port': {
|
|
181
|
+
case '-p': case '--port': {
|
|
150
182
|
const p = parseInt(args[++i], 10);
|
|
151
183
|
if (isNaN(p) || p < 1 || p > 65535) {
|
|
152
184
|
console.error('Error: --port must be between 1 and 65535');
|
|
@@ -220,13 +252,27 @@ function buildWhereClause(db, flags, openMounts = null) {
|
|
|
220
252
|
|
|
221
253
|
if (flags.source) {
|
|
222
254
|
const s = String(flags.source).toUpperCase();
|
|
223
|
-
if (s !== 'AUTO' && s !== 'MANUAL') {
|
|
224
|
-
throw new Error(`--source must be AUTO or
|
|
255
|
+
if (s !== 'AUTO' && s !== 'MANUAL' && s !== 'CONSOLIDATED') {
|
|
256
|
+
throw new Error(`--source must be AUTO, MANUAL or CONSOLIDATED (got '${flags.source}')`);
|
|
225
257
|
}
|
|
226
258
|
clauses.push('m.source = ?');
|
|
227
259
|
params.push(s);
|
|
228
260
|
}
|
|
229
261
|
|
|
262
|
+
if (flags.keystone) {
|
|
263
|
+
// Instances that haven't run add-memory-keystone-v1 have no keystones.
|
|
264
|
+
clauses.push(hasMemoryColumn(db, 'keystone') ? 'm.keystone = 1' : '0');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (flags.tier) {
|
|
268
|
+
const t = String(flags.tier).toLowerCase();
|
|
269
|
+
if (t !== 'hot' && t !== 'cold') {
|
|
270
|
+
throw new Error(`--tier must be hot or cold (got '${flags.tier}')`);
|
|
271
|
+
}
|
|
272
|
+
// Legacy instances without the column read every row as hot.
|
|
273
|
+
clauses.push(t === 'hot' ? "COALESCE(m.tier, 'hot') = 'hot'" : "m.tier = 'cold'");
|
|
274
|
+
}
|
|
275
|
+
|
|
230
276
|
if (flags.chat) {
|
|
231
277
|
if (flags.chat === 'none') {
|
|
232
278
|
clauses.push('m.chatId IS NULL');
|
|
@@ -313,7 +359,7 @@ function buildOrderBy(sortFlag, reverse) {
|
|
|
313
359
|
// SELECT fragment that pulls every column the renderers need, including the
|
|
314
360
|
// joined-in character / chat names. `m.*` is followed by aliases for the joins
|
|
315
361
|
// so callers don't have to re-resolve UUIDs.
|
|
316
|
-
const
|
|
362
|
+
const SELECT_BASE_TEMPLATE = `
|
|
317
363
|
SELECT
|
|
318
364
|
m.id,
|
|
319
365
|
m.characterId,
|
|
@@ -331,6 +377,7 @@ const SELECT_BASE = `
|
|
|
331
377
|
m.lastReinforcedAt,
|
|
332
378
|
m.lastAccessedAt,
|
|
333
379
|
m.source,
|
|
380
|
+
__TIER_COLUMNS__
|
|
334
381
|
m.relatedMemoryIds,
|
|
335
382
|
m.createdAt,
|
|
336
383
|
m.updatedAt,
|
|
@@ -346,6 +393,26 @@ const SELECT_BASE = `
|
|
|
346
393
|
LEFT JOIN projects ON projects.id = m.projectId
|
|
347
394
|
`;
|
|
348
395
|
|
|
396
|
+
function hasMemoryColumn(db, name) {
|
|
397
|
+
try {
|
|
398
|
+
return db.prepare("PRAGMA table_info('memories')").all().some((c) => c.name === name);
|
|
399
|
+
} catch {
|
|
400
|
+
return false; // treat as legacy
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Instances that haven't yet run add-memory-tiers-v1 lack the tier columns (and
|
|
405
|
+
// those before add-memory-keystone-v1 the keystone column); substitute
|
|
406
|
+
// constants so the CLI still reads them (rows read as hot, not keystones).
|
|
407
|
+
function selectBase(db) {
|
|
408
|
+
const tierCols = hasMemoryColumn(db, 'tier')
|
|
409
|
+
? 'm.tier, m.supersededById, m.consolidatedFrom, m.consolidatedAt,'
|
|
410
|
+
: "'hot' AS tier, NULL AS supersededById, '[]' AS consolidatedFrom, NULL AS consolidatedAt,";
|
|
411
|
+
const keystoneCol = hasMemoryColumn(db, 'keystone') ? 'm.keystone,' : '0 AS keystone,';
|
|
412
|
+
return SELECT_BASE_TEMPLATE.replace('__TIER_COLUMNS__', `${tierCols} ${keystoneCol}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
349
416
|
// ---------- output helpers ----------
|
|
350
417
|
|
|
351
418
|
function truncate(str, n) {
|
|
@@ -404,7 +471,7 @@ async function cmdLs(flags) {
|
|
|
404
471
|
const { where, params, meta } = buildWhereClause(db, flags, openMounts);
|
|
405
472
|
const { order, impField } = buildOrderBy(flags.sort, flags.reverse);
|
|
406
473
|
const limit = flags.limit > 0 ? flags.limit : 50;
|
|
407
|
-
const sql = `${
|
|
474
|
+
const sql = `${selectBase(db)} ${where} ORDER BY ${order} LIMIT ?`;
|
|
408
475
|
const rows = db.prepare(sql).all(...params, limit);
|
|
409
476
|
|
|
410
477
|
if (flags.json) {
|
|
@@ -441,6 +508,11 @@ function rowToJson(row) {
|
|
|
441
508
|
projectName: row.projectName,
|
|
442
509
|
sourceMessageId: row.sourceMessageId,
|
|
443
510
|
source: row.source,
|
|
511
|
+
tier: row.tier || 'hot',
|
|
512
|
+
supersededById: row.supersededById || null,
|
|
513
|
+
consolidatedFrom: (() => { try { return JSON.parse(row.consolidatedFrom || '[]'); } catch { return []; } })(),
|
|
514
|
+
consolidatedAt: row.consolidatedAt || null,
|
|
515
|
+
keystone: Number(row.keystone) === 1,
|
|
444
516
|
importance: row.importance,
|
|
445
517
|
reinforcedImportance: row.reinforcedImportance,
|
|
446
518
|
reinforcementCount: row.reinforcementCount,
|
|
@@ -469,7 +541,7 @@ function renderTable(rows, { showHolder, impField, fullTitles }) {
|
|
|
469
541
|
showHolder ? 'holder'.padEnd(14) : null,
|
|
470
542
|
impHeader.padStart(4),
|
|
471
543
|
'rein'.padStart(4),
|
|
472
|
-
'src'.padEnd(
|
|
544
|
+
'src'.padEnd(7),
|
|
473
545
|
'about'.padEnd(20),
|
|
474
546
|
'chat'.padEnd(fullTitles ? 24 : 32),
|
|
475
547
|
'links'.padStart(5),
|
|
@@ -483,12 +555,13 @@ function renderTable(rows, { showHolder, impField, fullTitles }) {
|
|
|
483
555
|
const impVal = impField === 'importance' ? row.importance : row.reinforcedImportance;
|
|
484
556
|
const impStr = colorize(formatImportance(impVal), importanceColor(impVal));
|
|
485
557
|
const rein = String(row.reinforcementCount ?? 0).padStart(4);
|
|
486
|
-
const src = (row.source || '').padEnd(
|
|
558
|
+
const src = ((row.source || '') + (row.tier === 'cold' ? '*' : '')).padEnd(7);
|
|
487
559
|
const about = truncate(aboutLabel(row), 20).padEnd(20);
|
|
488
560
|
const chat = truncate(chatLabel(row, fullTitles), fullTitles ? 24 : 32).padEnd(fullTitles ? 24 : 32);
|
|
489
561
|
const links = String(linkCount(row)).padStart(5);
|
|
490
562
|
const emb = row.hasEmbedding ? 'Y' : '-';
|
|
491
|
-
|
|
563
|
+
// A keystone's line is marked ◆ (a standing fact; never folded or demoted).
|
|
564
|
+
const summary = (Number(row.keystone) === 1 ? '◆ ' : '') + truncate(row.summary || '', sumW);
|
|
492
565
|
|
|
493
566
|
const cells = [
|
|
494
567
|
showHolder ? truncate(row.holderName || '(?)', 14).padEnd(14) : null,
|
|
@@ -556,7 +629,7 @@ async function cmdFind(flags, positional) {
|
|
|
556
629
|
|
|
557
630
|
const limit = flags.limit > 0 ? flags.limit : 50;
|
|
558
631
|
const fullWhere = where ? `${where} AND ${matchSql}` : `WHERE ${matchSql}`;
|
|
559
|
-
const sql = `${
|
|
632
|
+
const sql = `${selectBase(db)} ${fullWhere} ORDER BY ${order} LIMIT ?`;
|
|
560
633
|
const allParams = [...params, ...matchParams, ...orderParams, limit];
|
|
561
634
|
const rows = db.prepare(sql).all(...allParams);
|
|
562
635
|
|
|
@@ -608,7 +681,7 @@ async function cmdSemanticGrep(flags, query) {
|
|
|
608
681
|
|
|
609
682
|
const top = flags.top > 0 ? flags.top : 20;
|
|
610
683
|
const threshold = flags.threshold >= 0 ? flags.threshold : 0.5;
|
|
611
|
-
const port = flags
|
|
684
|
+
const { port } = resolveMemoriesServerPort(flags);
|
|
612
685
|
const url = `http://localhost:${port}/api/v1/memories?action=search`;
|
|
613
686
|
|
|
614
687
|
const body = { characterId, query, limit: top, minScore: threshold };
|
|
@@ -691,7 +764,7 @@ async function cmdGrep(flags, positional) {
|
|
|
691
764
|
const fullWhere = where ? `${where} AND ${contentClause}` : `WHERE ${contentClause}`;
|
|
692
765
|
|
|
693
766
|
const limit = flags.limit > 0 ? flags.limit : 50;
|
|
694
|
-
const sql = `${
|
|
767
|
+
const sql = `${selectBase(db)} ${fullWhere}
|
|
695
768
|
ORDER BY m.reinforcedImportance DESC, m.createdAt DESC
|
|
696
769
|
LIMIT ?`;
|
|
697
770
|
const rows = db.prepare(sql).all(...params, likeNeedle, limit);
|
|
@@ -805,7 +878,7 @@ async function cmdShow(flags, positional) {
|
|
|
805
878
|
const { db } = await openDb(flags);
|
|
806
879
|
try {
|
|
807
880
|
const id = resolveMemoryId(db, idArg);
|
|
808
|
-
const row = db.prepare(`${
|
|
881
|
+
const row = db.prepare(`${selectBase(db)} WHERE m.id = ?`).get(id);
|
|
809
882
|
if (!row) {
|
|
810
883
|
throw new Error(`Memory ${id} not found`);
|
|
811
884
|
}
|
|
@@ -878,6 +951,8 @@ function renderShowText(row, graph, depth) {
|
|
|
878
951
|
console.log(` Holder: ${holderName} (${shortId(row.characterId)})`);
|
|
879
952
|
console.log(` About: ${aboutName}${row.aboutCharacterId && row.aboutCharacterId !== row.characterId ? ` (${shortId(row.aboutCharacterId)})` : ''}`);
|
|
880
953
|
console.log(` Source: ${row.source || '(?)'}`);
|
|
954
|
+
console.log(` Tier: ${row.tier || 'hot'}${row.supersededById ? ` (superseded by ${shortId(row.supersededById)})` : ''}`);
|
|
955
|
+
console.log(` Keystone: ${Number(row.keystone) === 1 ? 'yes (standing fact)' : 'no'}`);
|
|
881
956
|
const reinf = row.reinforcedImportance != null ? Number(row.reinforcedImportance).toFixed(2) : '?';
|
|
882
957
|
const baseImp = row.importance != null ? Number(row.importance).toFixed(2) : '?';
|
|
883
958
|
console.log(` Importance: ${reinf} (reinforced from ${baseImp}, count: ${row.reinforcementCount ?? 0})`);
|
|
@@ -1065,9 +1140,23 @@ async function cmdStatus(flags) {
|
|
|
1065
1140
|
`).all();
|
|
1066
1141
|
}
|
|
1067
1142
|
|
|
1143
|
+
// The parent's maintenance ledger (last run per phase, last demotion's
|
|
1144
|
+
// effectiveness) lives in the running server; ask it, briefly. Without a
|
|
1145
|
+
// server the job rows still say when maintenance last ran.
|
|
1146
|
+
let ledgerPort = 3000;
|
|
1147
|
+
try {
|
|
1148
|
+
ledgerPort = resolveMemoriesServerPort(flags, { quiet: true }).port;
|
|
1149
|
+
} catch { /* openDb already validated the instance; fall back to the default */ }
|
|
1150
|
+
const ledger = await fetchMaintenanceLedger(ledgerPort);
|
|
1151
|
+
|
|
1068
1152
|
const report = [];
|
|
1069
1153
|
for (const holder of holderRows) {
|
|
1070
1154
|
const stats = computeHolderStats(db, holder.id);
|
|
1155
|
+
stats.maintenance = {
|
|
1156
|
+
recentJobs: recentMaintenanceJobs(db, holder.id),
|
|
1157
|
+
ledger: ledger ? ledger.find((e) => e.characterId === holder.id) || null : null,
|
|
1158
|
+
ledgerReachable: !!ledger,
|
|
1159
|
+
};
|
|
1071
1160
|
report.push({ holder, stats });
|
|
1072
1161
|
}
|
|
1073
1162
|
|
|
@@ -1100,6 +1189,41 @@ async function cmdStatus(flags) {
|
|
|
1100
1189
|
}
|
|
1101
1190
|
}
|
|
1102
1191
|
|
|
1192
|
+
/** The server's maintenance ledger, or null when the server cannot be reached. */
|
|
1193
|
+
async function fetchMaintenanceLedger(port) {
|
|
1194
|
+
try {
|
|
1195
|
+
const res = await fetch(`http://localhost:${port}/api/v1/memories?action=maintenance-status`, {
|
|
1196
|
+
signal: AbortSignal.timeout(1500),
|
|
1197
|
+
});
|
|
1198
|
+
if (!res.ok) return null;
|
|
1199
|
+
const payload = await res.json();
|
|
1200
|
+
return Array.isArray(payload && payload.ledger) ? payload.ledger : null;
|
|
1201
|
+
} catch {
|
|
1202
|
+
return null;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/** The character's most recent MEMORY_MAINTENANCE job rows, newest first. */
|
|
1207
|
+
function recentMaintenanceJobs(db, characterId) {
|
|
1208
|
+
try {
|
|
1209
|
+
return db.prepare(`
|
|
1210
|
+
SELECT id, status, updatedAt, lastError,
|
|
1211
|
+
json_extract(payload, '$.reason') AS reason,
|
|
1212
|
+
json_extract(payload, '$.phases') AS phases
|
|
1213
|
+
FROM background_jobs
|
|
1214
|
+
WHERE type = 'MEMORY_MAINTENANCE'
|
|
1215
|
+
AND json_extract(payload, '$.characterId') = ?
|
|
1216
|
+
ORDER BY updatedAt DESC
|
|
1217
|
+
LIMIT 3
|
|
1218
|
+
`).all(characterId).map((row) => ({
|
|
1219
|
+
...row,
|
|
1220
|
+
phases: row.phases ? JSON.parse(row.phases) : null,
|
|
1221
|
+
}));
|
|
1222
|
+
} catch {
|
|
1223
|
+
return [];
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1103
1227
|
function computeHolderStats(db, characterId) {
|
|
1104
1228
|
const counts = db.prepare(`
|
|
1105
1229
|
SELECT
|
|
@@ -1115,6 +1239,19 @@ function computeHolderStats(db, characterId) {
|
|
|
1115
1239
|
WHERE characterId = ?
|
|
1116
1240
|
`).get(characterId);
|
|
1117
1241
|
|
|
1242
|
+
// Tier rollup — guarded so pre-tier instances still report.
|
|
1243
|
+
let tiers = { hot: counts.total || 0, cold: 0, consolidated: 0 };
|
|
1244
|
+
try {
|
|
1245
|
+
const t = db.prepare(`
|
|
1246
|
+
SELECT
|
|
1247
|
+
SUM(CASE WHEN COALESCE(tier, 'hot') = 'hot' THEN 1 ELSE 0 END) AS hot,
|
|
1248
|
+
SUM(CASE WHEN tier = 'cold' THEN 1 ELSE 0 END) AS cold,
|
|
1249
|
+
SUM(CASE WHEN source = 'CONSOLIDATED' THEN 1 ELSE 0 END) AS consolidated
|
|
1250
|
+
FROM memories WHERE characterId = ?
|
|
1251
|
+
`).get(characterId);
|
|
1252
|
+
tiers = { hot: t.hot || 0, cold: t.cold || 0, consolidated: t.consolidated || 0 };
|
|
1253
|
+
} catch { /* tier column absent: everything is hot */ }
|
|
1254
|
+
|
|
1118
1255
|
const graph = scanDanglingEdges(db, { characterId });
|
|
1119
1256
|
|
|
1120
1257
|
const topMemories = db.prepare(`
|
|
@@ -1131,6 +1268,7 @@ function computeHolderStats(db, characterId) {
|
|
|
1131
1268
|
auto: counts.auto || 0,
|
|
1132
1269
|
manual: counts.manual || 0,
|
|
1133
1270
|
},
|
|
1271
|
+
tiers,
|
|
1134
1272
|
aboutDistribution: {
|
|
1135
1273
|
selfReferential: counts.selfRef || 0,
|
|
1136
1274
|
aboutOthers: counts.aboutOthers || 0,
|
|
@@ -1156,6 +1294,10 @@ function renderStatusBlock(holder, stats) {
|
|
|
1156
1294
|
console.log(` Total memories: ${stats.counts.total}`);
|
|
1157
1295
|
console.log(` AUTO: ${stats.counts.auto}`);
|
|
1158
1296
|
console.log(` MANUAL: ${stats.counts.manual}`);
|
|
1297
|
+
console.log(` Tiers:`);
|
|
1298
|
+
console.log(` hot: ${stats.tiers.hot}`);
|
|
1299
|
+
console.log(` cold: ${stats.tiers.cold}`);
|
|
1300
|
+
console.log(` digests (CONSOLIDATED): ${stats.tiers.consolidated}`);
|
|
1159
1301
|
console.log(` About-distribution:`);
|
|
1160
1302
|
console.log(` self-referential: ${stats.aboutDistribution.selfReferential}`);
|
|
1161
1303
|
console.log(` about-others: ${stats.aboutDistribution.aboutOthers}`);
|
|
@@ -1172,6 +1314,7 @@ function renderStatusBlock(holder, stats) {
|
|
|
1172
1314
|
console.log(` max degree: ${stats.graph.maxDegree}`);
|
|
1173
1315
|
const danglingSuffix = stats.graph.danglingEdges > 0 ? ' ⚠ JSON UUIDs that no longer resolve' : '';
|
|
1174
1316
|
console.log(` dangling edges: ${stats.graph.danglingEdges}${danglingSuffix}`);
|
|
1317
|
+
if (stats.maintenance) renderMaintenance(stats.maintenance);
|
|
1175
1318
|
if (stats.top.length) {
|
|
1176
1319
|
console.log(` Top by reinforcedImportance:`);
|
|
1177
1320
|
for (const t of stats.top) {
|
|
@@ -1181,81 +1324,248 @@ function renderStatusBlock(holder, stats) {
|
|
|
1181
1324
|
}
|
|
1182
1325
|
}
|
|
1183
1326
|
|
|
1327
|
+
function renderMaintenance(m) {
|
|
1328
|
+
console.log(` Maintenance:`);
|
|
1329
|
+
if (m.ledger) {
|
|
1330
|
+
// The phases are the server's; print the ones it reports a run for.
|
|
1331
|
+
const runs = Object.entries(m.ledger.lastRun || {});
|
|
1332
|
+
if (runs.length === 0) console.log(' no phase has run since the server started');
|
|
1333
|
+
for (const [phase, run] of runs) {
|
|
1334
|
+
console.log(` ${(phase + ':').padEnd(22)}${run.at} (${run.reason})`);
|
|
1335
|
+
}
|
|
1336
|
+
const d = m.ledger.lastDemotion;
|
|
1337
|
+
if (d) {
|
|
1338
|
+
if (d.excess === 0) {
|
|
1339
|
+
console.log(` last demotion: within the cap${d.effective ? '' : '; automatic demotion looks again in an hour'}`);
|
|
1340
|
+
} else {
|
|
1341
|
+
console.log(` last demotion: ${d.demoted} of ${d.excess} over the cap${d.effective ? '' : ' ⚠ ineffective; watermark backing off'}`);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
} else if (!m.ledgerReachable) {
|
|
1345
|
+
console.log(` ledger: (server not reachable; job history only)`);
|
|
1346
|
+
}
|
|
1347
|
+
if (m.recentJobs.length === 0) {
|
|
1348
|
+
console.log(` jobs: none recorded`);
|
|
1349
|
+
}
|
|
1350
|
+
for (const job of m.recentJobs) {
|
|
1351
|
+
const phases = Array.isArray(job.phases) ? job.phases.join(',') : 'all';
|
|
1352
|
+
const err = job.lastError ? ` ⚠ ${truncate(job.lastError, 60)}` : '';
|
|
1353
|
+
console.log(` job ${shortId(job.id)} ${job.status.padEnd(10)} ${job.updatedAt} ${job.reason || '?'} [${phases}]${err}`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1184
1357
|
// ---------- validate ----------
|
|
1185
1358
|
|
|
1186
1359
|
async function cmdValidate(flags) {
|
|
1360
|
+
let characterId = null;
|
|
1361
|
+
let local;
|
|
1187
1362
|
const { db, openMounts } = await openDb(flags);
|
|
1188
1363
|
try {
|
|
1189
|
-
let holderIds = null;
|
|
1190
1364
|
let holderRows;
|
|
1191
1365
|
if (flags.character && flags.character !== 'all') {
|
|
1192
1366
|
const c = resolveCharacter(db, flags.character, openMounts);
|
|
1193
1367
|
holderRows = [{ id: c.id, name: c.name }];
|
|
1194
|
-
|
|
1368
|
+
characterId = c.id;
|
|
1195
1369
|
} else {
|
|
1370
|
+
// Every holder in the memories table — including one whose character
|
|
1371
|
+
// row is gone — so the local scan covers the same rows the server's
|
|
1372
|
+
// unscoped repair scans and scrubs (review of PR 98).
|
|
1196
1373
|
holderRows = db.prepare(`
|
|
1197
|
-
SELECT
|
|
1198
|
-
|
|
1199
|
-
|
|
1374
|
+
SELECT h.characterId AS id, COALESCE(c.name, '(missing character ' || substr(h.characterId, 1, 8) || ')') AS name
|
|
1375
|
+
FROM (SELECT DISTINCT characterId FROM memories) h
|
|
1376
|
+
LEFT JOIN characters c ON c.id = h.characterId
|
|
1377
|
+
ORDER BY c.name IS NULL, c.name
|
|
1200
1378
|
`).all();
|
|
1201
1379
|
}
|
|
1380
|
+
local = scanForValidate(db, holderRows, Boolean(flags.list));
|
|
1381
|
+
} finally {
|
|
1382
|
+
db.close();
|
|
1383
|
+
}
|
|
1202
1384
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
}
|
|
1385
|
+
// --repair hands the fix (or its rehearsal) to the running server, which is
|
|
1386
|
+
// the only database writer; --write is the explicit opt-in to the real thing.
|
|
1387
|
+
let repair = null;
|
|
1388
|
+
if (flags.repair) {
|
|
1389
|
+
const body = { dryRun: !flags.write };
|
|
1390
|
+
if (characterId) body.characterId = characterId;
|
|
1391
|
+
const payload = await postMemoriesAction(flags, 'repair', body, {
|
|
1392
|
+
why: 'The repair runs inside the server (it is the only database writer, and it queues the embedding jobs).',
|
|
1393
|
+
announce: flags.json ? undefined : (url) => process.stderr.write(
|
|
1394
|
+
`${body.dryRun ? 'Rehearsing' : 'Running'} graph repair via ${url}\n`
|
|
1395
|
+
),
|
|
1396
|
+
});
|
|
1397
|
+
repair = (payload && payload.report) || null;
|
|
1398
|
+
}
|
|
1218
1399
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1400
|
+
if (flags.json) {
|
|
1401
|
+
renderJson({
|
|
1402
|
+
scanned: characterId ? 'single' : 'all',
|
|
1403
|
+
characterIds: local.perHolder.map(({ holder }) => holder.id),
|
|
1404
|
+
totals: local.totals,
|
|
1405
|
+
holders: local.perHolder.map(({ holder, stats, digests }) => ({
|
|
1406
|
+
holder: { id: holder.id, name: holder.name },
|
|
1407
|
+
...stats,
|
|
1408
|
+
...digests,
|
|
1409
|
+
})),
|
|
1410
|
+
...(flags.repair ? { repair } : {}),
|
|
1411
|
+
});
|
|
1412
|
+
} else {
|
|
1413
|
+
renderValidateText(local, characterId, flags);
|
|
1414
|
+
if (flags.repair) renderRepairText(repair);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// Exit 1 while dangling links remain: always after a read-only validate or
|
|
1418
|
+
// a rehearsal; after a --write repair only if the server reported nothing
|
|
1419
|
+
// repaired (a missing embedding alone is a warning, never a failure).
|
|
1420
|
+
const t = local.totals;
|
|
1421
|
+
const dangling = t.danglingEdges + t.danglingConsolidatedFrom + t.danglingSupersededBy;
|
|
1422
|
+
const repaired = Boolean(flags.repair && flags.write && repair && repair.repaired);
|
|
1423
|
+
if (dangling > 0 && !repaired) {
|
|
1424
|
+
process.exitCode = 1;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/** Run every read-only integrity scan over the holders. */
|
|
1429
|
+
function scanForValidate(db, holderRows, includePairs) {
|
|
1430
|
+
const allIds = loadMemoryIds(db);
|
|
1431
|
+
const columns = memoryColumns(db);
|
|
1432
|
+
const totals = {
|
|
1433
|
+
nodes: 0,
|
|
1434
|
+
edges: 0,
|
|
1435
|
+
danglingEdges: 0,
|
|
1436
|
+
danglingConsolidatedFrom: 0,
|
|
1437
|
+
digestsWithDanglingMembers: 0,
|
|
1438
|
+
danglingSupersededBy: 0,
|
|
1439
|
+
missingEmbeddings: 0,
|
|
1440
|
+
};
|
|
1441
|
+
const perHolder = [];
|
|
1442
|
+
for (const holder of holderRows) {
|
|
1443
|
+
const stats = scanDanglingEdges(db, { characterId: holder.id, includePairs, allIds });
|
|
1444
|
+
const digests = scanDigestIntegrity(db, { characterId: holder.id, includePairs, allIds, columns });
|
|
1445
|
+
totals.nodes += stats.nodes;
|
|
1446
|
+
totals.edges += stats.totalEdges;
|
|
1447
|
+
totals.danglingEdges += stats.danglingEdges;
|
|
1448
|
+
totals.danglingConsolidatedFrom += digests.consolidatedFrom.links;
|
|
1449
|
+
totals.digestsWithDanglingMembers += digests.consolidatedFrom.digests;
|
|
1450
|
+
totals.danglingSupersededBy += digests.supersededBy;
|
|
1451
|
+
totals.missingEmbeddings += digests.missingEmbeddings;
|
|
1452
|
+
perHolder.push({ holder, stats, digests });
|
|
1453
|
+
}
|
|
1454
|
+
return { totals, perHolder };
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function renderValidateText(local, characterId, flags) {
|
|
1458
|
+
const { totals: t, perHolder } = local;
|
|
1459
|
+
const scope = characterId ? `holder ${perHolder[0].holder.name}` : `${perHolder.length} holders`;
|
|
1460
|
+
const drift = t.edges > 0 ? ((t.danglingEdges / t.edges) * 100).toFixed(2) : '0.00';
|
|
1461
|
+
console.log(`Scanned ${scope}: ${t.nodes} memories, ${t.edges} edges, ${t.danglingEdges} dangling (${drift}%).`);
|
|
1462
|
+
console.log(`Digests: ${t.danglingConsolidatedFrom} dangling consolidatedFrom member(s) across ` +
|
|
1463
|
+
`${t.digestsWithDanglingMembers} digest(s); ${t.danglingSupersededBy} dangling supersededById.`);
|
|
1464
|
+
if (t.missingEmbeddings > 0) {
|
|
1465
|
+
console.log(colorize(`Warning: ${t.missingEmbeddings} memories have no embedding and cannot be recalled by meaning.`, YELLOW));
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
const dangling = t.danglingEdges + t.danglingConsolidatedFrom + t.danglingSupersededBy;
|
|
1469
|
+
if (dangling === 0) {
|
|
1470
|
+
console.log(colorize('Graph is clean.', GREEN));
|
|
1471
|
+
} else if (flags.list) {
|
|
1472
|
+
console.log('');
|
|
1473
|
+
for (const { holder, stats, digests } of perHolder) {
|
|
1474
|
+
const related = stats.danglingPairs || [];
|
|
1475
|
+
const members = digests.consolidatedFromPairs || [];
|
|
1476
|
+
const superseded = digests.supersededByPairs || [];
|
|
1477
|
+
if (related.length + members.length + superseded.length === 0) continue;
|
|
1478
|
+
console.log(colorize(`${holder.name} (${shortId(holder.id)})`, BOLD));
|
|
1479
|
+
for (const pair of related) {
|
|
1480
|
+
console.log(` related ${shortId(pair.sourceId)} → ${pair.targetIds.map(shortId).join(', ')}`);
|
|
1481
|
+
}
|
|
1482
|
+
for (const pair of members) {
|
|
1483
|
+
console.log(` digest ${shortId(pair.digestId)} ⊃ ${pair.memberIds.map(shortId).join(', ')}`);
|
|
1484
|
+
}
|
|
1485
|
+
for (const pair of superseded) {
|
|
1486
|
+
console.log(` superseded ${shortId(pair.memberId)} → ${shortId(pair.digestId)}`);
|
|
1250
1487
|
}
|
|
1251
1488
|
}
|
|
1489
|
+
} else {
|
|
1490
|
+
console.log(colorize('Run with --list to see the offending memory IDs and dangling targets.', YELLOW));
|
|
1491
|
+
}
|
|
1492
|
+
if ((dangling > 0 || t.missingEmbeddings > 0) && !flags.repair) {
|
|
1493
|
+
console.log(colorize('Run with --repair to rehearse the fix on the running server (add --write to apply it).', YELLOW));
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1252
1496
|
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1497
|
+
function renderRepairText(report) {
|
|
1498
|
+
if (!report) {
|
|
1499
|
+
console.log(colorize('The server returned no repair report.', YELLOW));
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
const f = report.found || {};
|
|
1503
|
+
const related = f.related || { links: 0, rows: 0 };
|
|
1504
|
+
const cf = f.consolidatedFrom || { links: 0, digests: 0 };
|
|
1505
|
+
console.log('');
|
|
1506
|
+
console.log(colorize(`Server graph repair (${report.dryRun ? 'dry run' : 'applied'}${report.characterId ? '' : ', every memory'}):`, BOLD));
|
|
1507
|
+
console.log(` found: ${related.links} dangling related link(s) on ${related.rows} row(s); ` +
|
|
1508
|
+
`${cf.links} dangling consolidatedFrom member(s) on ${cf.digests} digest(s); ` +
|
|
1509
|
+
`${f.supersededBy || 0} dangling supersededById; ${f.missingEmbeddings || 0} row(s) without an embedding.`);
|
|
1510
|
+
if (report.dryRun) {
|
|
1511
|
+
const anything = related.links + cf.links + (f.supersededBy || 0) + (f.missingEmbeddings || 0);
|
|
1512
|
+
console.log(anything > 0
|
|
1513
|
+
? colorize(' Nothing was changed. Add --write to scrub the links and queue the embedding jobs.', YELLOW)
|
|
1514
|
+
: colorize(' Nothing to repair.', GREEN));
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
const r = report.repaired || {};
|
|
1518
|
+
console.log(` repaired: ${r.relatedRowsTouched || 0} row(s) relinked, ${r.digestsTouched || 0} digest(s) touched, ` +
|
|
1519
|
+
`${r.membersCut || 0} member(s) cut loose, ${r.charactersAffected || 0} character(s) affected; ` +
|
|
1520
|
+
`${r.embeddingJobsQueued || 0} embedding job(s) queued.`);
|
|
1521
|
+
if (r.embeddingSkippedReason) {
|
|
1522
|
+
console.log(colorize(` Embedding jobs not queued: ${r.embeddingSkippedReason} (set a default embedding profile, then run again).`, YELLOW));
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// ---------- keystone ----------
|
|
1527
|
+
|
|
1528
|
+
/**
|
|
1529
|
+
* Mark or unmark a keystone. The id resolves locally (read-only); the change
|
|
1530
|
+
* itself goes to the running server's memory PUT, through the edit chokepoint
|
|
1531
|
+
* that enforces the per-subject limit and invalidates the frozen archive.
|
|
1532
|
+
*/
|
|
1533
|
+
async function cmdKeystone(flags, positional) {
|
|
1534
|
+
const idArg = positional[0];
|
|
1535
|
+
if (!idArg) {
|
|
1536
|
+
throw new Error('memories keystone: missing <id>. Usage: quilltap memories keystone <id|prefix> [--off] [--write]');
|
|
1537
|
+
}
|
|
1538
|
+
const want = !flags.off;
|
|
1539
|
+
const { db } = await openDb(flags);
|
|
1540
|
+
let row;
|
|
1541
|
+
try {
|
|
1542
|
+
const id = resolveMemoryId(db, idArg);
|
|
1543
|
+
row = db.prepare(`${selectBase(db)} WHERE m.id = ?`).get(id);
|
|
1544
|
+
if (!row) throw new Error(`Memory ${id} not found`);
|
|
1256
1545
|
} finally {
|
|
1257
1546
|
db.close();
|
|
1258
1547
|
}
|
|
1548
|
+
const is = Number(row.keystone) === 1;
|
|
1549
|
+
const label = `${shortId(row.id)} (${truncate(row.summary || '', 60)})`;
|
|
1550
|
+
if (is === want) {
|
|
1551
|
+
const msg = `${label} is ${want ? 'already' : 'not'} a keystone; nothing to do.`;
|
|
1552
|
+
if (flags.json) renderJson({ id: row.id, keystone: is, changed: false }); else console.log(msg);
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
if (!flags.write) {
|
|
1556
|
+
const msg = `Would ${want ? 'mark' : 'unmark'} ${label} as a keystone. Re-run with --write to apply.`;
|
|
1557
|
+
if (flags.json) renderJson({ id: row.id, keystone: is, wouldBe: want, changed: false, dryRun: true }); else console.log(msg);
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
const payload = await putMemory(flags, row.id, { keystone: want }, {
|
|
1561
|
+
why: 'Changing a keystone needs the running server (the only database writer).',
|
|
1562
|
+
});
|
|
1563
|
+
const now = Boolean(payload && payload.memory && payload.memory.keystone);
|
|
1564
|
+
if (flags.json) {
|
|
1565
|
+
renderJson({ id: row.id, keystone: now, changed: now === want });
|
|
1566
|
+
} else {
|
|
1567
|
+
console.log(`${want ? 'Marked' : 'Unmarked'} ${label} as a keystone.`);
|
|
1568
|
+
}
|
|
1259
1569
|
}
|
|
1260
1570
|
|
|
1261
1571
|
// ---------- help ----------
|
|
@@ -1287,16 +1597,67 @@ Subcommands:
|
|
|
1287
1597
|
tree <id|prefix> [--depth N] [--max-nodes N] [--json]
|
|
1288
1598
|
ASCII walk of the
|
|
1289
1599
|
related-memory graph.
|
|
1290
|
-
status [--character <name|id>] [--json]
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1600
|
+
status [--character <name|id>] [--json] [--port <n>]
|
|
1601
|
+
Per-holder rollup, maintenance
|
|
1602
|
+
ledger + dangling-edge check.
|
|
1603
|
+
validate [--character <name|id>] [--list] [--json]
|
|
1604
|
+
Read-only memory-graph health
|
|
1605
|
+
check: dangling relatedMemoryIds,
|
|
1606
|
+
consolidatedFrom members and
|
|
1607
|
+
supersededById links, plus rows
|
|
1608
|
+
with no embedding (a warning).
|
|
1609
|
+
Exits 1 if any dangling link
|
|
1610
|
+
remains.
|
|
1611
|
+
validate --repair [--write] [--character <name|id>] [--port N]
|
|
1612
|
+
Ask the running server to repair
|
|
1613
|
+
the graph. Without --write it is
|
|
1614
|
+
a dry run: it reports what would
|
|
1615
|
+
be repaired and changes nothing.
|
|
1616
|
+
With --write the server scrubs
|
|
1617
|
+
the dangling links and queues
|
|
1618
|
+
embedding jobs for unembedded
|
|
1619
|
+
rows. The write runs inside the
|
|
1620
|
+
server (the only database
|
|
1621
|
+
writer, holder of the instance
|
|
1622
|
+
lock), so the server must be up.
|
|
1623
|
+
keystone <id|prefix> [--off] [--write] [--port N]
|
|
1624
|
+
Mark a memory a keystone (a
|
|
1625
|
+
standing fact, always recalled in
|
|
1626
|
+
full, never folded, consolidated
|
|
1627
|
+
or demoted), or unmark it with
|
|
1628
|
+
--off. Without --write it only
|
|
1629
|
+
says what would change; with
|
|
1630
|
+
--write the running server makes
|
|
1631
|
+
the change (at most 5 keystones
|
|
1632
|
+
per holder and subject).
|
|
1633
|
+
maintain --character <name|id|all> [--phases a,b,…] [--dry-run]
|
|
1634
|
+
[--max N] [--threshold 0..1] [--cap N] [--port N] [--json]
|
|
1635
|
+
Run memory maintenance by hand on
|
|
1636
|
+
the running server. Phases, in
|
|
1637
|
+
run order: fold-near-duplicates,
|
|
1638
|
+
consolidate, demote, retire,
|
|
1639
|
+
mirror (default: all; naming
|
|
1640
|
+
consolidate brings mirror).
|
|
1641
|
+
--dry-run prints what each phase
|
|
1642
|
+
would do and writes nothing;
|
|
1643
|
+
otherwise one background job per
|
|
1644
|
+
character is queued. 'all' runs
|
|
1645
|
+
every character (a dry run over
|
|
1646
|
+
'all' must leave out consolidate).
|
|
1647
|
+
--max caps clusters consolidated,
|
|
1648
|
+
--threshold sets the cluster
|
|
1649
|
+
cosine (neural embedding
|
|
1650
|
+
profiles), --cap the hot-row cap
|
|
1651
|
+
for demotion (100-100000).
|
|
1652
|
+
To consolidate only:
|
|
1653
|
+
maintain --phases consolidate.
|
|
1295
1654
|
|
|
1296
1655
|
Shared filter flags:
|
|
1297
1656
|
--character <name|id|all> Holder. Default: all.
|
|
1298
1657
|
--about <name|id|self|none> Subject (aboutCharacterId).
|
|
1299
|
-
--source AUTO|MANUAL
|
|
1658
|
+
--source AUTO|MANUAL|CONSOLIDATED Restrict by source.
|
|
1659
|
+
--tier hot|cold Restrict by recall tier (default: both).
|
|
1660
|
+
--keystone Only keystones (marked ◆ in tables).
|
|
1300
1661
|
--chat <id|title|none> Source chat ('none' for manual memories).
|
|
1301
1662
|
--project <id|name> Project context.
|
|
1302
1663
|
--since <date> ISO date floor on createdAt.
|
|
@@ -1317,6 +1678,10 @@ Global flags (may appear before or after the subcommand):
|
|
|
1317
1678
|
-d, --data-dir <path> Override data directory.
|
|
1318
1679
|
--instance <name> Use a registered instance.
|
|
1319
1680
|
--passphrase <pass> Decrypt .dbkey if peppered.
|
|
1681
|
+
-p, --port <n> Server port for the verbs that call the running
|
|
1682
|
+
server (default: the port in the instance's
|
|
1683
|
+
lock when --instance/--data-dir is given, else
|
|
1684
|
+
3000).
|
|
1320
1685
|
--json Machine-readable output.
|
|
1321
1686
|
-h, --help Show this help.
|
|
1322
1687
|
|
|
@@ -1324,7 +1689,9 @@ Note: -i is reserved here for 'grep --ignore-case'. Use the long --instance
|
|
|
1324
1689
|
form to target a registered instance.
|
|
1325
1690
|
|
|
1326
1691
|
All memories verbs are read-only. They open the main encrypted database
|
|
1327
|
-
(quilltap.db) and never write to it.
|
|
1692
|
+
(quilltap.db) and never write to it. 'maintain', 'keystone' and 'validate
|
|
1693
|
+
--repair' leave the writing to the running server; 'keystone' and 'validate
|
|
1694
|
+
--repair' change data only with --write.
|
|
1328
1695
|
|
|
1329
1696
|
Examples:
|
|
1330
1697
|
quilltap memories ls
|
|
@@ -1337,11 +1704,30 @@ Examples:
|
|
|
1337
1704
|
quilltap memories status --character Ariadne
|
|
1338
1705
|
quilltap memories validate
|
|
1339
1706
|
quilltap memories validate --list
|
|
1707
|
+
quilltap memories validate --repair # rehearse the repair
|
|
1708
|
+
quilltap memories validate --repair --write # apply it
|
|
1709
|
+
quilltap memories ls --character Friday --keystone
|
|
1710
|
+
quilltap memories keystone abc12345 --write # mark a standing fact
|
|
1711
|
+
quilltap memories keystone abc12345 --off --write # unmark it
|
|
1712
|
+
quilltap memories maintain --instance Friday --character Friday --dry-run
|
|
1713
|
+
quilltap memories maintain --instance Friday --character Friday --phases consolidate --max 100
|
|
1714
|
+
quilltap memories maintain --character all --phases demote,retire --dry-run
|
|
1715
|
+
quilltap memories maintain --character all
|
|
1340
1716
|
`);
|
|
1341
1717
|
}
|
|
1342
1718
|
|
|
1343
1719
|
// ---------- dispatcher ----------
|
|
1344
1720
|
|
|
1721
|
+
/** Resolve --character locally to a UUID, for verbs that call the running server. */
|
|
1722
|
+
async function resolveCharacterIdForServer(flags) {
|
|
1723
|
+
const { db, openMounts } = await openDb(flags);
|
|
1724
|
+
try {
|
|
1725
|
+
return resolveCharacter(db, flags.character, openMounts).id;
|
|
1726
|
+
} finally {
|
|
1727
|
+
db.close();
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1345
1731
|
async function memoriesCommand(args) {
|
|
1346
1732
|
if (args.length === 0 || args[0] === '-h' || args[0] === '--help') {
|
|
1347
1733
|
printMemoriesHelp();
|
|
@@ -1357,6 +1743,15 @@ async function memoriesCommand(args) {
|
|
|
1357
1743
|
printMemoriesHelp();
|
|
1358
1744
|
return;
|
|
1359
1745
|
}
|
|
1746
|
+
if (flags.repair && verb !== 'validate') {
|
|
1747
|
+
throw new Error('--repair applies only to memories validate.');
|
|
1748
|
+
}
|
|
1749
|
+
if (flags.write && !(verb === 'validate' && flags.repair) && verb !== 'keystone') {
|
|
1750
|
+
throw new Error('--write applies only to memories validate --repair and memories keystone.');
|
|
1751
|
+
}
|
|
1752
|
+
if (flags.off && verb !== 'keystone') {
|
|
1753
|
+
throw new Error('--off applies only to memories keystone.');
|
|
1754
|
+
}
|
|
1360
1755
|
switch (verb) {
|
|
1361
1756
|
case 'ls': await cmdLs(flags); break;
|
|
1362
1757
|
case 'find': await cmdFind(flags, positional); break;
|
|
@@ -1365,6 +1760,8 @@ async function memoriesCommand(args) {
|
|
|
1365
1760
|
case 'tree': await cmdTree(flags, positional); break;
|
|
1366
1761
|
case 'status': await cmdStatus(flags); break;
|
|
1367
1762
|
case 'validate': await cmdValidate(flags); break;
|
|
1763
|
+
case 'keystone': await cmdKeystone(flags, positional); break;
|
|
1764
|
+
case 'maintain': await cmdMaintain(flags, resolveCharacterIdForServer); break;
|
|
1368
1765
|
default:
|
|
1369
1766
|
console.error(`Unknown memories subcommand: ${verb}`);
|
|
1370
1767
|
console.error("Run 'quilltap memories --help' for a list.");
|
|
@@ -1376,6 +1773,8 @@ module.exports = {
|
|
|
1376
1773
|
memoriesCommand,
|
|
1377
1774
|
// Exported for unit tests:
|
|
1378
1775
|
parseFlags,
|
|
1776
|
+
resolveCharacterIdForServer,
|
|
1777
|
+
renderRepairText,
|
|
1379
1778
|
buildWhereClause,
|
|
1380
1779
|
buildOrderBy,
|
|
1381
1780
|
traverseMemoryGraph,
|