pi-mega-compact 0.10.1 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/extensions/dashboard-server/routes-turns.js +22 -9
  2. package/dist/extensions/mega-events/session-handlers.js +1 -1
  3. package/dist/src/store/sqlite/schema.js +8 -5
  4. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DUulvXUY.js → AchievementTiles-DZp_xQOW.js} +2 -2
  5. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DUulvXUY.js.map → AchievementTiles-DZp_xQOW.js.map} +1 -1
  6. package/extensions/dashboard-client/dist/assets/{AchievementsTab-Dx0YtCNf.js → AchievementsTab-BzFHpAHM.js} +2 -2
  7. package/extensions/dashboard-client/dist/assets/{AchievementsTab-Dx0YtCNf.js.map → AchievementsTab-BzFHpAHM.js.map} +1 -1
  8. package/extensions/dashboard-client/dist/assets/{CacheTab-DtkaUo1v.js → CacheTab-DEQbTq2T.js} +2 -2
  9. package/extensions/dashboard-client/dist/assets/{CacheTab-DtkaUo1v.js.map → CacheTab-DEQbTq2T.js.map} +1 -1
  10. package/extensions/dashboard-client/dist/assets/{ConfigTab-CLa4KfiZ.js → ConfigTab-DQ4FG33J.js} +2 -2
  11. package/extensions/dashboard-client/dist/assets/{ConfigTab-CLa4KfiZ.js.map → ConfigTab-DQ4FG33J.js.map} +1 -1
  12. package/extensions/dashboard-client/dist/assets/{EventsTab-Q6tEIBBi.js → EventsTab-D5HoEVYo.js} +2 -2
  13. package/extensions/dashboard-client/dist/assets/{EventsTab-Q6tEIBBi.js.map → EventsTab-D5HoEVYo.js.map} +1 -1
  14. package/extensions/dashboard-client/dist/assets/{GameTab-Hq7GNWSY.js → GameTab-Jzsc4yeY.js} +2 -2
  15. package/extensions/dashboard-client/dist/assets/{GameTab-Hq7GNWSY.js.map → GameTab-Jzsc4yeY.js.map} +1 -1
  16. package/extensions/dashboard-client/dist/assets/{MetricsTab-BtmBV6lU.js → MetricsTab-D_XnsEYX.js} +2 -2
  17. package/extensions/dashboard-client/dist/assets/{MetricsTab-BtmBV6lU.js.map → MetricsTab-D_XnsEYX.js.map} +1 -1
  18. package/extensions/dashboard-client/dist/assets/{OverviewTab-CL10PwVU.js → OverviewTab-BMREvAth.js} +2 -2
  19. package/extensions/dashboard-client/dist/assets/{OverviewTab-CL10PwVU.js.map → OverviewTab-BMREvAth.js.map} +1 -1
  20. package/extensions/dashboard-client/dist/assets/{ReposTab-DAwANwbI.js → ReposTab-DuyhOktG.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{ReposTab-DAwANwbI.js.map → ReposTab-DuyhOktG.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{SessionsTab-Ec08TZRQ.js → SessionsTab-mz_vz8i2.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{SessionsTab-Ec08TZRQ.js.map → SessionsTab-mz_vz8i2.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-COb8dpTy.js → TimeSavedCard-B8AIjNTY.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-COb8dpTy.js.map → TimeSavedCard-B8AIjNTY.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{TopicsTab-kvX3F3dY.js → TopicsTab-DKuct4oD.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/TopicsTab-DKuct4oD.js.map +1 -0
  28. package/extensions/dashboard-client/dist/assets/{TurnsTab-CCUE73Aa.js → TurnsTab-11MGLxY8.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/TurnsTab-11MGLxY8.js.map +1 -0
  30. package/extensions/dashboard-client/dist/assets/{index-UMrussFX.js → index-CqLN7STl.js} +3 -3
  31. package/extensions/dashboard-client/dist/assets/{index-UMrussFX.js.map → index-CqLN7STl.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{useSSE-BDHZ7CeS.js → useSSE-CXrQSbVY.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{useSSE-BDHZ7CeS.js.map → useSSE-CXrQSbVY.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/index.html +1 -1
  35. package/extensions/dashboard-client/src/App.tsx +1 -1
  36. package/extensions/dashboard-client/src/api/client.ts +8 -2
  37. package/extensions/dashboard-client/src/tabs/TopicsTab.tsx +2 -1
  38. package/extensions/dashboard-client/src/tabs/TurnsTab.tsx +61 -51
  39. package/extensions/dashboard-server/api-contracts/endpoints.ts +10 -5
  40. package/extensions/dashboard-server/routes-turns.ts +38 -15
  41. package/extensions/mega-events/session-handlers.ts +3 -5
  42. package/package.json +1 -1
  43. package/src/store/sqlite/schema.ts +62 -49
  44. package/extensions/dashboard-client/dist/assets/TopicsTab-kvX3F3dY.js.map +0 -1
  45. package/extensions/dashboard-client/dist/assets/TurnsTab-CCUE73Aa.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import{r as e}from"./index-UMrussFX.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-BDHZ7CeS.js.map
1
+ import{r as e}from"./index-CqLN7STl.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-CXrQSbVY.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useSSE-BDHZ7CeS.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-CXrQSbVY.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-UMrussFX.js"></script>
7
+ <script type="module" crossorigin src="./assets/index-CqLN7STl.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="./assets/index-gn66lYD2.css">
9
9
  </head>
10
10
  <body>
@@ -99,7 +99,7 @@ export default function App(): React.ReactElement {
99
99
  {activeTab === "achievements" && <AchievementsTab />}
100
100
  {activeTab === "sessions" && <SessionsTab />}
101
101
  {activeTab === "topics" && <TopicsTab />}
102
- {activeTab === "turns" && <TurnsTab />}
102
+ {activeTab === "turns" && <TurnsTab />}
103
103
  </React.Suspense>
104
104
  </main>
105
105
  </div>
@@ -218,7 +218,10 @@ export function postFork(
218
218
  conversationId: string,
219
219
  turnIndex: number,
220
220
  ): Promise<ForkResponse> {
221
- return postJson<ForkResponse>(ENDPOINTS.fork.path, { conversationId, turnIndex });
221
+ return postJson<ForkResponse>(ENDPOINTS.fork.path, {
222
+ conversationId,
223
+ turnIndex,
224
+ });
222
225
  }
223
226
 
224
227
  export function postPruneTurns(
@@ -235,6 +238,9 @@ export function fetchTopicMemories(
235
238
  topicId: string,
236
239
  ): Promise<TopicMemoriesResponse> {
237
240
  return getJson<TopicMemoriesResponse>(
238
- ENDPOINTS.topicMemories.path.replace(":topicId", encodeURIComponent(topicId)),
241
+ ENDPOINTS.topicMemories.path.replace(
242
+ ":topicId",
243
+ encodeURIComponent(topicId),
244
+ ),
239
245
  );
240
246
  }
@@ -28,7 +28,8 @@ export default function TopicsTab(): React.ReactElement {
28
28
 
29
29
  const drill = useApi<TopicMemoriesResponse>(
30
30
  useCallback(
31
- () => (drillTopicId ? fetchTopicMemories(drillTopicId) : Promise.reject()),
31
+ () =>
32
+ drillTopicId ? fetchTopicMemories(drillTopicId) : Promise.reject(),
32
33
  [drillTopicId],
33
34
  ),
34
35
  { pollInterval: 0 },
@@ -54,9 +54,12 @@ export default function TurnsTab(): React.ReactElement {
54
54
  data: turns,
55
55
  loading,
56
56
  error,
57
- } = useApi<TurnsResponse>(useCallback(() => fetchTurns(), []), {
58
- pollInterval: 10_000,
59
- });
57
+ } = useApi<TurnsResponse>(
58
+ useCallback(() => fetchTurns(), []),
59
+ {
60
+ pollInterval: 10_000,
61
+ },
62
+ );
60
63
  const { data: intents } = useApi<RewindIntentsResponse>(
61
64
  useCallback(() => fetchTurnIntents(), []),
62
65
  { pollInterval: 5_000 },
@@ -67,22 +70,25 @@ export default function TurnsTab(): React.ReactElement {
67
70
  const [busy, setBusy] = useState<string | null>(null);
68
71
  const [notice, setNotice] = useState<string | null>(null);
69
72
 
70
- const expand = useCallback(async (conversationId: string) => {
71
- if (expanded === conversationId) {
72
- setExpanded(null);
73
+ const expand = useCallback(
74
+ async (conversationId: string) => {
75
+ if (expanded === conversationId) {
76
+ setExpanded(null);
77
+ setDetail(null);
78
+ return;
79
+ }
80
+ setExpanded(conversationId);
81
+ setDetailLoading(true);
73
82
  setDetail(null);
74
- return;
75
- }
76
- setExpanded(conversationId);
77
- setDetailLoading(true);
78
- setDetail(null);
79
- try {
80
- const d = await fetchConversationTurns(conversationId);
81
- setDetail(d);
82
- } finally {
83
- setDetailLoading(false);
84
- }
85
- }, [expanded]);
83
+ try {
84
+ const d = await fetchConversationTurns(conversationId);
85
+ setDetail(d);
86
+ } finally {
87
+ setDetailLoading(false);
88
+ }
89
+ },
90
+ [expanded],
91
+ );
86
92
 
87
93
  const onFork = useCallback(
88
94
  async (conversationId: string, turnIndex: number) => {
@@ -102,18 +108,21 @@ export default function TurnsTab(): React.ReactElement {
102
108
  [],
103
109
  );
104
110
 
105
- const onRewind = useCallback(async (conversationId: string, turnIndex: number) => {
106
- setBusy(`rewind:${conversationId}:${turnIndex}`);
107
- setNotice(null);
108
- try {
109
- await postTurnIntent(conversationId, turnIndex);
110
- setNotice(`Rewind intent queued for turn ${turnIndex}`);
111
- } catch (e) {
112
- setNotice(`Rewind failed: ${(e as Error).message}`);
113
- } finally {
114
- setBusy(null);
115
- }
116
- }, []);
111
+ const onRewind = useCallback(
112
+ async (conversationId: string, turnIndex: number) => {
113
+ setBusy(`rewind:${conversationId}:${turnIndex}`);
114
+ setNotice(null);
115
+ try {
116
+ await postTurnIntent(conversationId, turnIndex);
117
+ setNotice(`Rewind intent queued for turn ${turnIndex}`);
118
+ } catch (e) {
119
+ setNotice(`Rewind failed: ${(e as Error).message}`);
120
+ } finally {
121
+ setBusy(null);
122
+ }
123
+ },
124
+ [],
125
+ );
117
126
 
118
127
  if (error && !turns) {
119
128
  return <div className="tab-stub">Error loading turns: {error.message}</div>;
@@ -146,8 +155,8 @@ export default function TurnsTab(): React.ReactElement {
146
155
  <ul>
147
156
  {intents.intents.map((i) => (
148
157
  <li key={i.id}>
149
- {fmtTs(i.createdAt)} — rewind <code>{i.conversationId}</code>{" "}
150
- to turn {i.targetTurnIndex}
158
+ {fmtTs(i.createdAt)} — rewind <code>{i.conversationId}</code> to
159
+ turn {i.targetTurnIndex}
151
160
  </li>
152
161
  ))}
153
162
  </ul>
@@ -184,21 +193,21 @@ export default function TurnsTab(): React.ReactElement {
184
193
 
185
194
  {expanded && (
186
195
  <section className="turns-detail">
187
- <h4>
188
- Turns in <code>{expanded}</code>
189
- </h4>
190
- {detailLoading ? (
191
- <div className="tab-stub">Loading turns…</div>
192
- ) : detail ? (
193
- <TurnDetail
194
- detail={detail}
195
- busy={busy}
196
- onFork={onFork}
197
- onRewind={onRewind}
198
- />
199
- ) : (
200
- <div className="tab-stub">No detail.</div>
201
- )}
196
+ <h4>
197
+ Turns in <code>{expanded}</code>
198
+ </h4>
199
+ {detailLoading ? (
200
+ <div className="tab-stub">Loading turns…</div>
201
+ ) : detail ? (
202
+ <TurnDetail
203
+ detail={detail}
204
+ busy={busy}
205
+ onFork={onFork}
206
+ onRewind={onRewind}
207
+ />
208
+ ) : (
209
+ <div className="tab-stub">No detail.</div>
210
+ )}
202
211
  </section>
203
212
  )}
204
213
  </div>
@@ -271,7 +280,10 @@ function TurnDetail({
271
280
  <td>
272
281
  {t.ctxTokens ?? "—"}
273
282
  {t.ctxPercent != null && (
274
- <span className="muted"> · {(t.ctxPercent * 100).toFixed(0)}%</span>
283
+ <span className="muted">
284
+ {" "}
285
+ · {(t.ctxPercent * 100).toFixed(0)}%
286
+ </span>
275
287
  )}
276
288
  </td>
277
289
  <td>
@@ -279,9 +291,7 @@ function TurnDetail({
279
291
  {t.pressureBand ?? "—"}
280
292
  </span>
281
293
  </td>
282
- <td>
283
- {t.epochId ? <code>{t.epochId.slice(0, 8)}</code> : "—"}
284
- </td>
294
+ <td>{t.epochId ? <code>{t.epochId.slice(0, 8)}</code> : "—"}</td>
285
295
  <td className="recall-cell">
286
296
  {t.recall.length === 0 ? (
287
297
  <span className="muted">—</span>
@@ -511,35 +511,40 @@ export const ENDPOINTS = {
511
511
  conversationTurns: {
512
512
  method: "GET",
513
513
  path: "/api/turns/conversation/:convId",
514
- description: "Per-turn metrics + injected-checkpoint recall provenance for one conversation.",
514
+ description:
515
+ "Per-turn metrics + injected-checkpoint recall provenance for one conversation.",
515
516
  } as const satisfies EndpointDef<"GET", undefined, ConversationTurnsResponse>,
516
517
 
517
518
  /** GET /api/turns/intents — pending rewind intents (S52A). */
518
519
  turnIntents: {
519
520
  method: "GET",
520
521
  path: "/api/turns/intents",
521
- description: "Pending rewind intents queued by the dashboard for the host to consume.",
522
+ description:
523
+ "Pending rewind intents queued by the dashboard for the host to consume.",
522
524
  } as const satisfies EndpointDef<"GET", undefined, RewindIntentsResponse>,
523
525
 
524
526
  /** POST /api/turns/intent — post a rewind intent. */
525
527
  postTurnIntent: {
526
528
  method: "POST",
527
529
  path: "/api/turns/intent",
528
- description: "Queue a rewind-to-turn-N intent for the host to apply at before_agent_start.",
530
+ description:
531
+ "Queue a rewind-to-turn-N intent for the host to apply at before_agent_start.",
529
532
  } as const satisfies EndpointDef<"POST", PostIntentRequest, unknown>,
530
533
 
531
534
  /** POST /api/fork — fork a conversation at a turn. */
532
535
  fork: {
533
536
  method: "POST",
534
537
  path: "/api/fork",
535
- description: "Branch a child conversation off a parent turn + return the recall set to rehydrate.",
538
+ description:
539
+ "Branch a child conversation off a parent turn + return the recall set to rehydrate.",
536
540
  } as const satisfies EndpointDef<"POST", ForkRequest, ForkResponse>,
537
541
 
538
542
  /** POST /api/turns/prune — admin prune (capability-gated). */
539
543
  pruneTurns: {
540
544
  method: "POST",
541
545
  path: "/api/turns/prune",
542
- description: "Prune old turns (admin capability — dashboard uses asAdmin()).",
546
+ description:
547
+ "Prune old turns (admin capability — dashboard uses asAdmin()).",
543
548
  } as const satisfies EndpointDef<"POST", PruneRequest, PruneTurnsResponse>,
544
549
 
545
550
  /** GET /api/topics/:topicId/memories — wiki topic drill-down (S52). */
@@ -41,7 +41,11 @@ function sendJson(res: ServerResponse, status: number, body: unknown): void {
41
41
  /** Read a capped JSON body; returns { ok, value } or { error }. */
42
42
  function readJsonBody(
43
43
  req: IncomingMessage,
44
- cb: (result: { ok: true; value: Record<string, unknown> } | { ok: false; error: string }) => void,
44
+ cb: (
45
+ result:
46
+ | { ok: true; value: Record<string, unknown> }
47
+ | { ok: false; error: string },
48
+ ) => void,
45
49
  ): void {
46
50
  let body = "";
47
51
  let tooBig = false;
@@ -122,7 +126,10 @@ export function handleTurns(
122
126
  activeConversationId = c.conversationId;
123
127
  }
124
128
  }
125
- sendJson(res, 200, { conversations, activeConversationId } satisfies TurnsResponse);
129
+ sendJson(res, 200, {
130
+ conversations,
131
+ activeConversationId,
132
+ } satisfies TurnsResponse);
126
133
  return true;
127
134
  } catch (e) {
128
135
  sendJson(res, 500, { error: String(e) });
@@ -152,16 +159,19 @@ export function handleTurns(
152
159
  pressureBand: e.pressureBand ?? null,
153
160
  model: e.model ?? null,
154
161
  epochId: e.epochId ?? null,
155
- recall: reader
156
- .listRecallByIndex(convId, e.turnIndex)
157
- .map((r): RecallHit => ({
162
+ recall: reader.listRecallByIndex(convId, e.turnIndex).map(
163
+ (r): RecallHit => ({
158
164
  checkpointId: r.checkpointId,
159
165
  score: r.score,
160
166
  source: r.source,
161
167
  raptorLevel: r.raptorLevel ?? null,
162
- })),
168
+ }),
169
+ ),
163
170
  }));
164
- sendJson(res, 200, { conversationId: convId, turns } satisfies ConversationTurnsResponse);
171
+ sendJson(res, 200, {
172
+ conversationId: convId,
173
+ turns,
174
+ } satisfies ConversationTurnsResponse);
165
175
  return true;
166
176
  } catch (e) {
167
177
  sendJson(res, 500, { error: String(e) });
@@ -194,15 +204,22 @@ export function handleTurns(
194
204
  readJsonBody(req, (result) => {
195
205
  if (!result.ok) return sendJson(res, 400, { error: result.error });
196
206
  const v = result.value;
197
- const conversationId = typeof v.conversationId === "string" ? v.conversationId : "";
198
- const targetTurnIndex = typeof v.targetTurnIndex === "number" ? v.targetTurnIndex : -1;
207
+ const conversationId =
208
+ typeof v.conversationId === "string" ? v.conversationId : "";
209
+ const targetTurnIndex =
210
+ typeof v.targetTurnIndex === "number" ? v.targetTurnIndex : -1;
199
211
  if (!conversationId || targetTurnIndex < 0) {
200
- return sendJson(res, 400, { error: "missing_conversationId_or_targetTurnIndex" });
212
+ return sendJson(res, 400, {
213
+ error: "missing_conversationId_or_targetTurnIndex",
214
+ });
201
215
  }
202
216
  try {
203
217
  const tdb = openTurnStore(ctx.stateDir);
204
218
  const q = openIntentQueue(tdb);
205
- const intent = q.postIntent({ conversationId, targetTurnIndex } satisfies PostIntentRequest);
219
+ const intent = q.postIntent({
220
+ conversationId,
221
+ targetTurnIndex,
222
+ } satisfies PostIntentRequest);
206
223
  sendJson(res, 201, intent);
207
224
  } catch (e) {
208
225
  sendJson(res, 500, { error: String(e) });
@@ -216,10 +233,13 @@ export function handleTurns(
216
233
  readJsonBody(req, (result) => {
217
234
  if (!result.ok) return sendJson(res, 400, { error: result.error });
218
235
  const v = result.value;
219
- const conversationId = typeof v.conversationId === "string" ? v.conversationId : "";
236
+ const conversationId =
237
+ typeof v.conversationId === "string" ? v.conversationId : "";
220
238
  const turnIndex = typeof v.turnIndex === "number" ? v.turnIndex : -1;
221
239
  if (!conversationId || turnIndex < 0) {
222
- return sendJson(res, 400, { error: "missing_conversationId_or_turnIndex" });
240
+ return sendJson(res, 400, {
241
+ error: "missing_conversationId_or_turnIndex",
242
+ });
223
243
  }
224
244
  try {
225
245
  const store = createTurnStore({ stateDir: ctx.stateDir });
@@ -241,9 +261,12 @@ export function handleTurns(
241
261
  readJsonBody(req, (result) => {
242
262
  if (!result.ok) return sendJson(res, 400, { error: result.error });
243
263
  const v = result.value;
244
- const maxTurnAgeMs = typeof v.maxTurnAgeMs === "number" ? v.maxTurnAgeMs : 0;
264
+ const maxTurnAgeMs =
265
+ typeof v.maxTurnAgeMs === "number" ? v.maxTurnAgeMs : 0;
245
266
  const keepMinPerConversation =
246
- typeof v.keepMinPerConversation === "number" ? v.keepMinPerConversation : 50;
267
+ typeof v.keepMinPerConversation === "number"
268
+ ? v.keepMinPerConversation
269
+ : 50;
247
270
  if (maxTurnAgeMs <= 0) {
248
271
  return sendJson(res, 400, { error: "invalid_maxTurnAgeMs" });
249
272
  }
@@ -12,10 +12,7 @@ import {
12
12
  recentUserQuery,
13
13
  WIDGET_KEY,
14
14
  } from "../mega-runtime.js";
15
- import {
16
- doRecall,
17
- doRecallAsync,
18
- } from "../mega-pipeline.js";
15
+ import { doRecall, doRecallAsync } from "../mega-pipeline.js";
19
16
  import { recallMemoriesAndInline } from "../../src/recall.js";
20
17
  import { vectorStats } from "../../src/vectorStore.js";
21
18
  import { openTurnStore } from "../../src/store/turns/connection.js";
@@ -98,7 +95,8 @@ export function registerSessionHandlers(
98
95
  // one-line summary for diagnostics.
99
96
  try {
100
97
  const m = autoMaintain(runtime.currentStateDir);
101
- if (m && !m.endsWith("nothing to do")) runtime.logger.info("db-auto-maintain", { result: m });
98
+ if (m && !m.endsWith("nothing to do"))
99
+ runtime.logger.info("db-auto-maintain", { result: m });
102
100
  } catch (e) {
103
101
  runtime.logger.warn("db-auto-maintain-fail", { error: String(e) });
104
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
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",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * schema.ts — table creation, migrations, `ensureColumn`, PRAGMA setup.
3
3
  */
4
- import { DatabaseSync } from "node:sqlite";
4
+ import type { DatabaseSync } from "node:sqlite";
5
5
  import { ACHIEVEMENT_DEFS } from "../../game/scoring.js";
6
6
 
7
7
  const SCHEMA_VERSION = 2;
@@ -13,14 +13,21 @@ const SCHEMA_VERSION = 2;
13
13
  * input), so the unavoidable identifier interpolation here does not violate
14
14
  * PREVENT-002 (no external data reaches this SQL).
15
15
  */
16
- export function ensureColumn(db: DatabaseSync, table: string, column: string, decl: string): void {
17
- const cols = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
18
- if (cols.some((c) => c.name === column)) return;
19
- db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${decl}`);
16
+ export function ensureColumn(
17
+ db: DatabaseSync,
18
+ table: string,
19
+ column: string,
20
+ decl: string,
21
+ ): void {
22
+ const cols = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{
23
+ name: string;
24
+ }>;
25
+ if (cols.some((c) => c.name === column)) return;
26
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${decl}`);
20
27
  }
21
28
 
22
29
  export function initSchema(db: DatabaseSync): void {
23
- db.exec(`
30
+ db.exec(`
24
31
  CREATE TABLE IF NOT EXISTS context_chunks (
25
32
  id TEXT NOT NULL,
26
33
  session_id TEXT NOT NULL,
@@ -111,10 +118,6 @@ export function initSchema(db: DatabaseSync): void {
111
118
  );
112
119
  CREATE INDEX IF NOT EXISTS idx_raptor_session ON raptor_nodes(session_id);
113
120
  CREATE INDEX IF NOT EXISTS idx_raptor_parent ON raptor_nodes(parent_id);
114
- -- S42D/QA perf: session-scoped built_at ordering so the per-search cache
115
- -- invalidation MAX(built_at) is index-satisfied.
116
- CREATE INDEX IF NOT EXISTS idx_raptor_session_built
117
- ON raptor_nodes(session_id, built_at DESC);
118
121
 
119
122
  -- S42D: structured RAPTOR build history. One row per tree build; the
120
123
  -- freshness check (buildHistory.ts) uses completed_at + leaf_count to skip
@@ -364,45 +367,55 @@ export function initSchema(db: DatabaseSync): void {
364
367
  CREATE INDEX IF NOT EXISTS idx_perf_samples_ts ON perf_samples(ts);
365
368
  CREATE INDEX IF NOT EXISTS idx_perf_samples_kind_ts ON perf_samples(kind, ts);
366
369
  `);
367
- // Idempotent column migrations. `CREATE TABLE IF NOT EXISTS` is a no-op on a
368
- // pre-existing table, so new columns added to context_chunks after a store was
369
- // first created (e.g. original_token_estimate in v0.4.2) must be ALTERed in for
370
- // databases created by an older version — otherwise repoStats()/upsert crash
371
- // with "no such column" and the extension fails to load. Additive only.
372
- ensureColumn(db, "context_chunks", "original_token_estimate", "INTEGER");
373
- // S27 Task 6: content_ref column in raw_transcript for dedup_mirror references.
374
- ensureColumn(db, "raw_transcript", "content_ref", "TEXT");
375
- // S20 memory-RAG extension: additive columns for auto-review ops. Idempotent —
376
- // only alters DBs created by an older version that lack these columns.
377
- ensureColumn(db, "memories", "category", "TEXT");
378
- ensureColumn(db, "memories", "target", "TEXT");
379
- ensureColumn(db, "memories", "last_referenced", "INTEGER");
380
- ensureColumn(db, "memories", "source_turn", "INTEGER");
381
- // S25: RAPTOR freshness-guard timestamp. Additive; old DBs have NULL → 0 →
382
- // treated as stale → flat fallback (safe).
383
- ensureColumn(db, "raptor_nodes", "built_at", "INTEGER");
384
- // S43: turn_index on raw_transcript so a message points directly at its
385
- // conversation turn (otherwise it must be inferred from seq ordering). NULL
386
- // for legacy rows turns written before S43 have no turn link.
387
- ensureColumn(db, "raw_transcript", "turn_index", "INTEGER");
388
- // S43: conversation_id + last_turn_id on session_state (legacy DBs have NULL).
389
- ensureColumn(db, "session_state", "conversation_id", "TEXT");
390
- ensureColumn(db, "session_state", "last_turn_id", "INTEGER");
391
- // S35: idempotent seed of the 9 achievement rows. ON CONFLICT(id) DO
392
- // NOTHING so a re-open never clobbers an already-unlocked row's
393
- // unlocked_at. No user input reaches this SQL (PREVENT-002 safe).
394
- const seedAch = db.prepare(
395
- `INSERT INTO game_achievements (id, title, description, hidden, icon)
370
+ // Idempotent column migrations. `CREATE TABLE IF NOT EXISTS` is a no-op on a
371
+ // pre-existing table, so new columns added to context_chunks after a store was
372
+ // first created (e.g. original_token_estimate in v0.4.2) must be ALTERed in for
373
+ // databases created by an older version — otherwise repoStats()/upsert crash
374
+ // with "no such column" and the extension fails to load. Additive only.
375
+ ensureColumn(db, "context_chunks", "original_token_estimate", "INTEGER");
376
+ // S27 Task 6: content_ref column in raw_transcript for dedup_mirror references.
377
+ ensureColumn(db, "raw_transcript", "content_ref", "TEXT");
378
+ // S20 memory-RAG extension: additive columns for auto-review ops. Idempotent —
379
+ // only alters DBs created by an older version that lack these columns.
380
+ ensureColumn(db, "memories", "category", "TEXT");
381
+ ensureColumn(db, "memories", "target", "TEXT");
382
+ ensureColumn(db, "memories", "last_referenced", "INTEGER");
383
+ ensureColumn(db, "memories", "source_turn", "INTEGER");
384
+ // S25: RAPTOR freshness-guard timestamp. Additive; old DBs have NULL → 0 →
385
+ // treated as stale → flat fallback (safe).
386
+ ensureColumn(db, "raptor_nodes", "built_at", "INTEGER");
387
+ // S42D/QA perf: session-scoped built_at ordering. Must follow ensureColumn
388
+ // above old DBs already have raptor_nodes without built_at, so CREATE TABLE
389
+ // IF NOT EXISTS is a no-op that doesn't add the column, and moving this index
390
+ // inside the DDL block caused "no such column: built_at" on upgrade.
391
+ db.exec(
392
+ "CREATE INDEX IF NOT EXISTS idx_raptor_session_built ON raptor_nodes(session_id, built_at DESC)",
393
+ );
394
+ // S43: turn_index on raw_transcript so a message points directly at its
395
+ // conversation turn (otherwise it must be inferred from seq ordering). NULL
396
+ // for legacy rows turns written before S43 have no turn link.
397
+ ensureColumn(db, "raw_transcript", "turn_index", "INTEGER");
398
+ // S43: conversation_id + last_turn_id on session_state (legacy DBs have NULL).
399
+ ensureColumn(db, "session_state", "conversation_id", "TEXT");
400
+ ensureColumn(db, "session_state", "last_turn_id", "INTEGER");
401
+ // S35: idempotent seed of the 9 achievement rows. ON CONFLICT(id) DO
402
+ // NOTHING so a re-open never clobbers an already-unlocked row's
403
+ // unlocked_at. No user input reaches this SQL (PREVENT-002 safe).
404
+ const seedAch = db.prepare(
405
+ `INSERT INTO game_achievements (id, title, description, hidden, icon)
396
406
  VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING`,
397
- );
398
- for (const d of ACHIEVEMENT_DEFS) {
399
- seedAch.run(d.id, d.title, d.description, d.hidden ? 1 : 0, d.icon);
400
- }
407
+ );
408
+ for (const d of ACHIEVEMENT_DEFS) {
409
+ seedAch.run(d.id, d.title, d.description, d.hidden ? 1 : 0, d.icon);
410
+ }
401
411
 
402
- const v = db.prepare("SELECT value FROM meta WHERE key='schema_version'").get() as
403
- | { value: string }
404
- | undefined;
405
- if (!v) {
406
- db.prepare("INSERT INTO meta(key, value) VALUES(?, ?)").run("schema_version", String(SCHEMA_VERSION));
407
- }
412
+ const v = db
413
+ .prepare("SELECT value FROM meta WHERE key='schema_version'")
414
+ .get() as { value: string } | undefined;
415
+ if (!v) {
416
+ db.prepare("INSERT INTO meta(key, value) VALUES(?, ?)").run(
417
+ "schema_version",
418
+ String(SCHEMA_VERSION),
419
+ );
420
+ }
408
421
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"TopicsTab-kvX3F3dY.js","sources":["../../src/tabs/TopicsTab.tsx"],"sourcesContent":["/**\n * dashboard-client/src/tabs/TopicsTab.tsx — Auto-categorizing wiki (S51 + S52 polish).\n *\n * Fetches /api/topics and renders the k-means + TF-IDF topic clusters. S52 polish\n * adds: (1) a search box filtering topics by label / discriminative term, and\n * (2) a topic drill-down — click a topic to load its member memories\n * (/api/topics/:id/memories). Read-only; no write operations.\n */\n\nimport type React from \"react\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport { useApi } from \"../hooks/useApi\";\nimport { fetchTopics, fetchTopicMemories } from \"../api/client\";\nimport type { TopicsResponse, TopicMemoriesResponse } from \"@contracts\";\n\nfunction fmtTs(ms: number | null): string {\n\tif (!ms) return \"—\";\n\treturn new Date(ms).toLocaleString();\n}\n\nexport default function TopicsTab(): React.ReactElement {\n\tconst { data, loading, error } = useApi<TopicsResponse>(\n\t\tuseCallback(() => fetchTopics(), []),\n\t\t{ pollInterval: 30_000 },\n\t);\n\tconst [query, setQuery] = useState(\"\");\n\tconst [drillTopicId, setDrillTopicId] = useState<string | null>(null);\n\n\tconst drill = useApi<TopicMemoriesResponse>(\n\t\tuseCallback(\n\t\t\t() => (drillTopicId ? fetchTopicMemories(drillTopicId) : Promise.reject()),\n\t\t\t[drillTopicId],\n\t\t),\n\t\t{ pollInterval: 0 },\n\t);\n\n\tconst filtered = useMemo(() => {\n\t\tif (!data) return [];\n\t\tconst q = query.trim().toLowerCase();\n\t\tif (!q) return data.topics;\n\t\treturn data.topics.filter(\n\t\t\t(t) =>\n\t\t\t\tt.label.toLowerCase().includes(q) ||\n\t\t\t\tt.termScore.some((s) => s.term.toLowerCase().includes(q)),\n\t\t);\n\t}, [data, query]);\n\n\tif (error && !data) {\n\t\treturn (\n\t\t\t<div className=\"tab-stub\">Error loading topics: {error.message}</div>\n\t\t);\n\t}\n\tif (loading && !data) {\n\t\treturn <div className=\"tab-stub\">Loading topics…</div>;\n\t}\n\tif (!data) {\n\t\treturn <div className=\"tab-stub\">No topic data available.</div>;\n\t}\n\n\tif (data.totalTopics === 0) {\n\t\treturn (\n\t\t\t<div className=\"tab-stub\">\n\t\t\t\t<h3>Wiki</h3>\n\t\t\t\t<p>\n\t\t\t\t\tNo topics yet. Topics are auto-generated after every 10th compaction\n\t\t\t\t\tfrom real memory embeddings (k-means + TF-IDF).\n\t\t\t\t</p>\n\t\t\t\t<p>Check back after a few more compaction cycles.</p>\n\t\t\t</div>\n\t\t);\n\t}\n\n\treturn (\n\t\t<div className=\"tab-content topics-tab\">\n\t\t\t<h3>Wiki</h3>\n\t\t\t<p className=\"subtitle\">\n\t\t\t\t{data.totalTopics} topic{data.totalTopics !== 1 ? \"s\" : \"\"} ·{\" \"}\n\t\t\t\t{data.totalAssigned} assigned memori\n\t\t\t\t{data.totalAssigned !== 1 ? \"es\" : \"y\"}\n\t\t\t\t{data.lastRebuildAt != null && (\n\t\t\t\t\t<> · last rebuild {new Date(data.lastRebuildAt).toLocaleString()}</>\n\t\t\t\t)}\n\t\t\t</p>\n\n\t\t\t<input\n\t\t\t\ttype=\"search\"\n\t\t\t\tclassName=\"topics-search\"\n\t\t\t\tplaceholder=\"Filter topics by label or term…\"\n\t\t\t\tvalue={query}\n\t\t\t\tonChange={(e) => setQuery(e.target.value)}\n\t\t\t/>\n\n\t\t\t{drillTopicId && (\n\t\t\t\t<section className=\"topic-drilldown\">\n\t\t\t\t\t<h4>\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclassName=\"link-btn\"\n\t\t\t\t\t\t\tonClick={() => setDrillTopicId(null)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t← back\n\t\t\t\t\t\t</button>{\" \"}\n\t\t\t\t\t\tTopic <code>{drillTopicId}</code>\n\t\t\t\t\t\t{drill.data && <> · {drill.data.label}</>}\n\t\t\t\t\t\t{drill.data && <> · {drill.data.assignments.length} memories</>}\n\t\t\t\t\t</h4>\n\t\t\t\t\t{drill.error ? (\n\t\t\t\t\t\t<div className=\"tab-stub\">\n\t\t\t\t\t\t\tError: {(drill.error as Error).message}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t) : drill.loading && !drill.data ? (\n\t\t\t\t\t\t<div className=\"tab-stub\">Loading memories…</div>\n\t\t\t\t\t) : drill.data ? (\n\t\t\t\t\t\t<table className=\"data-table\">\n\t\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t<th>Memory id</th>\n\t\t\t\t\t\t\t\t\t<th>Confidence</th>\n\t\t\t\t\t\t\t\t\t<th>Assigned</th>\n\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t{drill.data.assignments.map((a) => (\n\t\t\t\t\t\t\t\t\t<tr key={a.memoryId}>\n\t\t\t\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\t\t\t\t<code>{a.memoryId}</code>\n\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\t\t\t\t{a.confidence != null ? a.confidence.toFixed(2) : \"—\"}\n\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t\t<td>{fmtTs(a.assignedAt)}</td>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t</table>\n\t\t\t\t\t) : null}\n\t\t\t\t</section>\n\t\t\t)}\n\n\t\t\t{!drillTopicId && (\n\t\t\t\t<table className=\"data-table\">\n\t\t\t\t\t<thead>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<th>Label</th>\n\t\t\t\t\t\t\t<th>Memories</th>\n\t\t\t\t\t\t\t<th>Top Terms</th>\n\t\t\t\t\t\t\t<th></th>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</thead>\n\t\t\t\t\t<tbody>\n\t\t\t\t\t\t{filtered.map((t) => (\n\t\t\t\t\t\t\t<tr\n\t\t\t\t\t\t\t\tkey={t.id}\n\t\t\t\t\t\t\t\tclassName=\"topic-row\"\n\t\t\t\t\t\t\t\tonClick={() => setDrillTopicId(t.id)}\n\t\t\t\t\t\t\t\tstyle={{ cursor: \"pointer\" }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<td className=\"topic-label\">{t.label}</td>\n\t\t\t\t\t\t\t\t<td className=\"topic-count\">{t.memoryCount}</td>\n\t\t\t\t\t\t\t\t<td className=\"topic-terms\">\n\t\t\t\t\t\t\t\t\t{t.termScore\n\t\t\t\t\t\t\t\t\t\t.slice(0, 8)\n\t\t\t\t\t\t\t\t\t\t.map((s) => s.term)\n\t\t\t\t\t\t\t\t\t\t.join(\", \")}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\t\t<button type=\"button\" className=\"mini-btn\">\n\t\t\t\t\t\t\t\t\t\tdrill\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t))}\n\t\t\t\t\t\t{filtered.length === 0 && (\n\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t<td colSpan={4} className=\"muted\">\n\t\t\t\t\t\t\t\t\tNo topics match “{query}”.\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</tbody>\n\t\t\t\t</table>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n"],"names":["fmtTs","ms","TopicsTab","data","loading","error","useApi","useCallback","fetchTopics","query","setQuery","useState","drillTopicId","setDrillTopicId","drill","fetchTopicMemories","filtered","useMemo","q","t","s","jsxs","jsx","Fragment","e","a"],"mappings":"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"}