pi-mega-compact 0.11.8 → 0.11.9

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 (63) hide show
  1. package/dist/extensions/dashboard-server/gather-debug-keys.js +18 -0
  2. package/dist/extensions/dashboard-server/gather-debug.js +129 -0
  3. package/dist/extensions/dashboard-server/routes-maintenance.js +6 -0
  4. package/dist/extensions/mega-config.js +1 -0
  5. package/dist/extensions/mega-events/agent-handlers.js +74 -5
  6. package/dist/extensions/mega-events/context-handler.js +67 -3
  7. package/dist/extensions/mega-runtime/snapshot.js +1 -1
  8. package/dist/extensions/mega-trim.js +10 -0
  9. package/extensions/dashboard-client/dist/assets/{AchievementTiles-BESF1HT1.js → AchievementTiles-DFitrb4Y.js} +2 -2
  10. package/extensions/dashboard-client/dist/assets/{AchievementTiles-BESF1HT1.js.map → AchievementTiles-DFitrb4Y.js.map} +1 -1
  11. package/extensions/dashboard-client/dist/assets/{AchievementsTab-8RzNnz8u.js → AchievementsTab-PJrmq8pa.js} +2 -2
  12. package/extensions/dashboard-client/dist/assets/{AchievementsTab-8RzNnz8u.js.map → AchievementsTab-PJrmq8pa.js.map} +1 -1
  13. package/extensions/dashboard-client/dist/assets/CacheTab-C1WE7dEx.js +2 -0
  14. package/extensions/dashboard-client/dist/assets/CacheTab-C1WE7dEx.js.map +1 -0
  15. package/extensions/dashboard-client/dist/assets/{ConfigTab-D85fB9HP.js → ConfigTab-Dx-RpO8C.js} +2 -2
  16. package/extensions/dashboard-client/dist/assets/{ConfigTab-D85fB9HP.js.map → ConfigTab-Dx-RpO8C.js.map} +1 -1
  17. package/extensions/dashboard-client/dist/assets/{EventsTab-TCLR81G5.js → EventsTab-C63BEWEs.js} +2 -2
  18. package/extensions/dashboard-client/dist/assets/{EventsTab-TCLR81G5.js.map → EventsTab-C63BEWEs.js.map} +1 -1
  19. package/extensions/dashboard-client/dist/assets/{GameTab-BqneNRYH.js → GameTab-DHpJE6km.js} +2 -2
  20. package/extensions/dashboard-client/dist/assets/{GameTab-BqneNRYH.js.map → GameTab-DHpJE6km.js.map} +1 -1
  21. package/extensions/dashboard-client/dist/assets/MaintenanceTab-BSv0GfBb.js +2 -0
  22. package/extensions/dashboard-client/dist/assets/MaintenanceTab-BSv0GfBb.js.map +1 -0
  23. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CpjMQwRr.js → MemoryMapTab-Cg1fgEx_.js} +2 -2
  24. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CpjMQwRr.js.map → MemoryMapTab-Cg1fgEx_.js.map} +1 -1
  25. package/extensions/dashboard-client/dist/assets/{MetricsTab-LxNtNzLQ.js → MetricsTab-DCahxNOQ.js} +2 -2
  26. package/extensions/dashboard-client/dist/assets/{MetricsTab-LxNtNzLQ.js.map → MetricsTab-DCahxNOQ.js.map} +1 -1
  27. package/extensions/dashboard-client/dist/assets/{OverviewTab-BXKUZbT6.js → OverviewTab-RYvO1SZL.js} +2 -2
  28. package/extensions/dashboard-client/dist/assets/{OverviewTab-BXKUZbT6.js.map → OverviewTab-RYvO1SZL.js.map} +1 -1
  29. package/extensions/dashboard-client/dist/assets/{ReposTab-DTooKeAi.js → ReposTab-BDGr_UXb.js} +2 -2
  30. package/extensions/dashboard-client/dist/assets/{ReposTab-DTooKeAi.js.map → ReposTab-BDGr_UXb.js.map} +1 -1
  31. package/extensions/dashboard-client/dist/assets/{SessionsTab-Devulgm9.js → SessionsTab-ByrTnHaB.js} +2 -2
  32. package/extensions/dashboard-client/dist/assets/{SessionsTab-Devulgm9.js.map → SessionsTab-ByrTnHaB.js.map} +1 -1
  33. package/extensions/dashboard-client/dist/assets/{SetupTab-By9eqUoE.js → SetupTab-CELafKis.js} +2 -2
  34. package/extensions/dashboard-client/dist/assets/{SetupTab-By9eqUoE.js.map → SetupTab-CELafKis.js.map} +1 -1
  35. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-z4M8TDLm.js → TimeSavedCard-BLWtmfr_.js} +2 -2
  36. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-z4M8TDLm.js.map → TimeSavedCard-BLWtmfr_.js.map} +1 -1
  37. package/extensions/dashboard-client/dist/assets/{TopicsTab-bCGZSfLN.js → TopicsTab-8Fsh_Igx.js} +2 -2
  38. package/extensions/dashboard-client/dist/assets/{TopicsTab-bCGZSfLN.js.map → TopicsTab-8Fsh_Igx.js.map} +1 -1
  39. package/extensions/dashboard-client/dist/assets/{TurnsTab-m_XzE0H4.js → TurnsTab-D7mgXDe2.js} +2 -2
  40. package/extensions/dashboard-client/dist/assets/{TurnsTab-m_XzE0H4.js.map → TurnsTab-D7mgXDe2.js.map} +1 -1
  41. package/extensions/dashboard-client/dist/assets/{index-PRkilPXw.js → index-h3jgEs6G.js} +10 -10
  42. package/extensions/dashboard-client/dist/assets/{index-PRkilPXw.js.map → index-h3jgEs6G.js.map} +1 -1
  43. package/extensions/dashboard-client/dist/assets/{useSSE-CGzgc2IK.js → useSSE-C5lmipBF.js} +2 -2
  44. package/extensions/dashboard-client/dist/assets/{useSSE-CGzgc2IK.js.map → useSSE-C5lmipBF.js.map} +1 -1
  45. package/extensions/dashboard-client/dist/index.html +1 -1
  46. package/extensions/dashboard-client/src/api/client.ts +5 -0
  47. package/extensions/dashboard-client/src/tabs/CacheTab.tsx +29 -2
  48. package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +101 -0
  49. package/extensions/dashboard-server/api-contracts/index.ts +1 -0
  50. package/extensions/dashboard-server/api-contracts/maintenance.ts +13 -0
  51. package/extensions/dashboard-server/gather-debug-keys.ts +18 -0
  52. package/extensions/dashboard-server/gather-debug.ts +136 -0
  53. package/extensions/dashboard-server/routes-maintenance.ts +7 -0
  54. package/extensions/mega-config.ts +4 -0
  55. package/extensions/mega-events/agent-handlers.ts +88 -5
  56. package/extensions/mega-events/context-handler.ts +72 -3
  57. package/extensions/mega-runtime/snapshot.ts +1 -1
  58. package/extensions/mega-trim.ts +15 -0
  59. package/package.json +1 -1
  60. package/extensions/dashboard-client/dist/assets/CacheTab-CUIiUgh3.js +0 -2
  61. package/extensions/dashboard-client/dist/assets/CacheTab-CUIiUgh3.js.map +0 -1
  62. package/extensions/dashboard-client/dist/assets/MaintenanceTab-LblMg3NB.js +0 -2
  63. package/extensions/dashboard-client/dist/assets/MaintenanceTab-LblMg3NB.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import{r as e}from"./index-PRkilPXw.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-CGzgc2IK.js.map
