@psiclawops/hypermem 0.8.4 → 0.9.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/INSTALL.md +203 -23
  3. package/README.md +139 -216
  4. package/bench/README.md +42 -0
  5. package/bench/data-access-bench.mjs +380 -0
  6. package/bin/hypermem-bench.mjs +2 -0
  7. package/bin/hypermem-doctor.mjs +412 -0
  8. package/bin/hypermem-model-audit.mjs +339 -0
  9. package/bin/hypermem-status.mjs +491 -70
  10. package/dist/adaptive-lifecycle.d.ts +81 -0
  11. package/dist/adaptive-lifecycle.d.ts.map +1 -0
  12. package/dist/adaptive-lifecycle.js +190 -0
  13. package/dist/background-indexer.js +9 -9
  14. package/dist/budget-policy.d.ts +1 -1
  15. package/dist/budget-policy.d.ts.map +1 -1
  16. package/dist/budget-policy.js +10 -5
  17. package/dist/cache.d.ts +4 -0
  18. package/dist/cache.d.ts.map +1 -1
  19. package/dist/cache.js +2 -0
  20. package/dist/composition-snapshot-integrity.d.ts +36 -0
  21. package/dist/composition-snapshot-integrity.d.ts.map +1 -0
  22. package/dist/composition-snapshot-integrity.js +131 -0
  23. package/dist/composition-snapshot-runtime.d.ts +59 -0
  24. package/dist/composition-snapshot-runtime.d.ts.map +1 -0
  25. package/dist/composition-snapshot-runtime.js +250 -0
  26. package/dist/composition-snapshot-store.d.ts +44 -0
  27. package/dist/composition-snapshot-store.d.ts.map +1 -0
  28. package/dist/composition-snapshot-store.js +117 -0
  29. package/dist/compositor.d.ts +125 -1
  30. package/dist/compositor.d.ts.map +1 -1
  31. package/dist/compositor.js +692 -44
  32. package/dist/cross-agent.d.ts +1 -1
  33. package/dist/cross-agent.js +17 -17
  34. package/dist/doc-chunk-store.d.ts +19 -0
  35. package/dist/doc-chunk-store.d.ts.map +1 -1
  36. package/dist/doc-chunk-store.js +56 -6
  37. package/dist/dreaming-promoter.d.ts +1 -1
  38. package/dist/dreaming-promoter.js +2 -2
  39. package/dist/hybrid-retrieval.d.ts +38 -0
  40. package/dist/hybrid-retrieval.d.ts.map +1 -1
  41. package/dist/hybrid-retrieval.js +86 -1
  42. package/dist/index.d.ts +15 -6
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +33 -7
  45. package/dist/knowledge-store.d.ts +4 -1
  46. package/dist/knowledge-store.d.ts.map +1 -1
  47. package/dist/knowledge-store.js +27 -4
  48. package/dist/library-schema.d.ts +12 -8
  49. package/dist/library-schema.d.ts.map +1 -1
  50. package/dist/library-schema.js +22 -8
  51. package/dist/message-store.d.ts.map +1 -1
  52. package/dist/message-store.js +7 -3
  53. package/dist/metrics-dashboard.d.ts +18 -1
  54. package/dist/metrics-dashboard.d.ts.map +1 -1
  55. package/dist/metrics-dashboard.js +52 -14
  56. package/dist/reranker.d.ts +1 -1
  57. package/dist/reranker.js +2 -2
  58. package/dist/schema.d.ts +1 -1
  59. package/dist/schema.d.ts.map +1 -1
  60. package/dist/schema.js +28 -1
  61. package/dist/seed.d.ts +1 -1
  62. package/dist/seed.d.ts.map +1 -1
  63. package/dist/seed.js +3 -1
  64. package/dist/session-flusher.d.ts +2 -2
  65. package/dist/session-flusher.js +2 -2
  66. package/dist/spawn-context.d.ts +1 -1
  67. package/dist/spawn-context.js +1 -1
  68. package/dist/topic-store.js +5 -5
  69. package/dist/topic-synthesizer.d.ts +20 -0
  70. package/dist/topic-synthesizer.d.ts.map +1 -1
  71. package/dist/topic-synthesizer.js +114 -4
  72. package/dist/trigger-registry.d.ts +1 -1
  73. package/dist/trigger-registry.d.ts.map +1 -1
  74. package/dist/trigger-registry.js +14 -6
  75. package/dist/types.d.ts +273 -3
  76. package/dist/types.d.ts.map +1 -1
  77. package/dist/version.d.ts +7 -7
  78. package/dist/version.d.ts.map +1 -1
  79. package/dist/version.js +17 -7
  80. package/docs/DIAGNOSTICS.md +205 -0
  81. package/docs/INTEGRATION_VALIDATION.md +186 -0
  82. package/docs/MIGRATION.md +9 -6
  83. package/docs/MIGRATION_GUIDE.md +125 -101
  84. package/docs/ROADMAP.md +238 -20
  85. package/docs/TUNING.md +30 -6
  86. package/install.sh +159 -408
  87. package/memory-plugin/LICENSE +190 -0
  88. package/memory-plugin/README.md +20 -0
  89. package/memory-plugin/dist/index.js +50 -0
  90. package/memory-plugin/package.json +2 -2
  91. package/package.json +18 -4
  92. package/plugin/LICENSE +190 -0
  93. package/plugin/README.md +20 -0
  94. package/plugin/dist/index.d.ts +55 -0
  95. package/plugin/dist/index.d.ts.map +1 -1
  96. package/plugin/dist/index.js +362 -42
  97. package/plugin/dist/index.js.map +1 -1
  98. package/plugin/package.json +2 -2
  99. package/scripts/install-runtime.mjs +13 -3
