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.
Files changed (78) hide show
  1. package/dist/extensions/dashboard-server/routes-health.js +125 -0
  2. package/dist/extensions/dashboard-server/routes.js +1 -0
  3. package/dist/extensions/dashboard-server/server.js +7 -1
  4. package/dist/extensions/mega-config.js +5 -0
  5. package/dist/extensions/mega-events/agent-handlers.js +31 -0
  6. package/dist/extensions/mega-events/health-handler.js +173 -0
  7. package/dist/extensions/mega-runtime/runtime.js +6 -0
  8. package/dist/src/contextHealth/cachePoison.js +183 -0
  9. package/dist/src/contextHealth/drift.js +89 -0
  10. package/dist/src/contextHealth/outputQuality.js +112 -0
  11. package/dist/src/contextHealth.js +110 -0
  12. package/dist/src/store/sqlite/context-health.js +193 -0
  13. package/dist/src/store/sqlite/meta.js +11 -0
  14. package/dist/src/store/sqlite/schema.js +43 -1
  15. package/dist/src/store/sqlite.js +1 -0
  16. package/extensions/dashboard-client/dist/assets/{AchievementTiles-9KQgKBEh.js → AchievementTiles-Cwg_Ab8R.js} +2 -2
  17. package/extensions/dashboard-client/dist/assets/{AchievementTiles-9KQgKBEh.js.map → AchievementTiles-Cwg_Ab8R.js.map} +1 -1
  18. package/extensions/dashboard-client/dist/assets/{AchievementsTab-B6xoQTEa.js → AchievementsTab-Dk32-4Ha.js} +2 -2
  19. package/extensions/dashboard-client/dist/assets/{AchievementsTab-B6xoQTEa.js.map → AchievementsTab-Dk32-4Ha.js.map} +1 -1
  20. package/extensions/dashboard-client/dist/assets/{CacheTab-Dkm9rawo.js → CacheTab-B9zR8UgD.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{CacheTab-Dkm9rawo.js.map → CacheTab-B9zR8UgD.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{ConfigTab-Bd_v_OUF.js → ConfigTab-DYRl3mJZ.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{ConfigTab-Bd_v_OUF.js.map → ConfigTab-DYRl3mJZ.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{EventsTab-CYWoFOdI.js → EventsTab-r_zXcrmZ.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{EventsTab-CYWoFOdI.js.map → EventsTab-r_zXcrmZ.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{GameTab-CH7lecNL.js → GameTab-DjwA_Q1v.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/{GameTab-CH7lecNL.js.map → GameTab-DjwA_Q1v.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/HealthTab-BjH73kQr.js +2 -0
  29. package/extensions/dashboard-client/dist/assets/HealthTab-BjH73kQr.js.map +1 -0
  30. package/extensions/dashboard-client/dist/assets/MaintenanceTab-DzeSq9vK.js +2 -0
  31. package/extensions/dashboard-client/dist/assets/MaintenanceTab-DzeSq9vK.js.map +1 -0
  32. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-tVRREYiG.js → MemoryMapTab-BU9iM3ir.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-tVRREYiG.js.map → MemoryMapTab-BU9iM3ir.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{MetricsTab-CHblYuTY.js → MetricsTab-lMVodOwY.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{MetricsTab-CHblYuTY.js.map → MetricsTab-lMVodOwY.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{OverviewTab-Dj0TgFvU.js → OverviewTab-D-laa6CW.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{OverviewTab-Dj0TgFvU.js.map → OverviewTab-D-laa6CW.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{ReposTab-BTJcfc1W.js → ReposTab-5Pn2cOOO.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{ReposTab-BTJcfc1W.js.map → ReposTab-5Pn2cOOO.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{SessionsTab-C8tU8K5u.js → SessionsTab-Dtdmvl2C.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{SessionsTab-C8tU8K5u.js.map → SessionsTab-Dtdmvl2C.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{SetupTab-BdxGMo6O.js → SetupTab-rKGgWpI0.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{SetupTab-BdxGMo6O.js.map → SetupTab-rKGgWpI0.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BWZSARzT.js → TimeSavedCard-B5Lw6fTp.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BWZSARzT.js.map → TimeSavedCard-B5Lw6fTp.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{TopicsTab-BCJwPYl8.js → TopicsTab-Ds1IHhPw.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{TopicsTab-BCJwPYl8.js.map → TopicsTab-Ds1IHhPw.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{TurnsTab-DQFTSDpz.js → TurnsTab-CG2RDRmh.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{TurnsTab-DQFTSDpz.js.map → TurnsTab-CG2RDRmh.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/health-lSeDFTz7.js +2 -0
  51. package/extensions/dashboard-client/dist/assets/health-lSeDFTz7.js.map +1 -0
  52. package/extensions/dashboard-client/dist/assets/{index-DrrDswj-.js → index-C9dTXvDQ.js} +11 -11
  53. package/extensions/dashboard-client/dist/assets/{index-DrrDswj-.js.map → index-C9dTXvDQ.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{useSSE-o4tteeTs.js → useSSE-B6l8O-yi.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{useSSE-o4tteeTs.js.map → useSSE-B6l8O-yi.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/index.html +1 -1
  57. package/extensions/dashboard-client/src/App.tsx +5 -1
  58. package/extensions/dashboard-client/src/api/health.ts +80 -0
  59. package/extensions/dashboard-client/src/tabs/HealthTab.tsx +165 -0
  60. package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +91 -1
  61. package/extensions/dashboard-server/routes-health.ts +155 -0
  62. package/extensions/dashboard-server/routes.ts +1 -0
  63. package/extensions/dashboard-server/server.ts +6 -0
  64. package/extensions/mega-config.ts +15 -0
  65. package/extensions/mega-events/agent-handlers.ts +31 -0
  66. package/extensions/mega-events/health-handler.ts +218 -0
  67. package/extensions/mega-runtime/runtime.ts +7 -0
  68. package/package.json +1 -1
  69. package/src/contextHealth/cachePoison.ts +243 -0
  70. package/src/contextHealth/drift.ts +107 -0
  71. package/src/contextHealth/outputQuality.ts +139 -0
  72. package/src/contextHealth.ts +191 -0
  73. package/src/store/sqlite/context-health.ts +342 -0
  74. package/src/store/sqlite/meta.ts +16 -0
  75. package/src/store/sqlite/schema.ts +43 -1
  76. package/src/store/sqlite.ts +1 -0
  77. package/extensions/dashboard-client/dist/assets/MaintenanceTab-DNZ1IF4t.js +0 -2
  78. package/extensions/dashboard-client/dist/assets/MaintenanceTab-DNZ1IF4t.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import{r as e}from"./index-DrrDswj-.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-o4tteeTs.js.map
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
@@ -1 +1 @@
1
- {"version":3,"file":"useSSE-o4tteeTs.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"}
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-DrrDswj-.js"></script>
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&apos;t full.</li>
589
+ <li><strong>Prefix break</strong> — when KV cache poison is detected
590
+ (score &lt; 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&apos;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),