pi-mega-compact 0.11.13 → 0.12.1
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 +125 -0
- package/dist/extensions/dashboard-server/routes.js +1 -0
- package/dist/extensions/dashboard-server/server.js +7 -1
- package/dist/extensions/mega-config.js +5 -0
- package/dist/extensions/mega-events/agent-handlers.js +31 -0
- package/dist/extensions/mega-events/health-handler.js +173 -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/meta.js +11 -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-Cwg_Ab8R.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-9KQgKBEh.js.map → AchievementTiles-Cwg_Ab8R.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-B6xoQTEa.js → AchievementsTab-Dk32-4Ha.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-B6xoQTEa.js.map → AchievementsTab-Dk32-4Ha.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-Dkm9rawo.js → CacheTab-B9zR8UgD.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-Dkm9rawo.js.map → CacheTab-B9zR8UgD.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ConfigTab-Bd_v_OUF.js → ConfigTab-DYRl3mJZ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-Bd_v_OUF.js.map → ConfigTab-DYRl3mJZ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-CYWoFOdI.js → EventsTab-r_zXcrmZ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-CYWoFOdI.js.map → EventsTab-r_zXcrmZ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{GameTab-CH7lecNL.js → GameTab-DjwA_Q1v.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-CH7lecNL.js.map → GameTab-DjwA_Q1v.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/HealthTab-BjH73kQr.js +2 -0
- package/extensions/dashboard-client/dist/assets/HealthTab-BjH73kQr.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-DzeSq9vK.js +2 -0
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-DzeSq9vK.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-tVRREYiG.js → MemoryMapTab-BU9iM3ir.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-tVRREYiG.js.map → MemoryMapTab-BU9iM3ir.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CHblYuTY.js → MetricsTab-lMVodOwY.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CHblYuTY.js.map → MetricsTab-lMVodOwY.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-Dj0TgFvU.js → OverviewTab-D-laa6CW.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-Dj0TgFvU.js.map → OverviewTab-D-laa6CW.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-BTJcfc1W.js → ReposTab-5Pn2cOOO.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-BTJcfc1W.js.map → ReposTab-5Pn2cOOO.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C8tU8K5u.js → SessionsTab-Dtdmvl2C.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C8tU8K5u.js.map → SessionsTab-Dtdmvl2C.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-BdxGMo6O.js → SetupTab-rKGgWpI0.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-BdxGMo6O.js.map → SetupTab-rKGgWpI0.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BWZSARzT.js → TimeSavedCard-B5Lw6fTp.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BWZSARzT.js.map → TimeSavedCard-B5Lw6fTp.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TopicsTab-BCJwPYl8.js → TopicsTab-Ds1IHhPw.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TopicsTab-BCJwPYl8.js.map → TopicsTab-Ds1IHhPw.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DQFTSDpz.js → TurnsTab-CG2RDRmh.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DQFTSDpz.js.map → TurnsTab-CG2RDRmh.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/health-lSeDFTz7.js +2 -0
- package/extensions/dashboard-client/dist/assets/health-lSeDFTz7.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{index-DrrDswj-.js → index-C9dTXvDQ.js} +11 -11
- package/extensions/dashboard-client/dist/assets/{index-DrrDswj-.js.map → index-C9dTXvDQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-o4tteeTs.js → useSSE-B6l8O-yi.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-o4tteeTs.js.map → useSSE-B6l8O-yi.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 +80 -0
- package/extensions/dashboard-client/src/tabs/HealthTab.tsx +165 -0
- package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +91 -1
- package/extensions/dashboard-server/routes-health.ts +155 -0
- package/extensions/dashboard-server/routes.ts +1 -0
- package/extensions/dashboard-server/server.ts +6 -0
- package/extensions/mega-config.ts +15 -0
- package/extensions/mega-events/agent-handlers.ts +31 -0
- package/extensions/mega-events/health-handler.ts +218 -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/meta.ts +16 -0
- package/src/store/sqlite/schema.ts +43 -1
- package/src/store/sqlite.ts +1 -0
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-DNZ1IF4t.js +0 -2
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-DNZ1IF4t.js.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as e}from"./index-
|
|
2
|
-
//# sourceMappingURL=useSSE-
|
|
1
|
+
import{r as e}from"./index-C9dTXvDQ.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-B6l8O-yi.js.map
|
package/extensions/dashboard-client/dist/assets/{useSSE-o4tteeTs.js.map → useSSE-B6l8O-yi.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSSE-
|
|
1
|
+
{"version":3,"file":"useSSE-B6l8O-yi.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-C9dTXvDQ.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,80 @@
|
|
|
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
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function fetchHealthSettings(): Promise<{ mitigate: boolean }> {
|
|
67
|
+
const r = await fetch("/api/context-health/settings");
|
|
68
|
+
if (!r.ok) throw new Error(`health-settings: ${r.status}`);
|
|
69
|
+
return r.json() as Promise<{ mitigate: boolean }>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function setHealthMitigate(mitigate: boolean): Promise<{ mitigate: boolean }> {
|
|
73
|
+
const r = await fetch("/api/context-health/settings", {
|
|
74
|
+
method: "PUT",
|
|
75
|
+
headers: { "Content-Type": "application/json" },
|
|
76
|
+
body: JSON.stringify({ mitigate }),
|
|
77
|
+
});
|
|
78
|
+
if (!r.ok) throw new Error(`health-settings PUT: ${r.status}`);
|
|
79
|
+
return r.json() as Promise<{ mitigate: boolean }>;
|
|
80
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type React from "react";
|
|
13
|
-
import { useState, useCallback } from "react";
|
|
13
|
+
import { useState, useCallback, useEffect } from "react";
|
|
14
14
|
import type {
|
|
15
15
|
DbStatsResponse,
|
|
16
16
|
SchemaHealthResponse,
|
|
@@ -519,6 +519,95 @@ function DebugBundleCard(): React.ReactElement {
|
|
|
519
519
|
);
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
// Health Mitigation card — toggle auto-mitigation for degraded context
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
|
|
526
|
+
function HealthMitigationCard(): React.ReactElement {
|
|
527
|
+
const [mitigate, setMitigate] = useState(false);
|
|
528
|
+
const [loading, setLoading] = useState(true);
|
|
529
|
+
const [saving, setSaving] = useState(false);
|
|
530
|
+
const [error, setError] = useState<string | null>(null);
|
|
531
|
+
|
|
532
|
+
const load = useCallback(async () => {
|
|
533
|
+
try {
|
|
534
|
+
const { fetchHealthSettings } = await import("../api/health");
|
|
535
|
+
const s = await fetchHealthSettings();
|
|
536
|
+
setMitigate(s.mitigate);
|
|
537
|
+
} catch (e) {
|
|
538
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
539
|
+
} finally {
|
|
540
|
+
setLoading(false);
|
|
541
|
+
}
|
|
542
|
+
}, []);
|
|
543
|
+
|
|
544
|
+
useEffect(() => { void load(); }, [load]);
|
|
545
|
+
|
|
546
|
+
const toggle = useCallback(async () => {
|
|
547
|
+
setSaving(true);
|
|
548
|
+
setError(null);
|
|
549
|
+
try {
|
|
550
|
+
const { setHealthMitigate } = await import("../api/health");
|
|
551
|
+
const next = !mitigate;
|
|
552
|
+
await setHealthMitigate(next);
|
|
553
|
+
setMitigate(next);
|
|
554
|
+
} catch (e) {
|
|
555
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
556
|
+
} finally {
|
|
557
|
+
setSaving(false);
|
|
558
|
+
}
|
|
559
|
+
}, [mitigate]);
|
|
560
|
+
|
|
561
|
+
return (
|
|
562
|
+
<div className="stat-section maintenance-card">
|
|
563
|
+
<div className="stat-label">Context Health Mitigation</div>
|
|
564
|
+
{loading ? (
|
|
565
|
+
<p className="repo-none">Loading…</p>
|
|
566
|
+
) : (
|
|
567
|
+
<>
|
|
568
|
+
<div className="health-mitigate-row">
|
|
569
|
+
<label className="health-mitigate-toggle">
|
|
570
|
+
<input
|
|
571
|
+
type="checkbox"
|
|
572
|
+
checked={mitigate}
|
|
573
|
+
onChange={toggle}
|
|
574
|
+
disabled={saving}
|
|
575
|
+
/>
|
|
576
|
+
<span>Auto-mitigation {mitigate ? "ON" : "OFF"}</span>
|
|
577
|
+
</label>
|
|
578
|
+
</div>
|
|
579
|
+
<div className="health-mitigate-info">
|
|
580
|
+
<p>
|
|
581
|
+
When <strong>ON</strong>, the extension automatically intervenes when
|
|
582
|
+
context health degrades:
|
|
583
|
+
</p>
|
|
584
|
+
<ul>
|
|
585
|
+
<li><strong>Forced compaction</strong> — when the composite health score
|
|
586
|
+
drops below 0.4 (severe drift or garbled output), a compaction is
|
|
587
|
+
triggered to flush the degraded context and rebuild from clean
|
|
588
|
+
checkpoints, even if the context window isn't full.</li>
|
|
589
|
+
<li><strong>Prefix break</strong> — when KV cache poison is detected
|
|
590
|
+
(score < 0.3), the cached prefix is invalidated to force a cache
|
|
591
|
+
miss on the next turn, bypassing the corrupted KV state.</li>
|
|
592
|
+
</ul>
|
|
593
|
+
<p className="health-mitigate-warning">
|
|
594
|
+
WARNING: both interventions have costs — forced compaction discards
|
|
595
|
+
live context, and prefix breaks kill cache savings. Only enable when
|
|
596
|
+
you're seeing degraded output (garbled text, hallucinations,
|
|
597
|
+
loss of coherence) and the Health tab confirms low scores.
|
|
598
|
+
</p>
|
|
599
|
+
<p>
|
|
600
|
+
Also settable via env var: <code>MEGACOMPACT_CONTEXT_HEALTH_MITIGATE=1</code>.
|
|
601
|
+
The dashboard toggle persists to the store and takes precedence.
|
|
602
|
+
</p>
|
|
603
|
+
{error && <p className="health-mitigate-error">Error: {error}</p>}
|
|
604
|
+
</div>
|
|
605
|
+
</>
|
|
606
|
+
)}
|
|
607
|
+
</div>
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
522
611
|
// ---------------------------------------------------------------------------
|
|
523
612
|
// Main tab component
|
|
524
613
|
// ---------------------------------------------------------------------------
|
|
@@ -554,6 +643,7 @@ export default function MaintenanceTab(): React.ReactElement {
|
|
|
554
643
|
</div>
|
|
555
644
|
<ActionsCard />
|
|
556
645
|
<DebugBundleCard />
|
|
646
|
+
<HealthMitigationCard />
|
|
557
647
|
</div>
|
|
558
648
|
);
|
|
559
649
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
}
|
|
105
|
+
|
|
106
|
+
/** GET/PUT /api/context-health/settings — read or toggle mitigation. */
|
|
107
|
+
export function handleHealthSettings(
|
|
108
|
+
req: IncomingMessage,
|
|
109
|
+
res: ServerResponse,
|
|
110
|
+
ctx: RouteContext,
|
|
111
|
+
): boolean {
|
|
112
|
+
if (!req.url?.startsWith("/api/context-health/settings")) return false;
|
|
113
|
+
try {
|
|
114
|
+
const pfReq = createRequire(import.meta.url);
|
|
115
|
+
const { getHealthMitigate, setHealthMitigate } = pfReq(
|
|
116
|
+
"../../src/store/sqlite.js",
|
|
117
|
+
) as typeof import("../../src/store/sqlite.js");
|
|
118
|
+
|
|
119
|
+
if (req.method === "GET") {
|
|
120
|
+
const mitigate = getHealthMitigate(ctx.stateDir);
|
|
121
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
122
|
+
res.end(JSON.stringify({ mitigate }));
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (req.method === "PUT") {
|
|
127
|
+
let body = "";
|
|
128
|
+
req.on("data", (c) => (body += c));
|
|
129
|
+
req.on("end", () => {
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(body) as { mitigate?: unknown };
|
|
132
|
+
if (typeof parsed.mitigate !== "boolean") {
|
|
133
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
134
|
+
res.end(JSON.stringify({ error: "mitigate must be boolean" }));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
setHealthMitigate(parsed.mitigate, ctx.stateDir);
|
|
138
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
139
|
+
res.end(JSON.stringify({ mitigate: parsed.mitigate }));
|
|
140
|
+
} catch {
|
|
141
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
142
|
+
res.end(JSON.stringify({ error: "invalid_json" }));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
149
|
+
res.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
150
|
+
} catch (e) {
|
|
151
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
152
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
@@ -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, handleHealthSettings } from "./routes-health.js";
|
|
@@ -44,6 +44,9 @@ import {
|
|
|
44
44
|
handleSetupDetect,
|
|
45
45
|
handleSetupConfigure,
|
|
46
46
|
handleMemoryMap,
|
|
47
|
+
handleContextHealth,
|
|
48
|
+
handleCachePoison,
|
|
49
|
+
handleHealthSettings,
|
|
47
50
|
handleStatic,
|
|
48
51
|
} from "./routes.js";
|
|
49
52
|
|
|
@@ -239,6 +242,9 @@ export async function launchDashboardServer(
|
|
|
239
242
|
if (handleSetupDetect(req, res, ctx)) return;
|
|
240
243
|
if (handleSetupConfigure(req, res, ctx)) return;
|
|
241
244
|
if (handleMemoryMap(req, res, ctx)) return;
|
|
245
|
+
if (handleContextHealth(req, res, ctx)) return;
|
|
246
|
+
if (handleCachePoison(req, res, ctx)) return;
|
|
247
|
+
if (handleHealthSettings(req, res, ctx)) return;
|
|
242
248
|
handleStatic(req, res, ctx);
|
|
243
249
|
});
|
|
244
250
|
|
|
@@ -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),
|