quilltap 4.10.0-dev.96 → 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.
- 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
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { resolveServerPort } = require('./lock-helpers');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* `quilltap recall-replay <chatId>` — replay a turn's memory recall against
|
|
5
7
|
* the running server and print the candidate table, old path (episodic
|
|
6
8
|
* signals inert) vs. new path (retrospective flip + time window + entity
|
|
7
9
|
* anchors + multi-probe) side by side.
|
|
8
10
|
*
|
|
11
|
+
* Then the text fallback's table and the stage trace (query, gather, hydrate,
|
|
12
|
+
* score, select, assemble): what the archive removed, what the budget cut, what
|
|
13
|
+
* the inter-character section took.
|
|
14
|
+
*
|
|
9
15
|
* Thin wrapper over POST /api/v1/chats/<chatId>?action=recall-replay.
|
|
10
16
|
* Read-only; nothing is persisted server-side.
|
|
11
17
|
*/
|
|
@@ -28,25 +34,62 @@ Replays the per-turn memory recall for a chat turn against the running
|
|
|
28
34
|
Quilltap server and prints the full candidate table twice — the pre-overhaul
|
|
29
35
|
ranking and the episodic (retrospective/time-window/entity) ranking — so the
|
|
30
36
|
recall constants can be tuned against real "the character forgot" turns.
|
|
37
|
+
After them: what the text fallback would have returned, and the stage trace
|
|
38
|
+
(query → gather → hydrate → score → select → assemble). The anti-repetition
|
|
39
|
+
ring is the one in force at that turn, read from the chat's recall records.
|
|
31
40
|
|
|
32
41
|
Options:
|
|
42
|
+
-d, --data-dir <path> Instance root; used to find the server's port
|
|
43
|
+
-i, --instance <name> Registered instance whose server to call (see 'quilltap instances')
|
|
44
|
+
--passphrase <pass> Accepted and ignored (this tool never opens the database)
|
|
33
45
|
--turn <number> 1-based interchange to replay at (default: last)
|
|
34
46
|
--char <characterId> Character whose memories are searched
|
|
35
47
|
(default: first LLM-controlled participant)
|
|
36
|
-
--limit <number> Candidate rows per path (default:
|
|
37
|
-
|
|
48
|
+
--limit <number> Candidate rows per path (default: the larger of 25
|
|
49
|
+
and the new path's head size + 10; max: 100)
|
|
50
|
+
--memory-budget <tokens>
|
|
51
|
+
Memory token budget (positive integer). The new
|
|
52
|
+
path's selected rows then follow the budget-sized
|
|
53
|
+
dynamic head (the head scales with the model's
|
|
54
|
+
memory budget); the old path keeps the historical
|
|
55
|
+
fixed 5-entry head.
|
|
56
|
+
--tuning '<json>' Retuning constants for the NEW path only, e.g.
|
|
57
|
+
'{"boostGateAbs":0.45,"boostGateMargin":0.15,
|
|
58
|
+
"boostGateRamp":0.1,"boostCap":1.6}'. Keys:
|
|
59
|
+
boostGateAbs, boostGateMargin, boostGateRamp,
|
|
60
|
+
boostCap, freshBypassesGate, windowBypassesGate,
|
|
61
|
+
multipliers
|
|
62
|
+
{RECALL_MULTIPLIERS key:
|
|
63
|
+
value}, specificAnchors, anchorMinHits,
|
|
64
|
+
anchorOrder, backgroundReserve.
|
|
65
|
+
Unknown keys are rejected.
|
|
66
|
+
--tuning-file <path> Read the --tuning JSON from a file
|
|
67
|
+
--signals-from <path> Reuse the signals saved in a previous --json
|
|
68
|
+
replay of the same chat and turn, instead of a
|
|
69
|
+
fresh (non-deterministic) distillation
|
|
70
|
+
--as-of Search only memories created before the turn's
|
|
71
|
+
opening message (the corpus as it stood then)
|
|
72
|
+
--port <number> Server port for API calls. Default: the port recorded in the
|
|
73
|
+
--instance/--data-dir server's lock file, else 3000
|
|
38
74
|
--json Print the raw JSON result instead of tables
|
|
39
75
|
-h, --help Show this help
|
|
40
76
|
|
|
41
77
|
Examples:
|
|
42
78
|
quilltap recall-replay <chatId>
|
|
43
79
|
quilltap recall-replay <chatId> --turn 42
|
|
80
|
+
quilltap recall-replay <chatId> --turn 42 --memory-budget 4000
|
|
44
81
|
quilltap recall-replay <chatId> --turn 42 --json > replay.json
|
|
82
|
+
quilltap recall-replay <chatId> --turn 42 --signals-from replay.json --as-of \
|
|
83
|
+
--tuning-file tuning-r1.json
|
|
45
84
|
`);
|
|
46
85
|
}
|
|
47
86
|
|
|
48
87
|
function parseFlags(args) {
|
|
49
|
-
const flags = {
|
|
88
|
+
const flags = {
|
|
89
|
+
turn: undefined, char: undefined, limit: undefined, memoryBudget: undefined,
|
|
90
|
+
tuning: undefined, tuningFile: undefined, signalsFrom: undefined, asOf: false,
|
|
91
|
+
port: undefined, dataDir: '', instance: '', json: false, help: false,
|
|
92
|
+
};
|
|
50
93
|
const positional = [];
|
|
51
94
|
let i = 0;
|
|
52
95
|
while (i < args.length) {
|
|
@@ -73,6 +116,40 @@ function parseFlags(args) {
|
|
|
73
116
|
flags.limit = n;
|
|
74
117
|
break;
|
|
75
118
|
}
|
|
119
|
+
case '--memory-budget': {
|
|
120
|
+
const n = Number(args[++i]);
|
|
121
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
122
|
+
console.error('Error: --memory-budget must be a positive integer');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
flags.memoryBudget = n;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case '--tuning':
|
|
129
|
+
flags.tuning = args[++i];
|
|
130
|
+
break;
|
|
131
|
+
case '--tuning-file':
|
|
132
|
+
flags.tuningFile = args[++i];
|
|
133
|
+
break;
|
|
134
|
+
case '--signals-from':
|
|
135
|
+
flags.signalsFrom = args[++i];
|
|
136
|
+
break;
|
|
137
|
+
case '--as-of':
|
|
138
|
+
flags.asOf = true;
|
|
139
|
+
break;
|
|
140
|
+
case '-d':
|
|
141
|
+
case '--data-dir':
|
|
142
|
+
flags.dataDir = args[++i];
|
|
143
|
+
break;
|
|
144
|
+
case '-i':
|
|
145
|
+
case '--instance':
|
|
146
|
+
flags.instance = args[++i];
|
|
147
|
+
break;
|
|
148
|
+
case '--passphrase':
|
|
149
|
+
// Accepted so universal flags never error; these tools talk to the
|
|
150
|
+
// running server and never open the database themselves.
|
|
151
|
+
i++;
|
|
152
|
+
break;
|
|
76
153
|
case '--port': {
|
|
77
154
|
const p = parseInt(args[++i], 10);
|
|
78
155
|
if (isNaN(p) || p < 1 || p > 65535) {
|
|
@@ -106,8 +183,9 @@ function fmt(n, digits = 3) {
|
|
|
106
183
|
return n.toFixed(digits);
|
|
107
184
|
}
|
|
108
185
|
|
|
109
|
-
function printPath(label, rows) {
|
|
110
|
-
|
|
186
|
+
function printPath(label, rows, headSize) {
|
|
187
|
+
const head = typeof headSize === 'number' ? `, head ${headSize}` : '';
|
|
188
|
+
console.log(`\n${BOLD}${label}${RESET} (${rows.length} candidates${head})`);
|
|
111
189
|
if (rows.length === 0) {
|
|
112
190
|
console.log(` ${DIM}(none)${RESET}`);
|
|
113
191
|
return;
|
|
@@ -127,6 +205,73 @@ function printPath(label, rows) {
|
|
|
127
205
|
}
|
|
128
206
|
}
|
|
129
207
|
|
|
208
|
+
/** The stage trace: one line per stage entry (query → gather → … → assemble). */
|
|
209
|
+
function printTrace(trace) {
|
|
210
|
+
if (!Array.isArray(trace) || trace.length === 0) return;
|
|
211
|
+
console.log(`\n${BOLD}STAGE TRACE${RESET}`);
|
|
212
|
+
for (const entry of trace) {
|
|
213
|
+
const label = entry.label ? `${DIM}(${entry.label})${RESET} ` : '';
|
|
214
|
+
const detail = Object.entries(entry.detail || {})
|
|
215
|
+
.map(([k, v]) => `${k}=${typeof v === 'object' && v !== null ? JSON.stringify(v) : v}`)
|
|
216
|
+
.join(' ');
|
|
217
|
+
console.log(` ${CYAN}${String(entry.stage).padEnd(9)}${RESET}${label}${detail}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function readJson(text, what) {
|
|
222
|
+
try {
|
|
223
|
+
return JSON.parse(text);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
console.error(`${RED}Could not parse ${what} as JSON: ${err.message}${RESET}`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function readJsonFile(path, what) {
|
|
231
|
+
const fs = require('fs');
|
|
232
|
+
let text;
|
|
233
|
+
try {
|
|
234
|
+
text = fs.readFileSync(path, 'utf8');
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.error(`${RED}Could not read ${what} ${path}: ${err.message}${RESET}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
return readJson(text, `${what} ${path}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The tuning object from --tuning or --tuning-file (never both). */
|
|
243
|
+
function loadTuning(flags) {
|
|
244
|
+
if (flags.tuning !== undefined && flags.tuningFile !== undefined) {
|
|
245
|
+
console.error('Error: pass --tuning or --tuning-file, not both');
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
if (flags.tuning !== undefined) return readJson(flags.tuning, '--tuning');
|
|
249
|
+
if (flags.tuningFile !== undefined) return readJsonFile(flags.tuningFile, '--tuning-file');
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The signals saved in a previous --json replay. Refuses a file from another
|
|
255
|
+
* chat or turn — reusing its signals would replay a different question.
|
|
256
|
+
*/
|
|
257
|
+
function loadSignals(path, chatId, turn) {
|
|
258
|
+
const saved = readJsonFile(path, '--signals-from');
|
|
259
|
+
const result = saved.data ?? saved;
|
|
260
|
+
if (!result.signals) {
|
|
261
|
+
console.error(`${RED}${path} has no saved signals (its distillation failed); replay without --signals-from${RESET}`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
if (result.chatId && result.chatId !== chatId) {
|
|
265
|
+
console.error(`${RED}${path} is a replay of chat ${result.chatId}, not ${chatId}${RESET}`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
if (turn !== undefined && result.turnIndex !== undefined && result.turnIndex !== turn) {
|
|
269
|
+
console.error(`${RED}${path} is a replay of turn ${result.turnIndex}, not ${turn}${RESET}`);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
return result.signals;
|
|
273
|
+
}
|
|
274
|
+
|
|
130
275
|
async function recallReplayCommand(args) {
|
|
131
276
|
const { flags, positional } = parseFlags(args);
|
|
132
277
|
|
|
@@ -140,11 +285,26 @@ async function recallReplayCommand(args) {
|
|
|
140
285
|
}
|
|
141
286
|
const chatId = positional[0];
|
|
142
287
|
|
|
143
|
-
|
|
288
|
+
let target;
|
|
289
|
+
try {
|
|
290
|
+
target = resolveServerPort({ port: flags.port, instance: flags.instance, dataDir: flags.dataDir });
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error(`Error: ${err.message}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
const port = target.port;
|
|
296
|
+
if (target.note) process.stderr.write(`${DIM}${target.note}${RESET}\n`);
|
|
297
|
+
|
|
298
|
+
const url = `http://localhost:${port}/api/v1/chats/${encodeURIComponent(chatId)}?action=recall-replay`;
|
|
144
299
|
const body = {};
|
|
145
300
|
if (flags.turn !== undefined) body.turnIndex = flags.turn;
|
|
146
301
|
if (flags.char) body.characterId = flags.char;
|
|
147
302
|
if (flags.limit !== undefined) body.limit = flags.limit;
|
|
303
|
+
if (flags.memoryBudget !== undefined) body.memoryBudget = flags.memoryBudget;
|
|
304
|
+
const tuning = loadTuning(flags);
|
|
305
|
+
if (tuning !== undefined) body.tuning = tuning;
|
|
306
|
+
if (flags.signalsFrom !== undefined) body.signals = loadSignals(flags.signalsFrom, chatId, flags.turn);
|
|
307
|
+
if (flags.asOf) body.asOf = true;
|
|
148
308
|
|
|
149
309
|
process.stderr.write(`${BOLD}Replaying recall${RESET} for chat ${DIM}${chatId}${RESET} via ${DIM}${url}${RESET}\n`);
|
|
150
310
|
|
|
@@ -156,8 +316,8 @@ async function recallReplayCommand(args) {
|
|
|
156
316
|
body: JSON.stringify(body),
|
|
157
317
|
});
|
|
158
318
|
} catch (err) {
|
|
159
|
-
console.error(`${RED}Could not reach Quilltap server at http://localhost:${
|
|
160
|
-
console.error(
|
|
319
|
+
console.error(`${RED}Could not reach Quilltap server at http://localhost:${port}: ${err.message}${RESET}`);
|
|
320
|
+
console.error(target.hint);
|
|
161
321
|
process.exit(1);
|
|
162
322
|
}
|
|
163
323
|
|
|
@@ -170,6 +330,11 @@ async function recallReplayCommand(args) {
|
|
|
170
330
|
}
|
|
171
331
|
if (!res.ok || payload?.success === false) {
|
|
172
332
|
console.error(`${RED}Replay failed (status ${res.status}): ${payload?.error || payload?.message || 'unknown error'}${RESET}`);
|
|
333
|
+
if (Array.isArray(payload?.details)) {
|
|
334
|
+
for (const issue of payload.details) {
|
|
335
|
+
console.error(` ${(issue.path || []).join('.') || '(body)'}: ${issue.message}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
173
338
|
process.exit(1);
|
|
174
339
|
}
|
|
175
340
|
|
|
@@ -193,9 +358,26 @@ async function recallReplayCommand(args) {
|
|
|
193
358
|
console.log(`${BOLD}Signals${RESET} ${YELLOW}distillation failed — new path ran inert${RESET}`);
|
|
194
359
|
}
|
|
195
360
|
|
|
196
|
-
|
|
197
|
-
|
|
361
|
+
const pinned = result.signalsPinned ? `${CYAN}pinned${RESET}` : `${DIM}fresh distillation${RESET}`;
|
|
362
|
+
const asOf = result.asOf
|
|
363
|
+
? `${CYAN}as of ${result.asOf}${RESET} ${DIM}(${result.excludedAfterAsOf} later memories left out)${RESET}`
|
|
364
|
+
: `${DIM}whole corpus${RESET}`;
|
|
365
|
+
console.log(`${BOLD}Harness${RESET} signals ${pinned} · ${asOf}`);
|
|
366
|
+
console.log(`${BOLD}Tuning${RESET} ${result.tuning === 'defaults' || !result.tuning ? `${DIM}defaults${RESET}` : `${YELLOW}${result.tuning}${RESET}`} ${DIM}(new path only)${RESET}`);
|
|
367
|
+
if (result.ringSource) {
|
|
368
|
+
const source = result.ringSource === 'debug-log'
|
|
369
|
+
? `${DIM}as of the turn, from its recall records${RESET}`
|
|
370
|
+
: `${YELLOW}the chat's current ring (no recall records that early)${RESET}`;
|
|
371
|
+
console.log(`${BOLD}Ring${RESET} ${(result.ring || []).length} recently whispered · ${source}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
printPath('OLD PATH (episodic signals inert)', result.oldPath || [], result.oldHeadSize);
|
|
375
|
+
printPath('NEW PATH (retrospective/window/entities live)', result.newPath || [], result.newHeadSize);
|
|
376
|
+
if (Array.isArray(result.fallbackPath)) {
|
|
377
|
+
printPath('TEXT FALLBACK (what the new path returns with no usable vector)', result.fallbackPath, result.newHeadSize);
|
|
378
|
+
}
|
|
379
|
+
printTrace(result.trace);
|
|
198
380
|
console.log('');
|
|
199
381
|
}
|
|
200
382
|
|
|
201
|
-
module.exports = { recallReplayCommand };
|
|
383
|
+
module.exports = { recallReplayCommand, parseFlags };
|
package/lib/text-codec.js
CHANGED
|
@@ -58,4 +58,33 @@ function registerTextCodecFunction(db) {
|
|
|
58
58
|
db.function('qt_text', { deterministic: true }, (value) => decodeText(value));
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Decode every compressed-text value in a set of result rows, in place, for
|
|
63
|
+
* printing. Raw SQL (`quilltap db "SELECT content …"`, `--repl`) hands BLOBs to
|
|
64
|
+
* `JSON.stringify` / `console.table` as Buffers, which print as
|
|
65
|
+
* `{"type":"Buffer","data":[…]}` and read as data loss (bug 173). Only values
|
|
66
|
+
* carrying the `0x51 0x01 0x01` header are touched — embedding BLOBs and other
|
|
67
|
+
* binary columns pass through unchanged, as does anything already text
|
|
68
|
+
* (including an explicit `qt_text()`). Returns the number of values decoded.
|
|
69
|
+
*/
|
|
70
|
+
function decodeCompressedTextInRows(rows) {
|
|
71
|
+
let decoded = 0;
|
|
72
|
+
if (!Array.isArray(rows)) return decoded;
|
|
73
|
+
for (const row of rows) {
|
|
74
|
+
if (!row || typeof row !== 'object') continue;
|
|
75
|
+
for (const key of Object.keys(row)) {
|
|
76
|
+
if (isCompressedTextBlob(row[key])) {
|
|
77
|
+
row[key] = decodeText(row[key]);
|
|
78
|
+
decoded++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return decoded;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
decodeText,
|
|
87
|
+
decodeCompressedTextInRows,
|
|
88
|
+
isCompressedTextBlob,
|
|
89
|
+
registerTextCodecFunction,
|
|
90
|
+
};
|