@@ -0,0 +1,42 @@
1
+ # hypermem Benchmark Suite
2
+
3
+ **Architecture-neutral memory system benchmark for OpenClaw agents.**
4
+
5
+ ## Methodology
6
+
7
+ Sequential A/B testing on identical OpenClaw stacks. The ONLY variable between runs is the memory system hook.
8
+
9
+ - Same Docker image (OpenClaw 2026.3.28)
10
+ - Same agent config, system prompt, model, token budget
11
+ - Same conversation dataset
12
+ - Same hardware (sequential, not parallel — no resource contention)
13
+ - Clean teardown between runs (`docker compose down -v`)
14
+
15
+ ## Memory Systems Tested
16
+
17
+ | System | Type | Hook adapter |
18
+ |---|---|---|
19
+ | noop (baseline) | No memory | Passes through raw context, no retrieval |
20
+ | hypermem | SQLite hot-cache compositor | Native Node.js hook |
21
+ | Mem0 | Vector + graph memory | Python subprocess adapter |
22
+ | Letta (MemGPT) | Self-editing memory | REST API adapter |
23
+
24
+ ## Running
25
+
26
+ ```bash
27
+ ./run-bench.sh
28
+ ```
29
+
30
+ Results land in `results/`. Final comparison: `results/comparison.md`.
31
+
32
+ ## Dataset
33
+
34
+ Uses LoCoMo (Long Conversation Memory) — 81 Q&A pairs across multi-session conversations.
35
+
36
+ ## Scoring
37
+
38
+ - F1 (token overlap)
39
+ - BLEU-1 (unigram precision)
40
+ - J (LLM-as-Judge — GPT-4 or equivalent rates answer quality 1-5)
41
+ - Latency (p50, p95, p99 for record + compose operations)
42
+ - Token usage (context tokens consumed per response)
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * HyperMem Data Access Benchmark
4
+ *
5
+ * Tests all critical data paths against REAL production data.
6
+ * No synthetic fixtures — this measures what the system actually does.
7
+ *
8
+ * Usage: node bench/data-access-bench.mjs [--iterations N] [--warmup N] [--agent AGENT] [--data-dir DIR]
9
+ */
10
+
11
+ import { DatabaseSync } from 'node:sqlite';
12
+ import path from 'node:path';
13
+ import fs from 'node:fs';
14
+
15
+ function argValue(name, fallback = null) {
16
+ return process.argv.find((a, i) => process.argv[i - 1] === name) ?? fallback;
17
+ }
18
+
19
+ const DATA_DIR = argValue('--data-dir', process.env.HYPERMEM_DATA_DIR ?? path.join(process.env.HOME || '/home/user', '.openclaw', 'hypermem'));
20
+ const ITERATIONS = parseInt(argValue('--iterations', '100'), 10);
21
+ const WARMUP = parseInt(argValue('--warmup', '3'), 10);
22
+ const TARGET_AGENT = argValue('--agent', null);
23
+
24
+ // ── Helpers ──────────────────────────────────────────────────
25
+
26
+ function openDb(dbPath) {
27
+ if (!fs.existsSync(dbPath)) return null;
28
+ const db = new DatabaseSync(dbPath, { readOnly: true });
29
+ db.exec('PRAGMA journal_mode = WAL');
30
+ db.exec('PRAGMA cache_size = -2000');
31
+ return db;
32
+ }
33
+
34
+ function bench(label, fn, iterations = ITERATIONS) {
35
+ // Warm up, discard
36
+ for (let i = 0; i < WARMUP; i++) fn();
37
+
38
+ const times = [];
39
+ for (let i = 0; i < iterations; i++) {
40
+ const start = performance.now();
41
+ fn();
42
+ times.push(performance.now() - start);
43
+ }
44
+
45
+ times.sort((a, b) => a - b);
46
+ const p50 = times[Math.floor(times.length * 0.50)];
47
+ const p95 = times[Math.floor(times.length * 0.95)];
48
+ const p99 = times[Math.floor(times.length * 0.99)];
49
+ const max = times[times.length - 1];
50
+ const min = times[0];
51
+ const avg = times.reduce((s, t) => s + t, 0) / times.length;
52
+
53
+ return { label, iterations, min, avg, p50, p95, p99, max };
54
+ }
55
+
56
+ function fmtMs(ms) {
57
+ if (ms < 0.01) return `${(ms * 1000).toFixed(1)}µs`;
58
+ if (ms < 1) return `${ms.toFixed(2)}ms`;
59
+ return `${ms.toFixed(1)}ms`;
60
+ }
61
+
62
+ function printResult(r) {
63
+ const flag = r.p95 > 10 ? '🔴' : r.p95 > 5 ? '🟡' : '✅';
64
+ console.log(` ${flag} ${r.label}`);
65
+ console.log(` min=${fmtMs(r.min)} avg=${fmtMs(r.avg)} p50=${fmtMs(r.p50)} p95=${fmtMs(r.p95)} p99=${fmtMs(r.p99)} max=${fmtMs(r.max)} (${r.iterations} runs)`);
66
+ }
67
+
68
+ // ── Main ─────────────────────────────────────────────────────
69
+
70
+ console.log(`\n${'═'.repeat(60)}`);
71
+ console.log(` HyperMem Data Access Benchmark`);
72
+ console.log(` Data: ${DATA_DIR}`);
73
+ console.log(` Iterations: ${ITERATIONS}`);
74
+ console.log(` Warmup: ${WARMUP}`);
75
+ console.log(`${'═'.repeat(60)}\n`);
76
+
77
+ // Discover agents
78
+ const agentsDir = path.join(DATA_DIR, 'agents');
79
+ if (!fs.existsSync(agentsDir)) {
80
+ console.error(`No HyperMem agent data found at ${agentsDir}`);
81
+ console.error('Run an OpenClaw agent turn with HyperMem active, or pass --data-dir /path/to/hypermem.');
82
+ process.exit(2);
83
+ }
84
+ const agents = fs.readdirSync(agentsDir).filter(a => {
85
+ if (TARGET_AGENT && a !== TARGET_AGENT) return false;
86
+ return fs.existsSync(path.join(agentsDir, a, 'messages.db'));
87
+ });
88
+
89
+ console.log(`Agents: ${agents.join(', ')}`);
90
+ console.log(`Library DB: ${fs.existsSync(path.join(DATA_DIR, 'library.db')) ? 'yes' : 'no'}`);
91
+
92
+ // ── Per-Agent Message DB Benchmarks ──────────────────────────
93
+
94
+ const allResults = [];
95
+
96
+ for (const agent of agents) {
97
+ const dbPath = path.join(agentsDir, agent, 'messages.db');
98
+ const db = openDb(dbPath);
99
+ if (!db) continue;
100
+
101
+ const msgCount = db.prepare('SELECT count(*) as c FROM messages').get().c;
102
+ const convCount = db.prepare('SELECT count(*) as c FROM conversations').get().c;
103
+ const dbSize = fs.statSync(dbPath).size;
104
+
105
+ console.log(`\n── ${agent} (${msgCount} msgs, ${convCount} convs, ${(dbSize / 1024 / 1024).toFixed(1)}MB) ──`);
106
+
107
+ if (msgCount === 0) {
108
+ console.log(' (empty — skipping)');
109
+ db.close();
110
+ continue;
111
+ }
112
+
113
+ // Get a real conversation to test with
114
+ const bigConv = db.prepare('SELECT id, message_count FROM conversations ORDER BY message_count DESC LIMIT 1').get();
115
+ const recentConv = db.prepare('SELECT id, message_count FROM conversations ORDER BY updated_at DESC LIMIT 1').get();
116
+
117
+ // 1. getRecentMessages — the HOT path (called every compose cycle)
118
+ if (bigConv) {
119
+ const stmtRecent = db.prepare('SELECT * FROM messages WHERE conversation_id = ? ORDER BY message_index DESC LIMIT ?');
120
+ allResults.push(bench(`${agent}: getRecentMessages(50) [${bigConv.message_count} msgs in conv]`, () => {
121
+ stmtRecent.all(bigConv.id, 50);
122
+ }));
123
+ printResult(allResults[allResults.length - 1]);
124
+
125
+ // With larger window
126
+ allResults.push(bench(`${agent}: getRecentMessages(200)`, () => {
127
+ stmtRecent.all(bigConv.id, 200);
128
+ }));
129
+ printResult(allResults[allResults.length - 1]);
130
+ }
131
+
132
+ // 2. Conversation lookup by session key (called on every ingest)
133
+ const someConv = db.prepare('SELECT session_key FROM conversations LIMIT 1').get();
134
+ if (someConv) {
135
+ const stmtConv = db.prepare('SELECT * FROM conversations WHERE session_key = ?');
136
+ allResults.push(bench(`${agent}: getConversation(sessionKey)`, () => {
137
+ stmtConv.get(someConv.session_key);
138
+ }));
139
+ printResult(allResults[allResults.length - 1]);
140
+ }
141
+
142
+ // 3. Message recording (write path — INSERT + UPDATE counter)
143
+ // We'll do this read-only by benchmarking the prepared statement overhead
144
+ // and the conversation counter update with a rollback
145
+ if (recentConv) {
146
+ const stmtInsert = db.prepare(`
147
+ SELECT 1 WHERE 0
148
+ `);
149
+ // Actually, let's measure a SELECT that simulates the write path index lookups
150
+ const stmtMaxIdx = db.prepare('SELECT MAX(message_index) AS max_idx FROM messages WHERE conversation_id = ?');
151
+ allResults.push(bench(`${agent}: MAX(message_index) lookup [write path]`, () => {
152
+ stmtMaxIdx.get(recentConv.id);
153
+ }));
154
+ printResult(allResults[allResults.length - 1]);
155
+ }
156
+
157
+ // 4. FTS search (fixed: no agent_id filter — per-agent DB is single-tenant)
158
+ const stmtFts = db.prepare(`
159
+ SELECT m.* FROM messages m
160
+ JOIN messages_fts fts ON m.id = fts.rowid
161
+ WHERE messages_fts MATCH ?
162
+ ORDER BY fts.rank
163
+ LIMIT ?
164
+ `);
165
+ allResults.push(bench(`${agent}: FTS search("deploy*")`, () => {
166
+ stmtFts.all('deploy*', 20);
167
+ }));
168
+ printResult(allResults[allResults.length - 1]);
169
+
170
+ // 5. Cross-session messages query (getAgentMessages)
171
+ const stmtCross = db.prepare('SELECT * FROM messages WHERE agent_id = ? AND is_heartbeat = 0 ORDER BY created_at DESC LIMIT ?');
172
+ allResults.push(bench(`${agent}: getAgentMessages(limit=50, no heartbeats)`, () => {
173
+ stmtCross.all(agent, 50);
174
+ }));
175
+ printResult(allResults[allResults.length - 1]);
176
+
177
+ // 6. Time-range query (since timestamp)
178
+ const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
179
+ const stmtSince = db.prepare('SELECT * FROM messages WHERE agent_id = ? AND created_at > ? ORDER BY created_at DESC LIMIT ?');
180
+ allResults.push(bench(`${agent}: messages since 24h ago`, () => {
181
+ stmtSince.all(agent, oneDayAgo, 200);
182
+ }));
183
+ printResult(allResults[allResults.length - 1]);
184
+
185
+ // 7. Conversation list (getConversations)
186
+ const stmtConvList = db.prepare('SELECT * FROM conversations WHERE agent_id = ? ORDER BY updated_at DESC LIMIT ?');
187
+ allResults.push(bench(`${agent}: getConversations(limit=20)`, () => {
188
+ stmtConvList.all(agent, 20);
189
+ }));
190
+ printResult(allResults[allResults.length - 1]);
191
+
192
+ db.close();
193
+ }
194
+
195
+ // ── Library DB Benchmarks ────────────────────────────────────
196
+
197
+ const libPath = path.join(DATA_DIR, 'library.db');
198
+ const libDb = openDb(libPath);
199
+
200
+ if (libDb) {
201
+ const factCount = libDb.prepare('SELECT count(*) as c FROM facts').get().c;
202
+ const episodeCount = libDb.prepare('SELECT count(*) as c FROM episodes').get().c;
203
+ const topicCount = libDb.prepare('SELECT count(*) as c FROM topics').get().c;
204
+ const fleetCount = libDb.prepare('SELECT count(*) as c FROM fleet_agents').get().c;
205
+ const libSize = fs.statSync(libPath).size;
206
+
207
+ console.log(`\n── Library DB (${factCount} facts, ${episodeCount} episodes, ${topicCount} topics, ${fleetCount} fleet, ${(libSize / 1024 / 1024).toFixed(1)}MB) ──`);
208
+
209
+ // 8. Active facts for agent (compositor hot path)
210
+ const stmtFacts = libDb.prepare(`
211
+ SELECT * FROM facts
212
+ WHERE agent_id = ? AND superseded_by IS NULL AND decay_score > 0
213
+ ORDER BY confidence DESC, updated_at DESC
214
+ LIMIT ?
215
+ `);
216
+ const libraryAgents = agents.length > 0 ? agents : ['main'];
217
+ for (const agent of libraryAgents.slice(0, 3)) {
218
+ allResults.push(bench(`library: activeFacts(${agent}, limit=50)`, () => {
219
+ stmtFacts.all(agent, 50);
220
+ }));
221
+ printResult(allResults[allResults.length - 1]);
222
+ }
223
+
224
+ // 9. Recent episodes (compositor hot path)
225
+ const stmtEpisodes = libDb.prepare(`
226
+ SELECT * FROM episodes
227
+ WHERE agent_id = ? AND decay_score > 0
228
+ ORDER BY created_at DESC
229
+ LIMIT ?
230
+ `);
231
+ for (const agent of libraryAgents.slice(0, 2)) {
232
+ allResults.push(bench(`library: recentEpisodes(${agent}, limit=20)`, () => {
233
+ stmtEpisodes.all(agent, 20);
234
+ }));
235
+ printResult(allResults[allResults.length - 1]);
236
+ }
237
+
238
+ // 10. Active topics
239
+ const stmtTopics = libDb.prepare(`
240
+ SELECT * FROM topics WHERE agent_id = ? AND status = 'active'
241
+ ORDER BY updated_at DESC LIMIT ?
242
+ `);
243
+ const primaryAgent = libraryAgents[0] ?? 'main';
244
+ allResults.push(bench(`library: activeTopics(${primaryAgent})`, () => {
245
+ stmtTopics.all(primaryAgent, 20);
246
+ }));
247
+ printResult(allResults[allResults.length - 1]);
248
+
249
+ // 11. Fleet agent lookup
250
+ const stmtFleet = libDb.prepare('SELECT * FROM fleet_agents WHERE id = ?');
251
+ allResults.push(bench(`library: fleetAgent lookup(${primaryAgent})`, () => {
252
+ stmtFleet.get(primaryAgent);
253
+ }));
254
+ printResult(allResults[allResults.length - 1]);
255
+
256
+ // 12. Fleet scan (all agents)
257
+ const stmtFleetAll = libDb.prepare('SELECT * FROM fleet_agents');
258
+ allResults.push(bench(`library: allFleetAgents (${fleetCount})`, () => {
259
+ stmtFleetAll.all();
260
+ }));
261
+ printResult(allResults[allResults.length - 1]);
262
+
263
+ // 13. Facts FTS (if available)
264
+ try {
265
+ const stmtFactFts = libDb.prepare(`
266
+ SELECT f.* FROM facts f
267
+ JOIN facts_fts ff ON f.id = ff.rowid
268
+ WHERE facts_fts MATCH ?
269
+ LIMIT ?
270
+ `);
271
+ allResults.push(bench(`library: facts FTS("redis")`, () => {
272
+ stmtFactFts.all('redis', 20);
273
+ }));
274
+ printResult(allResults[allResults.length - 1]);
275
+ } catch {
276
+ console.log(' ⚪ Facts FTS not available');
277
+ }
278
+
279
+ // 14. Episodes by type (filtered scan)
280
+ const stmtEpType = libDb.prepare(`
281
+ SELECT * FROM episodes WHERE agent_id = ? AND event_type = ? ORDER BY created_at DESC LIMIT ?
282
+ `);
283
+ allResults.push(bench(`library: episodes(${primaryAgent}, type=decision)`, () => {
284
+ stmtEpType.all(primaryAgent, 'decision', 20);
285
+ }));
286
+ printResult(allResults[allResults.length - 1]);
287
+
288
+ // 15. Cross-agent episodes (shared visibility)
289
+ const stmtSharedEp = libDb.prepare(`
290
+ SELECT * FROM episodes WHERE visibility IN ('org', 'fleet') ORDER BY created_at DESC LIMIT ?
291
+ `);
292
+ allResults.push(bench(`library: sharedEpisodes(limit=50)`, () => {
293
+ stmtSharedEp.all(50);
294
+ }));
295
+ printResult(allResults[allResults.length - 1]);
296
+
297
+ // 16. Doc chunks (trigger-based retrieval)
298
+ try {
299
+ const chunkCount = libDb.prepare('SELECT count(*) as c FROM doc_chunks').get().c;
300
+ if (chunkCount > 0) {
301
+ const stmtChunks = libDb.prepare(`
302
+ SELECT * FROM doc_chunks WHERE agent_id = ? AND collection = ? ORDER BY chunk_index LIMIT ?
303
+ `);
304
+ allResults.push(bench(`library: docChunks(${primaryAgent}, policy, limit=20) [${chunkCount} total]`, () => {
305
+ stmtChunks.all(primaryAgent, 'policy', 20);
306
+ }));
307
+ printResult(allResults[allResults.length - 1]);
308
+ }
309
+ } catch {
310
+ console.log(' ⚪ Doc chunks table not available');
311
+ }
312
+
313
+ libDb.close();
314
+ }
315
+
316
+ // ── Compaction Fence (new module) ────────────────────────────
317
+
318
+ const fenceAgent = agents[0] ?? 'main';
319
+ const agent1Db = openDb(path.join(agentsDir, fenceAgent, 'messages.db'));
320
+ if (agent1Db) {
321
+ try {
322
+ const hasFence = agent1Db.prepare("SELECT count(*) as c FROM sqlite_master WHERE type='table' AND name='compaction_fences'").get().c;
323
+ if (hasFence) {
324
+ const stmtFence = agent1Db.prepare('SELECT * FROM compaction_fences WHERE conversation_id = ?');
325
+ const someConvId = agent1Db.prepare('SELECT id FROM conversations LIMIT 1').get()?.id;
326
+ if (someConvId) {
327
+ allResults.push(bench(`${fenceAgent}: compactionFence lookup`, () => {
328
+ stmtFence.get(someConvId);
329
+ }));
330
+ printResult(allResults[allResults.length - 1]);
331
+ }
332
+ }
333
+ } catch {
334
+ console.log(' ⚪ Compaction fence not available');
335
+ }
336
+ agent1Db.close();
337
+ }
338
+
339
+ // ── Summary ──────────────────────────────────────────────────
340
+
341
+ console.log(`\n${'═'.repeat(60)}`);
342
+ console.log(` SUMMARY`);
343
+ console.log(`${'═'.repeat(60)}`);
344
+
345
+ const hot = allResults.filter(r => r.label.includes('getRecentMessages(50)') || r.label.includes('activeFacts') || r.label.includes('recentEpisodes') || r.label.includes('getConversation'));
346
+ const slow = allResults.filter(r => r.p95 > 5).sort((a, b) => b.p95 - a.p95);
347
+ const spikes = allResults.filter(r => r.max > 15).sort((a, b) => b.max - a.max);
348
+
349
+ console.log(`\nHot path performance (compositor critical):`);
350
+ for (const r of hot) {
351
+ printResult(r);
352
+ }
353
+
354
+ if (slow.length > 0) {
355
+ console.log(`\n🟡 Queries with p95 > 5ms:`);
356
+ for (const r of slow) {
357
+ printResult(r);
358
+ }
359
+ } else {
360
+ console.log(`\n✅ All queries under 5ms at p95`);
361
+ }
362
+
363
+ if (spikes.length > 0) {
364
+ console.log(`\n🔴 Queries with max > 15ms (spike alert):`);
365
+ for (const r of spikes) {
366
+ printResult(r);
367
+ }
368
+ } else {
369
+ console.log(`✅ No spikes above 15ms`);
370
+ }
371
+
372
+ // Overall stats
373
+ const allP95 = allResults.map(r => r.p95);
374
+ const worstP95 = Math.max(...allP95);
375
+ const avgP95 = allP95.reduce((s, t) => s + t, 0) / allP95.length;
376
+ const worstMax = Math.max(...allResults.map(r => r.max));
377
+
378
+ console.log(`\nOverall: worst p95=${fmtMs(worstP95)}, avg p95=${fmtMs(avgP95)}, worst max=${fmtMs(worstMax)}`);
379
+ console.log(`Queries benchmarked: ${allResults.length}`);
380
+ console.log(`${'═'.repeat(60)}\n`);
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../bench/data-access-bench.mjs';