pi-mega-compact 0.11.11 → 0.11.13

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 (55) hide show
  1. package/dist/extensions/dashboard-server/dashboard-client-repos.js +28 -2
  2. package/dist/extensions/dashboard-server/index-reader.js +14 -13
  3. package/dist/extensions/dashboard-server/routes-topics.js +18 -0
  4. package/dist/extensions/mega-events/context-handler.js +10 -5
  5. package/dist/extensions/mega-runtime/reset-runtime.js +5 -1
  6. package/dist/src/memoryGraph/gates.js +1 -1
  7. package/dist/src/memoryGraph/sources.js +44 -22
  8. package/dist/src/store/turns/sqlite-store.js +6 -6
  9. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DFitrb4Y.js → AchievementTiles-9KQgKBEh.js} +2 -2
  10. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DFitrb4Y.js.map → AchievementTiles-9KQgKBEh.js.map} +1 -1
  11. package/extensions/dashboard-client/dist/assets/{AchievementsTab-PJrmq8pa.js → AchievementsTab-B6xoQTEa.js} +2 -2
  12. package/extensions/dashboard-client/dist/assets/{AchievementsTab-PJrmq8pa.js.map → AchievementsTab-B6xoQTEa.js.map} +1 -1
  13. package/extensions/dashboard-client/dist/assets/{CacheTab-C1WE7dEx.js → CacheTab-Dkm9rawo.js} +2 -2
  14. package/extensions/dashboard-client/dist/assets/{CacheTab-C1WE7dEx.js.map → CacheTab-Dkm9rawo.js.map} +1 -1
  15. package/extensions/dashboard-client/dist/assets/{ConfigTab-Dx-RpO8C.js → ConfigTab-Bd_v_OUF.js} +2 -2
  16. package/extensions/dashboard-client/dist/assets/{ConfigTab-Dx-RpO8C.js.map → ConfigTab-Bd_v_OUF.js.map} +1 -1
  17. package/extensions/dashboard-client/dist/assets/{EventsTab-C63BEWEs.js → EventsTab-CYWoFOdI.js} +2 -2
  18. package/extensions/dashboard-client/dist/assets/{EventsTab-C63BEWEs.js.map → EventsTab-CYWoFOdI.js.map} +1 -1
  19. package/extensions/dashboard-client/dist/assets/{GameTab-DHpJE6km.js → GameTab-CH7lecNL.js} +2 -2
  20. package/extensions/dashboard-client/dist/assets/{GameTab-DHpJE6km.js.map → GameTab-CH7lecNL.js.map} +1 -1
  21. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-BSv0GfBb.js → MaintenanceTab-DNZ1IF4t.js} +2 -2
  22. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-BSv0GfBb.js.map → MaintenanceTab-DNZ1IF4t.js.map} +1 -1
  23. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-Cg1fgEx_.js → MemoryMapTab-tVRREYiG.js} +2 -2
  24. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-Cg1fgEx_.js.map → MemoryMapTab-tVRREYiG.js.map} +1 -1
  25. package/extensions/dashboard-client/dist/assets/{MetricsTab-DCahxNOQ.js → MetricsTab-CHblYuTY.js} +2 -2
  26. package/extensions/dashboard-client/dist/assets/{MetricsTab-DCahxNOQ.js.map → MetricsTab-CHblYuTY.js.map} +1 -1
  27. package/extensions/dashboard-client/dist/assets/{OverviewTab-RYvO1SZL.js → OverviewTab-Dj0TgFvU.js} +2 -2
  28. package/extensions/dashboard-client/dist/assets/{OverviewTab-RYvO1SZL.js.map → OverviewTab-Dj0TgFvU.js.map} +1 -1
  29. package/extensions/dashboard-client/dist/assets/{ReposTab-BDGr_UXb.js → ReposTab-BTJcfc1W.js} +2 -2
  30. package/extensions/dashboard-client/dist/assets/{ReposTab-BDGr_UXb.js.map → ReposTab-BTJcfc1W.js.map} +1 -1
  31. package/extensions/dashboard-client/dist/assets/{SessionsTab-ByrTnHaB.js → SessionsTab-C8tU8K5u.js} +2 -2
  32. package/extensions/dashboard-client/dist/assets/{SessionsTab-ByrTnHaB.js.map → SessionsTab-C8tU8K5u.js.map} +1 -1
  33. package/extensions/dashboard-client/dist/assets/{SetupTab-CELafKis.js → SetupTab-BdxGMo6O.js} +2 -2
  34. package/extensions/dashboard-client/dist/assets/{SetupTab-CELafKis.js.map → SetupTab-BdxGMo6O.js.map} +1 -1
  35. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BLWtmfr_.js → TimeSavedCard-BWZSARzT.js} +2 -2
  36. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BLWtmfr_.js.map → TimeSavedCard-BWZSARzT.js.map} +1 -1
  37. package/extensions/dashboard-client/dist/assets/{TopicsTab-8Fsh_Igx.js → TopicsTab-BCJwPYl8.js} +2 -2
  38. package/extensions/dashboard-client/dist/assets/{TopicsTab-8Fsh_Igx.js.map → TopicsTab-BCJwPYl8.js.map} +1 -1
  39. package/extensions/dashboard-client/dist/assets/{TurnsTab-D7mgXDe2.js → TurnsTab-DQFTSDpz.js} +2 -2
  40. package/extensions/dashboard-client/dist/assets/{TurnsTab-D7mgXDe2.js.map → TurnsTab-DQFTSDpz.js.map} +1 -1
  41. package/extensions/dashboard-client/dist/assets/{index-h3jgEs6G.js → index-DrrDswj-.js} +3 -3
  42. package/extensions/dashboard-client/dist/assets/{index-h3jgEs6G.js.map → index-DrrDswj-.js.map} +1 -1
  43. package/extensions/dashboard-client/dist/assets/{useSSE-C5lmipBF.js → useSSE-o4tteeTs.js} +2 -2
  44. package/extensions/dashboard-client/dist/assets/{useSSE-C5lmipBF.js.map → useSSE-o4tteeTs.js.map} +1 -1
  45. package/extensions/dashboard-client/dist/index.html +1 -1
  46. package/extensions/dashboard-client/src/tabs/SetupTab.tsx +8 -8
  47. package/extensions/dashboard-server/dashboard-client-repos.ts +28 -2
  48. package/extensions/dashboard-server/index-reader.ts +16 -17
  49. package/extensions/dashboard-server/routes-topics.ts +21 -0
  50. package/extensions/mega-events/context-handler.ts +111 -104
  51. package/extensions/mega-runtime/reset-runtime.ts +5 -1
  52. package/package.json +1 -1
  53. package/src/memoryGraph/gates.ts +1 -1
  54. package/src/memoryGraph/sources.ts +56 -28
  55. package/src/store/turns/sqlite-store.ts +6 -4
