pi-mega-compact 0.11.13 → 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/routes-health.js +81 -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/health-handler.js +156 -0
- 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/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/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/routes-health.ts +104 -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/health-handler.ts +190 -0
- 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/store/sqlite/context-health.ts +342 -0
- package/src/store/sqlite/schema.ts +43 -1
- package/src/store/sqlite.ts +1 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as e}from"./index-
|
|
2
|
-
//# sourceMappingURL=useSSE-
|
|
1
|
+
import{r as e}from"./index-B8k19B9g.js";function R(i={}){const{maxEvents:c=500,maxReconnects:l=5}=i,[f,v]=e.useState([]),[E,n]=e.useState("connecting"),[S,p]=e.useState(0),[m,d]=e.useState(null),o=e.useRef(0),s=e.useRef(null),r=e.useCallback(()=>{s.current&&s.current.close(),n("connecting");const t=new EventSource("/api/events");s.current=t,t.onopen=()=>{n("connected"),o.current=0},t.onmessage=g=>{try{const x=JSON.parse(g.data);v(u=>{const a=[...u,x];return a.length>c?a.slice(-c):a}),p(u=>u+1),d(Date.now())}catch{}},t.onerror=()=>{t.close(),o.current<l?(o.current+=1,n("connecting"),setTimeout(r,1e3)):n("disconnected")}},[c,l]);return e.useEffect(()=>(r(),()=>{s.current?.close()}),[r]),{events:f,status:E,eventCount:S,lastEventAt:m}}export{R as u};
|
|
2
|
+
//# sourceMappingURL=useSSE-CWBB4Me7.js.map
|
package/extensions/dashboard-client/dist/assets/{useSSE-o4tteeTs.js.map → useSSE-CWBB4Me7.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSSE-
|
|
1
|
+
{"version":3,"file":"useSSE-CWBB4Me7.js","sources":["../../src/hooks/useSSE.ts"],"sourcesContent":["/**\n * dashboard-client/src/hooks/useSSE.ts — Server-Sent Events hook.\n *\n * Connects to /api/events, provides real-time event stream.\n * SPRINT-B1: basic connection. SPRINT-D1: reconnect with backoff.\n */\n\nimport { useState, useEffect, useRef, useCallback } from 'react';\nimport type { SseEvent } from '../../../dashboard-server/api-contracts';\n\nexport type SseStatus = 'connecting' | 'connected' | 'disconnected' | 'error';\n\nexport interface UseSSEResult {\n events: SseEvent[];\n status: SseStatus;\n eventCount: number;\n lastEventAt: number | null;\n}\n\nexport interface UseSSEOptions {\n /** Max events to keep in memory (ring buffer). */\n maxEvents?: number;\n /** Max reconnect attempts before giving up. */\n maxReconnects?: number;\n}\n\n// SPRINT-D1-REMAINING: exponential backoff (1s → 2s → 4s → max 30s).\n// SPRINT-D1-REMAINING: after 5 failures, set status='disconnected'.\n\nexport function useSSE(options: UseSSEOptions = {}): UseSSEResult {\n const { maxEvents = 500, maxReconnects = 5 } = options;\n const [events, setEvents] = useState<SseEvent[]>([]);\n const [status, setStatus] = useState<SseStatus>('connecting');\n const [eventCount, setEventCount] = useState(0);\n const [lastEventAt, setLastEventAt] = useState<number | null>(null);\n const reconnectCount = useRef(0);\n const sourceRef = useRef<EventSource | null>(null);\n\n const connect = useCallback(() => {\n if (sourceRef.current) {\n sourceRef.current.close();\n }\n setStatus('connecting');\n const source = new EventSource('/api/events');\n sourceRef.current = source;\n\n source.onopen = () => {\n setStatus('connected');\n reconnectCount.current = 0;\n };\n\n source.onmessage = (event) => {\n try {\n const parsed = JSON.parse(event.data) as SseEvent;\n setEvents(prev => {\n const next = [...prev, parsed];\n return next.length > maxEvents ? next.slice(-maxEvents) : next;\n });\n setEventCount(c => c + 1);\n setLastEventAt(Date.now());\n } catch {\n // non-fatal: skip malformed events\n }\n };\n\n source.onerror = () => {\n source.close();\n if (reconnectCount.current < maxReconnects) {\n reconnectCount.current += 1;\n setStatus('connecting');\n // SPRINT-D1: use exponential backoff instead of fixed delay\n setTimeout(connect, 1000);\n } else {\n setStatus('disconnected');\n }\n };\n }, [maxEvents, maxReconnects]);\n\n useEffect(() => {\n connect();\n return () => {\n sourceRef.current?.close();\n };\n }, [connect]);\n\n return { events, status, eventCount, lastEventAt };\n}\n"],"names":["useSSE","options","maxEvents","maxReconnects","events","setEvents","useState","status","setStatus","eventCount","setEventCount","lastEventAt","setLastEventAt","reconnectCount","useRef","sourceRef","connect","useCallback","source","event","parsed","prev","next","c","useEffect"],"mappings":"wCA6BO,SAASA,EAAOC,EAAyB,GAAkB,CAChE,KAAM,CAAE,UAAAC,EAAY,IAAK,cAAAC,EAAgB,GAAMF,EACzC,CAACG,EAAQC,CAAS,EAAIC,EAAAA,SAAqB,CAAA,CAAE,EAC7C,CAACC,EAAQC,CAAS,EAAIF,EAAAA,SAAoB,YAAY,EACtD,CAACG,EAAYC,CAAa,EAAIJ,EAAAA,SAAS,CAAC,EACxC,CAACK,EAAaC,CAAc,EAAIN,EAAAA,SAAwB,IAAI,EAC5DO,EAAiBC,EAAAA,OAAO,CAAC,EACzBC,EAAYD,EAAAA,OAA2B,IAAI,EAE3CE,EAAUC,EAAAA,YAAY,IAAM,CAC5BF,EAAU,SACZA,EAAU,QAAQ,MAAA,EAEpBP,EAAU,YAAY,EACtB,MAAMU,EAAS,IAAI,YAAY,aAAa,EAC5CH,EAAU,QAAUG,EAEpBA,EAAO,OAAS,IAAM,CACpBV,EAAU,WAAW,EACrBK,EAAe,QAAU,CAC3B,EAEAK,EAAO,UAAaC,GAAU,CAC5B,GAAI,CACF,MAAMC,EAAS,KAAK,MAAMD,EAAM,IAAI,EACpCd,EAAUgB,GAAQ,CAChB,MAAMC,EAAO,CAAC,GAAGD,EAAMD,CAAM,EAC7B,OAAOE,EAAK,OAASpB,EAAYoB,EAAK,MAAM,CAACpB,CAAS,EAAIoB,CAC5D,CAAC,EACDZ,EAAca,GAAKA,EAAI,CAAC,EACxBX,EAAe,KAAK,KAAK,CAC3B,MAAQ,CAER,CACF,EAEAM,EAAO,QAAU,IAAM,CACrBA,EAAO,MAAA,EACHL,EAAe,QAAUV,GAC3BU,EAAe,SAAW,EAC1BL,EAAU,YAAY,EAEtB,WAAWQ,EAAS,GAAI,GAExBR,EAAU,cAAc,CAE5B,CACF,EAAG,CAACN,EAAWC,CAAa,CAAC,EAE7BqB,OAAAA,EAAAA,UAAU,KACRR,EAAA,EACO,IAAM,CACXD,EAAU,SAAS,MAAA,CACrB,GACC,CAACC,CAAO,CAAC,EAEL,CAAE,OAAAZ,EAAQ,OAAAG,EAAQ,WAAAE,EAAY,YAAAE,CAAA,CACvC"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>mega-compact dashboard</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-B8k19B9g.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="./assets/index-CUNn22iV.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -28,6 +28,7 @@ const TopicsTab = React.lazy(() => import("./tabs/TopicsTab"));
|
|
|
28
28
|
const TurnsTab = React.lazy(() => import("./tabs/TurnsTab"));
|
|
29
29
|
const MaintenanceTab = React.lazy(() => import("./tabs/MaintenanceTab"));
|
|
30
30
|
const MemoryMapTab = React.lazy(() => import("./tabs/MemoryMapTab"));
|
|
31
|
+
const HealthTab = React.lazy(() => import("./tabs/HealthTab"));
|
|
31
32
|
|
|
32
33
|
export type TabId =
|
|
33
34
|
| "overview"
|
|
@@ -43,7 +44,8 @@ export type TabId =
|
|
|
43
44
|
| "topics"
|
|
44
45
|
| "turns"
|
|
45
46
|
| "maintenance"
|
|
46
|
-
| "memory-map"
|
|
47
|
+
| "memory-map"
|
|
48
|
+
| "health";
|
|
47
49
|
|
|
48
50
|
type TabDef = { id: TabId; label: string };
|
|
49
51
|
|
|
@@ -52,6 +54,7 @@ const PRIMARY_TABS: TabDef[] = [
|
|
|
52
54
|
{ id: "cache", label: "Cache" },
|
|
53
55
|
{ id: "sessions", label: "Sessions" },
|
|
54
56
|
{ id: "turns", label: "Turns" },
|
|
57
|
+
{ id: "health", label: "Health" },
|
|
55
58
|
];
|
|
56
59
|
|
|
57
60
|
const ADVANCED_TABS: TabDef[] = [
|
|
@@ -124,6 +127,7 @@ export default function App(): React.ReactElement {
|
|
|
124
127
|
{activeTab === "topics" && <TopicsTab />}
|
|
125
128
|
{activeTab === "maintenance" && <MaintenanceTab />}
|
|
126
129
|
{activeTab === "memory-map" && <MemoryMapTab />}
|
|
130
|
+
{activeTab === "health" && <HealthTab />}
|
|
127
131
|
</React.Suspense>
|
|
128
132
|
</main>
|
|
129
133
|
</div>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-client/src/api/health.ts — Context Health API client.
|
|
3
|
+
* PREVENT-PI-004: relative paths only (loopback dashboard).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ContextHealthRow {
|
|
7
|
+
ts: number;
|
|
8
|
+
turnIndex: number;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
driftScore: number;
|
|
11
|
+
outputQuality: number;
|
|
12
|
+
errorScore: number;
|
|
13
|
+
cacheHealth: number;
|
|
14
|
+
cachePoison: number;
|
|
15
|
+
composite: number;
|
|
16
|
+
modelId: string | null;
|
|
17
|
+
repetitionRatio: number | null;
|
|
18
|
+
coherenceScore: number | null;
|
|
19
|
+
prefixHash: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ContextHealthResponse {
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
latest: {
|
|
25
|
+
composite: number;
|
|
26
|
+
driftScore: number;
|
|
27
|
+
outputQuality: number;
|
|
28
|
+
errorScore: number;
|
|
29
|
+
cacheHealth: number;
|
|
30
|
+
cachePoison: number;
|
|
31
|
+
ts: number;
|
|
32
|
+
modelId: string | null;
|
|
33
|
+
} | null;
|
|
34
|
+
trend: number[];
|
|
35
|
+
rows: ContextHealthRow[];
|
|
36
|
+
perModel: Array<{ modelId: string; avgComposite: number; sampleCount: number }>;
|
|
37
|
+
alerts: ContextHealthRow[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CachePoisonEvent {
|
|
41
|
+
ts: number;
|
|
42
|
+
turnIndex: number;
|
|
43
|
+
sessionId: string;
|
|
44
|
+
layer: number;
|
|
45
|
+
detail: string;
|
|
46
|
+
severity: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface CachePoisonResponse {
|
|
50
|
+
updatedAt: string;
|
|
51
|
+
events: CachePoisonEvent[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function fetchContextHealth(minutes?: number): Promise<ContextHealthResponse> {
|
|
55
|
+
const r = await fetch(`/api/context-health?minutes=${minutes ?? 30}`);
|
|
56
|
+
if (!r.ok) throw new Error(`context-health: ${r.status}`);
|
|
57
|
+
return r.json() as Promise<ContextHealthResponse>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function fetchCachePoison(): Promise<CachePoisonResponse> {
|
|
61
|
+
const r = await fetch("/api/cache-poison");
|
|
62
|
+
if (!r.ok) throw new Error(`cache-poison: ${r.status}`);
|
|
63
|
+
return r.json() as Promise<CachePoisonResponse>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HealthTab.tsx — Context Health dashboard tab.
|
|
3
|
+
* Shows composite health gauge, sparkline trend, sub-score bars,
|
|
4
|
+
* alerts, and per-model comparison. Polls /api/context-health every 5s.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useEffect, useCallback } from "react";
|
|
8
|
+
import { fetchContextHealth, type ContextHealthResponse } from "../api/health";
|
|
9
|
+
|
|
10
|
+
function healthColor(score: number): string {
|
|
11
|
+
if (score > 0.8) return "#4ade80";
|
|
12
|
+
if (score >= 0.5) return "#facc15";
|
|
13
|
+
return "#f87171";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function HealthGauge({ score }: { score: number }): React.ReactElement {
|
|
17
|
+
const r = 70;
|
|
18
|
+
const circ = 2 * Math.PI * r;
|
|
19
|
+
const offset = circ * (1 - Math.max(0, Math.min(1, score)));
|
|
20
|
+
const color = healthColor(score);
|
|
21
|
+
return (
|
|
22
|
+
<div className="health-gauge-wrap">
|
|
23
|
+
<svg width="180" height="180" viewBox="0 0 180 180">
|
|
24
|
+
<circle cx="90" cy="90" r={r} fill="none" stroke="#1e293b" strokeWidth="14" />
|
|
25
|
+
<circle
|
|
26
|
+
cx="90"
|
|
27
|
+
cy="90"
|
|
28
|
+
r={r}
|
|
29
|
+
fill="none"
|
|
30
|
+
stroke={color}
|
|
31
|
+
strokeWidth="14"
|
|
32
|
+
strokeDasharray={circ}
|
|
33
|
+
strokeDashoffset={offset}
|
|
34
|
+
strokeLinecap="round"
|
|
35
|
+
transform="rotate(-90 90 90)"
|
|
36
|
+
style={{ transition: "stroke-dashoffset 0.5s ease" }}
|
|
37
|
+
/>
|
|
38
|
+
<text x="90" y="85" textAnchor="middle" fontSize="32" fontWeight="bold" fill={color}>
|
|
39
|
+
{(score * 100).toFixed(0)}
|
|
40
|
+
</text>
|
|
41
|
+
<text x="90" y="108" textAnchor="middle" fontSize="14" fill="#94a3b8">/ 100</text>
|
|
42
|
+
</svg>
|
|
43
|
+
<div className="health-gauge-label">Context Health</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function HealthSparkline({ trend }: { trend: number[] }): React.ReactElement {
|
|
49
|
+
if (trend.length < 2) return <div className="health-spark-empty">No trend data yet</div>;
|
|
50
|
+
const w = 400;
|
|
51
|
+
const h = 60;
|
|
52
|
+
const max = 1;
|
|
53
|
+
const min = 0;
|
|
54
|
+
const pts = trend.map((v, i) => {
|
|
55
|
+
const x = (i / (trend.length - 1)) * w;
|
|
56
|
+
const y = h - ((v - min) / (max - min)) * h;
|
|
57
|
+
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
58
|
+
});
|
|
59
|
+
const last = trend[trend.length - 1];
|
|
60
|
+
return (
|
|
61
|
+
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} className="health-spark">
|
|
62
|
+
<polyline points={pts.join(" ")} fill="none" stroke={healthColor(last)} strokeWidth="2" />
|
|
63
|
+
</svg>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function SubScoreBar({ label, value }: { label: string; value: number }): React.ReactElement {
|
|
68
|
+
return (
|
|
69
|
+
<div className="health-subscore">
|
|
70
|
+
<span className="health-subscore-label">{label}</span>
|
|
71
|
+
<div className="health-subscore-bar">
|
|
72
|
+
<div
|
|
73
|
+
className="health-subscore-fill"
|
|
74
|
+
style={{ width: `${value * 100}%`, background: healthColor(value) }}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
<span className="health-subscore-val">{(value * 100).toFixed(0)}%</span>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default function HealthTab(): React.ReactElement {
|
|
83
|
+
const [data, setData] = useState<ContextHealthResponse | null>(null);
|
|
84
|
+
const [err, setError] = useState<string | null>(null);
|
|
85
|
+
const [loading, setLoading] = useState(true);
|
|
86
|
+
|
|
87
|
+
const poll = useCallback(() => {
|
|
88
|
+
fetchContextHealth()
|
|
89
|
+
.then(setData)
|
|
90
|
+
.catch((e: unknown) => setError(e instanceof Error ? e.message : String(e)))
|
|
91
|
+
.finally(() => setLoading(false));
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
poll();
|
|
96
|
+
const id = setInterval(poll, 5000);
|
|
97
|
+
return () => clearInterval(id);
|
|
98
|
+
}, [poll]);
|
|
99
|
+
|
|
100
|
+
if (loading && !data) return <div className="health-loading">Loading health data…</div>;
|
|
101
|
+
if (err && !data) return <div className="health-error">Error: {err}</div>;
|
|
102
|
+
if (!data) return <div className="health-empty">No health data available.</div>;
|
|
103
|
+
|
|
104
|
+
const latest = data.latest;
|
|
105
|
+
const composite = latest?.composite ?? 0;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className="health-tab">
|
|
109
|
+
<div className="health-top-row">
|
|
110
|
+
<HealthGauge score={composite} />
|
|
111
|
+
<div className="health-subscores">
|
|
112
|
+
<h3>Sub-Scores</h3>
|
|
113
|
+
<SubScoreBar label="Drift" value={latest?.driftScore ?? 0} />
|
|
114
|
+
<SubScoreBar label="Output Quality" value={latest?.outputQuality ?? 0} />
|
|
115
|
+
<SubScoreBar label="Error Rate" value={latest?.errorScore ?? 0} />
|
|
116
|
+
<SubScoreBar label="Cache Health" value={latest?.cacheHealth ?? 0} />
|
|
117
|
+
<SubScoreBar label="Cache Poison" value={latest?.cachePoison ?? 0} />
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div className="health-trend-section">
|
|
122
|
+
<h3>Health Trend (last {data.trend.length} turns)</h3>
|
|
123
|
+
<HealthSparkline trend={data.trend} />
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{data.alerts.length > 0 && (
|
|
127
|
+
<div className="health-alerts">
|
|
128
|
+
<h3>Recent Alerts ({data.alerts.length})</h3>
|
|
129
|
+
<div className="health-alert-list">
|
|
130
|
+
{data.alerts.slice(-10).map((a) => (
|
|
131
|
+
<div key={`${a.ts}-${a.turnIndex}`} className="health-alert-row">
|
|
132
|
+
<span className="health-alert-time">{new Date(a.ts).toLocaleTimeString()}</span>
|
|
133
|
+
<span className="health-alert-model">{a.modelId ?? "(unknown)"}</span>
|
|
134
|
+
<span className="health-alert-score" style={{ color: healthColor(a.composite) }}>
|
|
135
|
+
{(a.composite * 100).toFixed(0)}
|
|
136
|
+
</span>
|
|
137
|
+
{a.cachePoison < 0.3 && <span className="health-alert-tag">CACHE POISON</span>}
|
|
138
|
+
</div>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
{data.perModel.length > 0 && (
|
|
145
|
+
<div className="health-per-model">
|
|
146
|
+
<h3>Health by Model</h3>
|
|
147
|
+
<table className="health-model-table">
|
|
148
|
+
<thead><tr><th>Model</th><th>Avg Health</th><th>Samples</th></tr></thead>
|
|
149
|
+
<tbody>
|
|
150
|
+
{data.perModel.map((m) => (
|
|
151
|
+
<tr key={m.modelId}>
|
|
152
|
+
<td>{m.modelId}</td>
|
|
153
|
+
<td style={{ color: healthColor(m.avgComposite) }}>
|
|
154
|
+
{(m.avgComposite * 100).toFixed(0)}%
|
|
155
|
+
</td>
|
|
156
|
+
<td>{m.sampleCount}</td>
|
|
157
|
+
</tr>
|
|
158
|
+
))}
|
|
159
|
+
</tbody>
|
|
160
|
+
</table>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
|
|
12
|
+
import { createRequire } from "node:module";
|
|
13
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
14
|
+
import type { RouteContext } from "./routes-core.js";
|
|
15
|
+
|
|
16
|
+
export function handleContextHealth(
|
|
17
|
+
req: IncomingMessage,
|
|
18
|
+
res: ServerResponse,
|
|
19
|
+
ctx: RouteContext,
|
|
20
|
+
): boolean {
|
|
21
|
+
if (!req.url?.startsWith("/api/context-health")) return false;
|
|
22
|
+
try {
|
|
23
|
+
if (req.method !== "GET") {
|
|
24
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
25
|
+
res.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const url = new URL(req.url, "http://localhost");
|
|
29
|
+
const minutes = Math.max(1, Number(url.searchParams.get("minutes") ?? 30));
|
|
30
|
+
const sinceTs = Date.now() - minutes * 60_000;
|
|
31
|
+
|
|
32
|
+
const pfReq = createRequire(import.meta.url);
|
|
33
|
+
const {
|
|
34
|
+
readLatestContextHealth,
|
|
35
|
+
readContextHealthTrend,
|
|
36
|
+
readContextHealthRows,
|
|
37
|
+
} = pfReq("../../src/store/sqlite.js") as typeof import("../../src/store/sqlite.js");
|
|
38
|
+
|
|
39
|
+
const latest = readLatestContextHealth(ctx.stateDir);
|
|
40
|
+
const trend = readContextHealthTrend(ctx.stateDir, 50);
|
|
41
|
+
const rows = readContextHealthRows(ctx.stateDir, sinceTs);
|
|
42
|
+
|
|
43
|
+
const perModel = new Map<string, { sum: number; count: number }>();
|
|
44
|
+
for (const r of rows) {
|
|
45
|
+
const mid = r.modelId ?? "(unknown)";
|
|
46
|
+
const entry = perModel.get(mid) ?? { sum: 0, count: 0 };
|
|
47
|
+
entry.sum += r.composite;
|
|
48
|
+
entry.count += 1;
|
|
49
|
+
perModel.set(mid, entry);
|
|
50
|
+
}
|
|
51
|
+
const perModelAverages = [...perModel.entries()].map(([modelId, v]) => ({
|
|
52
|
+
modelId,
|
|
53
|
+
avgComposite: v.count > 0 ? v.sum / v.count : 0,
|
|
54
|
+
sampleCount: v.count,
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
const alerts = rows.filter(
|
|
58
|
+
(r) => r.composite < 0.5 || r.cachePoison < 0.3,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const body = {
|
|
62
|
+
updatedAt: new Date().toISOString(),
|
|
63
|
+
latest,
|
|
64
|
+
trend,
|
|
65
|
+
rows: rows.slice(-200),
|
|
66
|
+
perModel: perModelAverages,
|
|
67
|
+
alerts,
|
|
68
|
+
};
|
|
69
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
70
|
+
res.end(JSON.stringify(body));
|
|
71
|
+
} catch (e) {
|
|
72
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
73
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function handleCachePoison(
|
|
79
|
+
req: IncomingMessage,
|
|
80
|
+
res: ServerResponse,
|
|
81
|
+
ctx: RouteContext,
|
|
82
|
+
): boolean {
|
|
83
|
+
if (!req.url?.startsWith("/api/cache-poison")) return false;
|
|
84
|
+
try {
|
|
85
|
+
if (req.method !== "GET") {
|
|
86
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
87
|
+
res.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
const sinceTs = Date.now() - 30 * 60_000;
|
|
91
|
+
const pfReq = createRequire(import.meta.url);
|
|
92
|
+
const { readCachePoisonEvents } = pfReq(
|
|
93
|
+
"../../src/store/sqlite.js",
|
|
94
|
+
) as typeof import("../../src/store/sqlite.js");
|
|
95
|
+
|
|
96
|
+
const events = readCachePoisonEvents(ctx.stateDir, sinceTs);
|
|
97
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
98
|
+
res.end(JSON.stringify({ updatedAt: new Date().toISOString(), events }));
|
|
99
|
+
} catch (e) {
|
|
100
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
101
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
@@ -24,3 +24,4 @@ export { handleMemoryStatus } from "./routes-memory.js";
|
|
|
24
24
|
export { handleSetupStatus, handleSetupDetect, handleSetupConfigure } from "./routes-setup.js";
|
|
25
25
|
export { handleMemoryMap } from "./routes-memory-map.js";
|
|
26
26
|
export { handleCacheStripes } from "./routes-cache.js";
|
|
27
|
+
export { handleContextHealth, handleCachePoison } from "./routes-health.js";
|
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
handleSetupDetect,
|
|
45
45
|
handleSetupConfigure,
|
|
46
46
|
handleMemoryMap,
|
|
47
|
+
handleContextHealth,
|
|
48
|
+
handleCachePoison,
|
|
47
49
|
handleStatic,
|
|
48
50
|
} from "./routes.js";
|
|
49
51
|
|
|
@@ -239,6 +241,8 @@ export async function launchDashboardServer(
|
|
|
239
241
|
if (handleSetupDetect(req, res, ctx)) return;
|
|
240
242
|
if (handleSetupConfigure(req, res, ctx)) return;
|
|
241
243
|
if (handleMemoryMap(req, res, ctx)) return;
|
|
244
|
+
if (handleContextHealth(req, res, ctx)) return;
|
|
245
|
+
if (handleCachePoison(req, res, ctx)) return;
|
|
242
246
|
handleStatic(req, res, ctx);
|
|
243
247
|
});
|
|
244
248
|
|
|
@@ -198,6 +198,16 @@ export interface MegaConfig {
|
|
|
198
198
|
memoryGraphCrossTypeThreshold: number;
|
|
199
199
|
/** D3 edges: Cosine floor for within-type semantic edges. */
|
|
200
200
|
memoryGraphWithinTypeThreshold: number;
|
|
201
|
+
/** v0.12: Context health monitoring (drift + output quality + cache poison). Default ON. */
|
|
202
|
+
contextHealth: boolean;
|
|
203
|
+
/** Sub-flag: drift detection (topic drift + error escalation + prefix instability). */
|
|
204
|
+
contextHealthDrift: boolean;
|
|
205
|
+
/** Sub-flag: output quality analysis (repetition, coherence, token salad). */
|
|
206
|
+
contextHealthOutputQuality: boolean;
|
|
207
|
+
/** Sub-flag: tri-layer KV cache poison validation. */
|
|
208
|
+
contextHealthCachePoison: boolean;
|
|
209
|
+
/** v0.12: KV cache poison mitigation — inject prefix break on mismatch. Default OFF. */
|
|
210
|
+
contextHealthMitigate: boolean;
|
|
201
211
|
}
|
|
202
212
|
|
|
203
213
|
function envFlag(name: string, fallback: number): number {
|
|
@@ -372,6 +382,11 @@ export function loadConfig(): MegaConfig {
|
|
|
372
382
|
memoryGraphSeedMemories: envBool("MEGACOMPACT_MEMORY_GRAPH_SEED_MEMORIES", true),
|
|
373
383
|
memoryGraphCrossTypeThreshold: envFlag("MEGACOMPACT_MEMORY_GRAPH_CROSS_TYPE_THRESHOLD", 0.85),
|
|
374
384
|
memoryGraphWithinTypeThreshold: envFlag("MEGACOMPACT_MEMORY_GRAPH_WITHIN_TYPE_THRESHOLD", 0.7),
|
|
385
|
+
contextHealth: envBool("MEGACOMPACT_CONTEXT_HEALTH", true),
|
|
386
|
+
contextHealthDrift: envBool("MEGACOMPACT_CONTEXT_HEALTH_DRIFT", true),
|
|
387
|
+
contextHealthOutputQuality: envBool("MEGACOMPACT_CONTEXT_HEALTH_OUTPUT_QUALITY", true),
|
|
388
|
+
contextHealthCachePoison: envBool("MEGACOMPACT_CONTEXT_HEALTH_CACHE_POISON", true),
|
|
389
|
+
contextHealthMitigate: envBool("MEGACOMPACT_CONTEXT_HEALTH_MITIGATE", false),
|
|
375
390
|
// D.1: env-overridable recompact delta (minimum context growth % before
|
|
376
391
|
// re-compacting instead of replaying the cached live trim). Default 50.
|
|
377
392
|
recompactPctDelta: envFlag("MEGACOMPACT_RECOMPACT_PCT_DELTA", 50),
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
import {
|
|
36
36
|
refreshStripeAssignments as writeStripeAssignments,
|
|
37
37
|
} from "../../src/cache-stripe-impl.js";
|
|
38
|
+
import { handleTurnEndHealth } from "./health-handler.js";
|
|
38
39
|
|
|
39
40
|
/** Register agent/turn tracking event handlers. */
|
|
40
41
|
export function registerAgentHandlers(
|
|
@@ -296,6 +297,7 @@ export function registerAgentHandlers(
|
|
|
296
297
|
pi.on("turn_end", async (event, ctx) => {
|
|
297
298
|
runtime.dashboard.event("turn_end", { turnIndex: event.turnIndex });
|
|
298
299
|
runtime.snapshot(ctx);
|
|
300
|
+
runtime.lastErrorCategory = null;
|
|
299
301
|
|
|
300
302
|
// S53: consume staged recall blocks ONLY if they were actually injected
|
|
301
303
|
// into a view this turn. If no context event fired (edge: turn ended
|
|
@@ -436,6 +438,7 @@ export function registerAgentHandlers(
|
|
|
436
438
|
} else {
|
|
437
439
|
const category = classifyError(event.message);
|
|
438
440
|
const detail = classifyErrorDetailed(event.message);
|
|
441
|
+
runtime.lastErrorCategory = category;
|
|
439
442
|
// R3: stateful poisoned-context signal — repeated identical error text
|
|
440
443
|
// across consecutive turns. The classifier is stateless, so this
|
|
441
444
|
// upgrade happens here. A 'transient' (or 'poisoned-context') turn
|
|
@@ -860,5 +863,8 @@ export function registerAgentHandlers(
|
|
|
860
863
|
/* non-fatal: stripe refresh never breaks the agent loop */
|
|
861
864
|
}
|
|
862
865
|
}
|
|
866
|
+
|
|
867
|
+
// v0.12: Context Health — compute + persist health score. Non-fatal.
|
|
868
|
+
handleTurnEndHealth(event, runtime, config);
|
|
863
869
|
});
|
|
864
870
|
}
|