quilltap 4.10.0-dev.99 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Universal-flag parsing and server-port resolution for the HTTP diagnostic
3
+ * tools (anchor-probe, recall-replay, memory-diff).
4
+ *
5
+ * @jest-environment node
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+
14
+ const { resolveServerPort } = require('../lock-helpers');
15
+ const tools = {
16
+ 'anchor-probe': require('../anchor-probe-command').parseFlags,
17
+ 'recall-replay': require('../recall-replay-command').parseFlags,
18
+ 'memory-diff': require('../memory-diff-command').parseFlags,
19
+ };
20
+
21
+ describe.each(Object.keys(tools))('%s flag parsing', (name) => {
22
+ const parseFlags = tools[name];
23
+
24
+ it.each([
25
+ [['--instance', 'V4test', 'ID']],
26
+ [['ID', '--instance', 'V4test']],
27
+ [['-i', 'V4test', 'ID']],
28
+ [['ID', '-i', 'V4test']],
29
+ ])('accepts the instance flag around the positional id (%j)', (args) => {
30
+ const { flags, positional } = parseFlags(args);
31
+ expect(flags.instance).toBe('V4test');
32
+ expect(positional).toEqual(['ID']);
33
+ });
34
+
35
+ it.each([
36
+ [['--data-dir', '/x/y', 'ID']],
37
+ [['ID', '-d', '/x/y']],
38
+ ])('accepts the data-dir flag (%j)', (args) => {
39
+ const { flags, positional } = parseFlags(args);
40
+ expect(flags.dataDir).toBe('/x/y');
41
+ expect(positional).toEqual(['ID']);
42
+ });
43
+
44
+ it('accepts --passphrase without treating its value as the id', () => {
45
+ const { positional } = parseFlags(['--passphrase', 'hunter2', 'ID']);
46
+ expect(positional).toEqual(['ID']);
47
+ expect(parseFlags(['ID', '--passphrase', 'hunter2']).positional).toEqual(['ID']);
48
+ });
49
+
50
+ it('leaves the port undefined unless --port is given', () => {
51
+ expect(parseFlags(['ID']).flags.port).toBeUndefined();
52
+ expect(parseFlags(['ID', '--port', '3005']).flags.port).toBe(3005);
53
+ });
54
+ });
55
+
56
+ describe('resolveServerPort', () => {
57
+ let root;
58
+ let lockPath;
59
+
60
+ function writeLock(overrides = {}) {
61
+ const now = new Date().toISOString();
62
+ fs.writeFileSync(lockPath, JSON.stringify({
63
+ pid: process.pid,
64
+ hostname: os.hostname(),
65
+ startedAt: now,
66
+ lastHeartbeat: now,
67
+ environment: 'local',
68
+ processTitle: 'node',
69
+ processArgv0: process.argv[0],
70
+ history: [],
71
+ ...overrides,
72
+ }));
73
+ }
74
+
75
+ beforeEach(() => {
76
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'qtap-port-test-'));
77
+ fs.mkdirSync(path.join(root, 'data'));
78
+ lockPath = path.join(root, 'data', 'quilltap.lock');
79
+ });
80
+
81
+ afterEach(() => fs.rmSync(root, { recursive: true, force: true }));
82
+
83
+ it('defaults to 3000 with no flags at all', () => {
84
+ expect(resolveServerPort({})).toMatchObject({ port: 3000, source: 'default', note: null });
85
+ });
86
+
87
+ it('lets an explicit --port win over a live lock', () => {
88
+ writeLock({ port: 3005 });
89
+ expect(resolveServerPort({ port: 4000, dataDir: root })).toMatchObject({ port: 4000, source: 'flag' });
90
+ });
91
+
92
+ it('uses the port from a live lock and says so', () => {
93
+ writeLock({ port: 3005 });
94
+ const r = resolveServerPort({ dataDir: root });
95
+ expect(r).toMatchObject({ port: 3005, source: 'lock' });
96
+ expect(r.note).toContain('3005');
97
+ });
98
+
99
+ it('falls back to 3000 when the live lock records no port', () => {
100
+ writeLock();
101
+ const r = resolveServerPort({ dataDir: root });
102
+ expect(r).toMatchObject({ port: 3000, source: 'default', note: null });
103
+ expect(r.hint).toContain('records no port');
104
+ });
105
+
106
+ it('ignores the port of a stale lock', () => {
107
+ writeLock({ pid: 2 ** 22 + 12345, port: 3005 });
108
+ const r = resolveServerPort({ dataDir: root });
109
+ expect(r.port).toBe(3000);
110
+ expect(r.hint).toContain('stale');
111
+ });
112
+
113
+ it('falls back to 3000 when there is no lock', () => {
114
+ const r = resolveServerPort({ dataDir: root });
115
+ expect(r.port).toBe(3000);
116
+ expect(r.hint).toContain('no instance lock');
117
+ });
118
+
119
+ it('rejects --instance together with --data-dir', () => {
120
+ expect(() => resolveServerPort({ instance: 'x', dataDir: root })).toThrow(/not both/);
121
+ });
122
+ });
@@ -0,0 +1,222 @@
1
+ 'use strict';
2
+
3
+ const { resolveServerPort } = require('./lock-helpers');
4
+
5
+ /**
6
+ * `quilltap anchor-probe <characterId>` — measure whether the episodic anchor
7
+ * line embedded with each memory suppresses reinforcement at admission.
8
+ *
9
+ * Thin wrapper over POST /api/v1/memories?action=anchor-gate-probe.
10
+ * Read-only; nothing is persisted server-side, but it costs embedding calls.
11
+ */
12
+
13
+ const RESET = '\x1b[0m';
14
+ const BOLD = '\x1b[1m';
15
+ const DIM = '\x1b[2m';
16
+ const GREEN = '\x1b[32m';
17
+ const RED = '\x1b[31m';
18
+ const YELLOW = '\x1b[33m';
19
+ const CYAN = '\x1b[36m';
20
+
21
+ function printAnchorProbeHelp() {
22
+ console.log(`
23
+ Quilltap anchor-probe Tool
24
+
25
+ Usage: quilltap anchor-probe <characterId> [options]
26
+
27
+ Measures whether the episodic anchor line embedded with each memory suppresses
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.
33
+
34
+ Read-only, but it costs embedding calls (roughly up to 7 per sampled row).
35
+
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)
40
+ --limit <number> Recent memories to sample (default: 50, max: 200)
41
+ --port <number> Server port for API calls. Default: the port recorded in the
42
+ --instance/--data-dir server's lock file, else 3000
43
+ --json Print the raw JSON result instead of tables
44
+ -h, --help Show this help
45
+
46
+ Examples:
47
+ quilltap anchor-probe <characterId>
48
+ quilltap anchor-probe <characterId> --limit 20
49
+ quilltap anchor-probe <characterId> --json > probe.json
50
+ `);
51
+ }
52
+
53
+ function parseFlags(args) {
54
+ const flags = { limit: undefined, port: undefined, dataDir: '', instance: '', json: false, help: false };
55
+ const positional = [];
56
+ let i = 0;
57
+ while (i < args.length) {
58
+ const a = args[i];
59
+ switch (a) {
60
+ case '--limit': {
61
+ const n = parseInt(args[++i], 10);
62
+ if (isNaN(n) || n < 1 || n > 200) {
63
+ console.error('Error: --limit must be between 1 and 200');
64
+ process.exit(1);
65
+ }
66
+ flags.limit = n;
67
+ break;
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;
82
+ case '--port': {
83
+ const p = parseInt(args[++i], 10);
84
+ if (isNaN(p) || p < 1 || p > 65535) {
85
+ console.error('Error: --port must be between 1 and 65535');
86
+ process.exit(1);
87
+ }
88
+ flags.port = p;
89
+ break;
90
+ }
91
+ case '--json':
92
+ flags.json = true;
93
+ break;
94
+ case '-h':
95
+ case '--help':
96
+ flags.help = true;
97
+ break;
98
+ default:
99
+ if (a.startsWith('-')) {
100
+ console.error(`Unknown option: ${a}`);
101
+ process.exit(1);
102
+ }
103
+ positional.push(a);
104
+ }
105
+ i++;
106
+ }
107
+ return { flags, positional };
108
+ }
109
+
110
+ function fmt(n, digits = 3) {
111
+ if (n === null || n === undefined) return DIM + '—' + RESET;
112
+ return n.toFixed(digits);
113
+ }
114
+
115
+ // Pad a possibly colour-wrapped string to a visible width.
116
+ function pad(s, width) {
117
+ const visible = String(s).replace(/\x1b\[[0-9;]*m/g, '').length;
118
+ return String(s) + ' '.repeat(Math.max(0, width - visible));
119
+ }
120
+
121
+ function printRows(rows, thresholds) {
122
+ console.log(`\n${BOLD}Rows${RESET} (${rows.length})`);
123
+ if (rows.length === 0) {
124
+ console.log(` ${DIM}(none)${RESET}`);
125
+ return;
126
+ }
127
+ console.log(
128
+ ` ${DIM}${'anchored'.padEnd(10)}${'free'.padEnd(10)}${'delta'.padEnd(9)}${'anchor'.padEnd(8)}summary${RESET}`
129
+ );
130
+ const folds = (kind) => kind === 'reinforce' || kind === 'absorb' || kind === 'promote-and-reinforce';
131
+ for (const row of rows) {
132
+ const a = row.anchoredBest;
133
+ const f = row.anchorFreeBest;
134
+ const delta = typeof a === 'number' && typeof f === 'number' ? f - a : null;
135
+ const onlyFree = folds(row.anchorFreeAction) && !folds(row.anchoredAction);
136
+ const deltaStr = delta === null ? DIM + '—' + RESET : (delta >= 0 ? '+' : '') + delta.toFixed(3);
137
+ const anchorStr = row.hasAnchorLine ? 'yes' : `${DIM}no${RESET}`;
138
+ const summary = (row.summary || '').replace(/\s+/g, ' ').slice(0, 60);
139
+ const line = `${pad(fmt(a), 10)}${pad(fmt(f), 10)}${pad(deltaStr, 9)}${pad(anchorStr, 8)}${summary}`;
140
+ console.log(onlyFree ? `${YELLOW}▸ ${RESET}${line}` : ` ${line}`);
141
+ }
142
+ console.log(`\n ${YELLOW}▸${RESET} ${DIM}= would fold into an older row only without the anchor line (reinforce ≥ ${fmt(thresholds?.reinforce, 2)})${RESET}`);
143
+ }
144
+
145
+ async function anchorProbeCommand(args) {
146
+ const { flags, positional } = parseFlags(args);
147
+
148
+ if (flags.help || positional.length === 0) {
149
+ printAnchorProbeHelp();
150
+ process.exit(flags.help ? 0 : 1);
151
+ }
152
+ if (positional.length > 1) {
153
+ console.error('Error: only one characterId may be specified');
154
+ process.exit(1);
155
+ }
156
+ const characterId = positional[0];
157
+
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`;
169
+ const body = { characterId };
170
+ if (flags.limit !== undefined) body.limit = flags.limit;
171
+
172
+ process.stderr.write(`${BOLD}Probing anchor gate${RESET} for character ${DIM}${characterId}${RESET} via ${DIM}${url}${RESET}\n`);
173
+
174
+ let res;
175
+ try {
176
+ res = await fetch(url, {
177
+ method: 'POST',
178
+ headers: { 'content-type': 'application/json' },
179
+ body: JSON.stringify(body),
180
+ });
181
+ } catch (err) {
182
+ console.error(`${RED}Could not reach Quilltap server at http://localhost:${port}: ${err.message}${RESET}`);
183
+ console.error(target.hint);
184
+ process.exit(1);
185
+ }
186
+
187
+ let payload;
188
+ try {
189
+ payload = await res.json();
190
+ } catch {
191
+ console.error(`${RED}Server returned a non-JSON response (status ${res.status})${RESET}`);
192
+ process.exit(1);
193
+ }
194
+ if (!res.ok || payload?.success === false) {
195
+ console.error(`${RED}Probe failed (status ${res.status}): ${payload?.error || payload?.message || 'unknown error'}${RESET}`);
196
+ process.exit(1);
197
+ }
198
+
199
+ const result = payload.data ?? payload;
200
+ if (flags.json) {
201
+ console.log(JSON.stringify(result, null, 2));
202
+ return;
203
+ }
204
+
205
+ const t = result.thresholds || {};
206
+ const anchored = result.anchored || {};
207
+ const free = result.anchorFree || {};
208
+ console.log(`\n${BOLD}Character${RESET} ${result.characterId}`);
209
+ console.log(`${BOLD}Sampled${RESET} ${result.sampled} rows, ${result.anchoredRows} with an anchor line`);
210
+ console.log(`${BOLD}Embeddings${RESET} ${result.embeddingsGenerated} generated`);
211
+ if (result.note) console.log(`${YELLOW}${result.note}${RESET}`);
212
+ console.log(`\n${DIM}${''.padEnd(22)}${'anchored'.padEnd(11)}anchor-free${RESET}`);
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}`);
215
+ const only = result.crossedOnlyWithoutAnchors;
216
+ console.log(`${BOLD}Fold only without anchors${RESET} ${only > 0 ? YELLOW : GREEN}${only}${RESET}`);
217
+
218
+ printRows(result.rows || [], t);
219
+ console.log('');
220
+ }
221
+
222
+ module.exports = { anchorProbeCommand, printAnchorProbeHelp, parseFlags };
@@ -61,7 +61,7 @@ _quilltap_complete() {
61
61
  local global_opts="-d --data-dir -i --instance -p --port -o --open -v --version -h --help --update --passphrase"
62
62
 
63
63
  # Top-level subcommands
64
- local top_cmds="db docs sync themes instances memories memory-diff recall-replay logs migrations maintenance file-verify completion"
64
+ local top_cmds="db docs sync themes instances memories memory-diff recall-replay anchor-probe logs migrations maintenance file-verify completion"
65
65
 
66
66
  # Flags that swallow the word after them. A flat list will not do: -o is the
67
67
  # valueless global --open but themes' valued --output, and `memories`
@@ -69,14 +69,15 @@ _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 --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
- local vf_recall_replay=" --turn --char --limit --port "
79
+ local vf_recall_replay=" --turn --char --limit --memory-budget --tuning --tuning-file --signals-from --port "
80
+ local vf_anchor_probe=" --limit --port "
80
81
  local vf_file_verify=" --stall-ms "
81
82
 
82
83
  # Get the subcommand (first non-option word after quilltap)
@@ -100,6 +101,7 @@ _quilltap_complete() {
100
101
  logs) valued="$vf_global$vf_logs" ;;
101
102
  memory-diff) valued="$vf_global$vf_memory_diff" ;;
102
103
  recall-replay) valued="$vf_global$vf_recall_replay" ;;
104
+ anchor-probe) valued="$vf_global$vf_anchor_probe" ;;
103
105
  file-verify) valued="$vf_global$vf_file_verify" ;;
104
106
  esac
105
107
 
@@ -176,7 +178,15 @@ _quilltap_complete() {
176
178
  return
177
179
  ;;
178
180
  --source)
179
- COMPREPLY=($(compgen -W "AUTO MANUAL" -- "$cur"))
181
+ COMPREPLY=($(compgen -W "AUTO MANUAL CONSOLIDATED" -- "$cur"))
182
+ return
183
+ ;;
184
+ --tier)
185
+ COMPREPLY=($(compgen -W "hot cold" -- "$cur"))
186
+ return
187
+ ;;
188
+ --phases)
189
+ COMPREPLY=($(compgen -W "fold-near-duplicates consolidate demote retire mirror" -- "$cur"))
180
190
  return
181
191
  ;;
182
192
  --sort)
@@ -214,7 +224,7 @@ _quilltap_complete() {
214
224
  db)
215
225
  local db_verbs="schema find chats messages logs message log memories characters optimize backup integrity"
216
226
  local db_flags="--instance --data-dir --passphrase --json --limit --grep \
217
- --character --project --about --source --chat --message --rendered --field \
227
+ --character --project --about --source --tier --chat --message --rendered --field \
218
228
  --tail --last --full --from --type --out --id --diverged --blocked --help \
219
229
  --tables --count --repl --write --llm-logs --mount-points \
220
230
  --lock-status --lock-clean --lock-override"
@@ -311,13 +321,13 @@ _quilltap_complete() {
311
321
  fi
312
322
  ;;
313
323
  memories)
314
- local mem_verbs="ls find grep show tree status validate"
315
- local mem_flags="--character --about --source --chat --project --since --until \
324
+ local mem_verbs="ls find grep show tree status validate maintain"
325
+ local mem_flags="--character --about --source --tier --chat --project --since --until \
316
326
  --min-importance --min-reinforced --has-embedding --no-embedding \
317
327
  --sort -r --reverse --limit --full-titles --in --no-related --list \
318
328
  --ignore-case -i --paths-only -l --max --context --depth --max-nodes \
319
- --semantic --top --threshold \
320
- --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"
321
331
  if [[ -z "$subverb" ]]; then
322
332
  if [[ "$cur" == -* ]]; then
323
333
  COMPREPLY=($(compgen -W "$mem_flags" -- "$cur"))
@@ -333,9 +343,13 @@ _quilltap_complete() {
333
343
  COMPREPLY=($(compgen -W "$md_flags" -- "$cur"))
334
344
  ;;
335
345
  recall-replay)
336
- local rr_flags="--turn --char --limit --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"
337
347
  COMPREPLY=($(compgen -W "$rr_flags" -- "$cur"))
338
348
  ;;
349
+ anchor-probe)
350
+ local ap_flags="--instance --data-dir --passphrase --limit --port --json --help"
351
+ COMPREPLY=($(compgen -W "$ap_flags" -- "$cur"))
352
+ ;;
339
353
  logs)
340
354
  local logs_flags="--stream --tail -f --follow --grep \
341
355
  --instance --data-dir --passphrase --help"
@@ -19,7 +19,7 @@ function __quilltap_no_subcommand
19
19
  end
20
20
  for i in (seq 2 (count $cmd))
21
21
  switch $cmd[$i]
22
- case db docs sync themes instances memories memory-diff recall-replay logs migrations maintenance file-verify completion
22
+ case db docs sync themes instances memories memory-diff recall-replay anchor-probe logs migrations maintenance file-verify completion
23
23
  return 1
24
24
  end
25
25
  end
@@ -31,7 +31,7 @@ function __quilltap_using_subcommand
31
31
  set -l cmd (commandline -opc)
32
32
  for i in (seq 2 (count $cmd))
33
33
  switch $cmd[$i]
34
- case db docs sync themes instances memories memory-diff recall-replay logs migrations maintenance file-verify completion
34
+ case db docs sync themes instances memories memory-diff recall-replay anchor-probe logs migrations maintenance file-verify completion
35
35
  test "$cmd[$i]" = "$target"
36
36
  return $status
37
37
  end
@@ -61,6 +61,7 @@ complete -c quilltap -n '__quilltap_no_subcommand' -f -a 'instances' -d 'Registe
61
61
  complete -c quilltap -n '__quilltap_no_subcommand' -f -a 'memories' -d 'Search and browse memories'
62
62
  complete -c quilltap -n '__quilltap_no_subcommand' -f -a 'memory-diff' -d 'Memory extraction dry-run'
63
63
  complete -c quilltap -n '__quilltap_no_subcommand' -f -a 'recall-replay' -d 'Replay memory recall for a turn'
64
+ complete -c quilltap -n '__quilltap_no_subcommand' -f -a 'anchor-probe' -d 'Probe anchor-line effect on admission'
64
65
  complete -c quilltap -n '__quilltap_no_subcommand' -f -a 'logs' -d 'Tail or print log files'
65
66
  complete -c quilltap -n '__quilltap_no_subcommand' -f -a 'migrations' -d 'Inspect migration status'
66
67
  complete -c quilltap -n '__quilltap_no_subcommand' -f -a 'maintenance' -d 'Run retention/cleanup sweeps'
@@ -112,7 +113,8 @@ complete -c quilltap -n '__quilltap_using_subcommand db' -l 'grep' -d 'Substring
112
113
  complete -c quilltap -n '__quilltap_using_subcommand db' -l 'character' -d 'Character name or id' -x
113
114
  complete -c quilltap -n '__quilltap_using_subcommand db' -l 'project' -d 'Project name or id' -x
114
115
  complete -c quilltap -n '__quilltap_using_subcommand db' -l 'about' -d 'Subject character' -x
115
- complete -c quilltap -n '__quilltap_using_subcommand db' -l 'source' -d 'Memory source' -x -a 'AUTO MANUAL'
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'
116
118
  complete -c quilltap -n '__quilltap_using_subcommand db' -l 'chat' -d 'Chat name or id' -x
117
119
  complete -c quilltap -n '__quilltap_using_subcommand db' -l 'message' -d 'Message id' -x
118
120
  complete -c quilltap -n '__quilltap_using_subcommand db' -l 'rendered' -d 'Render rich content'
@@ -271,11 +273,18 @@ complete -c quilltap -n '__quilltap_using_subcommand memories' -f -a 'show' -d '
271
273
  complete -c quilltap -n '__quilltap_using_subcommand memories' -f -a 'tree' -d 'Graph walk'
272
274
  complete -c quilltap -n '__quilltap_using_subcommand memories' -f -a 'status' -d 'Memory status'
273
275
  complete -c quilltap -n '__quilltap_using_subcommand memories' -f -a 'validate' -d 'Health check'
276
+ complete -c quilltap -n '__quilltap_using_subcommand memories' -f -a 'maintain' -d 'Run memory maintenance'
274
277
 
275
278
  # memories flags
276
279
  complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'character' -d 'Character name or id' -x
277
280
  complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'about' -d 'Subject of memory' -x
278
- complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'source' -d 'Memory source' -x -a 'AUTO MANUAL'
281
+ complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'source' -d 'Memory source' -x -a 'AUTO MANUAL CONSOLIDATED'
282
+ complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'tier' -d 'Memory tier' -x -a 'hot cold'
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'
279
288
  complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'chat' -d 'Chat id or title' -x
280
289
  complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'project' -d 'Project name or id' -x
281
290
  complete -c quilltap -n '__quilltap_using_subcommand memories' -l 'since' -d 'Since timestamp' -x
@@ -311,8 +320,18 @@ complete -c quilltap -n '__quilltap_using_subcommand memory-diff' -l 'out' -d 'O
311
320
  complete -c quilltap -n '__quilltap_using_subcommand recall-replay' -l 'turn' -d 'Interchange to replay (1-based)' -x
312
321
  complete -c quilltap -n '__quilltap_using_subcommand recall-replay' -l 'char' -d 'Character whose memories are searched' -x
313
322
  complete -c quilltap -n '__quilltap_using_subcommand recall-replay' -l 'limit' -d 'Candidate rows per path' -x
323
+ complete -c quilltap -n '__quilltap_using_subcommand recall-replay' -l 'memory-budget' -d 'Memory token budget' -x
324
+ complete -c quilltap -n '__quilltap_using_subcommand recall-replay' -l 'tuning' -d 'New-path tuning JSON' -x
325
+ complete -c quilltap -n '__quilltap_using_subcommand recall-replay' -l 'tuning-file' -d 'New-path tuning JSON file' -r -F
326
+ complete -c quilltap -n '__quilltap_using_subcommand recall-replay' -l 'signals-from' -d 'Reuse signals from a saved replay' -r -F
327
+ complete -c quilltap -n '__quilltap_using_subcommand recall-replay' -l 'as-of' -d 'Search the corpus as it stood at the turn'
314
328
  complete -c quilltap -n '__quilltap_using_subcommand recall-replay' -l 'port' -d 'Server port' -x
315
329
 
330
+ # ---------- anchor-probe flags ----------
331
+ complete -c quilltap -n '__quilltap_using_subcommand anchor-probe' -l 'limit' -d 'Recent memories to sample' -x
332
+ complete -c quilltap -n '__quilltap_using_subcommand anchor-probe' -l 'port' -d 'Server port' -x
333
+ complete -c quilltap -n '__quilltap_using_subcommand anchor-probe' -l 'json' -d 'Raw JSON output'
334
+
316
335
  # ---------- file-verify flags ----------
317
336
  complete -c quilltap -n '__quilltap_using_subcommand file-verify' -l 'all' -d 'Read every top-level file, not just dataless ones'
318
337
  complete -c quilltap -n '__quilltap_using_subcommand file-verify' -l 'stall-ms' -d 'Per-chunk stall threshold (ms)' -x
@@ -43,6 +43,7 @@ _quilltap() {
43
43
  'memories:Search, browse, and graph memories'
44
44
  'memory-diff:Dump existing memories and dry-run re-extraction'
45
45
  'recall-replay:Replay a turn'\''s memory recall, old vs episodic ranking'
46
+ 'anchor-probe:Probe whether episodic anchor lines suppress reinforcement at admission'
46
47
  'logs:Tail or print an instance log file'
47
48
  'migrations:Inspect migration status'
48
49
  'maintenance:Run retention/cleanup sweeps'
@@ -99,6 +100,9 @@ _quilltap_subcommand() {
99
100
  recall-replay)
100
101
  _quilltap_recall_replay
101
102
  ;;
103
+ anchor-probe)
104
+ _quilltap_anchor_probe
105
+ ;;
102
106
  logs)
103
107
  _quilltap_logs
104
108
  ;;
@@ -147,7 +151,8 @@ _quilltap_db() {
147
151
  '--character[Character name or id]:character:'
148
152
  '--project[Project name or id]:project:'
149
153
  '--about[Subject character]:character:'
150
- '--source[Memory source]:source:(AUTO MANUAL)'
154
+ '--source[Memory source]:source:(AUTO MANUAL CONSOLIDATED)'
155
+ '--tier[Memory tier]:tier:(hot cold)'
151
156
  '--chat[Chat name or id]:chat:'
152
157
  '--message[Message id]:id:'
153
158
  '--rendered[Render rich content]'
@@ -534,17 +539,24 @@ _quilltap_memories() {
534
539
  'tree:ASCII walk of graph'
535
540
  'status:Per-character rollup'
536
541
  'validate:Health check'
542
+ 'maintain:Run memory maintenance (fold, consolidate, demote, retire, mirror)'
537
543
  )
538
544
 
539
545
  mem_opts=(
540
546
  '--instance[Registered instance name]:instance:_quilltap_instance_names'
541
547
  '(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
542
548
  '--passphrase[Database passphrase]:passphrase:'
543
- '--port[Server port]:port:'
549
+ '(-p --port)'{-p,--port}'[Server port]:port:'
544
550
  '--json[JSON output]'
545
551
  '--character[Character name or id]:character:'
546
552
  '--about[Subject of memory]:character:'
547
- '--source[Memory source]:source:(AUTO MANUAL)'
553
+ '--source[Memory source]:source:(AUTO MANUAL CONSOLIDATED)'
554
+ '--tier[Memory tier]:tier:(hot cold)'
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]'
548
560
  '--chat[Chat id or title]:chat:'
549
561
  '--project[Project name or id]:project:'
550
562
  '--since[Since timestamp]:date:'
@@ -604,6 +616,23 @@ _quilltap_recall_replay() {
604
616
  '--turn[interchange to replay]:turn:' \
605
617
  '--char[character id]:char:' \
606
618
  '--limit[rows per path]:limit:' \
619
+ '--memory-budget[memory token budget]:tokens:' \
620
+ '--tuning[new-path tuning JSON]:json:' \
621
+ '--tuning-file[new-path tuning JSON file]:file:_files' \
622
+ '--signals-from[reuse signals from a saved replay]:file:_files' \
623
+ '--as-of[search the corpus as it stood at the turn]' \
624
+ '--port[server port]:port:' \
625
+ '--json[raw JSON output]' \
626
+ '*: :'
627
+ }
628
+
629
+ _quilltap_anchor_probe() {
630
+ _arguments \
631
+ '(-h --help)'{-h,--help}'[Show help]' \
632
+ '(-i --instance)'{-i,--instance}'[Use instance]:instance:_quilltap_instance_names' \
633
+ '(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories' \
634
+ '--passphrase[Database passphrase]:passphrase:' \
635
+ '--limit[recent memories to sample]:limit:' \
607
636
  '--port[server port]:port:' \
608
637
  '--json[raw JSON output]' \
609
638
  '*: :'
@@ -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 || ''),