pi-mega-compact 0.11.12 → 0.12.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/dist/extensions/dashboard-server/dashboard-client-repos.js +28 -2
- package/dist/extensions/dashboard-server/index-reader.js +14 -13
- package/dist/extensions/dashboard-server/routes-health.js +81 -0
- package/dist/extensions/dashboard-server/routes-topics.js +18 -0
- package/dist/extensions/dashboard-server/routes.js +1 -0
- package/dist/extensions/dashboard-server/server.js +5 -1
- package/dist/extensions/mega-config.js +5 -0
- package/dist/extensions/mega-events/agent-handlers.js +5 -0
- package/dist/extensions/mega-events/context-handler.js +10 -5
- package/dist/extensions/mega-events/health-handler.js +156 -0
- package/dist/extensions/mega-runtime/reset-runtime.js +5 -1
- package/dist/extensions/mega-runtime/runtime.js +6 -0
- package/dist/src/contextHealth/cachePoison.js +183 -0
- package/dist/src/contextHealth/drift.js +89 -0
- package/dist/src/contextHealth/outputQuality.js +112 -0
- package/dist/src/contextHealth.js +110 -0
- package/dist/src/memoryGraph/gates.js +1 -1
- package/dist/src/memoryGraph/sources.js +44 -22
- package/dist/src/store/sqlite/context-health.js +193 -0
- package/dist/src/store/sqlite/schema.js +43 -1
- package/dist/src/store/sqlite.js +1 -0
- package/dist/src/store/turns/sqlite-store.js +6 -6
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-9KQgKBEh.js → AchievementTiles-DeQ0qnBX.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-9KQgKBEh.js.map → AchievementTiles-DeQ0qnBX.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-B6xoQTEa.js → AchievementsTab-Dtk_RJfs.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-B6xoQTEa.js.map → AchievementsTab-Dtk_RJfs.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-Dkm9rawo.js → CacheTab-kWcQt3PI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-Dkm9rawo.js.map → CacheTab-kWcQt3PI.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ConfigTab-Bd_v_OUF.js → ConfigTab-UFlo73fR.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-Bd_v_OUF.js.map → ConfigTab-UFlo73fR.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-CYWoFOdI.js → EventsTab-BENO2N0y.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-CYWoFOdI.js.map → EventsTab-BENO2N0y.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{GameTab-CH7lecNL.js → GameTab-BCJavOML.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-CH7lecNL.js.map → GameTab-BCJavOML.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/HealthTab-C7iw7rhx.js +2 -0
- package/extensions/dashboard-client/dist/assets/HealthTab-C7iw7rhx.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DNZ1IF4t.js → MaintenanceTab-C0rdAMWc.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DNZ1IF4t.js.map → MaintenanceTab-C0rdAMWc.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-tVRREYiG.js → MemoryMapTab-w5BZP3i-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-tVRREYiG.js.map → MemoryMapTab-w5BZP3i-.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CHblYuTY.js → MetricsTab-BBCXa3xS.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CHblYuTY.js.map → MetricsTab-BBCXa3xS.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-Dj0TgFvU.js → OverviewTab-CzABMSAQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-Dj0TgFvU.js.map → OverviewTab-CzABMSAQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-BTJcfc1W.js → ReposTab-DFiM_Ubg.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-BTJcfc1W.js.map → ReposTab-DFiM_Ubg.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C8tU8K5u.js → SessionsTab-BtW4bees.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C8tU8K5u.js.map → SessionsTab-BtW4bees.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-BdxGMo6O.js → SetupTab-Duy1LJhg.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-BdxGMo6O.js.map → SetupTab-Duy1LJhg.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BWZSARzT.js → TimeSavedCard-CC13EXij.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BWZSARzT.js.map → TimeSavedCard-CC13EXij.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TopicsTab-BCJwPYl8.js → TopicsTab-BrTB8uEK.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TopicsTab-BCJwPYl8.js.map → TopicsTab-BrTB8uEK.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DQFTSDpz.js → TurnsTab-BJK7FIHu.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DQFTSDpz.js.map → TurnsTab-BJK7FIHu.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-DrrDswj-.js → index-B8k19B9g.js} +11 -11
- package/extensions/dashboard-client/dist/assets/{index-DrrDswj-.js.map → index-B8k19B9g.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-o4tteeTs.js → useSSE-CWBB4Me7.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-o4tteeTs.js.map → useSSE-CWBB4Me7.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/App.tsx +5 -1
- package/extensions/dashboard-client/src/api/health.ts +64 -0
- package/extensions/dashboard-client/src/tabs/HealthTab.tsx +165 -0
- package/extensions/dashboard-server/dashboard-client-repos.ts +28 -2
- package/extensions/dashboard-server/index-reader.ts +16 -17
- package/extensions/dashboard-server/routes-health.ts +104 -0
- package/extensions/dashboard-server/routes-topics.ts +21 -0
- package/extensions/dashboard-server/routes.ts +1 -0
- package/extensions/dashboard-server/server.ts +4 -0
- package/extensions/mega-config.ts +15 -0
- package/extensions/mega-events/agent-handlers.ts +6 -0
- package/extensions/mega-events/context-handler.ts +111 -104
- package/extensions/mega-events/health-handler.ts +190 -0
- package/extensions/mega-runtime/reset-runtime.ts +5 -1
- package/extensions/mega-runtime/runtime.ts +7 -0
- package/package.json +1 -1
- package/src/contextHealth/cachePoison.ts +243 -0
- package/src/contextHealth/drift.ts +107 -0
- package/src/contextHealth/outputQuality.ts +139 -0
- package/src/contextHealth.ts +191 -0
- package/src/memoryGraph/gates.ts +1 -1
- package/src/memoryGraph/sources.ts +56 -28
- package/src/store/sqlite/context-health.ts +342 -0
- package/src/store/sqlite/schema.ts +43 -1
- package/src/store/sqlite.ts +1 -0
- package/src/store/turns/sqlite-store.ts +6 -4
|
@@ -63,7 +63,7 @@ function renderByModel(repos) {
|
|
|
63
63
|
var rows = document.getElementById('bm-rows');
|
|
64
64
|
if (!rows) return;
|
|
65
65
|
if (!repos || !repos.length) {
|
|
66
|
-
rows.innerHTML = '<tr><td colspan="
|
|
66
|
+
rows.innerHTML = '<tr><td colspan="16" class="repo-none">No repositories registered yet.</td></tr>';
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
69
|
var groups = {};
|
|
@@ -106,11 +106,35 @@ function renderByModel(repos) {
|
|
|
106
106
|
var fmt = function(v) { return '$' + v.toFixed(6); };
|
|
107
107
|
return lo === hi ? fmt(lo) : fmt(lo) + '–' + fmt(hi);
|
|
108
108
|
}
|
|
109
|
+
// Helper: compute cache hit % from providerCacheRead/Write tokens
|
|
110
|
+
function cacheHitPct(cr, cw, inp) {
|
|
111
|
+
var denom = cr + cw + (inp || 0);
|
|
112
|
+
return denom > 0 ? ((cr / denom) * 100).toFixed(1) + '%' : '—';
|
|
113
|
+
}
|
|
114
|
+
// Helper: compute cache $ saved (read = 0.9 * inputRate, write = 0.25 * inputRate)
|
|
115
|
+
function cacheDollar(cr, cw, rate) {
|
|
116
|
+
if (!cr || !rate) return '—';
|
|
117
|
+
var saved = cr * rate * 0.9 - cw * rate * 0.25;
|
|
118
|
+
return '$' + saved.toFixed(4);
|
|
119
|
+
}
|
|
120
|
+
// Aggregate providerCacheRead/Write across repos in this group
|
|
121
|
+
var groupCacheRead = 0, groupCacheWrite = 0, groupTokensIn = 0;
|
|
122
|
+
for (var ii = 0; ii < repos.length; ii++) {
|
|
123
|
+
var rr = repos[ii];
|
|
124
|
+
var key2 = (rr.modelName && String(rr.modelName).trim()) || '(unknown)';
|
|
125
|
+
if (key2 === g.model) {
|
|
126
|
+
groupCacheRead += (rr.providerCacheRead || 0);
|
|
127
|
+
groupCacheWrite += (rr.providerCacheWrite || 0);
|
|
128
|
+
groupTokensIn += (rr.tokensDropped || 0);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
109
131
|
rows.innerHTML = arr.map(function(g) {
|
|
110
132
|
var freed = (g.tokensIn || 0) - (g.tokensOut || 0);
|
|
111
|
-
|
|
133
|
+
// B: always show a dollar value (including $0.0000) — do not use '—' when usd is 0
|
|
134
|
+
var usd = '$' + (g.usd || 0).toFixed(4);
|
|
112
135
|
var when = g.lastAt ? new Date(g.lastAt).toLocaleString() : '—';
|
|
113
136
|
var reas = g.reasoning == null ? '—' : (g.reasoning ? 'yes' : 'no');
|
|
137
|
+
var rate0 = g.inRates && g.inRates.length ? g.inRates[0] : 0;
|
|
114
138
|
return '<tr>' +
|
|
115
139
|
'<td><span class="repo-model">' + sanitize(g.model) + '</span></td>' +
|
|
116
140
|
'<td>' + sanitize(g.provider) + '</td>' +
|
|
@@ -125,6 +149,8 @@ function renderByModel(repos) {
|
|
|
125
149
|
'<td class="num">' + collapseRate(g.inRates) + '</td>' +
|
|
126
150
|
'<td class="num">' + collapseRate(g.outRates) + '</td>' +
|
|
127
151
|
'<td class="num">' + sanitize(usd) + '</td>' +
|
|
152
|
+
'<td class="num">' + cacheHitPct(groupCacheRead, groupCacheWrite, groupTokensIn) + '</td>' +
|
|
153
|
+
'<td class="num">' + cacheDollar(groupCacheRead, groupCacheWrite, rate0) + '</td>' +
|
|
128
154
|
'<td class="num">' + sanitize(when) + '</td>' +
|
|
129
155
|
'</tr>';
|
|
130
156
|
}).join('');
|
|
@@ -12,6 +12,7 @@ import { existsSync } from "node:fs";
|
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { DatabaseSync } from "node:sqlite";
|
|
15
|
+
import { readProviderCacheForRepo, } from "../../src/store/sqlite/perf-samples.js";
|
|
15
16
|
export function getIndexDir() {
|
|
16
17
|
const override = process.env.MEGACOMPACT_INDEX_DIR;
|
|
17
18
|
if (override && override.trim() !== "")
|
|
@@ -89,21 +90,21 @@ export function readIndex() {
|
|
|
89
90
|
repo.maxTokens = Number(mrow.max_tokens ?? 0) || null;
|
|
90
91
|
repo.reasoning = Number(mrow.reasoning ?? 0) === 1;
|
|
91
92
|
}
|
|
92
|
-
// Provider prompt-cache stats (E.3)
|
|
93
|
+
// Provider prompt-cache stats (E.3 / F4)
|
|
94
|
+
// Use readProviderCacheForRepo which correctly reads cache token
|
|
95
|
+
// counts from the meta JSON column (cacheRead/cacheWrite), not
|
|
96
|
+
// from non-existent table columns cache_read/cache_write.
|
|
93
97
|
try {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
FROM perf_samples
|
|
100
|
-
WHERE cache_read > 0 OR cache_write > 0`)
|
|
101
|
-
.get();
|
|
102
|
-
if (pRow && pRow.totalRead > 0) {
|
|
103
|
-
repo.providerCacheRead = Number(pRow.totalRead);
|
|
104
|
-
repo.providerCacheWrite = Number(pRow.totalWrite);
|
|
105
|
-
repo.providerCachePct = Number((pRow.hitPct * 100).toFixed(1));
|
|
98
|
+
const pc = readProviderCacheForRepo(repo.stateDir);
|
|
99
|
+
if (pc.totalCacheRead > 0) {
|
|
100
|
+
repo.providerCacheRead = pc.totalCacheRead;
|
|
101
|
+
repo.providerCacheWrite = pc.totalCacheWrite;
|
|
102
|
+
repo.providerCachePct = Number(pc.avgHitPct.toFixed(1));
|
|
106
103
|
}
|
|
104
|
+
// Also enrich contextWindow/maxTokens/reasoning from the
|
|
105
|
+
// latest model_snapshots row (was already read above, but
|
|
106
|
+
// when latestModelSnapshot was used above we get the same
|
|
107
|
+
// data from the per-repo store — keep as-is).
|
|
107
108
|
}
|
|
108
109
|
catch {
|
|
109
110
|
/* best-effort — perf_samples may not exist */
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routes-health.ts — Context Health + KV Cache Poison route handlers.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/context-health — recent health rows, latest composite, trend,
|
|
5
|
+
* per-model averages, alert flags. Accepts ?minutes=N (default 30).
|
|
6
|
+
* GET /api/cache-poison — recent cache poison events (last 30 min).
|
|
7
|
+
*
|
|
8
|
+
* Guardrails: PREVENT-PI-004 (loopback-only), PREVENT-001 (null-safe),
|
|
9
|
+
* PREVENT-002 (parameterized SQL via accessors). Localhost dashboard.
|
|
10
|
+
*/
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
export function handleContextHealth(req, res, ctx) {
|
|
13
|
+
if (!req.url?.startsWith("/api/context-health"))
|
|
14
|
+
return false;
|
|
15
|
+
try {
|
|
16
|
+
if (req.method !== "GET") {
|
|
17
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
18
|
+
res.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
const url = new URL(req.url, "http://localhost");
|
|
22
|
+
const minutes = Math.max(1, Number(url.searchParams.get("minutes") ?? 30));
|
|
23
|
+
const sinceTs = Date.now() - minutes * 60_000;
|
|
24
|
+
const pfReq = createRequire(import.meta.url);
|
|
25
|
+
const { readLatestContextHealth, readContextHealthTrend, readContextHealthRows, } = pfReq("../../src/store/sqlite.js");
|
|
26
|
+
const latest = readLatestContextHealth(ctx.stateDir);
|
|
27
|
+
const trend = readContextHealthTrend(ctx.stateDir, 50);
|
|
28
|
+
const rows = readContextHealthRows(ctx.stateDir, sinceTs);
|
|
29
|
+
const perModel = new Map();
|
|
30
|
+
for (const r of rows) {
|
|
31
|
+
const mid = r.modelId ?? "(unknown)";
|
|
32
|
+
const entry = perModel.get(mid) ?? { sum: 0, count: 0 };
|
|
33
|
+
entry.sum += r.composite;
|
|
34
|
+
entry.count += 1;
|
|
35
|
+
perModel.set(mid, entry);
|
|
36
|
+
}
|
|
37
|
+
const perModelAverages = [...perModel.entries()].map(([modelId, v]) => ({
|
|
38
|
+
modelId,
|
|
39
|
+
avgComposite: v.count > 0 ? v.sum / v.count : 0,
|
|
40
|
+
sampleCount: v.count,
|
|
41
|
+
}));
|
|
42
|
+
const alerts = rows.filter((r) => r.composite < 0.5 || r.cachePoison < 0.3);
|
|
43
|
+
const body = {
|
|
44
|
+
updatedAt: new Date().toISOString(),
|
|
45
|
+
latest,
|
|
46
|
+
trend,
|
|
47
|
+
rows: rows.slice(-200),
|
|
48
|
+
perModel: perModelAverages,
|
|
49
|
+
alerts,
|
|
50
|
+
};
|
|
51
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
52
|
+
res.end(JSON.stringify(body));
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
56
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
export function handleCachePoison(req, res, ctx) {
|
|
61
|
+
if (!req.url?.startsWith("/api/cache-poison"))
|
|
62
|
+
return false;
|
|
63
|
+
try {
|
|
64
|
+
if (req.method !== "GET") {
|
|
65
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
66
|
+
res.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
const sinceTs = Date.now() - 30 * 60_000;
|
|
70
|
+
const pfReq = createRequire(import.meta.url);
|
|
71
|
+
const { readCachePoisonEvents } = pfReq("../../src/store/sqlite.js");
|
|
72
|
+
const events = readCachePoisonEvents(ctx.stateDir, sinceTs);
|
|
73
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
74
|
+
res.end(JSON.stringify({ updatedAt: new Date().toISOString(), events }));
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
78
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { openTurnStore } from "../../src/store/turns/connection.js";
|
|
9
9
|
import { createTopicStore } from "../../src/topics/store.js";
|
|
10
|
+
import { openStore } from "../../src/store/sqlite.js";
|
|
11
|
+
import { buildTopicModel } from "../../src/topics/cluster.js";
|
|
10
12
|
export function handleTopics(req, res, ctx) {
|
|
11
13
|
const url = req.url ?? "";
|
|
12
14
|
// ── GET /api/topics/:topicId/memories — wiki topic drill-down (S52) ──
|
|
@@ -42,6 +44,22 @@ export function handleTopics(req, res, ctx) {
|
|
|
42
44
|
return false;
|
|
43
45
|
try {
|
|
44
46
|
const tdb = openTurnStore(ctx.stateDir);
|
|
47
|
+
// Lazy build: when the topics table is empty, attempt a one-shot build
|
|
48
|
+
// from available context_chunks embeddings so the wiki is useful even
|
|
49
|
+
// before the first compaction fires.
|
|
50
|
+
const topicCount = tdb.prepare("SELECT COUNT(*) AS c FROM topics").get().c;
|
|
51
|
+
if (topicCount === 0) {
|
|
52
|
+
try {
|
|
53
|
+
const mainDb = openStore(ctx.stateDir);
|
|
54
|
+
const model = buildTopicModel(mainDb);
|
|
55
|
+
if (model.k > 0 && model.totalChunks > 0) {
|
|
56
|
+
createTopicStore(ctx.stateDir).replaceTopicModel(model);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* non-fatal: lazy wiki build is best-effort */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
45
63
|
const topics = tdb
|
|
46
64
|
.prepare(`SELECT id, label, term_scores, memory_count, last_updated
|
|
47
65
|
FROM topics ORDER BY memory_count DESC, id ASC`)
|
|
@@ -16,3 +16,4 @@ export { handleMemoryStatus } from "./routes-memory.js";
|
|
|
16
16
|
export { handleSetupStatus, handleSetupDetect, handleSetupConfigure } from "./routes-setup.js";
|
|
17
17
|
export { handleMemoryMap } from "./routes-memory-map.js";
|
|
18
18
|
export { handleCacheStripes } from "./routes-cache.js";
|
|
19
|
+
export { handleContextHealth, handleCachePoison } from "./routes-health.js";
|
|
@@ -12,7 +12,7 @@ import { join, dirname } from "node:path";
|
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { createRequire } from "node:module";
|
|
14
14
|
import { log, setLogPath, setDashboardServerVersion } from "./state.js";
|
|
15
|
-
import { buildRouteContext, handleIndex, handleRepoIndex, handleEvents, handleGameState, handleGameScores, handlePerf, handleAchievements, handleSessions, handleTopics, handleTurns, handleMaintenance, handleProviderCache, handleCacheStripes, handleMemoryStatus, handleSetupStatus, handleSetupDetect, handleSetupConfigure, handleMemoryMap, handleStatic, } from "./routes.js";
|
|
15
|
+
import { buildRouteContext, handleIndex, handleRepoIndex, handleEvents, handleGameState, handleGameScores, handlePerf, handleAchievements, handleSessions, handleTopics, handleTurns, handleMaintenance, handleProviderCache, handleCacheStripes, handleMemoryStatus, handleSetupStatus, handleSetupDetect, handleSetupConfigure, handleMemoryMap, handleContextHealth, handleCachePoison, handleStatic, } from "./routes.js";
|
|
16
16
|
export async function launchDashboardServer(stateDir) {
|
|
17
17
|
// Our own package version — exposed at /api/version so the launcher can
|
|
18
18
|
// detect a stale server (started by an older build) and replace it on
|
|
@@ -207,6 +207,10 @@ export async function launchDashboardServer(stateDir) {
|
|
|
207
207
|
return;
|
|
208
208
|
if (handleMemoryMap(req, res, ctx))
|
|
209
209
|
return;
|
|
210
|
+
if (handleContextHealth(req, res, ctx))
|
|
211
|
+
return;
|
|
212
|
+
if (handleCachePoison(req, res, ctx))
|
|
213
|
+
return;
|
|
210
214
|
handleStatic(req, res, ctx);
|
|
211
215
|
});
|
|
212
216
|
// Bind base + range are env-configurable so tests can use a private,
|
|
@@ -180,6 +180,11 @@ export function loadConfig() {
|
|
|
180
180
|
memoryGraphSeedMemories: envBool("MEGACOMPACT_MEMORY_GRAPH_SEED_MEMORIES", true),
|
|
181
181
|
memoryGraphCrossTypeThreshold: envFlag("MEGACOMPACT_MEMORY_GRAPH_CROSS_TYPE_THRESHOLD", 0.85),
|
|
182
182
|
memoryGraphWithinTypeThreshold: envFlag("MEGACOMPACT_MEMORY_GRAPH_WITHIN_TYPE_THRESHOLD", 0.7),
|
|
183
|
+
contextHealth: envBool("MEGACOMPACT_CONTEXT_HEALTH", true),
|
|
184
|
+
contextHealthDrift: envBool("MEGACOMPACT_CONTEXT_HEALTH_DRIFT", true),
|
|
185
|
+
contextHealthOutputQuality: envBool("MEGACOMPACT_CONTEXT_HEALTH_OUTPUT_QUALITY", true),
|
|
186
|
+
contextHealthCachePoison: envBool("MEGACOMPACT_CONTEXT_HEALTH_CACHE_POISON", true),
|
|
187
|
+
contextHealthMitigate: envBool("MEGACOMPACT_CONTEXT_HEALTH_MITIGATE", false),
|
|
183
188
|
// D.1: env-overridable recompact delta (minimum context growth % before
|
|
184
189
|
// re-compacting instead of replaying the cached live trim). Default 50.
|
|
185
190
|
recompactPctDelta: envFlag("MEGACOMPACT_RECOMPACT_PCT_DELTA", 50),
|
|
@@ -12,6 +12,7 @@ import { vectorStats } from "../../src/vectorStore.js";
|
|
|
12
12
|
import { defaultEmbedder } from "../../src/embedder.js";
|
|
13
13
|
import { storeTopicEmbedding, loadTopicEmbedding, detectTopicShift, } from "./separated-prompt.js";
|
|
14
14
|
import { refreshStripeAssignments as writeStripeAssignments, } from "../../src/cache-stripe-impl.js";
|
|
15
|
+
import { handleTurnEndHealth } from "./health-handler.js";
|
|
15
16
|
/** Register agent/turn tracking event handlers. */
|
|
16
17
|
export function registerAgentHandlers(pi, runtime, config) {
|
|
17
18
|
// ---- Agent tracking for real-time widget + status-line updates ---------
|
|
@@ -252,6 +253,7 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
252
253
|
pi.on("turn_end", async (event, ctx) => {
|
|
253
254
|
runtime.dashboard.event("turn_end", { turnIndex: event.turnIndex });
|
|
254
255
|
runtime.snapshot(ctx);
|
|
256
|
+
runtime.lastErrorCategory = null;
|
|
255
257
|
// S53: consume staged recall blocks ONLY if they were actually injected
|
|
256
258
|
// into a view this turn. If no context event fired (edge: turn ended
|
|
257
259
|
// before any LLM call), recallInjectedThisTurn stays false and the blocks
|
|
@@ -372,6 +374,7 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
372
374
|
else {
|
|
373
375
|
const category = classifyError(event.message);
|
|
374
376
|
const detail = classifyErrorDetailed(event.message);
|
|
377
|
+
runtime.lastErrorCategory = category;
|
|
375
378
|
// R3: stateful poisoned-context signal — repeated identical error text
|
|
376
379
|
// across consecutive turns. The classifier is stateless, so this
|
|
377
380
|
// upgrade happens here. A 'transient' (or 'poisoned-context') turn
|
|
@@ -781,5 +784,7 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
781
784
|
/* non-fatal: stripe refresh never breaks the agent loop */
|
|
782
785
|
}
|
|
783
786
|
}
|
|
787
|
+
// v0.12: Context Health — compute + persist health score. Non-fatal.
|
|
788
|
+
handleTurnEndHealth(event, runtime, config);
|
|
784
789
|
});
|
|
785
790
|
}
|
|
@@ -278,6 +278,11 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
278
278
|
// S51B: auto-categorizing wiki rebuild — every Nth compaction, derived
|
|
279
279
|
// from real context_chunks embeddings. Isolated-store only, gated on
|
|
280
280
|
// AUTO_WIKI_ENABLED; best-effort + non-fatal (never breaks compaction).
|
|
281
|
+
// Fire regardless of dbMirror — context_chunks is the isolated store.
|
|
282
|
+
// Uses the already-open db when inside the dbMirror block; opens its own
|
|
283
|
+
// connection otherwise.
|
|
284
|
+
// from real context_chunks embeddings. Isolated-store only, gated on
|
|
285
|
+
// AUTO_WIKI_ENABLED; best-effort + non-fatal (never breaks compaction).
|
|
281
286
|
try {
|
|
282
287
|
if (config.autoWikiEnabled && config.turnsDbEnabled) {
|
|
283
288
|
const every = Math.max(1, TurnsConfig.WIKI_REBUILD_EVERY_N_COMPACTS);
|
|
@@ -323,11 +328,11 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
323
328
|
if (chunkCount < floor) {
|
|
324
329
|
const transcriptRows = db
|
|
325
330
|
.prepare(`SELECT DISTINCT content_bytes
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
+
FROM raw_transcript
|
|
332
|
+
WHERE session_id = ?
|
|
333
|
+
AND length(content_bytes) > 10
|
|
334
|
+
ORDER BY seq ASC
|
|
335
|
+
LIMIT 200`)
|
|
331
336
|
.all(runtime.rt.sessionId);
|
|
332
337
|
if (transcriptRows.length > 0) {
|
|
333
338
|
const embedder = new TrigramEmbedder();
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* health-handler.ts — Context Health turn_end hook (v0.12).
|
|
3
|
+
*
|
|
4
|
+
* Computes a composite 0-1 health score per turn from five sub-scores
|
|
5
|
+
* (drift, output quality, error rate, cache health, cache poison),
|
|
6
|
+
* persists it to the context_health SQLite table, and emits a dashboard
|
|
7
|
+
* event. All paths non-fatal — health monitoring never breaks the agent loop.
|
|
8
|
+
*
|
|
9
|
+
* PREVENT-PI-004: zero network (local TrigramEmbedder + SQLite only).
|
|
10
|
+
*/
|
|
11
|
+
import { defaultEmbedder } from "../../src/embedder.js";
|
|
12
|
+
import { computeOutputQuality } from "../../src/contextHealth/outputQuality.js";
|
|
13
|
+
import { computeTopicDrift, computeErrorEscalation, computePrefixInstability, computeDriftScore, } from "../../src/contextHealth/drift.js";
|
|
14
|
+
import { computePrefixHash, checkPrefixHash, evaluateCachePoison, } from "../../src/contextHealth/cachePoison.js";
|
|
15
|
+
import { computeHealthScore, recordContextHealth, } from "../../src/contextHealth.js";
|
|
16
|
+
import { recordCachePoisonEvent, } from "../../src/store/sqlite/context-health.js";
|
|
17
|
+
const RING_MAX = 5;
|
|
18
|
+
/** Extract assistant text from the turn_end event message. */
|
|
19
|
+
function extractAssistantText(event) {
|
|
20
|
+
const msg = event.message;
|
|
21
|
+
if (!msg || msg.role !== "assistant")
|
|
22
|
+
return "";
|
|
23
|
+
const c = msg.content;
|
|
24
|
+
if (typeof c === "string")
|
|
25
|
+
return c;
|
|
26
|
+
if (Array.isArray(c)) {
|
|
27
|
+
return c
|
|
28
|
+
.map((p) => (p && typeof p === "object" && "text" in p ? String(p.text ?? "") : ""))
|
|
29
|
+
.join(" ");
|
|
30
|
+
}
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
/** Extract messages as string array for prefix hash (cache poison Layer 1). */
|
|
34
|
+
function extractMessageTexts(event) {
|
|
35
|
+
const c = event?.message?.content;
|
|
36
|
+
if (typeof c === "string")
|
|
37
|
+
return [c];
|
|
38
|
+
if (Array.isArray(c)) {
|
|
39
|
+
return c.map((p) => (p && typeof p === "object" && "text" in p ? String(p.text ?? "") : ""));
|
|
40
|
+
}
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
/** Handle turn_end: compute + persist context health. Non-fatal. */
|
|
44
|
+
export function handleTurnEndHealth(event, runtime, config) {
|
|
45
|
+
if (!config.contextHealth)
|
|
46
|
+
return;
|
|
47
|
+
try {
|
|
48
|
+
const text = extractAssistantText(event);
|
|
49
|
+
const embedder = defaultEmbedder();
|
|
50
|
+
const emb = text.length > 0 ? embedder.embed(text) : [];
|
|
51
|
+
// Output quality sub-score
|
|
52
|
+
let outputQuality = 1.0;
|
|
53
|
+
let repetitionRatio = 0;
|
|
54
|
+
let coherenceScore = 1.0;
|
|
55
|
+
if (config.contextHealthOutputQuality && text.length > 0) {
|
|
56
|
+
const oq = computeOutputQuality(text, embedder);
|
|
57
|
+
outputQuality = oq.score;
|
|
58
|
+
repetitionRatio = oq.repetitionRatio;
|
|
59
|
+
coherenceScore = oq.coherenceScore;
|
|
60
|
+
}
|
|
61
|
+
// Drift sub-score
|
|
62
|
+
let drift = 1.0;
|
|
63
|
+
if (config.contextHealthDrift) {
|
|
64
|
+
const topic = computeTopicDrift(emb, runtime.recentTurnEmbeddings);
|
|
65
|
+
const error = computeErrorEscalation(runtime.recentErrorCategories);
|
|
66
|
+
const prefix = computePrefixInstability(0, 5);
|
|
67
|
+
drift = computeDriftScore(topic, error, prefix);
|
|
68
|
+
}
|
|
69
|
+
// Error rate sub-score (1 = no errors)
|
|
70
|
+
const errorScore = computeErrorEscalation(runtime.recentErrorCategories);
|
|
71
|
+
// Cache health (from perf-handler)
|
|
72
|
+
const cacheHealth = runtime.rt._lastCacheHealthScore ?? 1.0;
|
|
73
|
+
// Cache poison sub-score
|
|
74
|
+
let cachePoison = 1.0;
|
|
75
|
+
if (config.contextHealthCachePoison) {
|
|
76
|
+
const msgTexts = extractMessageTexts(event);
|
|
77
|
+
const currentHash = computePrefixHash(msgTexts);
|
|
78
|
+
const cacheRead = event.message?.usage?.cacheRead ?? 0;
|
|
79
|
+
const l1 = checkPrefixHash(currentHash, runtime.lastPrefixHash, cacheRead);
|
|
80
|
+
const result = evaluateCachePoison({
|
|
81
|
+
currentHash,
|
|
82
|
+
storedHash: runtime.lastPrefixHash,
|
|
83
|
+
cacheRead,
|
|
84
|
+
qualityByCacheHit: [],
|
|
85
|
+
qualityByCacheMiss: [],
|
|
86
|
+
errorRateCacheHit: 0,
|
|
87
|
+
errorRateCacheMiss: 0,
|
|
88
|
+
sampleCount: 0,
|
|
89
|
+
});
|
|
90
|
+
cachePoison = result.score;
|
|
91
|
+
if (l1.poisoned) {
|
|
92
|
+
try {
|
|
93
|
+
recordCachePoisonEvent(runtime.currentStateDir, {
|
|
94
|
+
ts: Date.now(),
|
|
95
|
+
turnIndex: event.turnIndex,
|
|
96
|
+
sessionId: runtime.rt.sessionId,
|
|
97
|
+
layer: 1,
|
|
98
|
+
detail: l1.detail,
|
|
99
|
+
severity: "warn",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch { /* non-fatal */ }
|
|
103
|
+
}
|
|
104
|
+
runtime.lastPrefixHash = currentHash;
|
|
105
|
+
}
|
|
106
|
+
// Composite
|
|
107
|
+
const sub = {
|
|
108
|
+
drift,
|
|
109
|
+
outputQuality,
|
|
110
|
+
errorRate: errorScore,
|
|
111
|
+
cacheHealth,
|
|
112
|
+
cachePoison,
|
|
113
|
+
};
|
|
114
|
+
const composite = computeHealthScore(sub);
|
|
115
|
+
// Persist
|
|
116
|
+
recordContextHealth(runtime.currentStateDir, {
|
|
117
|
+
ts: Date.now(),
|
|
118
|
+
turnIndex: event.turnIndex,
|
|
119
|
+
sessionId: runtime.rt.sessionId,
|
|
120
|
+
driftScore: drift,
|
|
121
|
+
outputQuality,
|
|
122
|
+
errorScore,
|
|
123
|
+
cacheHealth,
|
|
124
|
+
cachePoison,
|
|
125
|
+
composite,
|
|
126
|
+
modelId: runtime.currentModel?.modelId,
|
|
127
|
+
repetitionRatio,
|
|
128
|
+
coherenceScore,
|
|
129
|
+
prefixHash: runtime.lastPrefixHash ?? undefined,
|
|
130
|
+
});
|
|
131
|
+
// Emit dashboard event
|
|
132
|
+
runtime.dashboard.event("context_health", {
|
|
133
|
+
composite,
|
|
134
|
+
drift,
|
|
135
|
+
outputQuality,
|
|
136
|
+
errorRate: errorScore,
|
|
137
|
+
cacheHealth,
|
|
138
|
+
cachePoison,
|
|
139
|
+
turnIndex: event.turnIndex,
|
|
140
|
+
});
|
|
141
|
+
// Update ring buffers
|
|
142
|
+
runtime.recentTurnEmbeddings.push(emb);
|
|
143
|
+
if (runtime.recentTurnEmbeddings.length > RING_MAX) {
|
|
144
|
+
runtime.recentTurnEmbeddings.shift();
|
|
145
|
+
}
|
|
146
|
+
runtime.recentErrorCategories.push(runtime.lastErrorCategory);
|
|
147
|
+
if (runtime.recentErrorCategories.length > RING_MAX) {
|
|
148
|
+
runtime.recentErrorCategories.shift();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
runtime.logger?.error("context_health_failed", {
|
|
153
|
+
error: String(e instanceof Error ? e.message : e),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
import { normalizeSessionId } from "../../src/store.js";
|
|
12
12
|
// --------------------------------------------------------------- resetRuntime
|
|
13
13
|
export function resetRuntimeImpl(self, sessionId) {
|
|
14
|
-
|
|
14
|
+
// Only call normalizeSessionId when a real sessionId string is passed.
|
|
15
|
+
// When undefined, keep the existing self.rt.sessionId (set by the real
|
|
16
|
+
// session-start / compact pipeline) so the early-return guard can fire and
|
|
17
|
+
// dashboard stats keyed by sessionId are not silently orphaned.
|
|
18
|
+
const sid = sessionId ? normalizeSessionId(sessionId) : self.rt.sessionId;
|
|
15
19
|
if (self.rt.sessionId === sid && self.rt.persistedThisSession)
|
|
16
20
|
return; // same session, keep checkpoint memory
|
|
17
21
|
self.rt = {
|
|
@@ -215,6 +215,12 @@ export class MegaRuntime {
|
|
|
215
215
|
diagCtxRunSkipped = 0; // runCompact() returned skipped
|
|
216
216
|
diagCtxCutNull = 0; // computeLiveTrimCut returned null (anchor/boundary)
|
|
217
217
|
diagCtxThrown = 0; // live-trim try threw (caught)
|
|
218
|
+
// Context health instrumentation (v0.12): rolling ring buffers for
|
|
219
|
+
// drift detection + cache poison Layer 1 hash baseline.
|
|
220
|
+
recentTurnEmbeddings = [];
|
|
221
|
+
recentErrorCategories = [];
|
|
222
|
+
lastPrefixHash = null;
|
|
223
|
+
lastErrorCategory = null;
|
|
218
224
|
/**
|
|
219
225
|
* S26 capture instrumentation: the "model_snapshots empty → $0.00 cost card"
|
|
220
226
|
* bug was invisible because captureModel swallowed the DB write in a silent
|