@@ -1,2 +1,2 @@
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
+ 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 +1 @@
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"}
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"}
@@ -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-h3jgEs6G.js"></script>
7
+ <script type="module" crossorigin src="./assets/index-DrrDswj-.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="./assets/index-CUNn22iV.css">
9
9
  </head>
10
10
  <body>
@@ -94,6 +94,14 @@ export default function SetupTab(): React.ReactElement {
94
94
  const [configResult, setConfigResult] = useState<SetupConfigureResponse | null>(null);
95
95
  const [configError, setConfigError] = useState<string | null>(null);
96
96
 
97
+ const loadStatus = useCallback(() => {
98
+ fetchSetupStatus()
99
+ .then(setStatus)
100
+ .catch((e: unknown) =>
101
+ setStatusError(e instanceof Error ? e.message : String(e)),
102
+ );
103
+ }, []);
104
+
97
105
  const applyEmbedder = useCallback((embedder: "ollama" | "llama" | "trigram") => {
98
106
  setConfiguring(embedder);
99
107
  setConfigError(null);
@@ -110,14 +118,6 @@ export default function SetupTab(): React.ReactElement {
110
118
  });
111
119
  }, [loadStatus]);
112
120
 
113
- const loadStatus = useCallback(() => {
114
- fetchSetupStatus()
115
- .then(setStatus)
116
- .catch((e: unknown) =>
117
- setStatusError(e instanceof Error ? e.message : String(e)),
118
- );
119
- }, []);
120
-
121
121
  const runDetect = useCallback(() => {
122
122
  setRunningDetect(true);
123
123
  setDetectError(null);
@@ -64,7 +64,7 @@ function renderByModel(repos) {
64
64
  var rows = document.getElementById('bm-rows');
65
65
  if (!rows) return;
66
66
  if (!repos || !repos.length) {
67
- rows.innerHTML = '<tr><td colspan="14" class="repo-none">No repositories registered yet.</td></tr>';
67
+ rows.innerHTML = '<tr><td colspan="16" class="repo-none">No repositories registered yet.</td></tr>';
68
68
  return;
69
69
  }
70
70
  var groups = {};
@@ -107,11 +107,35 @@ function renderByModel(repos) {
107
107
  var fmt = function(v) { return '$' + v.toFixed(6); };
108
108
  return lo === hi ? fmt(lo) : fmt(lo) + '–' + fmt(hi);
109
109
  }
110
+ // Helper: compute cache hit % from providerCacheRead/Write tokens
111
+ function cacheHitPct(cr, cw, inp) {
112
+ var denom = cr + cw + (inp || 0);
113
+ return denom > 0 ? ((cr / denom) * 100).toFixed(1) + '%' : '—';
114
+ }
115
+ // Helper: compute cache $ saved (read = 0.9 * inputRate, write = 0.25 * inputRate)
116
+ function cacheDollar(cr, cw, rate) {
117
+ if (!cr || !rate) return '—';
118
+ var saved = cr * rate * 0.9 - cw * rate * 0.25;
119
+ return '$' + saved.toFixed(4);
120
+ }
121
+ // Aggregate providerCacheRead/Write across repos in this group
122
+ var groupCacheRead = 0, groupCacheWrite = 0, groupTokensIn = 0;
123
+ for (var ii = 0; ii < repos.length; ii++) {
124
+ var rr = repos[ii];
125
+ var key2 = (rr.modelName && String(rr.modelName).trim()) || '(unknown)';
126
+ if (key2 === g.model) {
127
+ groupCacheRead += (rr.providerCacheRead || 0);
128
+ groupCacheWrite += (rr.providerCacheWrite || 0);
129
+ groupTokensIn += (rr.tokensDropped || 0);
130
+ }
131
+ }
110
132
  rows.innerHTML = arr.map(function(g) {
111
133
  var freed = (g.tokensIn || 0) - (g.tokensOut || 0);
112
- var usd = g.usd > 0 ? '$' + g.usd.toFixed(4) : '—';
134
+ // B: always show a dollar value (including $0.0000) do not use '' when usd is 0
135
+ var usd = '$' + (g.usd || 0).toFixed(4);
113
136
  var when = g.lastAt ? new Date(g.lastAt).toLocaleString() : '—';
114
137
  var reas = g.reasoning == null ? '—' : (g.reasoning ? 'yes' : 'no');
138
+ var rate0 = g.inRates && g.inRates.length ? g.inRates[0] : 0;
115
139
  return '<tr>' +
116
140
  '<td><span class="repo-model">' + sanitize(g.model) + '</span></td>' +
117
141
  '<td>' + sanitize(g.provider) + '</td>' +
@@ -126,6 +150,8 @@ function renderByModel(repos) {
126
150
  '<td class="num">' + collapseRate(g.inRates) + '</td>' +
127
151
  '<td class="num">' + collapseRate(g.outRates) + '</td>' +
128
152
  '<td class="num">' + sanitize(usd) + '</td>' +
153
+ '<td class="num">' + cacheHitPct(groupCacheRead, groupCacheWrite, groupTokensIn) + '</td>' +
154
+ '<td class="num">' + cacheDollar(groupCacheRead, groupCacheWrite, rate0) + '</td>' +
129
155
  '<td class="num">' + sanitize(when) + '</td>' +
130
156
  '</tr>';
131
157
  }).join('');
@@ -14,6 +14,9 @@ import { homedir } from "node:os";
14
14
  import { join } from "node:path";
15
15
  import { DatabaseSync } from "node:sqlite";
16
16
  import type { IndexRepo, IndexIndex } from "./types.js";
17
+ import {
18
+ readProviderCacheForRepo,
19
+ } from "../../src/store/sqlite/perf-samples.js";
17
20
 
18
21
  export function getIndexDir(): string {
19
22
  const override = process.env.MEGACOMPACT_INDEX_DIR;
@@ -100,25 +103,21 @@ export function readIndex(): IndexIndex | null {
100
103
  repo.maxTokens = Number(mrow.max_tokens ?? 0) || null;
101
104
  repo.reasoning = Number(mrow.reasoning ?? 0) === 1;
102
105
  }
103
- // Provider prompt-cache stats (E.3)
106
+ // Provider prompt-cache stats (E.3 / F4)
107
+ // Use readProviderCacheForRepo which correctly reads cache token
108
+ // counts from the meta JSON column (cacheRead/cacheWrite), not
109
+ // from non-existent table columns cache_read/cache_write.
104
110
  try {
105
- const pRow = sdb
106
- .prepare(
107
- `SELECT
108
- SUM(cache_read) AS totalRead,
109
- SUM(cache_write) AS totalWrite,
110
- SUM(cache_read + cache_write) * 1.0 / MAX(SUM(cache_read + cache_write + tokens_in), 1) AS hitPct
111
- FROM perf_samples
112
- WHERE cache_read > 0 OR cache_write > 0`,
113
- )
114
- .get() as
115
- | { totalRead: number; totalWrite: number; hitPct: number }
116
- | undefined;
117
- if (pRow && pRow.totalRead > 0) {
118
- repo.providerCacheRead = Number(pRow.totalRead);
119
- repo.providerCacheWrite = Number(pRow.totalWrite);
120
- repo.providerCachePct = Number((pRow.hitPct * 100).toFixed(1));
111
+ const pc = readProviderCacheForRepo(repo.stateDir);
112
+ if (pc.totalCacheRead > 0) {
113
+ repo.providerCacheRead = pc.totalCacheRead;
114
+ repo.providerCacheWrite = pc.totalCacheWrite;
115
+ repo.providerCachePct = Number(pc.avgHitPct.toFixed(1));
121
116
  }
117
+ // Also enrich contextWindow/maxTokens/reasoning from the
118
+ // latest model_snapshots row (was already read above, but
119
+ // when latestModelSnapshot was used above we get the same
120
+ // data from the per-repo store — keep as-is).
122
121
  } catch {
123
122
  /* best-effort — perf_samples may not exist */
124
123
  }
@@ -10,6 +10,8 @@ import type { IncomingMessage, ServerResponse } from "node:http";
10
10
  import type { RouteContext } from "./routes-core.js";
11
11
  import { openTurnStore } from "../../src/store/turns/connection.js";
12
12
  import { createTopicStore } from "../../src/topics/store.js";
13
+ import { openStore } from "../../src/store/sqlite.js";
14
+ import { buildTopicModel } from "../../src/topics/cluster.js";
13
15
  import type { TopicsResponse, TopicRow } from "./api-contracts/game-types.js";
14
16
  import type { TopicMemoriesResponse } from "./api-contracts/turns.js";
15
17
 
@@ -53,6 +55,25 @@ export function handleTopics(
53
55
 
54
56
  try {
55
57
  const tdb = openTurnStore(ctx.stateDir);
58
+
59
+ // Lazy build: when the topics table is empty, attempt a one-shot build
60
+ // from available context_chunks embeddings so the wiki is useful even
61
+ // before the first compaction fires.
62
+ const topicCount = (
63
+ tdb.prepare("SELECT COUNT(*) AS c FROM topics").get() as { c: number }
64
+ ).c;
65
+ if (topicCount === 0) {
66
+ try {
67
+ const mainDb = openStore(ctx.stateDir);
68
+ const model = buildTopicModel(mainDb);
69
+ if (model.k > 0 && model.totalChunks > 0) {
70
+ createTopicStore(ctx.stateDir).replaceTopicModel(model);
71
+ }
72
+ } catch {
73
+ /* non-fatal: lazy wiki build is best-effort */
74
+ }
75
+ }
76
+
56
77
  const topics: TopicRow[] = (
57
78
  tdb
58
79
  .prepare(
@@ -333,120 +333,127 @@ export function registerContextHandler(
333
333
  } catch {
334
334
  /* non-fatal: epoch stamping never breaks compaction */
335
335
  }
336
- // S51B: auto-categorizing wiki rebuild — every Nth compaction, derived
337
- // from real context_chunks embeddings. Isolated-store only, gated on
338
- // AUTO_WIKI_ENABLED; best-effort + non-fatal (never breaks compaction).
339
- try {
340
- if (config.autoWikiEnabled && config.turnsDbEnabled) {
341
- const every = Math.max(
342
- 1,
343
- TurnsConfig.WIKI_REBUILD_EVERY_N_COMPACTS,
344
- );
345
- const tdb = openTurnStore(runtime.currentStateDir);
346
- const n = bumpWikiCompactCounter(tdb);
347
- if (n % every === 0) {
348
- const model = buildTopicModel(db, {
349
- kRange: [TurnsConfig.WIKI_K_MIN, TurnsConfig.WIKI_K_MAX],
350
- labelTopTerms: TurnsConfig.WIKI_LABEL_TOP_TERMS,
351
- restarts: 5,
352
- seed: 0x9e3779b9,
336
+
337
+ // S51B: auto-categorizing wiki rebuild every Nth compaction, derived
338
+ // from real context_chunks embeddings. Isolated-store only, gated on
339
+ // AUTO_WIKI_ENABLED; best-effort + non-fatal (never breaks compaction).
340
+ // Fire regardless of dbMirror — context_chunks is the isolated store.
341
+ // Uses the already-open db when inside the dbMirror block; opens its own
342
+ // connection otherwise.
343
+ // from real context_chunks embeddings. Isolated-store only, gated on
344
+ // AUTO_WIKI_ENABLED; best-effort + non-fatal (never breaks compaction).
345
+ try {
346
+ if (config.autoWikiEnabled && config.turnsDbEnabled) {
347
+ const every = Math.max(
348
+ 1,
349
+ TurnsConfig.WIKI_REBUILD_EVERY_N_COMPACTS,
350
+ );
351
+ const tdb = openTurnStore(runtime.currentStateDir);
352
+ const n = bumpWikiCompactCounter(tdb);
353
+ if (n % every === 0) {
354
+ const model = buildTopicModel(db, {
355
+ kRange: [TurnsConfig.WIKI_K_MIN, TurnsConfig.WIKI_K_MAX],
356
+ labelTopTerms: TurnsConfig.WIKI_LABEL_TOP_TERMS,
357
+ restarts: 5,
358
+ seed: 0x9e3779b9,
359
+ });
360
+ createTopicStore(runtime.currentStateDir).replaceTopicModel(
361
+ model,
362
+ );
363
+ runtime.logger.info("wiki_rebuild", {
364
+ clusterCount: model.k,
365
+ totalChunks: model.totalChunks,
366
+ method: "kmeans+tfidf",
367
+ criterion: model.criterion,
368
+ silhouetteScore: model.silhouetteScore,
369
+ uncalibrated: false,
370
+ });
371
+ }
372
+ }
373
+ } catch (wikiErr) {
374
+ runtime.logger.warn("wiki_rebuild_failed", {
375
+ error: String(wikiErr),
376
+ });
377
+ }
378
+
379
+ // D1: seed the topic model from raw_transcript when context_chunks is
380
+ // thin (pre-compaction). Gated on WIKI_SEED_FROM_TURNS; non-fatal.
381
+ // Seeds buildTopicModel with on-the-fly trigram embeddings from
382
+ // recent raw_transcript rows for the current session.
383
+ try {
384
+ if (
385
+ config.autoWikiEnabled &&
386
+ config.turnsDbEnabled &&
387
+ TurnsConfig.WIKI_SEED_FROM_TURNS
388
+ ) {
389
+ const floor = 50;
390
+ const countRow = db
391
+ .prepare(
392
+ `SELECT COUNT(*) AS cnt FROM context_chunks WHERE session_id = ?`,
393
+ )
394
+ .get(runtime.rt.sessionId) as { cnt: number } | undefined;
395
+ const chunkCount = countRow?.cnt ?? 0;
396
+ if (chunkCount < floor) {
397
+ const transcriptRows = db
398
+ .prepare(
399
+ `SELECT DISTINCT content_bytes
400
+ FROM raw_transcript
401
+ WHERE session_id = ?
402
+ AND length(content_bytes) > 10
403
+ ORDER BY seq ASC
404
+ LIMIT 200`,
405
+ )
406
+ .all(runtime.rt.sessionId) as Array<{
407
+ content_bytes: string;
408
+ }>;
409
+ if (transcriptRows.length > 0) {
410
+ const embedder = new TrigramEmbedder();
411
+ const seedChunks: EmbeddedChunk[] = [];
412
+ for (let i = 0; i < transcriptRows.length; i++) {
413
+ const text = transcriptRows[i].content_bytes.trim();
414
+ if (text.length === 0) continue;
415
+ const vec = embedder.embed(text);
416
+ seedChunks.push({
417
+ chunkId: `seed_transcript_${i}`,
418
+ sessionId: runtime.rt.sessionId,
419
+ vec,
420
+ text,
353
421
  });
354
- createTopicStore(runtime.currentStateDir).replaceTopicModel(
355
- model,
422
+ }
423
+ if (seedChunks.length > 0) {
424
+ const model = buildTopicModel(
425
+ db,
426
+ {
427
+ kRange: [
428
+ TurnsConfig.WIKI_K_MIN,
429
+ TurnsConfig.WIKI_K_MAX,
430
+ ],
431
+ labelTopTerms:
432
+ TurnsConfig.WIKI_LABEL_TOP_TERMS,
433
+ restarts: 5,
434
+ seed: 0x9e3779b9,
435
+ },
436
+ seedChunks,
356
437
  );
357
- runtime.logger.info("wiki_rebuild", {
438
+ createTopicStore(
439
+ runtime.currentStateDir,
440
+ ).replaceTopicModel(model);
441
+ runtime.logger.info("wiki_seed", {
358
442
  clusterCount: model.k,
443
+ sourceChunks: seedChunks.length,
359
444
  totalChunks: model.totalChunks,
360
445
  method: "kmeans+tfidf",
361
- criterion: model.criterion,
362
- silhouetteScore: model.silhouetteScore,
363
- uncalibrated: false,
364
446
  });
365
447
  }
366
448
  }
367
- } catch (wikiErr) {
368
- runtime.logger.warn("wiki_rebuild_failed", {
369
- error: String(wikiErr),
370
- });
371
449
  }
450
+ }
451
+ } catch (seedErr) {
452
+ runtime.logger.warn("wiki_seed_failed", {
453
+ error: String(seedErr),
454
+ });
455
+ }
372
456
 
373
- // D1: seed the topic model from raw_transcript when context_chunks is
374
- // thin (pre-compaction). Gated on WIKI_SEED_FROM_TURNS; non-fatal.
375
- // Seeds buildTopicModel with on-the-fly trigram embeddings from
376
- // recent raw_transcript rows for the current session.
377
- try {
378
- if (
379
- config.autoWikiEnabled &&
380
- config.turnsDbEnabled &&
381
- TurnsConfig.WIKI_SEED_FROM_TURNS
382
- ) {
383
- const floor = 50;
384
- const countRow = db
385
- .prepare(
386
- `SELECT COUNT(*) AS cnt FROM context_chunks WHERE session_id = ?`,
387
- )
388
- .get(runtime.rt.sessionId) as { cnt: number } | undefined;
389
- const chunkCount = countRow?.cnt ?? 0;
390
- if (chunkCount < floor) {
391
- const transcriptRows = db
392
- .prepare(
393
- `SELECT DISTINCT content_bytes
394
- FROM raw_transcript
395
- WHERE session_id = ?
396
- AND length(content_bytes) > 10
397
- ORDER BY seq ASC
398
- LIMIT 200`,
399
- )
400
- .all(runtime.rt.sessionId) as Array<{
401
- content_bytes: string;
402
- }>;
403
- if (transcriptRows.length > 0) {
404
- const embedder = new TrigramEmbedder();
405
- const seedChunks: EmbeddedChunk[] = [];
406
- for (let i = 0; i < transcriptRows.length; i++) {
407
- const text = transcriptRows[i].content_bytes.trim();
408
- if (text.length === 0) continue;
409
- const vec = embedder.embed(text);
410
- seedChunks.push({
411
- chunkId: `seed_transcript_${i}`,
412
- sessionId: runtime.rt.sessionId,
413
- vec,
414
- text,
415
- });
416
- }
417
- if (seedChunks.length > 0) {
418
- const model = buildTopicModel(
419
- db,
420
- {
421
- kRange: [
422
- TurnsConfig.WIKI_K_MIN,
423
- TurnsConfig.WIKI_K_MAX,
424
- ],
425
- labelTopTerms:
426
- TurnsConfig.WIKI_LABEL_TOP_TERMS,
427
- restarts: 5,
428
- seed: 0x9e3779b9,
429
- },
430
- seedChunks,
431
- );
432
- createTopicStore(
433
- runtime.currentStateDir,
434
- ).replaceTopicModel(model);
435
- runtime.logger.info("wiki_seed", {
436
- clusterCount: model.k,
437
- sourceChunks: seedChunks.length,
438
- totalChunks: model.totalChunks,
439
- method: "kmeans+tfidf",
440
- });
441
- }
442
- }
443
- }
444
- }
445
- } catch (seedErr) {
446
- runtime.logger.warn("wiki_seed_failed", {
447
- error: String(seedErr),
448
- });
449
- }
450
457
  // S27 Task 6: Fire-and-forget dedup pipeline.
451
458
  // Deduplicates raw_transcript rows for the compacted range.
452
459
  try {
@@ -42,7 +42,11 @@ export function resetRuntimeImpl(
42
42
  self: ResetRuntimeContext,
43
43
  sessionId: string | undefined,
44
44
  ): void {
45
- const sid = normalizeSessionId(sessionId);
45
+ // Only call normalizeSessionId when a real sessionId string is passed.
46
+ // When undefined, keep the existing self.rt.sessionId (set by the real
47
+ // session-start / compact pipeline) so the early-return guard can fire and
48
+ // dashboard stats keyed by sessionId are not silently orphaned.
49
+ const sid = sessionId ? normalizeSessionId(sessionId) : self.rt.sessionId;
46
50
  if (self.rt.sessionId === sid && self.rt.persistedThisSession) return; // same session, keep checkpoint memory
47
51
  self.rt = {
48
52
  sessionId: sid,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.11.11",
3
+ "version": "0.11.13",
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",
@@ -112,7 +112,7 @@ export function gatePromotionGuard(ws: GraphWorkingSet): GateResult {
112
112
  continue;
113
113
  }
114
114
  const epochId = n.epochId;
115
- if (epochId && checkpointIds.has(epochId)) {
115
+ if (epochId && checkpointIds.size > 0 && checkpointIds.has(epochId)) {
116
116
  droppedIds.add(n.id);
117
117
  dropped++;
118
118
  log.warn("graph_orphaned_epoch", {
@@ -51,16 +51,26 @@ export function buildCheckpointNodes(
51
51
  nodes: MemoryGraphNode[],
52
52
  edges: MemoryGraphEdge[],
53
53
  ): void {
54
- const rows = db
55
- .prepare(
56
- `SELECT id, session_id, summary, token_estimate, timestamp,
57
- dedup_status, topic_summary, key_decisions,
58
- normalized_text, embedding_blob
59
- FROM context_chunks
60
- WHERE session_id = ?
61
- ORDER BY timestamp ASC`,
62
- )
63
- .all(sessionId) as Array<Record<string, unknown>>;
54
+ const rows = sessionId
55
+ ? (db
56
+ .prepare(
57
+ `SELECT id, session_id, summary, token_estimate, timestamp,
58
+ dedup_status, topic_summary, key_decisions,
59
+ normalized_text, embedding_blob
60
+ FROM context_chunks
61
+ WHERE session_id = ?
62
+ ORDER BY timestamp ASC`,
63
+ )
64
+ .all(sessionId) as Array<Record<string, unknown>>)
65
+ : (db
66
+ .prepare(
67
+ `SELECT id, session_id, summary, token_estimate, timestamp,
68
+ dedup_status, topic_summary, key_decisions,
69
+ normalized_text, embedding_blob
70
+ FROM context_chunks
71
+ ORDER BY timestamp ASC`,
72
+ )
73
+ .all() as Array<Record<string, unknown>>);
64
74
 
65
75
  if (rows.length === 0) return;
66
76
 
@@ -136,15 +146,24 @@ export function buildTurnNodes(
136
146
  edges: MemoryGraphEdge[],
137
147
  ): void {
138
148
  const existingIds = new Set(nodes.map((n) => n.id));
139
- const rows = db
140
- .prepare(
141
- `SELECT turn_index, role, pressure_band, ctx_tokens, ctx_percent,
142
- epoch_id, ended_at
143
- FROM turns
144
- WHERE session_id = ?
145
- ORDER BY turn_index ASC`,
146
- )
147
- .all(sessionId) as Array<Record<string, unknown>>;
149
+ const rows = sessionId
150
+ ? (db
151
+ .prepare(
152
+ `SELECT turn_index, role, pressure_band, ctx_tokens, ctx_percent,
153
+ epoch_id, ended_at
154
+ FROM turns
155
+ WHERE session_id = ?
156
+ ORDER BY turn_index ASC`,
157
+ )
158
+ .all(sessionId) as Array<Record<string, unknown>>)
159
+ : (db
160
+ .prepare(
161
+ `SELECT turn_index, role, pressure_band, ctx_tokens, ctx_percent,
162
+ epoch_id, ended_at
163
+ FROM turns
164
+ ORDER BY turn_index ASC`,
165
+ )
166
+ .all() as Array<Record<string, unknown>>);
148
167
 
149
168
  if (rows.length === 0) return;
150
169
 
@@ -197,15 +216,24 @@ export function buildTurnContentNodes(
197
216
  // (identity_merge) merges them with the richest nodeType winning. Using the
198
217
  // global nodes array here would skip every node (Source A already added them).
199
218
  const seen = new Set<string>();
200
- const rows = db
201
- .prepare(
202
- `SELECT t.turn_index, t.role, t.ended_at, r.content_bytes
203
- FROM turns t
204
- JOIN raw_transcript r ON r.session_id = t.session_id AND r.turn_index = t.turn_index
205
- WHERE t.session_id = ?
206
- ORDER BY t.turn_index ASC`,
207
- )
208
- .all(sessionId) as Array<Record<string, unknown>>;
219
+ const rows = sessionId
220
+ ? (db
221
+ .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`,
227
+ )
228
+ .all(sessionId) as Array<Record<string, unknown>>)
229
+ : (db
230
+ .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`,
235
+ )
236
+ .all() as Array<Record<string, unknown>>);
209
237
 
210
238
  if (rows.length === 0) return;
211
239
 
@@ -242,8 +242,8 @@ export class SqliteTurnStore implements TurnStore {
242
242
  this.db
243
243
  .prepare(
244
244
  `INSERT INTO turns (conversation_id, session_id, turn_index, role, ended_at,
245
- ctx_tokens, ctx_percent, pressure_band, model)
246
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
245
+ ctx_tokens, ctx_percent, pressure_band, model, epoch_id)
246
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
247
247
  )
248
248
  .run(
249
249
  entry.conversationId,
@@ -255,6 +255,7 @@ export class SqliteTurnStore implements TurnStore {
255
255
  entry.ctxPercent ?? null,
256
256
  entry.pressureBand ?? null,
257
257
  entry.model ?? null,
258
+ entry.epochId ?? null,
258
259
  );
259
260
  } catch (e) {
260
261
  if (
@@ -524,8 +525,8 @@ export class SqliteTurnStore implements TurnStore {
524
525
  this.clear();
525
526
  const insertTurn = this.db.prepare(
526
527
  `INSERT INTO turns (conversation_id, session_id, turn_index, role, ended_at,
527
- ctx_tokens, ctx_percent, pressure_band, model)
528
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
528
+ ctx_tokens, ctx_percent, pressure_band, model, epoch_id)
529
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
529
530
  );
530
531
  const insertRecall = this.db.prepare(
531
532
  `INSERT INTO turn_recall (turn_id, checkpoint_id, score, source, raptor_level)
@@ -553,6 +554,7 @@ export class SqliteTurnStore implements TurnStore {
553
554
  t.ctxPercent ?? null,
554
555
  t.pressureBand ?? null,
555
556
  t.model ?? null,
557
+ t.epochId ?? null,
556
558
  );
557
559
  const row = this.db
558
560
  .prepare(