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
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { resolveServerPort } = require('./lock-helpers');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* `quilltap anchor-probe <characterId>` — measure whether the episodic anchor
|
|
5
|
-
* line embedded with each memory suppresses
|
|
7
|
+
* line embedded with each memory suppresses reinforcement at admission.
|
|
6
8
|
*
|
|
7
9
|
* Thin wrapper over POST /api/v1/memories?action=anchor-gate-probe.
|
|
8
10
|
* Read-only; nothing is persisted server-side, but it costs embedding calls.
|
|
@@ -23,16 +25,21 @@ Quilltap anchor-probe Tool
|
|
|
23
25
|
Usage: quilltap anchor-probe <characterId> [options]
|
|
24
26
|
|
|
25
27
|
Measures whether the episodic anchor line embedded with each memory suppresses
|
|
26
|
-
|
|
27
|
-
and without the anchor line, compares each against older rows,
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
reinforcement at admission. Re-embeds the character's most recent memories with
|
|
29
|
+
and without the anchor line, compares each against older rows, asks admission's
|
|
30
|
+
own decider what it would do each way, and reports how many rows would fold
|
|
31
|
+
into an older one (reinforce or better) and how many would be absorbed as
|
|
32
|
+
near-duplicates.
|
|
30
33
|
|
|
31
34
|
Read-only, but it costs embedding calls (roughly up to 7 per sampled row).
|
|
32
35
|
|
|
33
36
|
Options:
|
|
37
|
+
-d, --data-dir <path> Instance root; used to find the server's port
|
|
38
|
+
-i, --instance <name> Registered instance whose server to call (see 'quilltap instances')
|
|
39
|
+
--passphrase <pass> Accepted and ignored (this tool never opens the database)
|
|
34
40
|
--limit <number> Recent memories to sample (default: 50, max: 200)
|
|
35
|
-
--port <number> Server port for API calls
|
|
41
|
+
--port <number> Server port for API calls. Default: the port recorded in the
|
|
42
|
+
--instance/--data-dir server's lock file, else 3000
|
|
36
43
|
--json Print the raw JSON result instead of tables
|
|
37
44
|
-h, --help Show this help
|
|
38
45
|
|
|
@@ -44,7 +51,7 @@ Examples:
|
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
function parseFlags(args) {
|
|
47
|
-
const flags = { limit: undefined, port:
|
|
54
|
+
const flags = { limit: undefined, port: undefined, dataDir: '', instance: '', json: false, help: false };
|
|
48
55
|
const positional = [];
|
|
49
56
|
let i = 0;
|
|
50
57
|
while (i < args.length) {
|
|
@@ -59,6 +66,19 @@ function parseFlags(args) {
|
|
|
59
66
|
flags.limit = n;
|
|
60
67
|
break;
|
|
61
68
|
}
|
|
69
|
+
case '-d':
|
|
70
|
+
case '--data-dir':
|
|
71
|
+
flags.dataDir = args[++i];
|
|
72
|
+
break;
|
|
73
|
+
case '-i':
|
|
74
|
+
case '--instance':
|
|
75
|
+
flags.instance = args[++i];
|
|
76
|
+
break;
|
|
77
|
+
case '--passphrase':
|
|
78
|
+
// Accepted so universal flags never error; these tools talk to the
|
|
79
|
+
// running server and never open the database themselves.
|
|
80
|
+
i++;
|
|
81
|
+
break;
|
|
62
82
|
case '--port': {
|
|
63
83
|
const p = parseInt(args[++i], 10);
|
|
64
84
|
if (isNaN(p) || p < 1 || p > 65535) {
|
|
@@ -107,19 +127,19 @@ function printRows(rows, thresholds) {
|
|
|
107
127
|
console.log(
|
|
108
128
|
` ${DIM}${'anchored'.padEnd(10)}${'free'.padEnd(10)}${'delta'.padEnd(9)}${'anchor'.padEnd(8)}summary${RESET}`
|
|
109
129
|
);
|
|
110
|
-
const
|
|
130
|
+
const folds = (kind) => kind === 'reinforce' || kind === 'absorb' || kind === 'promote-and-reinforce';
|
|
111
131
|
for (const row of rows) {
|
|
112
132
|
const a = row.anchoredBest;
|
|
113
133
|
const f = row.anchorFreeBest;
|
|
114
134
|
const delta = typeof a === 'number' && typeof f === 'number' ? f - a : null;
|
|
115
|
-
const onlyFree =
|
|
135
|
+
const onlyFree = folds(row.anchorFreeAction) && !folds(row.anchoredAction);
|
|
116
136
|
const deltaStr = delta === null ? DIM + '—' + RESET : (delta >= 0 ? '+' : '') + delta.toFixed(3);
|
|
117
137
|
const anchorStr = row.hasAnchorLine ? 'yes' : `${DIM}no${RESET}`;
|
|
118
138
|
const summary = (row.summary || '').replace(/\s+/g, ' ').slice(0, 60);
|
|
119
139
|
const line = `${pad(fmt(a), 10)}${pad(fmt(f), 10)}${pad(deltaStr, 9)}${pad(anchorStr, 8)}${summary}`;
|
|
120
140
|
console.log(onlyFree ? `${YELLOW}▸ ${RESET}${line}` : ` ${line}`);
|
|
121
141
|
}
|
|
122
|
-
console.log(`\n ${YELLOW}▸${RESET} ${DIM}=
|
|
142
|
+
console.log(`\n ${YELLOW}▸${RESET} ${DIM}= would fold into an older row only without the anchor line (reinforce ≥ ${fmt(thresholds?.reinforce, 2)})${RESET}`);
|
|
123
143
|
}
|
|
124
144
|
|
|
125
145
|
async function anchorProbeCommand(args) {
|
|
@@ -135,7 +155,17 @@ async function anchorProbeCommand(args) {
|
|
|
135
155
|
}
|
|
136
156
|
const characterId = positional[0];
|
|
137
157
|
|
|
138
|
-
|
|
158
|
+
let target;
|
|
159
|
+
try {
|
|
160
|
+
target = resolveServerPort({ port: flags.port, instance: flags.instance, dataDir: flags.dataDir });
|
|
161
|
+
} catch (err) {
|
|
162
|
+
console.error(`Error: ${err.message}`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
const port = target.port;
|
|
166
|
+
if (target.note) process.stderr.write(`${DIM}${target.note}${RESET}\n`);
|
|
167
|
+
|
|
168
|
+
const url = `http://localhost:${port}/api/v1/memories?action=anchor-gate-probe`;
|
|
139
169
|
const body = { characterId };
|
|
140
170
|
if (flags.limit !== undefined) body.limit = flags.limit;
|
|
141
171
|
|
|
@@ -149,8 +179,8 @@ async function anchorProbeCommand(args) {
|
|
|
149
179
|
body: JSON.stringify(body),
|
|
150
180
|
});
|
|
151
181
|
} catch (err) {
|
|
152
|
-
console.error(`${RED}Could not reach Quilltap server at http://localhost:${
|
|
153
|
-
console.error(
|
|
182
|
+
console.error(`${RED}Could not reach Quilltap server at http://localhost:${port}: ${err.message}${RESET}`);
|
|
183
|
+
console.error(target.hint);
|
|
154
184
|
process.exit(1);
|
|
155
185
|
}
|
|
156
186
|
|
|
@@ -178,14 +208,15 @@ async function anchorProbeCommand(args) {
|
|
|
178
208
|
console.log(`\n${BOLD}Character${RESET} ${result.characterId}`);
|
|
179
209
|
console.log(`${BOLD}Sampled${RESET} ${result.sampled} rows, ${result.anchoredRows} with an anchor line`);
|
|
180
210
|
console.log(`${BOLD}Embeddings${RESET} ${result.embeddingsGenerated} generated`);
|
|
211
|
+
if (result.note) console.log(`${YELLOW}${result.note}${RESET}`);
|
|
181
212
|
console.log(`\n${DIM}${''.padEnd(22)}${'anchored'.padEnd(11)}anchor-free${RESET}`);
|
|
182
|
-
console.log(`${BOLD}${
|
|
183
|
-
console.log(`${BOLD}${
|
|
213
|
+
console.log(`${BOLD}${'would fold'.padEnd(22)}${RESET}${String(anchored.reinforce).padEnd(11)}${CYAN}${free.reinforce}${RESET}`);
|
|
214
|
+
console.log(`${BOLD}${'near-dup absorbed'.padEnd(22)}${RESET}${String(anchored.nearDuplicate).padEnd(11)}${CYAN}${free.nearDuplicate}${RESET}`);
|
|
184
215
|
const only = result.crossedOnlyWithoutAnchors;
|
|
185
|
-
console.log(`${BOLD}
|
|
216
|
+
console.log(`${BOLD}Fold only without anchors${RESET} ${only > 0 ? YELLOW : GREEN}${only}${RESET}`);
|
|
186
217
|
|
|
187
218
|
printRows(result.rows || [], t);
|
|
188
219
|
console.log('');
|
|
189
220
|
}
|
|
190
221
|
|
|
191
|
-
module.exports = { anchorProbeCommand, printAnchorProbeHelp };
|
|
222
|
+
module.exports = { anchorProbeCommand, printAnchorProbeHelp, parseFlags };
|
|
@@ -69,11 +69,11 @@ _quilltap_complete() {
|
|
|
69
69
|
# makes a flag's value look like the subcommand's verb, which is how
|
|
70
70
|
# `quilltap docs --limit 5 <TAB>` used to lose the verb list.
|
|
71
71
|
local vf_global=" -d --data-dir -i --instance -p --port --passphrase "
|
|
72
|
-
local vf_db=" --limit --grep --character --project --about --source --chat --message --field --tail --last --from --type --out --id --count "
|
|
72
|
+
local vf_db=" --limit --grep --character --project --about --source --tier --chat --message --field --tail --last --from --type --out --id --count "
|
|
73
73
|
local vf_docs=" --mount --folder --type --ext --limit --max --context --top --threshold --sort --depth --max-nodes --format "
|
|
74
74
|
local vf_sync=" --direction --prefer "
|
|
75
75
|
local vf_themes=" -o --output -k --key -n --name "
|
|
76
|
-
local vf_memories=" -d --data-dir --instance --passphrase --port --character --about --source --tier --chat --project --since --until --min-importance --min-reinforced --sort --limit --in --max --context --depth --max-nodes --top --threshold "
|
|
76
|
+
local vf_memories=" -d --data-dir --instance --passphrase -p --port --phases --cap --character --about --source --tier --chat --project --since --until --min-importance --min-reinforced --sort --limit --in --max --context --depth --max-nodes --top --threshold "
|
|
77
77
|
local vf_logs=" --stream --tail --grep "
|
|
78
78
|
local vf_memory_diff=" --concurrency --out "
|
|
79
79
|
local vf_recall_replay=" --turn --char --limit --memory-budget --tuning --tuning-file --signals-from --port "
|
|
@@ -185,6 +185,10 @@ _quilltap_complete() {
|
|
|
185
185
|
COMPREPLY=($(compgen -W "hot cold" -- "$cur"))
|
|
186
186
|
return
|
|
187
187
|
;;
|
|
188
|
+
--phases)
|
|
189
|
+
COMPREPLY=($(compgen -W "fold-near-duplicates consolidate demote retire mirror" -- "$cur"))
|
|
190
|
+
return
|
|
191
|
+
;;
|
|
188
192
|
--sort)
|
|
189
193
|
COMPREPLY=($(compgen -W "name time size links reinforced importance created accessed reinforcement-count" -- "$cur"))
|
|
190
194
|
return
|
|
@@ -220,7 +224,7 @@ _quilltap_complete() {
|
|
|
220
224
|
db)
|
|
221
225
|
local db_verbs="schema find chats messages logs message log memories characters optimize backup integrity"
|
|
222
226
|
local db_flags="--instance --data-dir --passphrase --json --limit --grep \
|
|
223
|
-
--character --project --about --source --chat --message --rendered --field \
|
|
227
|
+
--character --project --about --source --tier --chat --message --rendered --field \
|
|
224
228
|
--tail --last --full --from --type --out --id --diverged --blocked --help \
|
|
225
229
|
--tables --count --repl --write --llm-logs --mount-points \
|
|
226
230
|
--lock-status --lock-clean --lock-override"
|
|
@@ -317,13 +321,13 @@ _quilltap_complete() {
|
|
|
317
321
|
fi
|
|
318
322
|
;;
|
|
319
323
|
memories)
|
|
320
|
-
local mem_verbs="ls find grep show tree status validate
|
|
324
|
+
local mem_verbs="ls find grep show tree status validate maintain"
|
|
321
325
|
local mem_flags="--character --about --source --tier --chat --project --since --until \
|
|
322
326
|
--min-importance --min-reinforced --has-embedding --no-embedding \
|
|
323
327
|
--sort -r --reverse --limit --full-titles --in --no-related --list \
|
|
324
328
|
--ignore-case -i --paths-only -l --max --context --depth --max-nodes \
|
|
325
|
-
--semantic --top --threshold --dry-run \
|
|
326
|
-
--instance --data-dir --passphrase --port --json --help"
|
|
329
|
+
--semantic --top --threshold --dry-run --phases --cap --repair --write \
|
|
330
|
+
--instance --data-dir --passphrase -p --port --json --help"
|
|
327
331
|
if [[ -z "$subverb" ]]; then
|
|
328
332
|
if [[ "$cur" == -* ]]; then
|
|
329
333
|
COMPREPLY=($(compgen -W "$mem_flags" -- "$cur"))
|
|
@@ -339,11 +343,11 @@ _quilltap_complete() {
|
|
|
339
343
|
COMPREPLY=($(compgen -W "$md_flags" -- "$cur"))
|
|
340
344
|
;;
|
|
341
345
|
recall-replay)
|
|
342
|
-
local rr_flags="--turn --char --limit --memory-budget --tuning --tuning-file --signals-from --as-of --port --json --help"
|
|
346
|
+
local rr_flags="--instance --data-dir --passphrase --turn --char --limit --memory-budget --tuning --tuning-file --signals-from --as-of --port --json --help"
|
|
343
347
|
COMPREPLY=($(compgen -W "$rr_flags" -- "$cur"))
|
|
344
348
|
;;
|
|
345
349
|
anchor-probe)
|
|
346
|
-
local ap_flags="--limit --port --json --help"
|
|
350
|
+
local ap_flags="--instance --data-dir --passphrase --limit --port --json --help"
|
|
347
351
|
COMPREPLY=($(compgen -W "$ap_flags" -- "$cur"))
|
|
348
352
|
;;
|
|
349
353
|
logs)
|
|
@@ -114,6 +114,7 @@ complete -c quilltap -n '__quilltap_using_subcommand db' -l 'character' -d 'Char
|
|
|
114
114
|
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'project' -d 'Project name or id' -x
|
|
115
115
|
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'about' -d 'Subject character' -x
|
|
116
116
|
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'source' -d 'Memory source' -x -a 'AUTO MANUAL CONSOLIDATED'
|
|
117
|
+
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'tier' -d 'Memory tier' -x -a 'hot cold'
|
|
117
118
|
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'chat' -d 'Chat name or id' -x
|
|
118
119
|
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'message' -d 'Message id' -x
|
|
119
120
|
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'rendered' -d 'Render rich content'
|
|
@@ -272,14 +273,18 @@ complete -c quilltap -n '__quilltap_using_subcommand memories' -f -a 'show' -d '
|
|
|
272
273
|
complete -c quilltap -n '__quilltap_using_subcommand memories' -f -a 'tree' -d 'Graph walk'
|
|
273
274
|
complete -c quilltap -n '__quilltap_using_subcommand memories' -f -a 'status' -d 'Memory status'
|
|
274
275
|
complete -c quilltap -n '__quilltap_using_subcommand memories' -f -a 'validate' -d 'Health check'
|
|
275
|
-
complete -c quilltap -n '__quilltap_using_subcommand memories' -f -a '
|
|
276
|
+
complete -c quilltap -n '__quilltap_using_subcommand memories' -f -a 'maintain' -d 'Run memory maintenance'
|
|
276
277
|
|
|
277
278
|
# memories flags
|
|
278
279
|
complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'character' -d 'Character name or id' -x
|
|
279
280
|
complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'about' -d 'Subject of memory' -x
|
|
280
281
|
complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'source' -d 'Memory source' -x -a 'AUTO MANUAL CONSOLIDATED'
|
|
281
282
|
complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'tier' -d 'Memory tier' -x -a 'hot cold'
|
|
282
|
-
complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'dry-run' -d '
|
|
283
|
+
complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'dry-run' -d 'Maintain: rehearse and print the report, write nothing'
|
|
284
|
+
complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'phases' -d 'Maintain: comma-separated phases' -x -a 'fold-near-duplicates consolidate demote retire mirror'
|
|
285
|
+
complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'cap' -d 'Maintain: hot-row cap for demotion' -x
|
|
286
|
+
complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'repair' -d 'Validate: repair the graph on the running server'
|
|
287
|
+
complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'write' -d 'Validate --repair: apply the repair'
|
|
283
288
|
complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'chat' -d 'Chat id or title' -x
|
|
284
289
|
complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'project' -d 'Project name or id' -x
|
|
285
290
|
complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'since' -d 'Since timestamp' -x
|
|
@@ -152,6 +152,7 @@ _quilltap_db() {
|
|
|
152
152
|
'--project[Project name or id]:project:'
|
|
153
153
|
'--about[Subject character]:character:'
|
|
154
154
|
'--source[Memory source]:source:(AUTO MANUAL CONSOLIDATED)'
|
|
155
|
+
'--tier[Memory tier]:tier:(hot cold)'
|
|
155
156
|
'--chat[Chat name or id]:chat:'
|
|
156
157
|
'--message[Message id]:id:'
|
|
157
158
|
'--rendered[Render rich content]'
|
|
@@ -538,20 +539,24 @@ _quilltap_memories() {
|
|
|
538
539
|
'tree:ASCII walk of graph'
|
|
539
540
|
'status:Per-character rollup'
|
|
540
541
|
'validate:Health check'
|
|
541
|
-
'
|
|
542
|
+
'maintain:Run memory maintenance (fold, consolidate, demote, retire, mirror)'
|
|
542
543
|
)
|
|
543
544
|
|
|
544
545
|
mem_opts=(
|
|
545
546
|
'--instance[Registered instance name]:instance:_quilltap_instance_names'
|
|
546
547
|
'(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
|
|
547
548
|
'--passphrase[Database passphrase]:passphrase:'
|
|
548
|
-
'--port[Server port]:port:'
|
|
549
|
+
'(-p --port)'{-p,--port}'[Server port]:port:'
|
|
549
550
|
'--json[JSON output]'
|
|
550
551
|
'--character[Character name or id]:character:'
|
|
551
552
|
'--about[Subject of memory]:character:'
|
|
552
553
|
'--source[Memory source]:source:(AUTO MANUAL CONSOLIDATED)'
|
|
553
554
|
'--tier[Memory tier]:tier:(hot cold)'
|
|
554
|
-
'--dry-run[
|
|
555
|
+
'--dry-run[Maintain: rehearse and print the report, write nothing]'
|
|
556
|
+
'--phases[Maintain: comma-separated phases]:phases:_sequence compadd - fold-near-duplicates consolidate demote retire mirror'
|
|
557
|
+
'--cap[Maintain: hot-row cap for demotion]:n:'
|
|
558
|
+
'--repair[Validate: repair the graph on the running server (dry run without --write)]'
|
|
559
|
+
'--write[Validate --repair: apply the repair]'
|
|
555
560
|
'--chat[Chat id or title]:chat:'
|
|
556
561
|
'--project[Project name or id]:project:'
|
|
557
562
|
'--since[Since timestamp]:date:'
|
package/lib/db-commands.js
CHANGED
|
@@ -658,10 +658,20 @@ function cmdMemories(args, ctx) {
|
|
|
658
658
|
const { flags } = parseSubArgs(args);
|
|
659
659
|
const json = asBool(flags.json);
|
|
660
660
|
const limit = asInt(flags.limit, 50);
|
|
661
|
-
if (!flags.character) throw new Error('Usage: quilltap db memories --character <id|name> [--about <id|name>] [--source AUTO|MANUAL] [--limit N]');
|
|
661
|
+
if (!flags.character) throw new Error('Usage: quilltap db memories --character <id|name> [--about <id|name>] [--source AUTO|MANUAL|CONSOLIDATED] [--tier hot|cold] [--limit N]');
|
|
662
|
+
const tier = flags.tier === undefined ? null : String(flags.tier).toLowerCase();
|
|
663
|
+
if (tier !== null && tier !== 'hot' && tier !== 'cold') {
|
|
664
|
+
throw new Error(`--tier must be hot or cold (got '${flags.tier === true ? '' : flags.tier}')`);
|
|
665
|
+
}
|
|
662
666
|
|
|
663
667
|
const db = ctx.openMain();
|
|
664
668
|
try {
|
|
669
|
+
// Instances that predate the memory tiers have no `tier` column: every
|
|
670
|
+
// row reads as hot there.
|
|
671
|
+
let hasTier = false;
|
|
672
|
+
try {
|
|
673
|
+
hasTier = db.prepare("PRAGMA table_info('memories')").all().some((c) => c.name === 'tier');
|
|
674
|
+
} catch { /* treat as absent */ }
|
|
665
675
|
const holder = resolveCharacter(db, String(flags.character), ctx.openMounts);
|
|
666
676
|
const conditions = ['characterId = ?'];
|
|
667
677
|
const params = [holder.id];
|
|
@@ -674,8 +684,13 @@ function cmdMemories(args, ctx) {
|
|
|
674
684
|
conditions.push('source = ?');
|
|
675
685
|
params.push(String(flags.source).toUpperCase());
|
|
676
686
|
}
|
|
687
|
+
if (tier) {
|
|
688
|
+
if (hasTier) conditions.push(tier === 'hot' ? "COALESCE(tier, 'hot') = 'hot'" : "tier = 'cold'");
|
|
689
|
+
else if (tier === 'cold') conditions.push('0');
|
|
690
|
+
}
|
|
691
|
+
const tierSelect = hasTier ? "COALESCE(tier, 'hot') AS tier" : "'hot' AS tier";
|
|
677
692
|
const rows = db.prepare(`
|
|
678
|
-
SELECT id, createdAt, source, importance, reinforcementCount,
|
|
693
|
+
SELECT id, createdAt, source, ${tierSelect}, importance, reinforcementCount,
|
|
679
694
|
aboutCharacterId, chatId, summary
|
|
680
695
|
FROM memories WHERE ${conditions.join(' AND ')}
|
|
681
696
|
ORDER BY createdAt DESC LIMIT ?
|
|
@@ -685,11 +700,12 @@ function cmdMemories(args, ctx) {
|
|
|
685
700
|
|
|
686
701
|
if (json) return printJson({ holder, total: totalRow.n, returned: rows.length, memories: rows });
|
|
687
702
|
|
|
688
|
-
console.log(`Memories held by ${holder.name} (${holder.id}) — ${rows.length} of ${totalRow.n}`);
|
|
703
|
+
console.log(`Memories held by ${holder.name} (${holder.id})${tier ? ` [${tier}]` : ''} — ${rows.length} of ${totalRow.n}`);
|
|
689
704
|
printTable(rows.map(r => ({
|
|
690
705
|
createdAt: r.createdAt,
|
|
691
706
|
id: r.id,
|
|
692
707
|
src: r.source,
|
|
708
|
+
tier: r.tier,
|
|
693
709
|
imp: r.importance,
|
|
694
710
|
rein: r.reinforcementCount,
|
|
695
711
|
about: r.aboutCharacterId === holder.id ? 'self' : (r.aboutCharacterId || ''),
|
package/lib/graph-integrity.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
// Shared graph-integrity scanner used by `memories status` (per-holder
|
|
4
4
|
// rollup) and `memories validate` (read-only dangling-edge check).
|
|
5
5
|
//
|
|
6
|
+
// `scanDanglingEdges` covers `relatedMemoryIds`; `scanDigestIntegrity`
|
|
7
|
+
// covers the consolidation links (`consolidatedFrom`, `supersededById`) and
|
|
8
|
+
// rows with no embedding — the same four things the server's graph repair
|
|
9
|
+
// (`POST /api/v1/memories?action=repair`) finds and fixes.
|
|
10
|
+
//
|
|
6
11
|
// "Dangling edges" are UUIDs in `memories.relatedMemoryIds` that no longer
|
|
7
12
|
// resolve to a row in the `memories` table. Cross-character links are
|
|
8
13
|
// legitimate — a memory's links may point at memories owned by other holders
|
|
@@ -34,10 +39,7 @@ function scanDanglingEdges(db, opts = {}) {
|
|
|
34
39
|
// The valid set is *every* memory ID in the table. Computed once even
|
|
35
40
|
// when restricted to a single holder, because cross-character links are
|
|
36
41
|
// legitimate and we don't want them counted as dangling.
|
|
37
|
-
const allIds =
|
|
38
|
-
for (const row of db.prepare('SELECT id FROM memories').all()) {
|
|
39
|
-
allIds.add(row.id);
|
|
40
|
-
}
|
|
42
|
+
const allIds = opts.allIds || loadMemoryIds(db);
|
|
41
43
|
|
|
42
44
|
const scanRows = characterId
|
|
43
45
|
? db.prepare('SELECT id, characterId, relatedMemoryIds FROM memories WHERE characterId = ?').all(characterId)
|
|
@@ -102,4 +104,105 @@ function scanDanglingEdges(db, opts = {}) {
|
|
|
102
104
|
return result;
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
|
|
107
|
+
/** Every memory id in the table — the universe a link may legitimately name. */
|
|
108
|
+
function loadMemoryIds(db) {
|
|
109
|
+
const allIds = new Set();
|
|
110
|
+
for (const row of db.prepare('SELECT id FROM memories').all()) {
|
|
111
|
+
allIds.add(row.id);
|
|
112
|
+
}
|
|
113
|
+
return allIds;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The memories table's column names (old instances lack the tier-era columns). */
|
|
117
|
+
function memoryColumns(db) {
|
|
118
|
+
try {
|
|
119
|
+
return new Set(db.prepare("PRAGMA table_info('memories')").all().map((c) => c.name));
|
|
120
|
+
} catch {
|
|
121
|
+
return new Set();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Scan the consolidation links and embeddings for one holder (or every row).
|
|
127
|
+
*
|
|
128
|
+
* - dangling `consolidatedFrom`: member ids a digest names that no longer exist
|
|
129
|
+
* - dangling `supersededById`: members pointing at a digest that is gone
|
|
130
|
+
* - missing embeddings: rows with `embedding IS NULL` (not recallable by
|
|
131
|
+
* meaning until re-embedded; a warning, not graph damage)
|
|
132
|
+
*
|
|
133
|
+
* Columns absent on very old instances read as nothing to scan.
|
|
134
|
+
*
|
|
135
|
+
* @param {import('better-sqlite3').Database} db Open readonly DB handle.
|
|
136
|
+
* @param {object} [opts]
|
|
137
|
+
* @param {string} [opts.characterId] Restrict the scan to one holder.
|
|
138
|
+
* @param {boolean} [opts.includePairs] Return the offending rows (for --list).
|
|
139
|
+
* @param {Set<string>} [opts.allIds] Reuse a precomputed id universe.
|
|
140
|
+
* @returns {{
|
|
141
|
+
* consolidatedFrom: { links: number, digests: number },
|
|
142
|
+
* supersededBy: number,
|
|
143
|
+
* missingEmbeddings: number,
|
|
144
|
+
* consolidatedFromPairs?: Array<{ digestId: string, characterId: string, memberIds: string[] }>,
|
|
145
|
+
* supersededByPairs?: Array<{ memberId: string, characterId: string, digestId: string }>
|
|
146
|
+
* }}
|
|
147
|
+
*/
|
|
148
|
+
function scanDigestIntegrity(db, opts = {}) {
|
|
149
|
+
const { characterId, includePairs = false } = opts;
|
|
150
|
+
const cols = opts.columns || memoryColumns(db);
|
|
151
|
+
const allIds = opts.allIds || loadMemoryIds(db);
|
|
152
|
+
const where = characterId ? ' AND characterId = ?' : '';
|
|
153
|
+
const params = characterId ? [characterId] : [];
|
|
154
|
+
|
|
155
|
+
const result = {
|
|
156
|
+
consolidatedFrom: { links: 0, digests: 0 },
|
|
157
|
+
supersededBy: 0,
|
|
158
|
+
missingEmbeddings: 0,
|
|
159
|
+
};
|
|
160
|
+
if (includePairs) {
|
|
161
|
+
result.consolidatedFromPairs = [];
|
|
162
|
+
result.supersededByPairs = [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (cols.has('consolidatedFrom')) {
|
|
166
|
+
const rows = db.prepare(
|
|
167
|
+
`SELECT id, characterId, consolidatedFrom FROM memories
|
|
168
|
+
WHERE consolidatedFrom IS NOT NULL AND consolidatedFrom != '[]'${where}`
|
|
169
|
+
).all(...params);
|
|
170
|
+
for (const row of rows) {
|
|
171
|
+
let arr;
|
|
172
|
+
try { arr = JSON.parse(row.consolidatedFrom || '[]'); } catch { arr = []; }
|
|
173
|
+
if (!Array.isArray(arr)) continue;
|
|
174
|
+
const missing = arr.filter((id) => !allIds.has(id));
|
|
175
|
+
if (missing.length === 0) continue;
|
|
176
|
+
result.consolidatedFrom.links += missing.length;
|
|
177
|
+
result.consolidatedFrom.digests++;
|
|
178
|
+
if (includePairs) {
|
|
179
|
+
result.consolidatedFromPairs.push({ digestId: row.id, characterId: row.characterId, memberIds: missing });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (cols.has('supersededById')) {
|
|
185
|
+
const rows = db.prepare(
|
|
186
|
+
`SELECT id, characterId, supersededById FROM memories
|
|
187
|
+
WHERE supersededById IS NOT NULL${where}`
|
|
188
|
+
).all(...params);
|
|
189
|
+
for (const row of rows) {
|
|
190
|
+
if (allIds.has(row.supersededById)) continue;
|
|
191
|
+
result.supersededBy++;
|
|
192
|
+
if (includePairs) {
|
|
193
|
+
result.supersededByPairs.push({ memberId: row.id, characterId: row.characterId, digestId: row.supersededById });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (cols.has('embedding')) {
|
|
199
|
+
const row = db.prepare(
|
|
200
|
+
`SELECT COUNT(*) AS n FROM memories WHERE embedding IS NULL${where}`
|
|
201
|
+
).get(...params);
|
|
202
|
+
result.missingEmbeddings = row ? row.n || 0 : 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = { scanDanglingEdges, scanDigestIntegrity, loadMemoryIds, memoryColumns };
|
package/lib/lock-helpers.js
CHANGED
|
@@ -110,6 +110,70 @@ function getLockStatus(dataDir) {
|
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Server port resolution
|
|
115
|
+
//
|
|
116
|
+
// CLI tools that talk to a RUNNING server over HTTP (memory-diff, recall-replay,
|
|
117
|
+
// anchor-probe, memories consolidate) need its port. The server records the
|
|
118
|
+
// port it listens on in the instance lock, so naming an instance (or a data
|
|
119
|
+
// dir) is enough to find it. An explicit --port always wins.
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
const DEFAULT_SERVER_PORT = 3000;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Decide which port to call.
|
|
126
|
+
*
|
|
127
|
+
* @param {{ port?: number, instance?: string, dataDir?: string }} opts
|
|
128
|
+
* `port` is the explicit --port (undefined when not given).
|
|
129
|
+
* @returns {{ port: number, source: 'flag'|'lock'|'default', note: string|null,
|
|
130
|
+
* hint: string }}
|
|
131
|
+
* `note` is a one-line message to print when the lock supplied the port;
|
|
132
|
+
* `hint` is the connection-failure advice.
|
|
133
|
+
* @throws if both instance and dataDir are given, or the instance is unknown.
|
|
134
|
+
*/
|
|
135
|
+
function resolveServerPort({ port, instance, dataDir } = {}) {
|
|
136
|
+
const baseHint = 'Start the server (npm run dev) or pass --port to match a non-default port.';
|
|
137
|
+
if (port !== undefined && port !== null) {
|
|
138
|
+
return { port, source: 'flag', note: null, hint: baseHint };
|
|
139
|
+
}
|
|
140
|
+
if (!instance && !dataDir) {
|
|
141
|
+
return { port: DEFAULT_SERVER_PORT, source: 'default', note: null, hint: baseHint };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Lazy require: db-helpers is heavier and nothing else here needs it.
|
|
145
|
+
const { resolveDataDirAndPassphrase } = require('./db-helpers');
|
|
146
|
+
const resolved = resolveDataDirAndPassphrase({ instance, dataDir });
|
|
147
|
+
const label = resolved.instanceName || resolved.dataDir;
|
|
148
|
+
const status = getLockStatus(resolved.dataDir);
|
|
149
|
+
const lockPort = status.lock && Number.isInteger(status.lock.port) &&
|
|
150
|
+
status.lock.port >= 1 && status.lock.port <= 65535
|
|
151
|
+
? status.lock.port
|
|
152
|
+
: null;
|
|
153
|
+
|
|
154
|
+
if (status.state === 'active' && lockPort !== null) {
|
|
155
|
+
return {
|
|
156
|
+
port: lockPort,
|
|
157
|
+
source: 'lock',
|
|
158
|
+
note: `Using port ${lockPort} from the ${label} instance lock`,
|
|
159
|
+
hint: baseHint,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let why;
|
|
164
|
+
if (status.state === 'active') why = 'its lock records no port (server predates port recording)';
|
|
165
|
+
else if (status.state === 'absent') why = 'it has no instance lock, so no server appears to be running';
|
|
166
|
+
else if (status.state === 'stale') why = `its lock is stale (${status.reason})`;
|
|
167
|
+
else if (status.state === 'suspect') why = `its lock looks suspect (${status.reason})`;
|
|
168
|
+
else why = 'its lock file is unreadable';
|
|
169
|
+
return {
|
|
170
|
+
port: DEFAULT_SERVER_PORT,
|
|
171
|
+
source: 'default',
|
|
172
|
+
note: null,
|
|
173
|
+
hint: `${baseHint}\nNo port found for the ${label} instance: ${why}; fell back to ${DEFAULT_SERVER_PORT}.`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
113
177
|
// ============================================================================
|
|
114
178
|
// Write-lock acquire / release
|
|
115
179
|
//
|
|
@@ -354,4 +418,6 @@ module.exports = {
|
|
|
354
418
|
acquireWriteLock,
|
|
355
419
|
releaseWriteLock,
|
|
356
420
|
detectEnvironmentType,
|
|
421
|
+
resolveServerPort,
|
|
422
|
+
DEFAULT_SERVER_PORT,
|
|
357
423
|
};
|