1
+ import{r as e}from"./index-h3jgEs6G.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-C5lmipBF.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useSSE-CGzgc2IK.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-C5lmipBF.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-PRkilPXw.js"></script>
7
+ <script type="module" crossorigin src="./assets/index-h3jgEs6G.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="./assets/index-CUNn22iV.css">
9
9
  </head>
10
10
  <body>
@@ -39,6 +39,7 @@ import type {
39
39
  SchemaHealthResponse,
40
40
  MaintenanceAction,
41
41
  MaintenanceActionResult,
42
+ DebugBundleResponse,
42
43
  ProviderCacheResponse,
43
44
  CacheStripesResponse,
44
45
  SetupStatusResponse,
@@ -284,6 +285,10 @@ export function postMaintenanceAction(
284
285
  );
285
286
  }
286
287
 
288
+ export function fetchDebugBundle(): Promise<DebugBundleResponse> {
289
+ return postJson<DebugBundleResponse>("/api/maintenance/gather-debug", {});
290
+ }
291
+
287
292
  // ─── Setup wizard (P0b) ────────────────────────────────────────────────────
288
293
 
289
294
  /** GET /api/setup-status — current embedder configuration. */
@@ -11,8 +11,7 @@
11
11
  * 5s polling on provider cache and activity snapshot; 15s on stripes + trend.
