quilltap 4.8.0-dev → 4.8.0-dev.103

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 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.
@@ -278,6 +290,18 @@ quilltap migrations run --dry-run # List what would run (refuses without --dry-r
278
290
 
279
291
  `--json` works on all three. "Not yet recorded" includes migrations whose `shouldRun()` is `false` on this instance — the CLI doesn't evaluate the predicate, so it can't distinguish "would skip" from "would run."
280
292
 
293
+ ## File Verification
294
+
295
+ `quilltap file-verify` force-downloads an instance's cloud-evicted (dataless) top-level data files so the databases are fully local before anything opens them. It reads each placeholder, which faults it in through the cloud provider (iCloud Drive, etc.). Safe to run repeatedly — a no-op when nothing is evicted. macOS only for now.
296
+
297
+ ```bash
298
+ quilltap file-verify --instance Ignite # Fault in only the dataless top-level files
299
+ quilltap file-verify --instance Friday --all # Read every top-level file, not just dataless ones
300
+ quilltap file-verify --instance Friday --json # Machine-readable report
301
+ ```
302
+
303
+ Only the **top-level** files of the data directory are considered; the `backups/` subdirectory is left alone. Flags: `--all` (read every top-level file, not just dataless ones), `--stall-ms <ms>` (treat a download as stalled after this many ms with no bytes per chunk; default 30000), `--json`. Resolves the instance via the usual `--instance` / `--data-dir` plumbing.
304
+
281
305
  ## Theme Management
282
306
 
283
307
  The CLI includes theme management commands:
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quilltap",
3
- "version": "4.8.0-dev",
3
+ "version": "4.8.0-dev.103",
4
4
  "description": "Self-hosted AI workspace for writers, worldbuilders, and roleplayers. Run with npx quilltap.",
5
5
  "author": {
6
6
  "name": "Charles Sebold",
@@ -40,7 +40,7 @@
40
40
  "better-sqlite3-multiple-ciphers": "^12.11.1",
41
41
  "node-pty": "^1.1.0",
42
42
  "sharp": "^0.34.5",
43
- "tar": "^7.5.16",
43
+ "tar": "^7.5.20",
44
44
  "yauzl": "^3.4.0"
45
45
  },
46
46
  "engines": {