pi-mega-compact 0.12.0 → 0.12.2

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 (75) hide show
  1. package/dist/extensions/dashboard-server/gather-debug.js +63 -0
  2. package/dist/extensions/dashboard-server/routes-health.js +44 -0
  3. package/dist/extensions/dashboard-server/routes-setup.js +30 -4
  4. package/dist/extensions/dashboard-server/routes.js +1 -1
  5. package/dist/extensions/dashboard-server/server.js +3 -1
  6. package/dist/extensions/mega-events/agent-handlers.js +27 -1
  7. package/dist/extensions/mega-events/health-handler.js +19 -2
  8. package/dist/src/memoryGraph/sources.js +42 -15
  9. package/dist/src/memoryGraph.js +17 -5
  10. package/dist/src/store/sqlite/meta.js +11 -0
  11. package/dist/src/store/turns/schema.js +18 -2
  12. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DeQ0qnBX.js → AchievementTiles-Cntu89Xw.js} +2 -2
  13. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DeQ0qnBX.js.map → AchievementTiles-Cntu89Xw.js.map} +1 -1
  14. package/extensions/dashboard-client/dist/assets/{AchievementsTab-Dtk_RJfs.js → AchievementsTab-4ckY9z4n.js} +2 -2
  15. package/extensions/dashboard-client/dist/assets/{AchievementsTab-Dtk_RJfs.js.map → AchievementsTab-4ckY9z4n.js.map} +1 -1
  16. package/extensions/dashboard-client/dist/assets/{CacheTab-kWcQt3PI.js → CacheTab-BfGJkiaf.js} +2 -2
  17. package/extensions/dashboard-client/dist/assets/{CacheTab-kWcQt3PI.js.map → CacheTab-BfGJkiaf.js.map} +1 -1
  18. package/extensions/dashboard-client/dist/assets/{ConfigTab-UFlo73fR.js → ConfigTab-DV08rHoV.js} +2 -2
  19. package/extensions/dashboard-client/dist/assets/{ConfigTab-UFlo73fR.js.map → ConfigTab-DV08rHoV.js.map} +1 -1
  20. package/extensions/dashboard-client/dist/assets/{EventsTab-BENO2N0y.js → EventsTab-DHytZ0xs.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{EventsTab-BENO2N0y.js.map → EventsTab-DHytZ0xs.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{GameTab-BCJavOML.js → GameTab-Ofpoz-6s.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{GameTab-BCJavOML.js.map → GameTab-Ofpoz-6s.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/HealthTab-B0klAxD8.js +2 -0
  25. package/extensions/dashboard-client/dist/assets/HealthTab-B0klAxD8.js.map +1 -0
  26. package/extensions/dashboard-client/dist/assets/MaintenanceTab-CHKh3KPI.js +2 -0
  27. package/extensions/dashboard-client/dist/assets/MaintenanceTab-CHKh3KPI.js.map +1 -0
  28. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-w5BZP3i-.js → MemoryMapTab-B6FLQ-32.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-w5BZP3i-.js.map → MemoryMapTab-B6FLQ-32.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/{MetricsTab-BBCXa3xS.js → MetricsTab-DvOjTDkW.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{MetricsTab-BBCXa3xS.js.map → MetricsTab-DvOjTDkW.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{OverviewTab-CzABMSAQ.js → OverviewTab-CqbONEwi.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{OverviewTab-CzABMSAQ.js.map → OverviewTab-CqbONEwi.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{ReposTab-DFiM_Ubg.js → ReposTab-sc1GLn7i.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{ReposTab-DFiM_Ubg.js.map → ReposTab-sc1GLn7i.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{SessionsTab-BtW4bees.js → SessionsTab-C5gB45oc.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{SessionsTab-BtW4bees.js.map → SessionsTab-C5gB45oc.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/SetupTab-BEEX4OF5.js +2 -0
  39. package/extensions/dashboard-client/dist/assets/SetupTab-BEEX4OF5.js.map +1 -0
  40. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CC13EXij.js → TimeSavedCard-BBwvb4cC.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CC13EXij.js.map → TimeSavedCard-BBwvb4cC.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{TopicsTab-BrTB8uEK.js → TopicsTab-BsCf512_.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{TopicsTab-BrTB8uEK.js.map → TopicsTab-BsCf512_.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{TurnsTab-BJK7FIHu.js → TurnsTab-C3ti7NB5.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{TurnsTab-BJK7FIHu.js.map → TurnsTab-C3ti7NB5.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/health-lSeDFTz7.js +2 -0
  47. package/extensions/dashboard-client/dist/assets/health-lSeDFTz7.js.map +1 -0
  48. package/extensions/dashboard-client/dist/assets/{index-B8k19B9g.js → index-BEeaI9uo.js} +3 -3
  49. package/extensions/dashboard-client/dist/assets/{index-B8k19B9g.js.map → index-BEeaI9uo.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{useSSE-CWBB4Me7.js → useSSE-BMSSO9wB.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{useSSE-CWBB4Me7.js.map → useSSE-BMSSO9wB.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/index.html +1 -1
  53. package/extensions/dashboard-client/src/api/health.ts +16 -0
  54. package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +91 -1
  55. package/extensions/dashboard-client/src/tabs/SetupTab.tsx +19 -0
  56. package/extensions/dashboard-server/api-contracts/maintenance.ts +1 -0
  57. package/extensions/dashboard-server/api-contracts/setup.ts +7 -1
  58. package/extensions/dashboard-server/gather-debug.ts +63 -0
  59. package/extensions/dashboard-server/routes-health.ts +51 -0
  60. package/extensions/dashboard-server/routes-setup.ts +28 -4
  61. package/extensions/dashboard-server/routes.ts +1 -1
  62. package/extensions/dashboard-server/server.ts +2 -0
  63. package/extensions/mega-events/agent-handlers.ts +26 -1
  64. package/extensions/mega-events/health-handler.ts +31 -3
  65. package/package.json +1 -1
  66. package/src/memoryGraph/sources.ts +49 -15
  67. package/src/memoryGraph.ts +17 -5
  68. package/src/store/sqlite/meta.ts +16 -0
  69. package/src/store/turns/schema.ts +18 -2
  70. package/extensions/dashboard-client/dist/assets/HealthTab-C7iw7rhx.js +0 -2
  71. package/extensions/dashboard-client/dist/assets/HealthTab-C7iw7rhx.js.map +0 -1
  72. package/extensions/dashboard-client/dist/assets/MaintenanceTab-C0rdAMWc.js +0 -2
  73. package/extensions/dashboard-client/dist/assets/MaintenanceTab-C0rdAMWc.js.map +0 -1
  74. package/extensions/dashboard-client/dist/assets/SetupTab-Duy1LJhg.js +0 -2
  75. package/extensions/dashboard-client/dist/assets/SetupTab-Duy1LJhg.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import{r as e}from"./index-B8k19B9g.js";function R(i={}){const{maxEvents:c=500,maxReconnects:l=5}=i,[f,v]=e.useState([]),[E,n]=e.useState("connecting"),[S,p]=e.useState(0),[m,d]=e.useState(null),o=e.useRef(0),s=e.useRef(null),r=e.useCallback(()=>{s.current&&s.current.close(),n("connecting");const t=new EventSource("/api/events");s.current=t,t.onopen=()=>{n("connected"),o.current=0},t.onmessage=g=>{try{const x=JSON.parse(g.data);v(u=>{const a=[...u,x];return a.length>c?a.slice(-c):a}),p(u=>u+1),d(Date.now())}catch{}},t.onerror=()=>{t.close(),o.current<l?(o.current+=1,n("connecting"),setTimeout(r,1e3)):n("disconnected")}},[c,l]);return e.useEffect(()=>(r(),()=>{s.current?.close()}),[r]),{events:f,status:E,eventCount:S,lastEventAt:m}}export{R as u};
2
- //# sourceMappingURL=useSSE-CWBB4Me7.js.map
1
+ import{r as e}from"./index-BEeaI9uo.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-BMSSO9wB.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useSSE-CWBB4Me7.js","sources":["../../src/hooks/useSSE.ts"],"sourcesContent":["/**\n * dashboard-client/src/hooks/useSSE.ts — Server-Sent Events hook.\n *\n * Connects to /api/events, provides real-time event stream.\n * SPRINT-B1: basic connection. SPRINT-D1: reconnect with backoff.\n */\n\nimport { useState, useEffect, useRef, useCallback } from 'react';\nimport type { SseEvent } from '../../../dashboard-server/api-contracts';\n\nexport type SseStatus = 'connecting' | 'connected' | 'disconnected' | 'error';\n\nexport interface UseSSEResult {\n events: SseEvent[];\n status: SseStatus;\n eventCount: number;\n lastEventAt: number | null;\n}\n\nexport interface UseSSEOptions {\n /** Max events to keep in memory (ring buffer). */\n maxEvents?: number;\n /** Max reconnect attempts before giving up. */\n maxReconnects?: number;\n}\n\n// SPRINT-D1-REMAINING: exponential backoff (1s → 2s → 4s → max 30s).\n// SPRINT-D1-REMAINING: after 5 failures, set status='disconnected'.\n\nexport function useSSE(options: UseSSEOptions = {}): UseSSEResult {\n const { maxEvents = 500, maxReconnects = 5 } = options;\n const [events, setEvents] = useState<SseEvent[]>([]);\n const [status, setStatus] = useState<SseStatus>('connecting');\n const [eventCount, setEventCount] = useState(0);\n const [lastEventAt, setLastEventAt] = useState<number | null>(null);\n const reconnectCount = useRef(0);\n const sourceRef = useRef<EventSource | null>(null);\n\n const connect = useCallback(() => {\n if (sourceRef.current) {\n sourceRef.current.close();\n }\n setStatus('connecting');\n const source = new EventSource('/api/events');\n sourceRef.current = source;\n\n source.onopen = () => {\n setStatus('connected');\n reconnectCount.current = 0;\n };\n\n source.onmessage = (event) => {\n try {\n const parsed = JSON.parse(event.data) as SseEvent;\n setEvents(prev => {\n const next = [...prev, parsed];\n return next.length > maxEvents ? next.slice(-maxEvents) : next;\n });\n setEventCount(c => c + 1);\n setLastEventAt(Date.now());\n } catch {\n // non-fatal: skip malformed events\n }\n };\n\n source.onerror = () => {\n source.close();\n if (reconnectCount.current < maxReconnects) {\n reconnectCount.current += 1;\n setStatus('connecting');\n // SPRINT-D1: use exponential backoff instead of fixed delay\n setTimeout(connect, 1000);\n } else {\n setStatus('disconnected');\n }\n };\n }, [maxEvents, maxReconnects]);\n\n useEffect(() => {\n connect();\n return () => {\n sourceRef.current?.close();\n };\n }, [connect]);\n\n return { events, status, eventCount, lastEventAt };\n}\n"],"names":["useSSE","options","maxEvents","maxReconnects","events","setEvents","useState","status","setStatus","eventCount","setEventCount","lastEventAt","setLastEventAt","reconnectCount","useRef","sourceRef","connect","useCallback","source","event","parsed","prev","next","c","useEffect"],"mappings":"wCA6BO,SAASA,EAAOC,EAAyB,GAAkB,CAChE,KAAM,CAAE,UAAAC,EAAY,IAAK,cAAAC,EAAgB,GAAMF,EACzC,CAACG,EAAQC,CAAS,EAAIC,EAAAA,SAAqB,CAAA,CAAE,EAC7C,CAACC,EAAQC,CAAS,EAAIF,EAAAA,SAAoB,YAAY,EACtD,CAACG,EAAYC,CAAa,EAAIJ,EAAAA,SAAS,CAAC,EACxC,CAACK,EAAaC,CAAc,EAAIN,EAAAA,SAAwB,IAAI,EAC5DO,EAAiBC,EAAAA,OAAO,CAAC,EACzBC,EAAYD,EAAAA,OAA2B,IAAI,EAE3CE,EAAUC,EAAAA,YAAY,IAAM,CAC5BF,EAAU,SACZA,EAAU,QAAQ,MAAA,EAEpBP,EAAU,YAAY,EACtB,MAAMU,EAAS,IAAI,YAAY,aAAa,EAC5CH,EAAU,QAAUG,EAEpBA,EAAO,OAAS,IAAM,CACpBV,EAAU,WAAW,EACrBK,EAAe,QAAU,CAC3B,EAEAK,EAAO,UAAaC,GAAU,CAC5B,GAAI,CACF,MAAMC,EAAS,KAAK,MAAMD,EAAM,IAAI,EACpCd,EAAUgB,GAAQ,CAChB,MAAMC,EAAO,CAAC,GAAGD,EAAMD,CAAM,EAC7B,OAAOE,EAAK,OAASpB,EAAYoB,EAAK,MAAM,CAACpB,CAAS,EAAIoB,CAC5D,CAAC,EACDZ,EAAca,GAAKA,EAAI,CAAC,EACxBX,EAAe,KAAK,KAAK,CAC3B,MAAQ,CAER,CACF,EAEAM,EAAO,QAAU,IAAM,CACrBA,EAAO,MAAA,EACHL,EAAe,QAAUV,GAC3BU,EAAe,SAAW,EAC1BL,EAAU,YAAY,EAEtB,WAAWQ,EAAS,GAAI,GAExBR,EAAU,cAAc,CAE5B,CACF,EAAG,CAACN,EAAWC,CAAa,CAAC,EAE7BqB,OAAAA,EAAAA,UAAU,KACRR,EAAA,EACO,IAAM,CACXD,EAAU,SAAS,MAAA,CACrB,GACC,CAACC,CAAO,CAAC,EAEL,CAAE,OAAAZ,EAAQ,OAAAG,EAAQ,WAAAE,EAAY,YAAAE,CAAA,CACvC"}
1
+ {"version":3,"file":"useSSE-BMSSO9wB.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-B8k19B9g.js"></script>
7
+ <script type="module" crossorigin src="./assets/index-BEeaI9uo.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="./assets/index-CUNn22iV.css">
9
9
  </head>
10
10
  <body>
@@ -62,3 +62,19 @@ export async function fetchCachePoison(): Promise<CachePoisonResponse> {
62
62
  if (!r.ok) throw new Error(`cache-poison: ${r.status}`);
63
63
  return r.json() as Promise<CachePoisonResponse>;
64
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
+ }
@@ -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
  }
@@ -207,6 +207,25 @@ export default function SetupTab(): React.ReactElement {
207
207
  {embedderLabel(status.currentEmbedder)}
208
208
  </span>
209
209
  </div>
210
+ {"configuredEmbedder" in status && status.configuredEmbedder !== status.currentEmbedder && (
211
+ <div style={styles.warning}>
212
+ <strong>Configured but not active:</strong>{" "}
213
+ {embedderLabel(status.configuredEmbedder)} is configured in
214
+ .mega-compact.env but not yet loaded.{" "}
215
+ <strong>Restart pi</strong> to activate it.
216
+ {status.configuredUrl && (
217
+ <> ({status.configuredUrl})</>
218
+ )}
219
+ </div>
220
+ )}
221
+ {"restartRequired" in status && status.restartRequired && (
222
+ <div style={{ ...styles.row, color: "#ff9800" }}>
223
+ <span style={styles.label}>Status:</span>
224
+ <span style={styles.value}>
225
+ Restart required to activate the configured embedder
226
+ </span>
227
+ </div>
228
+ )}
210
229
  <div style={styles.row}>
211
230
  <span style={styles.label}>Embedding URL:</span>
212
231
  <span style={styles.value}>
@@ -94,4 +94,5 @@ export interface DebugBundleResponse {
94
94
  dashboardSnapshot: unknown | { error: string };
95
95
  recentEvents: unknown[];
96
96
  criticalEvents: unknown[];
97
+ dataSourceAudit?: Record<string, unknown> | { error: string };
97
98
  }
@@ -13,8 +13,14 @@
13
13
  * Returns the current embedder configuration read from the environment.
14
14
  */
15
15
  export interface SetupStatusResponse {
16
- /** Which embedder implementation is currently active. */
16
+ /** Which embedder implementation is currently active in the running process. */
17
17
  readonly currentEmbedder: "trigram" | "http" | "minilm" | "unknown";
18
+ /** Which embedder was configured via the dashboard (from .mega-compact.env). */
19
+ readonly configuredEmbedder: "trigram" | "http" | "minilm";
20
+ /** The URL configured in .mega-compact.env, if any. */
21
+ readonly configuredUrl: string | null;
22
+ /** True when the configured embedder differs from the active one (restart needed). */
23
+ readonly restartRequired: boolean;
18
24
  /** The embedding URL if httpEmbedder is active, otherwise null. */
19
25
  readonly embeddingUrl: string | null;
20
26
  /** The MEGACOMPACT_EMBED_CACHE value (number as string), or null. */
@@ -44,6 +44,7 @@ export function sendGatherDebug(res: ServerResponse, ctx: RouteContext): void {
44
44
  dashboardSnapshot: { error: "not fetched" },
45
45
  recentEvents: [],
46
46
  criticalEvents: [],
47
+ dataSourceAudit: {},
47
48
  };
48
49
 
49
50
  // 1. Config flags (no secrets — MEGACOMPACT_* only)
@@ -123,6 +124,68 @@ export function sendGatherDebug(res: ServerResponse, ctx: RouteContext): void {
123
124
  result.recentEvents = [{ error: e instanceof Error ? e.message : String(e) }];
124
125
  }
125
126
 
127
+ // 6. Data source audit — per-tab data population check
128
+ try {
129
+ const audit: Record<string, unknown> = {};
130
+ const db = openStoreDb(ctx);
131
+
132
+ // Main db tables
133
+ for (const table of ["context_chunks", "memories", "raw_transcript", "checkpoint_epochs", "dedup_mirror"]) {
134
+ try {
135
+ const row = db.prepare(`SELECT COUNT(*) AS c FROM ${table}`).get() as { c: number };
136
+ audit[`main.${table}`] = row.c;
137
+ } catch {
138
+ audit[`main.${table}`] = "table missing";
139
+ }
140
+ }
141
+
142
+ // context_health + cache_poison_events
143
+ for (const table of ["context_health", "cache_poison_events"]) {
144
+ try {
145
+ const row = db.prepare(`SELECT COUNT(*) AS c FROM ${table}`).get() as { c: number };
146
+ audit[`main.${table}`] = row.c;
147
+ } catch {
148
+ audit[`main.${table}`] = "table missing";
149
+ }
150
+ }
151
+
152
+ // turns.db tables
153
+ try {
154
+ const turnMod = require("../../src/store/turns/connection.js") as typeof import("../../src/store/turns/connection.js");
155
+ const tdb = turnMod.openTurnDb(ctx.stateDir);
156
+ for (const table of ["turns", "turn_recall", "session_conversations", "topics", "memory_topics"]) {
157
+ try {
158
+ const row = tdb.prepare(`SELECT COUNT(*) AS c FROM ${table}`).get() as { c: number };
159
+ audit[`turns.${table}`] = row.c;
160
+ } catch {
161
+ audit[`turns.${table}`] = "table missing";
162
+ }
163
+ }
164
+ } catch (e) {
165
+ audit["turns.db"] = `error: ${e instanceof Error ? e.message : String(e)}`;
166
+ }
167
+
168
+ // .mega-compact.env
169
+ try {
170
+ const envPath = ctx.stateDir + "/.mega-compact.env";
171
+ audit["mega-compact.env"] = existsSync(envPath) ? "exists" : "not found";
172
+ } catch {
173
+ audit["mega-compact.env"] = "check failed";
174
+ }
175
+
176
+ // dashboard.json
177
+ try {
178
+ const metricsPath = defaultMetricsPath(ctx.stateDir);
179
+ audit["dashboard.json"] = existsSync(metricsPath) ? "exists" : "not found";
180
+ } catch {
181
+ audit["dashboard.json"] = "check failed";
182
+ }
183
+
184
+ (result as { dataSourceAudit: Record<string, unknown> }).dataSourceAudit = audit;
185
+ } catch (e) {
186
+ (result as { dataSourceAudit: unknown }).dataSourceAudit = { error: e instanceof Error ? e.message : String(e) };
187
+ }
188
+
126
189
  // Send (local helper to avoid importing sendJson from a non-exporting module)
127
190
  try {
128
191
  const body = JSON.stringify(result);
@@ -102,3 +102,54 @@ export function handleCachePoison(
102
102
  }
103
103
  return true;
104
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
+ }
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { spawnSync } from "node:child_process"; // guardrails-allow PREVENT-PI-004: local subprocess detection only
13
13
  import { createRequire } from "node:module";
14
- import { writeFileSync, mkdirSync } from "node:fs";
14
+ import { writeFileSync, mkdirSync, readFileSync, existsSync } from "node:fs";
15
15
  import { join } from "node:path";
16
16
  import type { IncomingMessage, ServerResponse } from "node:http";
17
17
  import type { RouteContext } from "./routes-core.js";
@@ -37,10 +37,29 @@ function detectCurrentEmbedder(): SetupStatusResponse["currentEmbedder"] {
37
37
  return "trigram";
38
38
  }
39
39
 
40
+ /** Read the configured embedder from the .mega-compact.env file (written by
41
+ * the configure endpoint). This shows what the user SELECTED even before pi
42
+ * restarts and loads the env file into process.env. */
43
+ function detectConfiguredEmbedder(stateDir: string): { embedder: "trigram" | "http" | "minilm"; url: string | null } {
44
+ try {
45
+ const envPath = join(stateDir, ".mega-compact.env");
46
+ if (!existsSync(envPath)) return { embedder: "trigram", url: null };
47
+ const content = readFileSync(envPath, "utf-8");
48
+ const urlMatch = content.match(/^export\s+MEGACOMPACT_EMBEDDING_URL="([^"]+)"/m);
49
+ if (urlMatch) return { embedder: "http", url: urlMatch[1] };
50
+ const minilmMatch = content.match(/^export\s+MEGACOMPACT_MINILM="?(.+?)"?\s*$/m);
51
+ if (minilmMatch && ["1", "true", "yes"].includes(minilmMatch[1].trim().toLowerCase()))
52
+ return { embedder: "minilm", url: null };
53
+ return { embedder: "trigram", url: null };
54
+ } catch {
55
+ return { embedder: "trigram", url: null };
56
+ }
57
+ }
58
+
40
59
  export function handleSetupStatus(
41
60
  req: IncomingMessage,
42
61
  res: ServerResponse,
43
- _ctx: RouteContext,
62
+ ctx: RouteContext,
44
63
  ): boolean {
45
64
  if (req.url !== "/api/setup-status") return false;
46
65
  if (req.method !== "GET") {
@@ -49,9 +68,14 @@ export function handleSetupStatus(
49
68
  res.end(JSON.stringify({ error: "method_not_allowed" }));
50
69
  return true;
51
70
  }
71
+ const configured = detectConfiguredEmbedder(ctx.stateDir);
72
+ const active = detectCurrentEmbedder();
52
73
  const body: SetupStatusResponse = {
53
- currentEmbedder: detectCurrentEmbedder(),
54
- embeddingUrl: process.env["MEGACOMPACT_EMBEDDING_URL"] ?? null,
74
+ currentEmbedder: active,
75
+ configuredEmbedder: configured.embedder,
76
+ configuredUrl: configured.url,
77
+ restartRequired: active !== configured.embedder,
78
+ embeddingUrl: process.env["MEGACOMPACT_EMBEDDING_URL"] ?? configured.url,
55
79
  embedCache: process.env["MEGACOMPACT_EMBED_CACHE"] ?? null,
56
80
  minilm: ["1", "true", "yes"].includes(
57
81
  (process.env["MEGACOMPACT_MINILM"] ?? "").trim().toLowerCase(),
@@ -24,4 +24,4 @@ export { handleMemoryStatus } from "./routes-memory.js";
24
24
  export { handleSetupStatus, handleSetupDetect, handleSetupConfigure } from "./routes-setup.js";
25
25
  export { handleMemoryMap } from "./routes-memory-map.js";
26
26
  export { handleCacheStripes } from "./routes-cache.js";
27
- export { handleContextHealth, handleCachePoison } from "./routes-health.js";
27
+ export { handleContextHealth, handleCachePoison, handleHealthSettings } from "./routes-health.js";
@@ -46,6 +46,7 @@ import {
46
46
  handleMemoryMap,
47
47
  handleContextHealth,
48
48
  handleCachePoison,
49
+ handleHealthSettings,
49
50
  handleStatic,
50
51
  } from "./routes.js";
51
52
 
@@ -243,6 +244,7 @@ export async function launchDashboardServer(
243
244
  if (handleMemoryMap(req, res, ctx)) return;
244
245
  if (handleContextHealth(req, res, ctx)) return;
245
246
  if (handleCachePoison(req, res, ctx)) return;
247
+ if (handleHealthSettings(req, res, ctx)) return;
246
248
  handleStatic(req, res, ctx);
247
249
  });
248
250
 
@@ -865,6 +865,31 @@ export function registerAgentHandlers(
865
865
  }
866
866
 
867
867
  // v0.12: Context Health — compute + persist health score. Non-fatal.
868
- handleTurnEndHealth(event, runtime, config);
868
+ // Mitigation signals are acted on here (ctx is in scope at turn_end).
869
+ const healthSignal = handleTurnEndHealth(event, runtime, config);
870
+ if (healthSignal.forceCompact) {
871
+ runtime.logger?.info("health_mitigate_compact", {
872
+ composite: healthSignal.composite,
873
+ turnIndex: event.turnIndex,
874
+ });
875
+ try {
876
+ // Force a compaction to flush degraded context. Same race-guarded
877
+ // deferred path as the critical-over escape hatch above.
878
+ if (!piCompactWouldNoop(ctx)) {
879
+ ctx.compact();
880
+ }
881
+ } catch {
882
+ /* non-fatal: mitigation never breaks the agent loop */
883
+ }
884
+ }
885
+ if (healthSignal.breakPrefix) {
886
+ runtime.logger?.info("health_mitigate_prefix_break", {
887
+ turnIndex: event.turnIndex,
888
+ });
889
+ // Invalidate the cached trim so the next context event regenerates
890
+ // from scratch — the prefix change forces a cache miss, bypassing
891
+ // the corrupted KV state.
892
+ runtime.trimCache = null;
893
+ }
869
894
  });
870
895
  }
@@ -32,6 +32,7 @@ import {
32
32
  import {
33
33
  recordCachePoisonEvent,
34
34
  } from "../../src/store/sqlite/context-health.js";
35
+ import { getHealthMitigate } from "../../src/store/sqlite/meta.js";
35
36
 
36
37
  const RING_MAX = 5;
37
38
 
@@ -64,13 +65,24 @@ interface TurnEvent {
64
65
  message?: { role?: string; content?: unknown; stopReason?: string; usage?: { cacheRead?: number } };
65
66
  }
66
67
 
67
- /** Handle turn_end: compute + persist context health. Non-fatal. */
68
+ export interface HealthMitigationSignal {
69
+ /** Force a compaction to flush degraded context (composite < 0.4). */
70
+ forceCompact: boolean;
71
+ /** Inject a prefix break to bypass corrupted KV cache (cachePoison < 0.3). */
72
+ breakPrefix: boolean;
73
+ /** The composite health score that triggered mitigation (if any). */
74
+ composite: number;
75
+ }
76
+
77
+ /** Handle turn_end: compute + persist context health. Non-fatal.
78
+ * Returns mitigation signals for the caller to act on (agent-handlers has ctx). */
68
79
  export function handleTurnEndHealth(
69
80
  event: TurnEvent,
70
81
  runtime: MegaRuntime,
71
82
  config: MegaConfig,
72
- ): void {
73
- if (!config.contextHealth) return;
83
+ ): HealthMitigationSignal {
84
+ const noSignal: HealthMitigationSignal = { forceCompact: false, breakPrefix: false, composite: 1 };
85
+ if (!config.contextHealth) return noSignal;
74
86
  try {
75
87
  const text = extractAssistantText(event);
76
88
  const embedder = defaultEmbedder();
@@ -173,6 +185,11 @@ export function handleTurnEndHealth(
173
185
  turnIndex: event.turnIndex,
174
186
  });
175
187
 
188
+ // Mitigation: check the runtime-toggleable flag (meta table), not just
189
+ // the env var. The dashboard Maintenance tab toggles this at runtime.
190
+ const mitigate = config.contextHealthMitigate ||
191
+ getHealthMitigate(runtime.currentStateDir);
192
+
176
193
  // Update ring buffers
177
194
  runtime.recentTurnEmbeddings.push(emb);
178
195
  if (runtime.recentTurnEmbeddings.length > RING_MAX) {
@@ -182,9 +199,20 @@ export function handleTurnEndHealth(
182
199
  if (runtime.recentErrorCategories.length > RING_MAX) {
183
200
  runtime.recentErrorCategories.shift();
184
201
  }
202
+
203
+ // Return mitigation signals so agent-handlers.ts (which has ctx) can act.
204
+ if (mitigate) {
205
+ return {
206
+ forceCompact: composite < 0.4,
207
+ breakPrefix: cachePoison < 0.3,
208
+ composite,
209
+ };
210
+ }
211
+ return { forceCompact: false, breakPrefix: false, composite };
185
212
  } catch (e) {
186
213
  runtime.logger?.error("context_health_failed", {
187
214
  error: String(e instanceof Error ? e.message : e),
188
215
  });
216
+ return noSignal;
189
217
  }
190
218
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
5
5
  "type": "module",
6
6
  "license": "BSD-3-Clause",
@@ -206,7 +206,8 @@ export function buildTurnNodes(
206
206
  // ---------------------------------------------------------------------------
207
207
 
208
208
  export function buildTurnContentNodes(
209
- db: DatabaseSync,
209
+ turnDb: DatabaseSync,
210
+ mainDb: DatabaseSync,
210
211
  sessionId: string,
211
212
  nodes: MemoryGraphNode[],
212
213
  edges: MemoryGraphEdge[],
@@ -216,25 +217,58 @@ export function buildTurnContentNodes(
216
217
  // (identity_merge) merges them with the richest nodeType winning. Using the
217
218
  // global nodes array here would skip every node (Source A already added them).
218
219
  const seen = new Set<string>();
219
- const rows = sessionId
220
- ? (db
220
+
221
+ // turns live in turns.db, raw_transcript in the main db — can't JOIN across
222
+ // databases, so query separately and merge in JS.
223
+ const turnRows = sessionId
224
+ ? (turnDb
221
225
  .prepare(
222
- `SELECT t.turn_index, t.role, t.ended_at, r.content_bytes
223
- FROM turns t
224
- JOIN raw_transcript r ON r.session_id = t.session_id AND r.turn_index = t.turn_index
225
- WHERE t.session_id = ?
226
- ORDER BY t.turn_index ASC`,
226
+ `SELECT turn_index, role, ended_at
227
+ FROM turns
228
+ WHERE session_id = ?
229
+ ORDER BY turn_index ASC`,
227
230
  )
228
231
  .all(sessionId) as Array<Record<string, unknown>>)
229
- : (db
232
+ : (turnDb
230
233
  .prepare(
231
- `SELECT t.turn_index, t.role, t.ended_at, r.content_bytes
232
- FROM turns t
233
- JOIN raw_transcript r ON r.session_id = t.session_id AND r.turn_index = t.turn_index
234
- ORDER BY t.turn_index ASC`,
234
+ `SELECT turn_index, role, ended_at
235
+ FROM turns
236
+ ORDER BY turn_index ASC`,
235
237
  )
236
238
  .all() as Array<Record<string, unknown>>);
237
239
 
240
+ // Build a map of (session_id, turn_index) → content from raw_transcript.
241
+ const contentMap = new Map<string, string>();
242
+ try {
243
+ const transcriptRows = sessionId
244
+ ? (mainDb
245
+ .prepare(
246
+ `SELECT turn_index, content_bytes FROM raw_transcript WHERE session_id = ?`,
247
+ )
248
+ .all(sessionId) as Array<Record<string, unknown>>)
249
+ : (mainDb
250
+ .prepare(
251
+ `SELECT turn_index, content_bytes FROM raw_transcript`,
252
+ )
253
+ .all() as Array<Record<string, unknown>>);
254
+ for (const r of transcriptRows) {
255
+ contentMap.set(String(r.turn_index), String(r.content_bytes ?? ""));
256
+ }
257
+ } catch {
258
+ // raw_transcript may not exist in the main db (dbMirror off) — return empty.
259
+ return;
260
+ }
261
+
262
+ // Merge: only produce nodes where both a turn row AND transcript content exist.
263
+ const rows = turnRows
264
+ .filter((t) => contentMap.has(String(t.turn_index)))
265
+ .map((t) => ({
266
+ turn_index: t.turn_index,
267
+ role: t.role,
268
+ ended_at: t.ended_at,
269
+ content_bytes: contentMap.get(String(t.turn_index)) ?? "",
270
+ }));
271
+
238
272
  if (rows.length === 0) return;
239
273
 
240
274
  const turnContentNodes: MemoryGraphNode[] = [];
@@ -269,7 +303,7 @@ export function buildTurnContentNodes(
269
303
  if (turnContentNodes.length > 1) {
270
304
  const embeddings = turnContentNodes.map((n) => {
271
305
  try {
272
- return { id: n.id, vec: getOrComputeEmbedding(db, n.textSnippet) };
306
+ return { id: n.id, vec: getOrComputeEmbedding(mainDb, n.textSnippet) };
273
307
  } catch {
274
308
  return { id: n.id, vec: null as number[] | null };
275
309
  }
@@ -291,7 +325,7 @@ export function buildTurnContentNodes(
291
325
  }
292
326
  }
293
327
 
294
- linkTurnToCheckpointEdges(db, sessionId, turnContentNodes, edges);
328
+ linkTurnToCheckpointEdges(mainDb, sessionId, turnContentNodes, edges);
295
329
  }
296
330
 
297
331
  nodes.push(...turnContentNodes);