12
12
  */
13
13
 
14
- import type React from "react";
15
- import { useCallback } from "react";
14
+ import { useState, useCallback } from "react";
16
15
  import type {
17
16
  SnapshotResponse,
18
17
  ProviderCacheResponse,
@@ -33,6 +32,7 @@ import { StripeDistributionCard } from "../components/StripeDistributionCard";
33
32
  import { CacheHitRateTrendCard } from "../components/CacheHitRateTrendCard";
34
33
 
35
34
  export default function CacheTab(): React.ReactElement {
35
+ const [infoExpanded, setInfoExpanded] = useState(false);
36
36
  /* --- Provider prompt cache (5s poll) --- */
37
37
  const {
38
38
  data: providerCache,
@@ -75,6 +75,33 @@ export default function CacheTab(): React.ReactElement {
75
75
 
76
76
  return (
77
77
  <div className="cache-tab">
78
+ <div className="card-section">
79
+ <button
80
+ type="button"
81
+ className="info-toggle"
82
+ onClick={() => setInfoExpanded(!infoExpanded)}
83
+ >
84
+ {infoExpanded ? "Hide Guidance" : "Show Cache-Friendly Prompt Ordering Guidance"}
85
+ </button>
86
+ {infoExpanded && (
87
+ <div className="info-panel">
88
+ <h3>Cache-Friendly Prompt Ordering</h3>
89
+ <ul>
90
+ <li>Keep system prompts and stable context at the top of your conversation — the provider caches the leading prefix.</li>
91
+ <li>Tool results and volatile content are automatically moved to the tail by message separation (<code>MEGACOMPACT_MESSAGE_SEPARATION</code>) so they don't invalidate the cache prefix.</li>
92
+ <li>Cache striping (<code>MEGACOMPACT_CACHE_STRIPING</code>) further orders stable context by a stability score so the most durable chunks lead.</li>
93
+ <li>Avoid inserting new instructions mid-conversation — prepend them instead.</li>
94
+ </ul>
95
+ {snapshot && (
96
+ <div className="cache-status-indicator">
97
+ <strong>Status:</strong>{" "}
98
+ {snapshot.config.messageSeparation ? "Separation Enabled" : "Separation Disabled (Check env)"} |{" "}
99
+ {snapshot.config.cacheStriping ? "Striping Enabled" : "Striping Disabled (Check env)"}
100
+ </div>
101
+ )}
102
+ </div>
103
+ )}
104
+ </div>
78
105
  {/* ==============================================================
79
106
  Section 1 -- Provider Prompt Cache
80
107
  ============================================================== */}
@@ -16,12 +16,14 @@ import type {
16
16
  SchemaHealthResponse,
17
17
  MaintenanceAction,
18
18
  MaintenanceActionResult,
19
+ DebugBundleResponse,
19
20
  } from "@contracts";
20
21
  import { useApi } from "../hooks/useApi";
21
22
  import {
22
23
  fetchDbStats,
23
24
  fetchSchemaHealth,
24
25
  postMaintenanceAction,
26
+ fetchDebugBundle,
25
27
  } from "../api/client";
26
28
 
27
29
  // ---------------------------------------------------------------------------
@@ -419,6 +421,104 @@ function ActionsCard(): React.ReactElement {
419
421
  );
420
422
  }
421
423
 
424
+ // ---------------------------------------------------------------------------
425
+ // Debug bundle card — gather diagnostic info for bug reports
426
+ // ---------------------------------------------------------------------------
427
+
428
+ function DebugBundleCard(): React.ReactElement {
429
+ const [bundle, setBundle] = useState<DebugBundleResponse | null>(null);
430
+ const [loading, setLoading] = useState(false);
431
+ const [error, setError] = useState<string | null>(null);
432
+ const [showJson, setShowJson] = useState(false);
433
+
434
+ const gather = useCallback(async () => {
435
+ setLoading(true);
436
+ setError(null);
437
+ try {
438
+ const res = await fetchDebugBundle();
439
+ setBundle(res);
440
+ setShowJson(true);
441
+ } catch (err) {
442
+ setError(err instanceof Error ? err.message : String(err));
443
+ } finally {
444
+ setLoading(false);
445
+ }
446
+ }, []);
447
+
448
+ const copyToClipboard = useCallback(() => {
449
+ if (!bundle) return;
450
+ const text = JSON.stringify(bundle, null, 2);
451
+ void navigator.clipboard.writeText(text).catch(() => {});
452
+ }, [bundle]);
453
+
454
+ const downloadJson = useCallback(() => {
455
+ if (!bundle) return;
456
+ const text = JSON.stringify(bundle, null, 2);
457
+ const blob = new Blob([text], { type: "application/json" });
458
+ const url = URL.createObjectURL(blob);
459
+ const a = document.createElement("a");
460
+ a.href = url;
461
+ a.download = `mega-compact-debug-bundle-${bundle.builtAt}.json`;
462
+ a.click();
463
+ URL.revokeObjectURL(url);
464
+ }, [bundle]);
465
+
466
+ const criticalCount = bundle?.criticalEvents.length ?? 0;
467
+
468
+ return (
469
+ <div className="card">
470
+ <div className="card-header">
471
+ Debug Bundle
472
+ {criticalCount > 0 && (
473
+ <span className="text-warn" style={{ marginLeft: "1em" }}>
474
+ {criticalCount} critical event{criticalCount !== 1 ? "s" : ""} found
475
+ </span>
476
+ )}
477
+ </div>
478
+ <div className="card-body">
479
+ <p className="text-muted" style={{ marginBottom: "0.75em" }}>
480
+ Gather a diagnostic bundle (recent events, config flags, schema health,
481
+ store stats) to attach to a bug report. Critical/compaction events are
482
+ highlighted.
483
+ </p>
484
+ <button onClick={gather} disabled={loading} style={{ marginRight: "0.5em" }}>
485
+ {loading ? "Gathering…" : "Gather Debug Logs"}
486
+ </button>
487
+ {error && <div className="text-error">{error}</div>}
488
+ {bundle && showJson && (
489
+ <>
490
+ <div style={{ marginTop: "0.75em", marginBottom: "0.5em" }}>
491
+ <button onClick={copyToClipboard} style={{ marginRight: "0.5em" }}>
492
+ Copy to clipboard
493
+ </button>
494
+ <button onClick={downloadJson}>Download JSON</button>
495
+ <button
496
+ onClick={() => setShowJson((v) => !v)}
497
+ style={{ marginLeft: "0.5em" }}
498
+ >
499
+ {showJson ? "Hide" : "Show"}
500
+ </button>
501
+ </div>
502
+ {criticalCount > 0 && (
503
+ <div className="stat-section">
504
+ <div className="stat-label text-warn">
505
+ Critical events ({criticalCount})
506
+ </div>
507
+ <pre className="text-mono" style={{ maxHeight: "12em", overflow: "auto" }}>
508
+ {JSON.stringify(bundle.criticalEvents, null, 2)}
509
+ </pre>
510
+ </div>
511
+ )}
512
+ <pre className="text-mono" style={{ maxHeight: "24em", overflow: "auto" }}>
513
+ {JSON.stringify(bundle, null, 2)}
514
+ </pre>
515
+ </>
516
+ )}
517
+ </div>
518
+ </div>
519
+ );
520
+ }
521
+
422
522
  // ---------------------------------------------------------------------------
423
523
  // Main tab component
424
524
  // ---------------------------------------------------------------------------
@@ -453,6 +553,7 @@ export default function MaintenanceTab(): React.ReactElement {
453
553
  <DbStatsCard data={dbStats} loading={statsLoading} error={statsError} />
454
554
  </div>
455
555
  <ActionsCard />
556
+ <DebugBundleCard />
456
557
  </div>
457
558
  );
458
559
  }
@@ -186,6 +186,7 @@ export type {
186
186
  SchemaHealthRow,
187
187
  SchemaHealthResponse,
188
188
  MaintenanceAction,
189
+ DebugBundleResponse,
189
190
  } from "./maintenance.js";
190
191
 
191
192
  // S52 — turn-by-turn memory tracking + recall + rewind
@@ -82,3 +82,16 @@ export type MaintenanceAction =
82
82
  | { action: "reconcile-dedup" }
83
83
  | { action: "prune"; daysOld: number }
84
84
  | { action: "integrity-check" };
85
+
86
+ /** Debug bundle for gathering diagnostic info. */
87
+ export interface DebugBundleResponse {
88
+ builtAt: number;
89
+ version: string;
90
+ stateDir: string;
91
+ config: Record<string, string>;
92
+ schemaHealth: SchemaHealthResponse | { error: string };
93
+ storeStats: DbStatsResponse | { error: string };
94
+ dashboardSnapshot: unknown | { error: string };
95
+ recentEvents: unknown[];
96
+ criticalEvents: unknown[];
97
+ }
@@ -0,0 +1,18 @@
1
+ export const MEGACOMPACT_ENV_KEYS = [
2
+ "MEGACOMPACT_AUTO",
3
+ "MEGACOMPACT_THRESHOLD_TOKENS",
4
+ "MEGACOMPACT_TIER",
5
+ "MEGACOMPACT_DBMIRROR",
6
+ "MEGACOMPACT_MESSAGE_SEPARATION",
7
+ "MEGACOMPACT_CACHE_STRIPING",
8
+ "MEGACOMPACT_LEGACY_DURABLE_TRIM",
9
+ "MEGACOMPACT_RACE_GUARD_STRICT",
10
+ "MEGACOMPACT_RECALL_TAIL_INJECT",
11
+ "MEGACOMPACT_PGLITE_DISABLED",
12
+ "MEGACOMPACT_WIKI_REBUILD_EVERY",
13
+ "MEGACOMPACT_WIKI_SEED_FROM_TURNS",
14
+ "MEGACOMPACT_MEMORY_GRAPH_SEED_TURNS",
15
+ "MEGACOMPACT_MEMORY_GRAPH_SEED_TIER",
16
+ "MEGACOMPACT_MEMORY_GRAPH_SEED_BATCH",
17
+ "MEGACOMPACT_MEMORY_GRAPH_SEED_MAX",
18
+ ];
@@ -0,0 +1,136 @@
1
+ /**
2
+ * gather-debug.ts — assemble a diagnostic bundle for bug reports.
3
+ *
4
+ * POST /api/maintenance/gather-debug → DebugBundleResponse. Reads local
5
+ * SQLite + FS only (PREVENT-PI-004: zero network). Each section is non-fatal:
6
+ * a read failure records `{ error }` for that section and continues, so the
7
+ * bundle always returns 200 with whatever could be gathered.
8
+ */
9
+ import { readFileSync, existsSync } from "node:fs";
10
+ import { createRequire } from "node:module";
11
+ import type { ServerResponse } from "node:http";
12
+ import type { RouteContext } from "./routes-core.js";
13
+ import type { DebugBundleResponse } from "./api-contracts/maintenance.js";
14
+ import { defaultMetricsPath, defaultEventsPath } from "../../src/monitoring.js";
15
+ import { ownVersion } from "../mega-runtime/helpers.js";
16
+ import { MEGACOMPACT_ENV_KEYS } from "./gather-debug-keys.js";
17
+
18
+ /** Event-name patterns that indicate a critical/compaction class of error. */
19
+ const CRITICAL_PATTERN =
20
+ /critical|overflow|already[.\s-]?compacted|compaction[.\s-]?failed|stream[.\s-]?death|poisoned|prefix_stability/i;
21
+
22
+ const require = createRequire(import.meta.url);
23
+
24
+ /** Lazy store opener (mirrors routes-maintenance.ts's lazyStoreUtils). */
25
+ function openStoreDb(ctx: RouteContext) {
26
+ const mod = require("../../src/store/sqlite/utils.js") as typeof import("../../src/store/sqlite/utils.js");
27
+ return mod.openStore(ctx.stateDir);
28
+ }
29
+
30
+ /** Lazy maintenance loaders (mirrors routes-maintenance.ts's lazyMaintenance). */
31
+ function loadMaintenance() {
32
+ return require("../../src/store/sqlite/maintenance.js") as typeof import("../../src/store/sqlite/maintenance.js");
33
+ }
34
+
35
+ /** Send the debug bundle as JSON. Always 200 (partial on read failure). */
36
+ export function sendGatherDebug(res: ServerResponse, ctx: RouteContext): void {
37
+ const result: DebugBundleResponse = {
38
+ builtAt: Date.now(),
39
+ version: ownVersion(),
40
+ stateDir: ctx.stateDir,
41
+ config: {},
42
+ schemaHealth: { error: "not fetched" },
43
+ storeStats: { error: "not fetched" },
44
+ dashboardSnapshot: { error: "not fetched" },
45
+ recentEvents: [],
46
+ criticalEvents: [],
47
+ };
48
+
49
+ // 1. Config flags (no secrets — MEGACOMPACT_* only)
50
+ for (const key of MEGACOMPACT_ENV_KEYS) {
51
+ result.config[key] = process.env[key] ?? "<default>";
52
+ }
53
+
54
+ // 2. Schema health
55
+ try {
56
+ const maint = loadMaintenance();
57
+ const integrity = maint.integrityCheck(ctx.stateDir);
58
+ const db = openStoreDb(ctx);
59
+ const metaRow = db
60
+ .prepare("SELECT value FROM meta WHERE key = ?")
61
+ .get("schema_version") as { value: string } | undefined;
62
+ const schemaVersion = metaRow ? Number(metaRow.value) : 0;
63
+ const fkRows = db.prepare("PRAGMA foreign_key_check").all() as Array<Record<string, unknown>>;
64
+ result.schemaHealth = {
65
+ type: "schema-health" as const,
66
+ schemaVersion,
67
+ integrity,
68
+ fkCheck: fkRows.map((r) => JSON.stringify(r)),
69
+ columns: [],
70
+ healthy: true,
71
+ };
72
+ } catch (e) {
73
+ result.schemaHealth = { error: e instanceof Error ? e.message : String(e) };
74
+ }
75
+
76
+ // 3. Store stats
77
+ try {
78
+ const maint = loadMaintenance();
79
+ const raw = maint.getDbStats(ctx.stateDir);
80
+ result.storeStats = {
81
+ type: "db-stats" as const,
82
+ tables: Object.entries(raw.tableCounts).map(([table, rowCount]) => ({ table, rowCount })),
83
+ storage: {
84
+ files: { dbBytes: raw.dbBytes, walBytes: raw.walBytes, shmBytes: raw.shmBytes },
85
+ pageSize: raw.pageSize,
86
+ pageCount: raw.pageCount,
87
+ freelistPages: raw.freelistPages,
88
+ },
89
+ };
90
+ } catch (e) {
91
+ result.storeStats = { error: e instanceof Error ? e.message : String(e) };
92
+ }
93
+
94
+ // 4. Dashboard snapshot
95
+ try {
96
+ const path = defaultMetricsPath(ctx.stateDir);
97
+ if (existsSync(path)) {
98
+ const raw = readFileSync(path, "utf-8");
99
+ result.dashboardSnapshot = JSON.parse(raw) as unknown;
100
+ } else {
101
+ result.dashboardSnapshot = { error: "no snapshot" };
102
+ }
103
+ } catch (e) {
104
+ result.dashboardSnapshot = { error: e instanceof Error ? e.message : String(e) };
105
+ }
106
+
107
+ // 5. Recent + critical events (tail ~200 lines of events.log)
108
+ try {
109
+ const path = defaultEventsPath(ctx.stateDir);
110
+ if (existsSync(path)) {
111
+ const lines = readFileSync(path, "utf-8").split("\n").filter(Boolean);
112
+ for (const line of lines.slice(-200)) {
113
+ try {
114
+ const ev = JSON.parse(line) as unknown;
115
+ result.recentEvents.push(ev);
116
+ if (CRITICAL_PATTERN.test(line)) result.criticalEvents.push(ev);
117
+ } catch {
118
+ continue; // skip unparseable line (PREVENT-001)
119
+ }
120
+ }
121
+ }
122
+ } catch (e) {
123
+ result.recentEvents = [{ error: e instanceof Error ? e.message : String(e) }];
124
+ }
125
+
126
+ // Send (local helper to avoid importing sendJson from a non-exporting module)
127
+ try {
128
+ const body = JSON.stringify(result);
129
+ res.writeHead(200, { "Content-Type": "application/json" }); // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
130
+ res.end(body);
131
+ } catch {
132
+ // last resort — never leave the response hanging
133
+ res.writeHead(500, { "Content-Type": "application/json" }); // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
134
+ res.end('{"error":"failed to serialize debug bundle"}');
135
+ }
136
+ }
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { createRequire } from "node:module";
15
15
  import type { IncomingMessage, ServerResponse } from "node:http";
16
+ import { sendGatherDebug } from "./gather-debug.js";
16
17
 
17
18
  import type { RouteContext } from "./routes-core.js";
18
19
 
@@ -77,6 +78,12 @@ export function handleMaintenance(
77
78
  return true;
78
79
  }
79
80
 
81
+ // ── POST /api/maintenance/gather-debug ──────────────────────────
82
+ if (method === "POST" && url === "/api/maintenance/gather-debug") {
83
+ sendGatherDebug(res, ctx);
84
+ return true;
85
+ }
86
+
80
87
  // ── GET /api/maintenance ──────────────────────────────────────────
81
88
  if (method === "GET" && url === "/api/maintenance") {
82
89
  sendDbStats(res, ctx);
@@ -172,6 +172,9 @@ export interface MegaConfig {
172
172
  * from volatile tool results so the prompt-cache prefix stays stable.
173
173
  * Default OFF (flag-OFF = byte-identical pre-sprint). */
174
174
  messageSeparation: boolean;
175
+ /** P3: Cache-aware striping (PLAN_V2 Phase 3). Inserts stability-ordered
176
+ * cache stripes between summaries and thread. Default OFF. */
177
+ cacheStriping: boolean;
175
178
  debug: boolean;
176
179
  /** Master reconciliation: TUI shutdown widget (MEGACOMPACT_TUI_WIDGET=0 to disable). */
177
180
  tuiWidget: boolean;
@@ -357,6 +360,7 @@ export function loadConfig(): MegaConfig {
357
360
  windowDedupe: envBool("MEGACOMPACT_WINDOW_DEDUPE", true),
358
361
  recallTailInject: envBool("MEGACOMPACT_RECALL_TAIL_INJECT", true),
359
362
  messageSeparation: envBool("MEGACOMPACT_MESSAGE_SEPARATION", false),
363
+ cacheStriping: envBool("MEGACOMPACT_CACHE_STRIPING", false),
360
364
  tuiWidget: envBool("MEGACOMPACT_TUI_WIDGET", true),
361
365
  ragQueryReformulation: envBool("MEGACOMPACT_QUERY_REFORMULATION", false),
362
366
  ragTieredRouter: envBool("MEGACOMPACT_TIERED_ROUTER", false),
@@ -26,6 +26,15 @@ import {
26
26
  import { safeSendUserMessage } from "./send-safe.js";
27
27
  import { maybeSendProviderOutageAdvisory } from "./outage-advisor.js";
28
28
  import { vectorStats } from "../../src/vectorStore.js";
29
+ import { defaultEmbedder } from "../../src/embedder.js";
30
+ import {
31
+ storeTopicEmbedding,
32
+ loadTopicEmbedding,
33
+ detectTopicShift,
34
+ } from "./separated-prompt.js";
35
+ import {
36
+ refreshStripeAssignments as writeStripeAssignments,
37
+ } from "../../src/cache-stripe-impl.js";
29
38
 
30
39
  /** Register agent/turn tracking event handlers. */
31
40
  export function registerAgentHandlers(
@@ -167,9 +176,20 @@ export function registerAgentHandlers(
167
176
  // context-handler.ts:258-287 so both call sites stay in sync.
168
177
  const cooldownMs = config.raceGuardStrict ? 30_000 : 10_000;
169
178
  const sinceCompact = now - (runtime.rt.lastNativeCompactAt ?? 0);
179
+ // CRITICAL-OVER ESCAPE HATCH: when context is critically over the
180
+ // window (>= 90%), force the durable ctx.compact() even if
181
+ // piCompactWouldNoop says it would no-op ("Already compacted").
182
+ // Without this, the on-disk transcript never truncates and the
183
+ // session stays overflowed on every resume — the "Already
184
+ // compacted" + overflow death-spiral (2026-08-01 incident). The
185
+ // cooldown still applies (we don't spam compact), but the no-op
186
+ // gate is bypassed when critical. pi's ctx.compact() may throw
187
+ // "Already compacted" in the truly-no-op case — wrapped in
188
+ // try/catch so the throw is non-fatal.
189
+ const criticalOver = (runtime.lastCtxPercent ?? 0) >= 90;
170
190
  if (sinceCompact < cooldownMs) {
171
191
  runtime.diagAgentEndDurableSkipRecent++;
172
- } else if (!piCompactWouldNoop(ctx)) {
192
+ } else if (criticalOver || !piCompactWouldNoop(ctx)) {
173
193
  runtime.debounceUntil = now + 2000;
174
194
  runtime.diagAgentEndDurable++;
175
195
  runtime.logger.info("agent-end-durable-trigger", {
@@ -186,6 +206,7 @@ export function registerAgentHandlers(
186
206
  // would re-check before it lands.
187
207
  const stamp = runtime.rt.lastNativeCompactAt;
188
208
  const liveSid = runtime.rt.sessionId;
209
+ const liveCritical = criticalOver;
189
210
  setTimeout(() => {
190
211
  try {
191
212
  if (runtime.rt.sessionId !== liveSid) return; // session reset
@@ -198,16 +219,22 @@ export function registerAgentHandlers(
198
219
  since2 < cooldownMs
199
220
  )
200
221
  return;
201
- if (piCompactWouldNoop(ctx)) return;
222
+ // CRITICAL-OVER ESCAPE HATCH: bypass the no-op gate when
223
+ // critical (captured at handler time) so the durable trim fires.
224
+ if (!liveCritical && piCompactWouldNoop(ctx)) return;
202
225
  ctx.compact({
203
226
  customInstructions: undefined,
204
- }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; deferred + re-validated.
227
+ }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; deferred + re-validated. May throw "Already compacted" when the escape hatch forces it — caught below.
205
228
  } catch {
206
- /* non-fatal */
229
+ /* non-fatal: "Already compacted" throws are expected when the critical-over hatch forces a no-op compact */
207
230
  }
208
231
  }, 500);
209
232
  } else {
210
- ctx.compact({ customInstructions: undefined }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; agent settled so no in-flight abort. Race-guarded by lastNativeCompactAt cooldown above (ctx.compact returns void → throw is surfaced by pi as compaction_end; the cooldown prevents the call entirely).
233
+ try {
234
+ ctx.compact({ customInstructions: undefined }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; agent settled so no in-flight abort. May throw "Already compacted" when the critical-over escape hatch forces it — caught.
235
+ } catch {
236
+ /* non-fatal */
237
+ }
211
238
  }
212
239
  didDurableTrim = true;
213
240
  }
@@ -777,5 +804,61 @@ export function registerAgentHandlers(
777
804
  } catch {
778
805
  /* non-fatal: a classifier/retry failure never breaks the agent loop */
779
806
  }
807
+
808
+ // P3.5: topic-shift stripe refresh. When MEGACOMPACT_CACHE_STRIPING is ON,
809
+ // embed the current turn, detect topic shift vs. the previous turn, and
810
+ // refresh cache stripe assignments when shifted (or first turn). Best-effort:
811
+ // failures log + continue, never break the agent loop.
812
+ if (config.cacheStriping) {
813
+ try {
814
+ const stateDir = runtime.currentStateDir;
815
+ const sessionId = runtime.rt.sessionId;
816
+ const currentTurn = event.turnIndex;
817
+ const embedder = defaultEmbedder();
818
+
819
+ // (a) Extract text from the assistant's response for topic embedding.
820
+ let textToEmbed = "";
821
+ const msg = event.message;
822
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
823
+ for (const part of msg.content) {
824
+ if ("text" in part && typeof part.text === "string") {
825
+ textToEmbed += part.text + " ";
826
+ }
827
+ }
828
+ }
829
+ textToEmbed = textToEmbed.trim() || `turn-${currentTurn}`;
830
+ const embedding = new Float32Array(embedder.embed(textToEmbed));
831
+ storeTopicEmbedding(stateDir, sessionId, currentTurn, embedding);
832
+
833
+ // (b) Load previous turn's embedding and detect topic shift.
834
+ const prevEmb = currentTurn > 0
835
+ ? loadTopicEmbedding(stateDir, sessionId, currentTurn - 1)
836
+ : null;
837
+ const shifted = currentTurn === 0 || detectTopicShift(embedding, prevEmb);
838
+
839
+ // (c) If shifted (or first turn), refresh stripe assignments (WRITE path).
840
+ let chunkCount = 0;
841
+ if (shifted) {
842
+ chunkCount = writeStripeAssignments(
843
+ stateDir,
844
+ undefined,
845
+ embedder,
846
+ (detail) => runtime.logger.info("stripe_refresh_detail", {
847
+ detail,
848
+ turnIndex: currentTurn,
849
+ }),
850
+ );
851
+ }
852
+
853
+ // (d) Log the refresh outcome.
854
+ runtime.logger.info("stripe_refresh", {
855
+ turnIndex: currentTurn,
856
+ shifted,
857
+ chunkCount,
858
+ });
859
+ } catch {
860
+ /* non-fatal: stripe refresh never breaks the agent loop */
861
+ }
862
+ }
780
863
  });
781
864
  }