@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/bin/hypermem-status.mjs
CHANGED
|
@@ -3,27 +3,41 @@
|
|
|
3
3
|
* hypermem status — health check and metrics dashboard CLI
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* hypermem-status # metrics dashboard
|
|
7
|
+
* hypermem-status --master # concise operator health surface
|
|
8
|
+
* hypermem-status --agent forge --master # scoped operator health surface
|
|
9
|
+
* hypermem-status --json # machine-readable output
|
|
10
|
+
* hypermem-status --health # health checks only (exit 1 on failure)
|
|
10
11
|
*
|
|
11
12
|
* Requires: compiled dist/ (run `npm run build` first)
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
|
-
import { existsSync } from 'node:fs';
|
|
15
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
15
16
|
import { resolve, join, dirname } from 'node:path';
|
|
16
17
|
import { fileURLToPath } from 'node:url';
|
|
17
18
|
import os from 'node:os';
|
|
19
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
18
20
|
|
|
19
21
|
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
20
22
|
const root = resolve(__dir, '..');
|
|
23
|
+
const homeDir = process.env.HOME || os.homedir();
|
|
24
|
+
|
|
25
|
+
const DEFAULT_EMBEDDING = {
|
|
26
|
+
provider: 'ollama',
|
|
27
|
+
model: 'nomic-embed-text',
|
|
28
|
+
dimensions: 768,
|
|
29
|
+
batchSize: null,
|
|
30
|
+
openaiBaseUrl: null,
|
|
31
|
+
ollamaUrl: null,
|
|
32
|
+
geminiBaseUrl: null,
|
|
33
|
+
};
|
|
21
34
|
|
|
22
35
|
// ── Arg parsing ──────────────────────────────────────────────────
|
|
23
36
|
const args = process.argv.slice(2);
|
|
24
37
|
const flags = {
|
|
25
38
|
json: args.includes('--json'),
|
|
26
39
|
health: args.includes('--health'),
|
|
40
|
+
master: args.includes('--master') || args.includes('--planks'),
|
|
27
41
|
help: args.includes('--help') || args.includes('-h'),
|
|
28
42
|
agent: null,
|
|
29
43
|
};
|
|
@@ -38,20 +52,94 @@ if (flags.help) {
|
|
|
38
52
|
hypermem status — health check and metrics dashboard
|
|
39
53
|
|
|
40
54
|
Usage:
|
|
41
|
-
hypermem-status
|
|
55
|
+
hypermem-status [options]
|
|
42
56
|
|
|
43
57
|
Options:
|
|
44
|
-
--
|
|
45
|
-
--
|
|
46
|
-
--
|
|
47
|
-
|
|
58
|
+
--master Concise master health status for main HyperMem planks
|
|
59
|
+
--agent <id> Scope metrics to a specific agent
|
|
60
|
+
--json Output raw JSON instead of formatted summary
|
|
61
|
+
--health Health checks only (exits 1 if any check fails)
|
|
62
|
+
-h, --help Show this help
|
|
48
63
|
`);
|
|
49
64
|
process.exit(0);
|
|
50
65
|
}
|
|
51
66
|
|
|
52
|
-
// ── Resolve data directory
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
// ── Resolve config and data directory ────────────────────────────
|
|
68
|
+
function readJsonIfExists(filePath) {
|
|
69
|
+
if (!existsSync(filePath)) return null;
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function stripSecretFields(obj) {
|
|
78
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
79
|
+
const out = Array.isArray(obj) ? [] : {};
|
|
80
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
81
|
+
if (/apiKey|token|secret|password|authorization/i.test(key)) {
|
|
82
|
+
out[key] = value ? '[redacted]' : value;
|
|
83
|
+
} else if (value && typeof value === 'object') {
|
|
84
|
+
out[key] = stripSecretFields(value);
|
|
85
|
+
} else {
|
|
86
|
+
out[key] = value;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readOpenClawPluginConfig(defaultDataDir) {
|
|
93
|
+
const openclawConfigPath = process.env.OPENCLAW_CONFIG
|
|
94
|
+
|| join(homeDir, '.openclaw', 'openclaw.json');
|
|
95
|
+
const parsed = readJsonIfExists(openclawConfigPath);
|
|
96
|
+
const config = parsed?.plugins?.entries?.hypercompositor?.config
|
|
97
|
+
?? parsed?.plugins?.entries?.hypermem?.config
|
|
98
|
+
?? null;
|
|
99
|
+
if (!config || typeof config !== 'object') return { config: {}, source: null, dataDir: defaultDataDir };
|
|
100
|
+
return {
|
|
101
|
+
config,
|
|
102
|
+
source: openclawConfigPath,
|
|
103
|
+
dataDir: config.dataDir || defaultDataDir,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveRuntimeConfig() {
|
|
108
|
+
const defaultDataDir = process.env.HYPERMEM_DATA_DIR || join(homeDir, '.openclaw', 'hypermem');
|
|
109
|
+
const central = readOpenClawPluginConfig(defaultDataDir);
|
|
110
|
+
const dataDir = process.env.HYPERMEM_DATA_DIR || central.dataDir || defaultDataDir;
|
|
111
|
+
const legacyConfigPath = join(dataDir, 'config.json');
|
|
112
|
+
const fileConfig = readJsonIfExists(legacyConfigPath) ?? {};
|
|
113
|
+
const pluginConfig = central.config ?? {};
|
|
114
|
+
|
|
115
|
+
// Mirrors plugin loadUserConfig(): config.json fallback, plugin config wins.
|
|
116
|
+
const merged = { ...fileConfig };
|
|
117
|
+
for (const key of [
|
|
118
|
+
'contextWindowSize', 'contextWindowReserve', 'deferToolPruning',
|
|
119
|
+
'verboseLogging', 'warmCacheReplayThresholdMs', 'subagentWarming',
|
|
120
|
+
]) {
|
|
121
|
+
if (pluginConfig[key] != null) merged[key] = pluginConfig[key];
|
|
122
|
+
}
|
|
123
|
+
for (const key of ['contextWindowOverrides', 'compositor', 'eviction', 'embedding', 'reranker']) {
|
|
124
|
+
if (pluginConfig[key]) merged[key] = { ...(merged[key] ?? {}), ...pluginConfig[key] };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const embedding = { ...DEFAULT_EMBEDDING, ...(merged.embedding ?? {}) };
|
|
128
|
+
const sources = [];
|
|
129
|
+
if (existsSync(legacyConfigPath)) sources.push(legacyConfigPath);
|
|
130
|
+
if (central.source && Object.keys(pluginConfig).length > 0) sources.push(`${central.source}:plugins.entries.hypercompositor.config`);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
dataDir,
|
|
134
|
+
config: merged,
|
|
135
|
+
embedding,
|
|
136
|
+
configSources: sources.length > 0 ? sources : ['defaults'],
|
|
137
|
+
redactedConfig: stripSecretFields(merged),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const runtime = resolveRuntimeConfig();
|
|
142
|
+
const dataDir = runtime.dataDir;
|
|
55
143
|
|
|
56
144
|
if (!existsSync(dataDir)) {
|
|
57
145
|
console.error(`Error: data directory not found: ${dataDir}`);
|
|
@@ -59,13 +147,14 @@ if (!existsSync(dataDir)) {
|
|
|
59
147
|
process.exit(1);
|
|
60
148
|
}
|
|
61
149
|
|
|
62
|
-
// ──
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
function openDb(filePath, label) {
|
|
150
|
+
// ── DB helpers ───────────────────────────────────────────────────
|
|
151
|
+
function openDb(filePath, label, required = true) {
|
|
66
152
|
if (!existsSync(filePath)) {
|
|
67
|
-
|
|
68
|
-
|
|
153
|
+
if (required) {
|
|
154
|
+
console.error(`Error: ${label} not found: ${filePath}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
69
158
|
}
|
|
70
159
|
try {
|
|
71
160
|
const db = new DatabaseSync(filePath, { open: true });
|
|
@@ -73,42 +162,360 @@ function openDb(filePath, label) {
|
|
|
73
162
|
db.exec('PRAGMA busy_timeout = 3000');
|
|
74
163
|
return db;
|
|
75
164
|
} catch (err) {
|
|
76
|
-
|
|
77
|
-
|
|
165
|
+
if (required) {
|
|
166
|
+
console.error(`Error opening ${label}: ${err.message}`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
78
170
|
}
|
|
79
171
|
}
|
|
80
172
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
173
|
+
function safeGet(db, sql, params = []) {
|
|
174
|
+
try { return db.prepare(sql).get(...params); } catch { return null; }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function safeAll(db, sql, params = []) {
|
|
178
|
+
try { return db.prepare(sql).all(...params); } catch { return []; }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function fileInfo(filePath) {
|
|
182
|
+
if (!existsSync(filePath)) return { exists: false, bytes: 0, mb: 0 };
|
|
183
|
+
const bytes = statSync(filePath).size;
|
|
184
|
+
return { exists: true, bytes, mb: Math.round((bytes / 1024 / 1024) * 10) / 10 };
|
|
185
|
+
}
|
|
85
186
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
187
|
+
function pct(indexed, total) {
|
|
188
|
+
if (!total) return null;
|
|
189
|
+
return Math.round((indexed / total) * 1000) / 10;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function statusForCoverage(percent, minimum) {
|
|
193
|
+
if (percent === null) return 'n/a';
|
|
194
|
+
return percent >= minimum ? 'ok' : 'warn';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function icon(status) {
|
|
198
|
+
if (status === 'ok') return '✅';
|
|
199
|
+
if (status === 'warn') return '⚠️';
|
|
200
|
+
if (status === 'fail') return '❌';
|
|
201
|
+
return '•';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function findMessageDb(agentId) {
|
|
205
|
+
if (agentId) return join(dataDir, 'agents', agentId, 'messages.db');
|
|
90
206
|
const agentsDir = join(dataDir, 'agents');
|
|
91
|
-
if (existsSync(agentsDir))
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
207
|
+
if (!existsSync(agentsDir)) return null;
|
|
208
|
+
const agents = readdirSync(agentsDir, { withFileTypes: true })
|
|
209
|
+
.filter(d => d.isDirectory())
|
|
210
|
+
.map(d => d.name)
|
|
211
|
+
.sort();
|
|
212
|
+
for (const agent of agents) {
|
|
213
|
+
const candidate = join(agentsDir, agent, 'messages.db');
|
|
214
|
+
if (existsSync(candidate)) return candidate;
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function collectMessageStats(agentId) {
|
|
220
|
+
const agentsDir = join(dataDir, 'agents');
|
|
221
|
+
if (!existsSync(agentsDir)) return { agentsWithMessages: 0, totalMessages: 0, newestMessageAt: null };
|
|
222
|
+
const agents = agentId ? [agentId] : readdirSync(agentsDir, { withFileTypes: true })
|
|
223
|
+
.filter(d => d.isDirectory())
|
|
224
|
+
.map(d => d.name);
|
|
225
|
+
let agentsWithMessages = 0;
|
|
226
|
+
let totalMessages = 0;
|
|
227
|
+
let newestMessageAt = null;
|
|
228
|
+
for (const agent of agents) {
|
|
229
|
+
const dbPath = join(agentsDir, agent, 'messages.db');
|
|
230
|
+
if (!existsSync(dbPath)) continue;
|
|
231
|
+
const db = openDb(dbPath, `${agent} messages.db`, false);
|
|
232
|
+
if (!db) continue;
|
|
233
|
+
agentsWithMessages += 1;
|
|
234
|
+
const count = safeGet(db, 'SELECT COUNT(*) AS count FROM messages')?.count ?? 0;
|
|
235
|
+
const newest = safeGet(db, 'SELECT MAX(created_at) AS newest FROM messages')?.newest ?? null;
|
|
236
|
+
totalMessages += count;
|
|
237
|
+
if (newest && (!newestMessageAt || newest > newestMessageAt)) newestMessageAt = newest;
|
|
238
|
+
try { db.close(); } catch {}
|
|
239
|
+
}
|
|
240
|
+
return { agentsWithMessages, totalMessages, newestMessageAt };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function getVectorDimensions(vectorDb, tableName) {
|
|
244
|
+
if (!vectorDb) return null;
|
|
245
|
+
const row = safeGet(vectorDb, 'SELECT sql FROM sqlite_master WHERE type = ? AND name = ?', ['table', tableName]);
|
|
246
|
+
const match = row?.sql?.match(/float\[(\d+)\]/i);
|
|
247
|
+
return match ? Number(match[1]) : null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function getQuickCheck(db) {
|
|
251
|
+
const row = safeGet(db, 'PRAGMA quick_check');
|
|
252
|
+
return row ? Object.values(row)[0] : 'unknown';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function collectMasterHealth(libraryDb, vectorDb, mainDb) {
|
|
256
|
+
const agentClause = flags.agent ? 'AND agent_id = ?' : '';
|
|
257
|
+
const params = flags.agent ? [flags.agent] : [];
|
|
258
|
+
|
|
259
|
+
const factsTotal = safeGet(libraryDb, `SELECT COUNT(*) AS count FROM facts WHERE 1=1 ${agentClause}`, params)?.count ?? 0;
|
|
260
|
+
const factsActive = safeGet(libraryDb, `SELECT COUNT(*) AS count FROM facts WHERE superseded_by IS NULL AND decay_score < 0.8 ${agentClause}`, params)?.count ?? 0;
|
|
261
|
+
const episodesTotal = safeGet(libraryDb, `SELECT COUNT(*) AS count FROM episodes WHERE 1=1 ${agentClause}`, params)?.count ?? 0;
|
|
262
|
+
const episodesEligible = safeGet(libraryDb, `SELECT COUNT(*) AS count FROM episodes WHERE significance >= 0.5 ${agentClause}`, params)?.count ?? 0;
|
|
263
|
+
const knowledgeActive = safeGet(libraryDb, `SELECT COUNT(*) AS count FROM knowledge WHERE superseded_by IS NULL ${agentClause}`, params)?.count ?? 0;
|
|
264
|
+
const docChunks = safeGet(libraryDb, `SELECT COUNT(*) AS count FROM doc_chunks WHERE 1=1 ${agentClause}`, params)?.count ?? 0;
|
|
265
|
+
|
|
266
|
+
let factsIndexed = 0;
|
|
267
|
+
let episodesIndexed = 0;
|
|
268
|
+
let knowledgeIndexed = 0;
|
|
269
|
+
let totalVectors = 0;
|
|
270
|
+
let vectorByTable = {};
|
|
271
|
+
let oldestIndexedAt = null;
|
|
272
|
+
let newestIndexedAt = null;
|
|
273
|
+
let vectorDimensions = { facts: null, episodes: null, knowledge: null };
|
|
274
|
+
|
|
275
|
+
if (vectorDb) {
|
|
276
|
+
totalVectors = safeGet(vectorDb, 'SELECT COUNT(*) AS count FROM vec_index_map')?.count ?? 0;
|
|
277
|
+
vectorByTable = Object.fromEntries(safeAll(vectorDb, 'SELECT source_table, COUNT(*) AS count FROM vec_index_map GROUP BY source_table').map(r => [r.source_table, r.count]));
|
|
278
|
+
oldestIndexedAt = safeGet(vectorDb, 'SELECT MIN(indexed_at) AS value FROM vec_index_map')?.value ?? null;
|
|
279
|
+
newestIndexedAt = safeGet(vectorDb, 'SELECT MAX(indexed_at) AS value FROM vec_index_map')?.value ?? null;
|
|
280
|
+
vectorDimensions = {
|
|
281
|
+
facts: getVectorDimensions(vectorDb, 'vec_facts'),
|
|
282
|
+
episodes: getVectorDimensions(vectorDb, 'vec_episodes'),
|
|
283
|
+
knowledge: getVectorDimensions(vectorDb, 'vec_knowledge'),
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
if (flags.agent) {
|
|
287
|
+
factsIndexed = safeGet(vectorDb, `
|
|
288
|
+
SELECT COUNT(*) AS count
|
|
289
|
+
FROM vec_index_map m
|
|
290
|
+
JOIN library.facts f ON f.id = m.source_id
|
|
291
|
+
WHERE m.source_table = 'facts'
|
|
292
|
+
AND f.superseded_by IS NULL
|
|
293
|
+
AND f.decay_score < 0.8
|
|
294
|
+
AND f.agent_id = ?`, params)?.count ?? 0;
|
|
295
|
+
episodesIndexed = safeGet(vectorDb, `
|
|
296
|
+
SELECT COUNT(*) AS count
|
|
297
|
+
FROM vec_index_map m
|
|
298
|
+
JOIN library.episodes e ON e.id = m.source_id
|
|
299
|
+
WHERE m.source_table = 'episodes'
|
|
300
|
+
AND e.significance >= 0.5
|
|
301
|
+
AND e.agent_id = ?`, params)?.count ?? 0;
|
|
302
|
+
knowledgeIndexed = safeGet(vectorDb, `
|
|
303
|
+
SELECT COUNT(*) AS count
|
|
304
|
+
FROM vec_index_map m
|
|
305
|
+
JOIN library.knowledge k ON k.id = m.source_id
|
|
306
|
+
WHERE m.source_table = 'knowledge'
|
|
307
|
+
AND k.superseded_by IS NULL
|
|
308
|
+
AND k.agent_id = ?`, params)?.count ?? 0;
|
|
309
|
+
const rawFactsIndexed = safeGet(vectorDb, `
|
|
310
|
+
SELECT COUNT(*) AS count
|
|
311
|
+
FROM vec_index_map m
|
|
312
|
+
JOIN library.facts f ON f.id = m.source_id
|
|
313
|
+
WHERE m.source_table = 'facts'
|
|
314
|
+
AND f.agent_id = ?`, params)?.count ?? 0;
|
|
315
|
+
const rawEpisodesIndexed = safeGet(vectorDb, `
|
|
316
|
+
SELECT COUNT(*) AS count
|
|
317
|
+
FROM vec_index_map m
|
|
318
|
+
JOIN library.episodes e ON e.id = m.source_id
|
|
319
|
+
WHERE m.source_table = 'episodes'
|
|
320
|
+
AND e.agent_id = ?`, params)?.count ?? 0;
|
|
321
|
+
const rawKnowledgeIndexed = safeGet(vectorDb, `
|
|
322
|
+
SELECT COUNT(*) AS count
|
|
323
|
+
FROM vec_index_map m
|
|
324
|
+
JOIN library.knowledge k ON k.id = m.source_id
|
|
325
|
+
WHERE m.source_table = 'knowledge'
|
|
326
|
+
AND k.agent_id = ?`, params)?.count ?? 0;
|
|
327
|
+
vectorByTable = {
|
|
328
|
+
...(rawEpisodesIndexed ? { episodes: rawEpisodesIndexed } : {}),
|
|
329
|
+
...(rawFactsIndexed ? { facts: rawFactsIndexed } : {}),
|
|
330
|
+
...(rawKnowledgeIndexed ? { knowledge: rawKnowledgeIndexed } : {}),
|
|
331
|
+
};
|
|
332
|
+
totalVectors = rawFactsIndexed + rawEpisodesIndexed + rawKnowledgeIndexed;
|
|
333
|
+
} else {
|
|
334
|
+
factsIndexed = safeGet(vectorDb, `
|
|
335
|
+
SELECT COUNT(*) AS count
|
|
336
|
+
FROM vec_index_map m
|
|
337
|
+
JOIN library.facts f ON f.id = m.source_id
|
|
338
|
+
WHERE m.source_table = 'facts'
|
|
339
|
+
AND f.superseded_by IS NULL
|
|
340
|
+
AND f.decay_score < 0.8`)?.count ?? 0;
|
|
341
|
+
episodesIndexed = safeGet(vectorDb, `
|
|
342
|
+
SELECT COUNT(*) AS count
|
|
343
|
+
FROM vec_index_map m
|
|
344
|
+
JOIN library.episodes e ON e.id = m.source_id
|
|
345
|
+
WHERE m.source_table = 'episodes'
|
|
346
|
+
AND e.significance >= 0.5`)?.count ?? 0;
|
|
347
|
+
knowledgeIndexed = safeGet(vectorDb, `
|
|
348
|
+
SELECT COUNT(*) AS count
|
|
349
|
+
FROM vec_index_map m
|
|
350
|
+
JOIN library.knowledge k ON k.id = m.source_id
|
|
351
|
+
WHERE m.source_table = 'knowledge'
|
|
352
|
+
AND k.superseded_by IS NULL`)?.count ?? 0;
|
|
102
353
|
}
|
|
103
354
|
}
|
|
355
|
+
|
|
356
|
+
const factCoverage = pct(factsIndexed, factsActive);
|
|
357
|
+
const episodeCoverage = pct(episodesIndexed, episodesEligible);
|
|
358
|
+
const knowledgeCoverage = pct(knowledgeIndexed, knowledgeActive);
|
|
359
|
+
const factStatus = statusForCoverage(factCoverage, 80);
|
|
360
|
+
const episodeStatus = statusForCoverage(episodeCoverage, 80);
|
|
361
|
+
const knowledgeStatus = knowledgeActive === 0 ? 'n/a' : statusForCoverage(knowledgeCoverage, 80);
|
|
362
|
+
const configuredDimensions = Number(runtime.embedding.dimensions) || null;
|
|
363
|
+
const dimensionMatches = !configuredDimensions
|
|
364
|
+
? 'n/a'
|
|
365
|
+
: [vectorDimensions.facts, vectorDimensions.episodes, vectorDimensions.knowledge]
|
|
366
|
+
.filter(v => v != null)
|
|
367
|
+
.every(v => v === configuredDimensions)
|
|
368
|
+
? 'ok'
|
|
369
|
+
: 'fail';
|
|
370
|
+
|
|
371
|
+
const libraryQuickCheck = getQuickCheck(libraryDb);
|
|
372
|
+
const vectorQuickCheck = vectorDb ? getQuickCheck(vectorDb) : 'missing';
|
|
373
|
+
const mainQuickCheck = mainDb ? getQuickCheck(mainDb) : 'missing';
|
|
374
|
+
const dbStatus = libraryQuickCheck === 'ok' && (vectorQuickCheck === 'ok' || vectorQuickCheck === 'missing') && (mainQuickCheck === 'ok' || mainQuickCheck === 'missing') ? 'ok' : 'fail';
|
|
375
|
+
|
|
376
|
+
const messageStats = collectMessageStats(flags.agent);
|
|
377
|
+
const totalTurns = safeGet(libraryDb, `SELECT COUNT(*) AS count FROM output_metrics WHERE 1=1 ${agentClause}`, params)?.count ?? 0;
|
|
378
|
+
const avgLatency = safeGet(libraryDb, `SELECT AVG(latency_ms) AS value FROM output_metrics WHERE latency_ms IS NOT NULL ${agentClause}`, params)?.value ?? null;
|
|
379
|
+
const avgInput = safeGet(libraryDb, `SELECT AVG(input_tokens) AS value FROM output_metrics WHERE input_tokens IS NOT NULL ${agentClause}`, params)?.value ?? null;
|
|
380
|
+
const avgOutput = safeGet(libraryDb, `SELECT AVG(output_tokens) AS value FROM output_metrics WHERE output_tokens IS NOT NULL ${agentClause}`, params)?.value ?? null;
|
|
381
|
+
|
|
382
|
+
const issues = [];
|
|
383
|
+
if (dbStatus === 'fail') issues.push('database quick_check failed');
|
|
384
|
+
if (dimensionMatches === 'fail') issues.push('vector table dimensions do not match configured embedding dimensions');
|
|
385
|
+
if (factStatus === 'warn') issues.push(`fact vector coverage low (${factCoverage}%)`);
|
|
386
|
+
if (episodeStatus === 'warn') issues.push(`episode vector coverage low (${episodeCoverage}%)`);
|
|
387
|
+
if (knowledgeStatus === 'warn') issues.push(`knowledge vector coverage low (${knowledgeCoverage}%)`);
|
|
388
|
+
if (!vectorDb) issues.push('shared vectors.db missing');
|
|
389
|
+
|
|
390
|
+
const overall = issues.length === 0 ? 'healthy' : (dbStatus === 'fail' || dimensionMatches === 'fail' ? 'degraded' : 'attention');
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
snapshotAt: new Date().toISOString(),
|
|
394
|
+
scope: flags.agent ? { agent: flags.agent } : { fleet: true },
|
|
395
|
+
overall,
|
|
396
|
+
issues,
|
|
397
|
+
config: {
|
|
398
|
+
dataDir,
|
|
399
|
+
sources: runtime.configSources,
|
|
400
|
+
embedding: {
|
|
401
|
+
provider: runtime.embedding.provider ?? null,
|
|
402
|
+
model: runtime.embedding.model ?? null,
|
|
403
|
+
dimensions: configuredDimensions,
|
|
404
|
+
batchSize: runtime.embedding.batchSize ?? null,
|
|
405
|
+
baseUrl: runtime.embedding.openaiBaseUrl ?? runtime.embedding.ollamaUrl ?? runtime.embedding.geminiBaseUrl ?? null,
|
|
406
|
+
},
|
|
407
|
+
reranker: runtime.config.reranker ? stripSecretFields(runtime.config.reranker) : null,
|
|
408
|
+
},
|
|
409
|
+
databases: {
|
|
410
|
+
library: { path: libraryDbPath, ...fileInfo(libraryDbPath), quickCheck: libraryQuickCheck },
|
|
411
|
+
vectors: { path: vectorDbPath, ...fileInfo(vectorDbPath), quickCheck: vectorQuickCheck, dimensions: vectorDimensions },
|
|
412
|
+
messages: { sampledPath: mainDbPath, ...(mainDbPath ? fileInfo(mainDbPath) : { exists: false, bytes: 0, mb: 0 }), quickCheck: mainQuickCheck },
|
|
413
|
+
},
|
|
414
|
+
memory: {
|
|
415
|
+
facts: { total: factsTotal, active: factsActive },
|
|
416
|
+
episodes: { total: episodesTotal, eligible: episodesEligible },
|
|
417
|
+
knowledge: { active: knowledgeActive },
|
|
418
|
+
docChunks: { total: docChunks },
|
|
419
|
+
messages: messageStats,
|
|
420
|
+
},
|
|
421
|
+
vectors: {
|
|
422
|
+
total: totalVectors,
|
|
423
|
+
byTable: vectorByTable,
|
|
424
|
+
coverage: {
|
|
425
|
+
facts: { indexed: factsIndexed, total: factsActive, percent: factCoverage, status: factStatus },
|
|
426
|
+
episodes: { indexed: episodesIndexed, total: episodesEligible, percent: episodeCoverage, status: episodeStatus },
|
|
427
|
+
knowledge: { indexed: knowledgeIndexed, total: knowledgeActive, percent: knowledgeCoverage, status: knowledgeStatus },
|
|
428
|
+
},
|
|
429
|
+
oldestIndexedAt,
|
|
430
|
+
newestIndexedAt,
|
|
431
|
+
dimensionStatus: dimensionMatches,
|
|
432
|
+
},
|
|
433
|
+
composition: {
|
|
434
|
+
turns: totalTurns,
|
|
435
|
+
avgLatencyMs: avgLatency == null ? null : Math.round(avgLatency),
|
|
436
|
+
avgInputTokens: avgInput == null ? null : Math.round(avgInput),
|
|
437
|
+
avgOutputTokens: avgOutput == null ? null : Math.round(avgOutput),
|
|
438
|
+
semanticDiagnosticsPersisted: false,
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function formatCoverage(label, item) {
|
|
444
|
+
const percent = item.percent == null ? 'n/a' : `${item.percent}%`;
|
|
445
|
+
return ` ${icon(item.status)} ${label.padEnd(9)} ${item.indexed.toLocaleString()} / ${item.total.toLocaleString()} (${percent})`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function formatMasterHealth(h) {
|
|
449
|
+
const lines = [];
|
|
450
|
+
lines.push(`hypermem master health — ${h.snapshotAt}`);
|
|
451
|
+
lines.push(`scope: ${h.scope.agent ? `agent=${h.scope.agent}` : 'fleet'}`);
|
|
452
|
+
lines.push(`overall: ${h.overall === 'healthy' ? '✅ healthy' : h.overall === 'degraded' ? '❌ degraded' : '⚠️ attention'}`);
|
|
453
|
+
if (h.issues.length > 0) {
|
|
454
|
+
lines.push(`issues: ${h.issues.join('; ')}`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
lines.push('');
|
|
458
|
+
lines.push('## Configuration');
|
|
459
|
+
lines.push(` dataDir: ${h.config.dataDir}`);
|
|
460
|
+
lines.push(` source: ${h.config.sources.join(' + ')}`);
|
|
461
|
+
lines.push(` embed: ${h.config.embedding.provider ?? 'unknown'} / ${h.config.embedding.model ?? 'unknown'}${h.config.embedding.dimensions ? ` (${h.config.embedding.dimensions}d)` : ''}${h.config.embedding.batchSize ? ` batch=${h.config.embedding.batchSize}` : ''}`);
|
|
462
|
+
if (h.config.embedding.baseUrl) lines.push(` base: ${h.config.embedding.baseUrl}`);
|
|
463
|
+
lines.push(` rerank: ${h.config.reranker?.provider ?? 'none'}`);
|
|
464
|
+
|
|
465
|
+
lines.push('');
|
|
466
|
+
lines.push('## Databases');
|
|
467
|
+
lines.push(` ${icon(h.databases.library.quickCheck === 'ok' ? 'ok' : 'fail')} library.db ${h.databases.library.mb} MB quick_check=${h.databases.library.quickCheck}`);
|
|
468
|
+
lines.push(` ${icon(h.databases.vectors.quickCheck === 'ok' ? 'ok' : h.databases.vectors.exists ? 'fail' : 'warn')} vectors.db ${h.databases.vectors.exists ? `${h.databases.vectors.mb} MB` : 'missing'} quick_check=${h.databases.vectors.quickCheck}`);
|
|
469
|
+
lines.push(` ${icon(h.databases.messages.quickCheck === 'ok' ? 'ok' : h.databases.messages.exists ? 'fail' : 'warn')} messages sample ${h.databases.messages.exists ? `${h.databases.messages.mb} MB` : 'missing'} quick_check=${h.databases.messages.quickCheck}`);
|
|
470
|
+
lines.push(` ${icon(h.vectors.dimensionStatus)} vector dims facts=${h.databases.vectors.dimensions.facts ?? 'n/a'} episodes=${h.databases.vectors.dimensions.episodes ?? 'n/a'} knowledge=${h.databases.vectors.dimensions.knowledge ?? 'n/a'}`);
|
|
471
|
+
|
|
472
|
+
lines.push('');
|
|
473
|
+
lines.push('## Memory data');
|
|
474
|
+
lines.push(` facts: ${h.memory.facts.active.toLocaleString()} retrieval-eligible / ${h.memory.facts.total.toLocaleString()} total`);
|
|
475
|
+
lines.push(` episodes: ${h.memory.episodes.eligible.toLocaleString()} significant / ${h.memory.episodes.total.toLocaleString()} total`);
|
|
476
|
+
lines.push(` knowledge:${String(h.memory.knowledge.active.toLocaleString()).padStart(7)} active`);
|
|
477
|
+
lines.push(` chunks: ${h.memory.docChunks.total.toLocaleString()}`);
|
|
478
|
+
lines.push(` messages: ${h.memory.messages.totalMessages.toLocaleString()} across ${h.memory.messages.agentsWithMessages} agent DBs${h.memory.messages.newestMessageAt ? `, newest ${h.memory.messages.newestMessageAt}` : ''}`);
|
|
479
|
+
|
|
480
|
+
lines.push('');
|
|
481
|
+
lines.push('## Vector coverage');
|
|
482
|
+
lines.push(` total vectors: ${h.vectors.total.toLocaleString()}`);
|
|
483
|
+
if (Object.keys(h.vectors.byTable).length > 0) {
|
|
484
|
+
const byTable = Object.entries(h.vectors.byTable)
|
|
485
|
+
.map(([table, count]) => `${table}=${Number(count).toLocaleString()}`)
|
|
486
|
+
.join(' ');
|
|
487
|
+
lines.push(` by table: ${byTable}`);
|
|
488
|
+
}
|
|
489
|
+
lines.push(formatCoverage('facts', h.vectors.coverage.facts));
|
|
490
|
+
lines.push(formatCoverage('episodes', h.vectors.coverage.episodes));
|
|
491
|
+
lines.push(formatCoverage('knowledge', h.vectors.coverage.knowledge));
|
|
492
|
+
lines.push(` indexed window: ${h.vectors.oldestIndexedAt ?? 'n/a'} → ${h.vectors.newestIndexedAt ?? 'n/a'}`);
|
|
493
|
+
|
|
494
|
+
lines.push('');
|
|
495
|
+
lines.push('## Composition');
|
|
496
|
+
if (h.composition.turns > 0) {
|
|
497
|
+
lines.push(` turns: ${h.composition.turns.toLocaleString()}`);
|
|
498
|
+
if (h.composition.avgLatencyMs != null) lines.push(` avg latency: ${h.composition.avgLatencyMs}ms`);
|
|
499
|
+
if (h.composition.avgInputTokens != null) lines.push(` avg input: ${h.composition.avgInputTokens.toLocaleString()} tokens`);
|
|
500
|
+
if (h.composition.avgOutputTokens != null) lines.push(` avg output: ${h.composition.avgOutputTokens.toLocaleString()} tokens`);
|
|
501
|
+
} else {
|
|
502
|
+
lines.push(' no persisted output_metrics rows');
|
|
503
|
+
}
|
|
504
|
+
lines.push(' semantic recall counts: runtime log only, not persisted yet');
|
|
505
|
+
|
|
506
|
+
return lines.join('\n');
|
|
104
507
|
}
|
|
105
508
|
|
|
509
|
+
// ── Resolve DB paths ─────────────────────────────────────────────
|
|
510
|
+
const mainDbPath = findMessageDb(flags.agent);
|
|
511
|
+
const libraryDbPath = join(dataDir, 'library.db');
|
|
512
|
+
const vectorDbPath = join(dataDir, 'vectors.db');
|
|
513
|
+
|
|
106
514
|
if (!mainDbPath || !existsSync(mainDbPath)) {
|
|
107
|
-
if (
|
|
108
|
-
const result = { status: 'no_sessions', message: 'Installed but no agent sessions ingested yet. Send a message to any agent, then re-run.' };
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
} else {
|
|
515
|
+
if (flags.health || flags.json || flags.master) {
|
|
516
|
+
const result = { status: 'no_sessions', message: 'Installed but no agent sessions ingested yet. Send a message to any agent, then re-run.', dataDir };
|
|
517
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
518
|
+
else {
|
|
112
519
|
console.log('Status: installed, no sessions ingested yet.');
|
|
113
520
|
console.log('Send a message to any agent, then re-run this health check.');
|
|
114
521
|
}
|
|
@@ -118,31 +525,48 @@ if (!mainDbPath || !existsSync(mainDbPath)) {
|
|
|
118
525
|
process.exit(1);
|
|
119
526
|
}
|
|
120
527
|
|
|
121
|
-
const libraryDbPath = join(dataDir, 'library.db');
|
|
122
|
-
|
|
123
528
|
const mainDb = openDb(mainDbPath, 'messages.db');
|
|
124
529
|
const libraryDb = openDb(libraryDbPath, 'library.db');
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
530
|
+
const vectorDb = openDb(vectorDbPath, 'vectors.db', false);
|
|
531
|
+
if (vectorDb) {
|
|
532
|
+
try {
|
|
533
|
+
vectorDb.exec(`ATTACH DATABASE '${libraryDbPath.replaceAll("'", "''")}' AS library`);
|
|
534
|
+
} catch {
|
|
535
|
+
// Coverage joins degrade to zero via safeGet if the live library cannot be attached.
|
|
536
|
+
}
|
|
131
537
|
}
|
|
132
538
|
|
|
133
|
-
|
|
539
|
+
try {
|
|
540
|
+
if (flags.master) {
|
|
541
|
+
const master = collectMasterHealth(libraryDb, vectorDb, mainDb);
|
|
542
|
+
if (flags.json) console.log(JSON.stringify(master, null, 2));
|
|
543
|
+
else console.log(formatMasterHealth(master));
|
|
544
|
+
process.exit(master.overall === 'degraded' ? 1 : 0);
|
|
545
|
+
}
|
|
134
546
|
|
|
135
|
-
// ──
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
547
|
+
// ── Import metrics functions ───────────────────────────────────
|
|
548
|
+
const distPath = join(root, 'dist', 'metrics-dashboard.js');
|
|
549
|
+
if (!existsSync(distPath)) {
|
|
550
|
+
console.error('Error: dist/metrics-dashboard.js not found. Run `npm run build` first.');
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
140
553
|
|
|
141
|
-
|
|
142
|
-
const
|
|
554
|
+
const { collectMetrics, formatMetricsSummary } = await import(distPath);
|
|
555
|
+
const opts = {};
|
|
556
|
+
if (flags.agent) opts.agentIds = [flags.agent];
|
|
557
|
+
|
|
558
|
+
const metrics = await collectMetrics(
|
|
559
|
+
mainDb,
|
|
560
|
+
libraryDb,
|
|
561
|
+
{
|
|
562
|
+
...opts,
|
|
563
|
+
embeddingProvider: runtime.embedding.provider,
|
|
564
|
+
embeddingModel: runtime.embedding.model,
|
|
565
|
+
},
|
|
566
|
+
vectorDb,
|
|
567
|
+
);
|
|
143
568
|
|
|
144
569
|
if (flags.health) {
|
|
145
|
-
// Health-only mode: check and exit
|
|
146
570
|
const h = metrics.health;
|
|
147
571
|
const ok = h.mainDbOk && h.libraryDbOk && (h.cacheOk === null || h.cacheOk);
|
|
148
572
|
|
|
@@ -150,22 +574,18 @@ try {
|
|
|
150
574
|
console.log(JSON.stringify(h, null, 2));
|
|
151
575
|
} else {
|
|
152
576
|
console.log(`hypermem ${h.packageVersion} health check`);
|
|
577
|
+
console.log(` embedding: provider=${h.embeddingProvider ?? 'unknown'}${h.embeddingModel ? ` model=${h.embeddingModel}` : ''}`);
|
|
153
578
|
console.log(` main db: ${h.mainDbOk ? '✅' : '❌'}${h.mainSchemaVersion !== null ? ` (schema v${h.mainSchemaVersion})` : ''}`);
|
|
154
579
|
console.log(` library db: ${h.libraryDbOk ? '✅' : '❌'}${h.librarySchemaVersion !== null ? ` (schema v${h.librarySchemaVersion})` : ''}`);
|
|
155
|
-
if (h.cacheOk !== null) {
|
|
156
|
-
console.log(` cache: ${h.cacheOk ? '✅' : '❌'}`);
|
|
157
|
-
}
|
|
580
|
+
if (h.cacheOk !== null) console.log(` cache: ${h.cacheOk ? '✅' : '❌'}`);
|
|
158
581
|
console.log(` status: ${ok ? '✅ healthy' : '❌ degraded'}`);
|
|
159
582
|
}
|
|
160
583
|
|
|
161
584
|
process.exit(ok ? 0 : 1);
|
|
162
585
|
}
|
|
163
586
|
|
|
164
|
-
if (flags.json)
|
|
165
|
-
|
|
166
|
-
} else {
|
|
167
|
-
console.log(formatMetricsSummary(metrics));
|
|
168
|
-
}
|
|
587
|
+
if (flags.json) console.log(JSON.stringify(metrics, null, 2));
|
|
588
|
+
else console.log(formatMetricsSummary(metrics));
|
|
169
589
|
} catch (err) {
|
|
170
590
|
console.error(`Error collecting metrics: ${err.message}`);
|
|
171
591
|
if (err.stack) console.error(err.stack);
|
|
@@ -173,4 +593,5 @@ try {
|
|
|
173
593
|
} finally {
|
|
174
594
|
try { mainDb.close(); } catch {}
|
|
175
595
|
try { libraryDb.close(); } catch {}
|
|
596
|
+
try { vectorDb?.close(); } catch {}
|
|
176
597
|
}
|