quilltap 4.10.0-dev.99 → 4.11.0-dev.6
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 +69 -11
- package/bin/quilltap.js +19 -2
- 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 +23 -4
- package/lib/completion/zsh.template +32 -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 +396 -76
- 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/migrations-commands.js +18 -4
- package/lib/recall-replay-command.js +193 -11
- package/lib/text-codec.js +30 -1
- package/package.json +1 -1
|
@@ -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
|
'*: :'
|
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 || ''),
|