quilltap 4.8.0-dev.92 → 4.8.0-dev.97
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 +12 -0
- package/bin/quilltap.js +8 -1
- package/lib/completion/bash.template +5 -1
- package/lib/completion/fish.template +2 -2
- package/lib/completion/zsh.template +4 -0
- package/lib/recall-replay-command.js +201 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -241,6 +241,18 @@ quilltap memory-diff <chatId> --out /tmp/diff --concurrency 8
|
|
|
241
241
|
|
|
242
242
|
Needs a running server (`--port`, default 3000) to reach the extraction pipeline. `--out <dir>` sets the report destination (default: cwd); `--concurrency N` bounds parallel turns (default 4, max 32).
|
|
243
243
|
|
|
244
|
+
## Recall Replay
|
|
245
|
+
|
|
246
|
+
`quilltap recall-replay <chatId>` replays a turn's memory recall against the running server and prints the candidate table twice — the pre-overhaul ranking vs. the episodic (retrospective / time-window / entity-anchored) ranking — with cosine, blend, every multiplier fired, and head selection per row. Read-only; used to tune the recall constants against real "the character forgot" turns.
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
quilltap recall-replay <chatId> # Replay the last turn
|
|
250
|
+
quilltap recall-replay <chatId> --turn 42 # Replay at interchange 42 (its own clock)
|
|
251
|
+
quilltap recall-replay <chatId> --json # Raw JSON for scripting
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Flags: `--turn <n>` (default: last), `--char <characterId>` (default: first LLM-controlled participant), `--limit <n>` (default 25), `--port <n>` (default 3000), `--json`.
|
|
255
|
+
|
|
244
256
|
## Maintenance & Cleanup
|
|
245
257
|
|
|
246
258
|
`quilltap maintenance` is the manual trigger for the retention sweeps that otherwise run on the server's daily maintenance tick. It reaps data with no bearing on characters, stories, or memories.
|
package/bin/quilltap.js
CHANGED
|
@@ -99,6 +99,7 @@ Subcommands:
|
|
|
99
99
|
maintenance Run retention / cleanup sweeps (status / run)
|
|
100
100
|
file-verify Force-download cloud-evicted data files (iCloud, etc.)
|
|
101
101
|
memory-diff <chatId> Dump existing memories and dry-run re-extraction for a chat
|
|
102
|
+
recall-replay <chatId> Replay a turn's memory recall, old vs episodic ranking
|
|
102
103
|
completion <shell> Generate a shell completion script (bash / zsh / fish)
|
|
103
104
|
|
|
104
105
|
Options:
|
|
@@ -1108,7 +1109,7 @@ async function dbCommand(args) {
|
|
|
1108
1109
|
// to the subcommand. Each subcommand parses these flags position-independently,
|
|
1109
1110
|
// so they behave the same before or after the verb.
|
|
1110
1111
|
const SUBCOMMANDS = new Set([
|
|
1111
|
-
'db', 'themes', 'docs', 'memories', 'instances', 'memory-diff', 'completion', 'logs', 'migrations', 'maintenance', 'file-verify',
|
|
1112
|
+
'db', 'themes', 'docs', 'memories', 'instances', 'memory-diff', 'recall-replay', 'completion', 'logs', 'migrations', 'maintenance', 'file-verify',
|
|
1112
1113
|
]);
|
|
1113
1114
|
// Global flags that consume the following token as their value.
|
|
1114
1115
|
const GLOBAL_VALUE_FLAGS = new Set(['-p', '--port', '-d', '--data-dir', '-i', '--instance', '--passphrase']);
|
|
@@ -1168,6 +1169,12 @@ if (subName === 'db') {
|
|
|
1168
1169
|
console.error(`Error: ${err.message}`);
|
|
1169
1170
|
process.exit(1);
|
|
1170
1171
|
});
|
|
1172
|
+
} else if (subName === 'recall-replay') {
|
|
1173
|
+
const { recallReplayCommand } = require('../lib/recall-replay-command');
|
|
1174
|
+
recallReplayCommand(subArgs).catch(err => {
|
|
1175
|
+
console.error(`Error: ${err.message}`);
|
|
1176
|
+
process.exit(1);
|
|
1177
|
+
});
|
|
1171
1178
|
} else if (subName === 'completion') {
|
|
1172
1179
|
const { completionCommand } = require('../lib/completion-commands');
|
|
1173
1180
|
completionCommand(subArgs).catch(err => {
|
|
@@ -15,7 +15,7 @@ _quilltap_complete() {
|
|
|
15
15
|
local global_opts="-d --data-dir -i --instance -p --port -o --open -v --version -h --help --update --passphrase"
|
|
16
16
|
|
|
17
17
|
# Top-level subcommands
|
|
18
|
-
local top_cmds="db docs themes instances memories memory-diff logs migrations maintenance file-verify completion"
|
|
18
|
+
local top_cmds="db docs themes instances memories memory-diff recall-replay logs migrations maintenance file-verify completion"
|
|
19
19
|
|
|
20
20
|
# Get the subcommand (first non-option word after quilltap)
|
|
21
21
|
local subcommand=""
|
|
@@ -209,6 +209,10 @@ _quilltap_complete() {
|
|
|
209
209
|
local md_flags="--instance --data-dir --passphrase --port --concurrency --out --help"
|
|
210
210
|
COMPREPLY=($(compgen -W "$md_flags" -- "$cur"))
|
|
211
211
|
;;
|
|
212
|
+
recall-replay)
|
|
213
|
+
local rr_flags="--turn --char --limit --port --json --help"
|
|
214
|
+
COMPREPLY=($(compgen -W "$rr_flags" -- "$cur"))
|
|
215
|
+
;;
|
|
212
216
|
logs)
|
|
213
217
|
local logs_flags="--stream --tail -f --follow --grep \
|
|
214
218
|
--instance --data-dir --passphrase --help"
|
|
@@ -15,7 +15,7 @@ function __quilltap_no_subcommand
|
|
|
15
15
|
end
|
|
16
16
|
for i in (seq 2 (count $cmd))
|
|
17
17
|
switch $cmd[$i]
|
|
18
|
-
case db docs themes instances memories memory-diff logs migrations maintenance file-verify completion
|
|
18
|
+
case db docs themes instances memories memory-diff recall-replay logs migrations maintenance file-verify completion
|
|
19
19
|
return 1
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -27,7 +27,7 @@ function __quilltap_using_subcommand
|
|
|
27
27
|
set -l cmd (commandline -opc)
|
|
28
28
|
for i in (seq 2 (count $cmd))
|
|
29
29
|
switch $cmd[$i]
|
|
30
|
-
case db docs themes instances memories memory-diff logs migrations maintenance file-verify completion
|
|
30
|
+
case db docs themes instances memories memory-diff recall-replay logs migrations maintenance file-verify completion
|
|
31
31
|
test "$cmd[$i]" = "$target"
|
|
32
32
|
return $status
|
|
33
33
|
end
|
|
@@ -26,6 +26,7 @@ _quilltap() {
|
|
|
26
26
|
'instances:Register or inspect named Quilltap instances'
|
|
27
27
|
'memories:Search, browse, and graph memories'
|
|
28
28
|
'memory-diff:Dump existing memories and dry-run re-extraction'
|
|
29
|
+
'recall-replay:Replay a turn'\''s memory recall, old vs episodic ranking'
|
|
29
30
|
'logs:Tail or print an instance log file'
|
|
30
31
|
'migrations:Inspect migration status'
|
|
31
32
|
'maintenance:Run retention/cleanup sweeps'
|
|
@@ -72,6 +73,9 @@ _quilltap_subcommand() {
|
|
|
72
73
|
memory-diff)
|
|
73
74
|
_quilltap_memory_diff
|
|
74
75
|
;;
|
|
76
|
+
recall-replay)
|
|
77
|
+
_arguments '--turn[interchange to replay]:turn:' '--char[character id]:char:' '--limit[rows per path]:limit:' '--port[server port]:port:' '--json[raw JSON output]' '--help[show help]'
|
|
78
|
+
;;
|
|
75
79
|
logs)
|
|
76
80
|
_quilltap_logs
|
|
77
81
|
;;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `quilltap recall-replay <chatId>` — replay a turn's memory recall against
|
|
5
|
+
* the running server and print the candidate table, old path (episodic
|
|
6
|
+
* signals inert) vs. new path (retrospective flip + time window + entity
|
|
7
|
+
* anchors + multi-probe) side by side.
|
|
8
|
+
*
|
|
9
|
+
* Thin wrapper over POST /api/v1/chats/<chatId>?action=recall-replay.
|
|
10
|
+
* Read-only; nothing is persisted server-side.
|
|
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 printRecallReplayHelp() {
|
|
22
|
+
console.log(`
|
|
23
|
+
Quilltap recall-replay Tool
|
|
24
|
+
|
|
25
|
+
Usage: quilltap recall-replay <chatId> [options]
|
|
26
|
+
|
|
27
|
+
Replays the per-turn memory recall for a chat turn against the running
|
|
28
|
+
Quilltap server and prints the full candidate table twice — the pre-overhaul
|
|
29
|
+
ranking and the episodic (retrospective/time-window/entity) ranking — so the
|
|
30
|
+
recall constants can be tuned against real "the character forgot" turns.
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--turn <number> 1-based interchange to replay at (default: last)
|
|
34
|
+
--char <characterId> Character whose memories are searched
|
|
35
|
+
(default: first LLM-controlled participant)
|
|
36
|
+
--limit <number> Candidate rows per path (default: 25, max: 100)
|
|
37
|
+
--port <number> Server port for API calls (default: 3000)
|
|
38
|
+
--json Print the raw JSON result instead of tables
|
|
39
|
+
-h, --help Show this help
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
quilltap recall-replay <chatId>
|
|
43
|
+
quilltap recall-replay <chatId> --turn 42
|
|
44
|
+
quilltap recall-replay <chatId> --turn 42 --json > replay.json
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseFlags(args) {
|
|
49
|
+
const flags = { turn: undefined, char: undefined, limit: undefined, port: 3000, json: false, help: false };
|
|
50
|
+
const positional = [];
|
|
51
|
+
let i = 0;
|
|
52
|
+
while (i < args.length) {
|
|
53
|
+
const a = args[i];
|
|
54
|
+
switch (a) {
|
|
55
|
+
case '--turn': {
|
|
56
|
+
const n = parseInt(args[++i], 10);
|
|
57
|
+
if (isNaN(n) || n < 1) {
|
|
58
|
+
console.error('Error: --turn must be a positive integer');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
flags.turn = n;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case '--char':
|
|
65
|
+
flags.char = args[++i];
|
|
66
|
+
break;
|
|
67
|
+
case '--limit': {
|
|
68
|
+
const n = parseInt(args[++i], 10);
|
|
69
|
+
if (isNaN(n) || n < 1 || n > 100) {
|
|
70
|
+
console.error('Error: --limit must be between 1 and 100');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
flags.limit = n;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case '--port': {
|
|
77
|
+
const p = parseInt(args[++i], 10);
|
|
78
|
+
if (isNaN(p) || p < 1 || p > 65535) {
|
|
79
|
+
console.error('Error: --port must be between 1 and 65535');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
flags.port = p;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case '--json':
|
|
86
|
+
flags.json = true;
|
|
87
|
+
break;
|
|
88
|
+
case '-h':
|
|
89
|
+
case '--help':
|
|
90
|
+
flags.help = true;
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
if (a.startsWith('-')) {
|
|
94
|
+
console.error(`Unknown option: ${a}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
positional.push(a);
|
|
98
|
+
}
|
|
99
|
+
i++;
|
|
100
|
+
}
|
|
101
|
+
return { flags, positional };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function fmt(n, digits = 3) {
|
|
105
|
+
if (n === null || n === undefined) return DIM + '—' + RESET;
|
|
106
|
+
return n.toFixed(digits);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function printPath(label, rows) {
|
|
110
|
+
console.log(`\n${BOLD}${label}${RESET} (${rows.length} candidates)`);
|
|
111
|
+
if (rows.length === 0) {
|
|
112
|
+
console.log(` ${DIM}(none)${RESET}`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const header = ['sel', 'cosine', 'blend', '×mult', 'after', 'kind', 'occurredAt', 'fired', 'summary'];
|
|
116
|
+
console.log(
|
|
117
|
+
` ${DIM}${header[0].padEnd(4)}${header[1].padEnd(8)}${header[2].padEnd(8)}${header[3].padEnd(7)}${header[4].padEnd(8)}${header[5].padEnd(9)}${header[6].padEnd(12)}${header[7].padEnd(24)}${header[8]}${RESET}`
|
|
118
|
+
);
|
|
119
|
+
for (const row of rows) {
|
|
120
|
+
const sel = row.selected ? `${GREEN}✓${RESET} ` : ' ';
|
|
121
|
+
const occurred = row.occurredAt ? row.occurredAt.slice(0, 10) : '—';
|
|
122
|
+
const fired = (row.fired || []).join(' ') || '—';
|
|
123
|
+
const summary = (row.summary || '').slice(0, 60);
|
|
124
|
+
console.log(
|
|
125
|
+
` ${sel} ${fmt(row.cosine).padEnd(8)}${fmt(row.blendedBefore).padEnd(8)}${fmt(row.multiplier, 2).padEnd(7)}${fmt(row.blendedAfter).padEnd(8)}${(row.kind || 'semantic').padEnd(9)}${occurred.padEnd(12)}${fired.padEnd(24).slice(0, 24)}${summary}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function recallReplayCommand(args) {
|
|
131
|
+
const { flags, positional } = parseFlags(args);
|
|
132
|
+
|
|
133
|
+
if (flags.help || positional.length === 0) {
|
|
134
|
+
printRecallReplayHelp();
|
|
135
|
+
process.exit(flags.help ? 0 : 1);
|
|
136
|
+
}
|
|
137
|
+
if (positional.length > 1) {
|
|
138
|
+
console.error('Error: only one chatId may be specified');
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
const chatId = positional[0];
|
|
142
|
+
|
|
143
|
+
const url = `http://localhost:${flags.port}/api/v1/chats/${encodeURIComponent(chatId)}?action=recall-replay`;
|
|
144
|
+
const body = {};
|
|
145
|
+
if (flags.turn !== undefined) body.turnIndex = flags.turn;
|
|
146
|
+
if (flags.char) body.characterId = flags.char;
|
|
147
|
+
if (flags.limit !== undefined) body.limit = flags.limit;
|
|
148
|
+
|
|
149
|
+
process.stderr.write(`${BOLD}Replaying recall${RESET} for chat ${DIM}${chatId}${RESET} via ${DIM}${url}${RESET}\n`);
|
|
150
|
+
|
|
151
|
+
let res;
|
|
152
|
+
try {
|
|
153
|
+
res = await fetch(url, {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: { 'content-type': 'application/json' },
|
|
156
|
+
body: JSON.stringify(body),
|
|
157
|
+
});
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error(`${RED}Could not reach Quilltap server at http://localhost:${flags.port}: ${err.message}${RESET}`);
|
|
160
|
+
console.error('Start the server (npm run dev) or pass --port to match a non-default port.');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let payload;
|
|
165
|
+
try {
|
|
166
|
+
payload = await res.json();
|
|
167
|
+
} catch {
|
|
168
|
+
console.error(`${RED}Server returned a non-JSON response (status ${res.status})${RESET}`);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
if (!res.ok || payload?.success === false) {
|
|
172
|
+
console.error(`${RED}Replay failed (status ${res.status}): ${payload?.error || payload?.message || 'unknown error'}${RESET}`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = payload.data ?? payload;
|
|
177
|
+
if (flags.json) {
|
|
178
|
+
console.log(JSON.stringify(result, null, 2));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(`\n${BOLD}Chat${RESET} ${result.chatId}`);
|
|
183
|
+
console.log(`${BOLD}Character${RESET} ${result.characterName} ${DIM}(${result.characterId})${RESET}`);
|
|
184
|
+
console.log(`${BOLD}Turn${RESET} ${result.turnIndex} of ${result.totalTurns} ${DIM}(clock ${result.clockIso})${RESET}`);
|
|
185
|
+
console.log(`${BOLD}Query${RESET} ${result.query}`);
|
|
186
|
+
const s = result.signals;
|
|
187
|
+
if (s) {
|
|
188
|
+
const retro = s.retrospective ? `${GREEN}retrospective${RESET}` : `${DIM}not retrospective${RESET}`;
|
|
189
|
+
const range = s.timeRange ? `${s.timeRange.from.slice(0, 10)} → ${s.timeRange.to.slice(0, 10)}` : '—';
|
|
190
|
+
const entities = (s.entities || []).join(', ') || '—';
|
|
191
|
+
console.log(`${BOLD}Signals${RESET} ${retro} · timeRange ${CYAN}${range}${RESET} · entities ${CYAN}${entities}${RESET}`);
|
|
192
|
+
} else {
|
|
193
|
+
console.log(`${BOLD}Signals${RESET} ${YELLOW}distillation failed — new path ran inert${RESET}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
printPath('OLD PATH (episodic signals inert)', result.oldPath || []);
|
|
197
|
+
printPath('NEW PATH (retrospective/window/entities live)', result.newPath || []);
|
|
198
|
+
console.log('');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = { recallReplayCommand };
|