@psiclawops/hypermem 0.8.5 → 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.
- package/CHANGELOG.md +26 -0
- package/INSTALL.md +132 -9
- package/README.md +119 -272
- package/bench/README.md +42 -0
- package/bench/data-access-bench.mjs +380 -0
- package/bin/hypermem-bench.mjs +2 -0
- package/bin/hypermem-doctor.mjs +412 -0
- package/bin/hypermem-model-audit.mjs +339 -0
- package/bin/hypermem-status.mjs +491 -70
- package/dist/adaptive-lifecycle.d.ts +81 -0
- package/dist/adaptive-lifecycle.d.ts.map +1 -0
- package/dist/adaptive-lifecycle.js +190 -0
- package/dist/budget-policy.d.ts +1 -1
- package/dist/budget-policy.d.ts.map +1 -1
- package/dist/budget-policy.js +10 -5
- package/dist/cache.d.ts +1 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +2 -0
- package/dist/composition-snapshot-integrity.d.ts +36 -0
- package/dist/composition-snapshot-integrity.d.ts.map +1 -0
- package/dist/composition-snapshot-integrity.js +131 -0
- package/dist/composition-snapshot-runtime.d.ts +59 -0
- package/dist/composition-snapshot-runtime.d.ts.map +1 -0
- package/dist/composition-snapshot-runtime.js +250 -0
- package/dist/composition-snapshot-store.d.ts +44 -0
- package/dist/composition-snapshot-store.d.ts.map +1 -0
- package/dist/composition-snapshot-store.js +117 -0
- package/dist/compositor.d.ts +125 -1
- package/dist/compositor.d.ts.map +1 -1
- package/dist/compositor.js +692 -44
- package/dist/doc-chunk-store.d.ts +19 -0
- package/dist/doc-chunk-store.d.ts.map +1 -1
- package/dist/doc-chunk-store.js +56 -6
- package/dist/hybrid-retrieval.d.ts +38 -0
- package/dist/hybrid-retrieval.d.ts.map +1 -1
- package/dist/hybrid-retrieval.js +86 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -2
- package/dist/knowledge-store.d.ts +4 -1
- package/dist/knowledge-store.d.ts.map +1 -1
- package/dist/knowledge-store.js +27 -4
- package/dist/library-schema.d.ts +12 -8
- package/dist/library-schema.d.ts.map +1 -1
- package/dist/library-schema.js +22 -8
- package/dist/message-store.d.ts.map +1 -1
- package/dist/message-store.js +7 -3
- package/dist/metrics-dashboard.d.ts +18 -1
- package/dist/metrics-dashboard.d.ts.map +1 -1
- package/dist/metrics-dashboard.js +52 -14
- package/dist/reranker.d.ts +1 -1
- package/dist/reranker.js +2 -2
- package/dist/schema.d.ts +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +28 -1
- package/dist/seed.d.ts.map +1 -1
- package/dist/seed.js +2 -0
- package/dist/topic-synthesizer.d.ts +20 -0
- package/dist/topic-synthesizer.d.ts.map +1 -1
- package/dist/topic-synthesizer.js +113 -3
- package/dist/trigger-registry.d.ts.map +1 -1
- package/dist/trigger-registry.js +10 -2
- package/dist/types.d.ts +271 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +7 -7
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +17 -7
- package/docs/DIAGNOSTICS.md +205 -0
- package/docs/INTEGRATION_VALIDATION.md +186 -0
- package/docs/MIGRATION.md +9 -6
- package/docs/MIGRATION_GUIDE.md +125 -101
- package/docs/ROADMAP.md +238 -20
- package/docs/TUNING.md +19 -5
- package/install.sh +152 -401
- package/memory-plugin/LICENSE +190 -0
- package/memory-plugin/README.md +20 -0
- package/memory-plugin/dist/index.js +50 -0
- package/memory-plugin/package.json +2 -2
- package/package.json +18 -4
- package/plugin/LICENSE +190 -0
- package/plugin/README.md +20 -0
- package/plugin/dist/index.d.ts +29 -0
- package/plugin/dist/index.d.ts.map +1 -1
- package/plugin/dist/index.js +288 -23
- package/plugin/dist/index.js.map +1 -1
- package/plugin/package.json +2 -2
- package/scripts/install-runtime.mjs +12 -1
package/bench/README.md
ADDED
|
@@ -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`);
|