pi-mega-compact 0.11.3 → 0.11.5

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 (307) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +19 -0
  2. package/dist/extensions/dashboard-server/api-contracts/memory.js +8 -0
  3. package/dist/extensions/dashboard-server/api-contracts/provider-cache.js +1 -0
  4. package/dist/extensions/dashboard-server/api-contracts/setup.js +10 -0
  5. package/dist/extensions/dashboard-server/index-reader.js +6 -2
  6. package/dist/extensions/dashboard-server/routes-cache.js +115 -55
  7. package/dist/extensions/dashboard-server/routes-memory.js +51 -0
  8. package/dist/extensions/dashboard-server/routes-repo.js +2 -2
  9. package/dist/extensions/dashboard-server/routes-setup.js +162 -0
  10. package/dist/extensions/dashboard-server/routes.js +2 -0
  11. package/dist/extensions/dashboard-server/server.js +7 -1
  12. package/dist/extensions/mega-commands.js +189 -1
  13. package/dist/extensions/mega-config.js +3 -0
  14. package/dist/extensions/mega-events/agent-handlers.js +41 -6
  15. package/dist/extensions/mega-events/compact-handlers.js +1 -0
  16. package/dist/extensions/mega-events/context-handler.js +37 -92
  17. package/dist/extensions/mega-events/context-hwm.js +51 -0
  18. package/dist/extensions/mega-events/error-classifier.js +118 -128
  19. package/dist/extensions/mega-events/mirror-append.js +107 -0
  20. package/dist/extensions/mega-events/outage-advisor.js +59 -0
  21. package/dist/extensions/mega-events/perf-handler.js +51 -1
  22. package/dist/extensions/mega-events/recall-tail.js +59 -0
  23. package/dist/extensions/mega-events/session-handlers.js +15 -8
  24. package/dist/extensions/mega-runtime/reset-runtime.js +6 -0
  25. package/dist/extensions/mega-runtime/runtime.js +7 -0
  26. package/dist/src/embedder.js +41 -2
  27. package/dist/src/memoryStats.js +75 -0
  28. package/dist/src/prefix-break.js +43 -0
  29. package/dist/src/pricing.js +1 -1
  30. package/dist/src/store/sqlite/perf-samples.js +144 -16
  31. package/dist/src/store/turns/memory-store.js +8 -0
  32. package/dist/src/store/turns/sqlite-store.js +14 -3
  33. package/dist/src/store/turns/types.js +18 -1
  34. package/dist/src/store/vectorIndex.js +26 -5
  35. package/extensions/dashboard-client/dist/assets/{AchievementTiles-vEo2MtE2.js → AchievementTiles-BoL7qBOJ.js} +2 -2
  36. package/extensions/dashboard-client/dist/assets/{AchievementTiles-vEo2MtE2.js.map → AchievementTiles-BoL7qBOJ.js.map} +1 -1
  37. package/extensions/dashboard-client/dist/assets/AchievementsTab-BFDE3HYc.js +2 -0
  38. package/extensions/dashboard-client/dist/assets/{AchievementsTab-DjxfYrDc.js.map → AchievementsTab-BFDE3HYc.js.map} +1 -1
  39. package/extensions/dashboard-client/dist/assets/CacheTab-eKVc-NV1.js +2 -0
  40. package/extensions/dashboard-client/dist/assets/CacheTab-eKVc-NV1.js.map +1 -0
  41. package/extensions/dashboard-client/dist/assets/{ConfigTab-Cm6ymbmx.js → ConfigTab-DDFE-ckA.js} +2 -2
  42. package/extensions/dashboard-client/dist/assets/{ConfigTab-Cm6ymbmx.js.map → ConfigTab-DDFE-ckA.js.map} +1 -1
  43. package/extensions/dashboard-client/dist/assets/{EventsTab-DXWl_YzI.js → EventsTab-B6UxZ7PM.js} +2 -2
  44. package/extensions/dashboard-client/dist/assets/{EventsTab-DXWl_YzI.js.map → EventsTab-B6UxZ7PM.js.map} +1 -1
  45. package/extensions/dashboard-client/dist/assets/{GameTab-BLDudcO5.js → GameTab-DehUX-KE.js} +2 -2
  46. package/extensions/dashboard-client/dist/assets/{GameTab-BLDudcO5.js.map → GameTab-DehUX-KE.js.map} +1 -1
  47. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DLkKCztn.js → MaintenanceTab-0GWRt4wn.js} +2 -2
  48. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DLkKCztn.js.map → MaintenanceTab-0GWRt4wn.js.map} +1 -1
  49. package/extensions/dashboard-client/dist/assets/{MetricsTab-Cq4u-rZF.js → MetricsTab-C3rwMVsN.js} +2 -2
  50. package/extensions/dashboard-client/dist/assets/{MetricsTab-Cq4u-rZF.js.map → MetricsTab-C3rwMVsN.js.map} +1 -1
  51. package/extensions/dashboard-client/dist/assets/OverviewTab-39O9_x5j.js +2 -0
  52. package/extensions/dashboard-client/dist/assets/{OverviewTab-D4kGvsiQ.js.map → OverviewTab-39O9_x5j.js.map} +1 -1
  53. package/extensions/dashboard-client/dist/assets/ReposTab-Cfj6Q5sH.js +2 -0
  54. package/extensions/dashboard-client/dist/assets/ReposTab-Cfj6Q5sH.js.map +1 -0
  55. package/extensions/dashboard-client/dist/assets/{SessionsTab-DZC2-GCn.js → SessionsTab-DXDTCi3r.js} +2 -2
  56. package/extensions/dashboard-client/dist/assets/{SessionsTab-DZC2-GCn.js.map → SessionsTab-DXDTCi3r.js.map} +1 -1
  57. package/extensions/dashboard-client/dist/assets/SetupTab-mMlfunJa.js +2 -0
  58. package/extensions/dashboard-client/dist/assets/SetupTab-mMlfunJa.js.map +1 -0
  59. package/extensions/dashboard-client/dist/assets/TimeSavedCard-Cw1dJ4e9.js +2 -0
  60. package/extensions/dashboard-client/dist/assets/TimeSavedCard-Cw1dJ4e9.js.map +1 -0
  61. package/extensions/dashboard-client/dist/assets/{TopicsTab-01G71hv1.js → TopicsTab-BAw972BV.js} +2 -2
  62. package/extensions/dashboard-client/dist/assets/{TopicsTab-01G71hv1.js.map → TopicsTab-BAw972BV.js.map} +1 -1
  63. package/extensions/dashboard-client/dist/assets/{TurnsTab-DLRg0LZT.js → TurnsTab-Cf5mGIdl.js} +2 -2
  64. package/extensions/dashboard-client/dist/assets/{TurnsTab-DLRg0LZT.js.map → TurnsTab-Cf5mGIdl.js.map} +1 -1
  65. package/extensions/dashboard-client/dist/assets/format-COjDIVGj.js +2 -0
  66. package/extensions/dashboard-client/dist/assets/format-COjDIVGj.js.map +1 -0
  67. package/extensions/dashboard-client/dist/assets/{index-BX0bcgLu.js → index-4gQ7auad.js} +10 -10
  68. package/extensions/dashboard-client/dist/assets/index-4gQ7auad.js.map +1 -0
  69. package/extensions/dashboard-client/dist/assets/pricing-CvE4NllQ.js +2 -0
  70. package/extensions/dashboard-client/dist/assets/pricing-CvE4NllQ.js.map +1 -0
  71. package/extensions/dashboard-client/dist/assets/{useSSE-BBYt5UPB.js → useSSE-DNgMuyP6.js} +2 -2
  72. package/extensions/dashboard-client/dist/assets/{useSSE-BBYt5UPB.js.map → useSSE-DNgMuyP6.js.map} +1 -1
  73. package/extensions/dashboard-client/dist/index.html +1 -1
  74. package/extensions/dashboard-client/src/App.tsx +4 -0
  75. package/extensions/dashboard-client/src/api/client.ts +14 -0
  76. package/extensions/dashboard-client/src/components/ActiveReposTable.tsx +21 -12
  77. package/extensions/dashboard-client/src/components/ProviderCacheCard.tsx +56 -0
  78. package/extensions/dashboard-client/src/components/SavingsByModelTable.tsx +38 -57
  79. package/extensions/dashboard-client/src/tabs/CacheTab.tsx +1 -0
  80. package/extensions/dashboard-client/src/tabs/SetupTab.tsx +355 -0
  81. package/extensions/dashboard-client/tsconfig.json +3 -2
  82. package/extensions/dashboard-client/vite.config.ts +2 -0
  83. package/extensions/dashboard-server/api-contracts/endpoints.ts +36 -3
  84. package/extensions/dashboard-server/api-contracts/index.ts +7 -0
  85. package/extensions/dashboard-server/api-contracts/memory.ts +27 -0
  86. package/extensions/dashboard-server/api-contracts/multi-repo.ts +231 -231
  87. package/extensions/dashboard-server/api-contracts/provider-cache.ts +37 -0
  88. package/extensions/dashboard-server/api-contracts/setup.ts +58 -0
  89. package/extensions/dashboard-server/index-reader.ts +138 -121
  90. package/extensions/dashboard-server/routes-cache.ts +161 -85
  91. package/extensions/dashboard-server/routes-memory.ts +63 -0
  92. package/extensions/dashboard-server/routes-repo.ts +36 -36
  93. package/extensions/dashboard-server/routes-setup.ts +181 -0
  94. package/extensions/dashboard-server/routes.ts +2 -0
  95. package/extensions/dashboard-server/server.ts +6 -0
  96. package/extensions/mega-commands.ts +211 -1
  97. package/extensions/mega-config.ts +18 -0
  98. package/extensions/mega-events/agent-handlers.ts +44 -5
  99. package/extensions/mega-events/compact-handlers.ts +1 -0
  100. package/extensions/mega-events/context-handler.ts +45 -110
  101. package/extensions/mega-events/context-hwm.ts +85 -0
  102. package/extensions/mega-events/error-classifier.ts +156 -124
  103. package/extensions/mega-events/mirror-append.ts +155 -0
  104. package/extensions/mega-events/outage-advisor.ts +79 -0
  105. package/extensions/mega-events/perf-handler.ts +79 -5
  106. package/extensions/mega-events/recall-tail.ts +88 -0
  107. package/extensions/mega-events/session-handlers.ts +14 -7
  108. package/extensions/mega-runtime/helpers.ts +14 -0
  109. package/extensions/mega-runtime/reset-runtime.ts +6 -0
  110. package/extensions/mega-runtime/runtime.ts +7 -0
  111. package/package.json +5 -2
  112. package/src/embedder.ts +42 -2
  113. package/src/memoryStats.ts +107 -0
  114. package/src/prefix-break.ts +83 -0
  115. package/src/pricing.ts +2 -2
  116. package/src/store/sqlite/perf-samples.ts +215 -22
  117. package/src/store/turns/memory-store.ts +10 -1
  118. package/src/store/turns/sqlite-store.ts +27 -15
  119. package/src/store/turns/types.ts +21 -0
  120. package/src/store/vectorIndex.ts +33 -4
  121. package/dist/extensions/conflict-scan.test.js +0 -115
  122. package/dist/extensions/dashboard-server/api-contracts.test.js +0 -884
  123. package/dist/extensions/dashboard-server/html.test.js +0 -52
  124. package/dist/extensions/dashboard-server/perf-server.test.js +0 -80
  125. package/dist/extensions/dashboard-server/routes-cache.test.js +0 -248
  126. package/dist/extensions/dashboard-server/server.test.js +0 -120
  127. package/dist/extensions/dashboard-server-s32.test.js +0 -181
  128. package/dist/extensions/dashboard-server.test.js +0 -339
  129. package/dist/extensions/mega-cache-replay.test.js +0 -400
  130. package/dist/extensions/mega-compact-s38.test.js +0 -820
  131. package/dist/extensions/mega-compact.test.js +0 -1112
  132. package/dist/extensions/mega-events.test.js +0 -47
  133. package/dist/extensions/mega-game-cmds.test.js +0 -163
  134. package/dist/extensions/mega-runtime/snapshot.test.js +0 -127
  135. package/dist/extensions/mega-runtime/state.test.js +0 -266
  136. package/dist/extensions/mega-runtime/widget.test.js +0 -343
  137. package/dist/extensions/mega-shutdown-widget.test.js +0 -121
  138. package/dist/extensions/mega-teamrun.test.js +0 -156
  139. package/dist/extensions/mega-trim.test.js +0 -58
  140. package/dist/extensions/mega-turn-store.test.js +0 -68
  141. package/dist/src/boundary.test.js +0 -170
  142. package/dist/src/compact.test.js +0 -78
  143. package/dist/src/config/themes.test.js +0 -94
  144. package/dist/src/dedup/dedup.test.js +0 -41
  145. package/dist/src/dedup/l1.test.js +0 -50
  146. package/dist/src/dedup/raptor/buildHistory.test.js +0 -292
  147. package/dist/src/dedup/raptor/multilevel-serve.test.js +0 -229
  148. package/dist/src/dedup/raptor/multilevel.test.js +0 -238
  149. package/dist/src/dedup/raptor/promote.test.js +0 -69
  150. package/dist/src/dedup/raptor/raptor.test.js +0 -248
  151. package/dist/src/dedup/raptor/retrieval.test.js +0 -95
  152. package/dist/src/dedup/raptor/serve-gate.test.js +0 -298
  153. package/dist/src/dedup/sprint12.test.js +0 -219
  154. package/dist/src/dedup-engine.test.js +0 -472
  155. package/dist/src/driftDetection.test.js +0 -87
  156. package/dist/src/e2e.test.js +0 -698
  157. package/dist/src/engine.test.js +0 -111
  158. package/dist/src/extractive.test.js +0 -147
  159. package/dist/src/fork.test.js +0 -81
  160. package/dist/src/game/scoring.test.js +0 -98
  161. package/dist/src/httpEmbedder.test.js +0 -277
  162. package/dist/src/importance.test.js +0 -372
  163. package/dist/src/intent.test.js +0 -78
  164. package/dist/src/log.test.js +0 -42
  165. package/dist/src/mechanical-fix.test.js +0 -65
  166. package/dist/src/memory.test.js +0 -70
  167. package/dist/src/memoryConsolidate.test.js +0 -38
  168. package/dist/src/memoryOps.test.js +0 -129
  169. package/dist/src/memoryRecall.test.js +0 -171
  170. package/dist/src/memoryRoundtrip.test.js +0 -137
  171. package/dist/src/metrics/turns.test.js +0 -152
  172. package/dist/src/mirror/mirror.test.js +0 -185
  173. package/dist/src/raptor-inject-summaries.test.js +0 -162
  174. package/dist/src/ratio.bench.test.js +0 -897
  175. package/dist/src/recall.integration.test.js +0 -77
  176. package/dist/src/recall.test.js +0 -379
  177. package/dist/src/sprint14.test.js +0 -219
  178. package/dist/src/sprint4x-rag-verification.test.js +0 -93
  179. package/dist/src/store/compression.test.js +0 -139
  180. package/dist/src/store/memoryIndex.test.js +0 -51
  181. package/dist/src/store/migrate.test.js +0 -139
  182. package/dist/src/store/pgOpenGuard.test.js +0 -74
  183. package/dist/src/store/phase01.test.js +0 -81
  184. package/dist/src/store/phase04.test.js +0 -54
  185. package/dist/src/store/sprint10.test.js +0 -186
  186. package/dist/src/store/sqlite/game-achievements.test.js +0 -67
  187. package/dist/src/store/sqlite/game-scores.test.js +0 -106
  188. package/dist/src/store/sqlite/game-state.test.js +0 -76
  189. package/dist/src/store/sqlite/global-index.test.js +0 -136
  190. package/dist/src/store/sqlite/mechanical-fix.test.js +0 -146
  191. package/dist/src/store/sqlite/perf-samples.test.js +0 -146
  192. package/dist/src/store/sqlite/raptor.test.js +0 -106
  193. package/dist/src/store/sqlite/turns.test.js +0 -183
  194. package/dist/src/store/sqlite.cachehit.test.js +0 -55
  195. package/dist/src/store/sqlite.dbmaint.test.js +0 -158
  196. package/dist/src/store/sqlite.dbmirror.test.js +0 -184
  197. package/dist/src/store/turns/contract-compliance.test.js +0 -428
  198. package/dist/src/store/turns/memory-store.test.js +0 -9
  199. package/dist/src/store/turns/migrations.test.js +0 -138
  200. package/dist/src/store/turns/sqlite-store.test.js +0 -144
  201. package/dist/src/store/vectorIndex.test.js +0 -123
  202. package/dist/src/store.test.js +0 -161
  203. package/dist/src/supersede.test.js +0 -36
  204. package/dist/src/topics/cluster.test.js +0 -150
  205. package/dist/src/topics/store.test.js +0 -117
  206. package/dist/src/types.test.js +0 -9
  207. package/dist/src/vector-search-cache.test.js +0 -157
  208. package/dist/src/vectorStore.test.js +0 -479
  209. package/dist/src/wiki.test.js +0 -171
  210. package/extensions/conflict-scan.test.ts +0 -129
  211. package/extensions/dashboard-client/dist/assets/AchievementsTab-DjxfYrDc.js +0 -2
  212. package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js +0 -2
  213. package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js.map +0 -1
  214. package/extensions/dashboard-client/dist/assets/OverviewTab-D4kGvsiQ.js +0 -2
  215. package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js +0 -2
  216. package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js.map +0 -1
  217. package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js +0 -2
  218. package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js.map +0 -1
  219. package/extensions/dashboard-client/dist/assets/index-BX0bcgLu.js.map +0 -1
  220. package/extensions/dashboard-server/api-contracts.test.ts +0 -1065
  221. package/extensions/dashboard-server/html.test.ts +0 -62
  222. package/extensions/dashboard-server/perf-server.test.ts +0 -101
  223. package/extensions/dashboard-server/routes-cache.test.ts +0 -302
  224. package/extensions/dashboard-server/server.test.ts +0 -131
  225. package/extensions/dashboard-server-s32.test.ts +0 -195
  226. package/extensions/dashboard-server.test.ts +0 -382
  227. package/extensions/mega-cache-replay.test.ts +0 -526
  228. package/extensions/mega-compact-s38.test.ts +0 -872
  229. package/extensions/mega-compact.test.ts +0 -1509
  230. package/extensions/mega-events.test.ts +0 -55
  231. package/extensions/mega-game-cmds.test.ts +0 -197
  232. package/extensions/mega-runtime/snapshot.test.ts +0 -139
  233. package/extensions/mega-runtime/state.test.ts +0 -262
  234. package/extensions/mega-runtime/widget.test.ts +0 -468
  235. package/extensions/mega-shutdown-widget.test.ts +0 -141
  236. package/extensions/mega-teamrun.test.ts +0 -176
  237. package/extensions/mega-trim.test.ts +0 -64
  238. package/extensions/mega-turn-store.test.ts +0 -108
  239. package/src/boundary.test.ts +0 -187
  240. package/src/compact.test.ts +0 -99
  241. package/src/config/themes.test.ts +0 -116
  242. package/src/dedup/dedup.test.ts +0 -46
  243. package/src/dedup/l1.test.ts +0 -57
  244. package/src/dedup/raptor/buildHistory.test.ts +0 -353
  245. package/src/dedup/raptor/multilevel-serve.test.ts +0 -273
  246. package/src/dedup/raptor/multilevel.test.ts +0 -325
  247. package/src/dedup/raptor/promote.test.ts +0 -82
  248. package/src/dedup/raptor/raptor.test.ts +0 -297
  249. package/src/dedup/raptor/retrieval.test.ts +0 -118
  250. package/src/dedup/raptor/serve-gate.test.ts +0 -348
  251. package/src/dedup/sprint12.test.ts +0 -242
  252. package/src/dedup-engine.test.ts +0 -670
  253. package/src/driftDetection.test.ts +0 -100
  254. package/src/e2e.test.ts +0 -843
  255. package/src/engine.test.ts +0 -123
  256. package/src/extractive.test.ts +0 -174
  257. package/src/fork.test.ts +0 -94
  258. package/src/game/scoring.test.ts +0 -103
  259. package/src/httpEmbedder.test.ts +0 -286
  260. package/src/importance.test.ts +0 -538
  261. package/src/intent.test.ts +0 -87
  262. package/src/log.test.ts +0 -47
  263. package/src/mechanical-fix.test.ts +0 -70
  264. package/src/memory.test.ts +0 -92
  265. package/src/memoryConsolidate.test.ts +0 -47
  266. package/src/memoryOps.test.ts +0 -148
  267. package/src/memoryRecall.test.ts +0 -194
  268. package/src/memoryRoundtrip.test.ts +0 -155
  269. package/src/metrics/turns.test.ts +0 -165
  270. package/src/mirror/mirror.test.ts +0 -240
  271. package/src/raptor-inject-summaries.test.ts +0 -228
  272. package/src/ratio.bench.test.ts +0 -1316
  273. package/src/recall.integration.test.ts +0 -96
  274. package/src/recall.test.ts +0 -467
  275. package/src/sprint14.test.ts +0 -245
  276. package/src/sprint4x-rag-verification.test.ts +0 -119
  277. package/src/store/compression.test.ts +0 -168
  278. package/src/store/memoryIndex.test.ts +0 -61
  279. package/src/store/migrate.test.ts +0 -158
  280. package/src/store/pgOpenGuard.test.ts +0 -89
  281. package/src/store/phase01.test.ts +0 -88
  282. package/src/store/phase04.test.ts +0 -58
  283. package/src/store/sprint10.test.ts +0 -214
  284. package/src/store/sqlite/game-achievements.test.ts +0 -80
  285. package/src/store/sqlite/game-scores.test.ts +0 -132
  286. package/src/store/sqlite/game-state.test.ts +0 -89
  287. package/src/store/sqlite/global-index.test.ts +0 -222
  288. package/src/store/sqlite/mechanical-fix.test.ts +0 -162
  289. package/src/store/sqlite/perf-samples.test.ts +0 -169
  290. package/src/store/sqlite/raptor.test.ts +0 -139
  291. package/src/store/sqlite/turns.test.ts +0 -218
  292. package/src/store/sqlite.cachehit.test.ts +0 -68
  293. package/src/store/sqlite.dbmaint.test.ts +0 -193
  294. package/src/store/sqlite.dbmirror.test.ts +0 -229
  295. package/src/store/turns/contract-compliance.test.ts +0 -562
  296. package/src/store/turns/memory-store.test.ts +0 -15
  297. package/src/store/turns/migrations.test.ts +0 -161
  298. package/src/store/turns/sqlite-store.test.ts +0 -161
  299. package/src/store/vectorIndex.test.ts +0 -140
  300. package/src/store.test.ts +0 -191
  301. package/src/supersede.test.ts +0 -42
  302. package/src/topics/cluster.test.ts +0 -176
  303. package/src/topics/store.test.ts +0 -132
  304. package/src/types.test.ts +0 -10
  305. package/src/vector-search-cache.test.ts +0 -190
  306. package/src/vectorStore.test.ts +0 -533
  307. package/src/wiki.test.ts +0 -217
@@ -24,6 +24,9 @@ export function resetRuntimeImpl(self, sessionId) {
24
24
  dedupAttempts: 0,
25
25
  tokensSaved: 0,
26
26
  lastCompactAt: null,
27
+ lastRecallAt: null,
28
+ lastInjectAt: null,
29
+ _prevCacheHitPct: null,
27
30
  lastNativeCompactAt: null,
28
31
  compactCount: 0,
29
32
  recallInjections: 0,
@@ -38,8 +41,11 @@ export function resetRuntimeImpl(self, sessionId) {
38
41
  lastErrorText: undefined,
39
42
  errorTextRepeatCount: 0,
40
43
  poisonedAdviseSent: false,
44
+ providerOutageAdvised: false,
41
45
  poisonedCompactSignatures: new Set(),
46
+ recallInjectedThisTurn: false,
42
47
  poisonedCount: 0,
48
+ extensionInitiatedTurn: false,
43
49
  };
44
50
  self.trimCache = null; // v0.8.6: never replay a stale trim into a new session
45
51
  self.statusKey = undefined;
@@ -49,6 +49,9 @@ export class MegaRuntime {
49
49
  dedupAttempts: 0,
50
50
  tokensSaved: 0,
51
51
  lastCompactAt: null,
52
+ lastRecallAt: null,
53
+ lastInjectAt: null,
54
+ _prevCacheHitPct: null,
52
55
  lastNativeCompactAt: null,
53
56
  compactCount: 0,
54
57
  recallInjections: 0,
@@ -63,8 +66,12 @@ export class MegaRuntime {
63
66
  lastErrorText: undefined,
64
67
  errorTextRepeatCount: 0,
65
68
  poisonedAdviseSent: false,
69
+ providerOutageAdvised: false,
66
70
  poisonedCompactSignatures: new Set(),
71
+ // S53: tracks whether withRecallTail injected this turn (consumed at turn_end).
72
+ recallInjectedThisTurn: false,
67
73
  poisonedCount: 0,
74
+ extensionInitiatedTurn: false,
68
75
  };
69
76
  // v0.8.6 cache-stability: the cached live-trim view for the current
70
77
  // compaction epoch. Set after a fresh runCompact + computeLiveTrimCut, and
@@ -49,20 +49,60 @@ function fnv1a(str) {
49
49
  * Default embedder: character 3-gram bag-of-counts, hashed into a fixed-dim
50
50
  * vector, L2-normalized. Captures local lexical/structure overlap well enough
51
51
  * for checkpoint relevance ranking.
52
+ *
53
+ * S53B: includes a 256-entry (configurable) FIFO cache keyed by FNV-1a(text).
54
+ * Each cache entry holds a DEFENSIVE COPY of the embedding to prevent mutation
55
+ * from corrupting stored vectors. Set MEGACOMPACT_EMBED_CACHE=0 to disable.
52
56
  */
53
57
  export class TrigramEmbedder {
54
58
  dim;
55
59
  seed;
60
+ /** @type {Map<string, Vector>} */
61
+ _cache;
62
+ _cacheSize;
63
+ _hits = 0;
64
+ _misses = 0;
56
65
  constructor(dim = 512, seed = 0x9e3779b9) {
57
66
  this.dim = dim;
58
67
  this.seed = seed >>> 0;
68
+ const cap = Number(process.env.MEGACOMPACT_EMBED_CACHE ?? "256");
69
+ this._cacheSize = cap > 0 ? Math.floor(cap) : 0;
70
+ this._cache = new Map();
71
+ }
72
+ /** Cache statistics — always { hits: 0, misses: 0 } when cache is disabled. */
73
+ getEmbedCacheStats() {
74
+ return { hits: this._hits, misses: this._misses };
59
75
  }
60
76
  embed(text) {
77
+ if (this._cacheSize === 0) {
78
+ return this._embedRaw(text);
79
+ }
80
+ const key = fnv1a(text).toString(36);
81
+ const cached = this._cache.get(key);
82
+ if (cached !== undefined) {
83
+ this._hits++;
84
+ // Defensive copy: caller mutating the returned vector must not corrupt cache.
85
+ return cached.slice();
86
+ }
87
+ this._misses++;
88
+ const vec = this._embedRaw(text);
89
+ // Store a defensive copy (slice) so caller mutation cannot corrupt stored value.
90
+ this._cache.set(key, vec.slice());
91
+ // FIFO eviction: remove the oldest entry when at capacity.
92
+ if (this._cache.size > this._cacheSize) {
93
+ // Map insertion order is guaranteed — first key is the oldest.
94
+ const firstKey = this._cache.keys().next().value;
95
+ if (firstKey !== undefined)
96
+ this._cache.delete(firstKey);
97
+ }
98
+ return vec;
99
+ }
100
+ /** Raw embed computation (no cache logic). Public so tests can bypass cache. */
101
+ _embedRaw(text) {
61
102
  const vec = new Array(this.dim).fill(0);
62
103
  const norm = text.toLowerCase().replace(/\s+/g, " ");
63
104
  if (norm.length === 0)
64
105
  return l2Normalize(vec);
65
- // Whole-string + word + char-trigram signals.
66
106
  vec[fnv1a(norm) % this.dim] += 1;
67
107
  for (const word of norm.split(" ")) {
68
108
  if (word.length === 0)
@@ -74,7 +114,6 @@ export class TrigramEmbedder {
74
114
  vec[idx] += 1;
75
115
  }
76
116
  }
77
- // Edge: very short tokens still get a slot.
78
117
  if (norm.length < 3)
79
118
  vec[fnv1a(norm) % this.dim] += 1;
80
119
  return l2Normalize(vec);
@@ -0,0 +1,75 @@
1
+ /**
2
+ * memoryStats.ts — aggregate memory-store statistics (S53B).
3
+ *
4
+ * Reads from the durable SQLite memories table to produce:
5
+ * - totalMemories: all memories in the store
6
+ * - memoriesInLast30Days: memories created within the last 30 days
7
+ * - topStableMemories: top-N memories by recall count (desc)
8
+ * - avgRecallScore: mean recallCount across all memories
9
+ *
10
+ * recallCount = 1 if last_recalled_at IS NOT NULL, else 0.
11
+ * This is the durable signal from recallMemory() — a memory that was
12
+ * recalled at least once is "stable" (still relevant). The exact recall
13
+ * frequency is tracked via turn_recall rows (source='memory') for
14
+ * cross-session analysis, but the memories table provides a reliable
15
+ * on-store signal that is always present without a schema migration.
16
+ *
17
+ * Real-store only (PREVENT-PI-004). No mocks. No network.
18
+ */
19
+ import { openStore } from "./store/sqlite/utils.js";
20
+ /**
21
+ * Aggregate memory statistics from the SQLite store.
22
+ * Always returns a valid MemoryStatsResult; never throws.
23
+ */
24
+ export async function memoryStats(stateDir, opts = {}) {
25
+ const topN = opts.topN ?? 5;
26
+ try {
27
+ const db = openStore(stateDir);
28
+ // ── totalMemories ──────────────────────────────────────────────────────
29
+ const totalRow = db
30
+ .prepare("SELECT COUNT(*) AS n FROM memories")
31
+ .get();
32
+ const totalMemories = totalRow.n;
33
+ // ── memoriesInLast30Days ───────────────────────────────────────────────
34
+ const thirtyDaysAgo = Math.floor(Date.now() / 1000) - 30 * 86_400;
35
+ const recentRow = db
36
+ .prepare("SELECT COUNT(*) AS n FROM memories WHERE created_at >= ?")
37
+ .get(thirtyDaysAgo);
38
+ const memoriesInLast30Days = recentRow.n;
39
+ // ── topStableMemories + avgRecallScore ─────────────────────────────────
40
+ // recallCount: 1 if last_recalled_at IS NOT NULL (recalled at least once),
41
+ // 0 otherwise. Stable memories score high.
42
+ const topRows = db
43
+ .prepare(`SELECT
44
+ m.id,
45
+ m.content AS text,
46
+ m.last_recalled_at AS lastRecalledAt,
47
+ CASE WHEN m.last_recalled_at IS NOT NULL THEN 1 ELSE 0 END AS recallCount
48
+ FROM memories m
49
+ ORDER BY recallCount DESC, m.id ASC
50
+ LIMIT ?`)
51
+ .all(topN);
52
+ // avgRecallScore = (count of recalled memories) / totalMemories.
53
+ // This represents the fraction of memories that are "stable" (recalled at least once).
54
+ const recalledRow = db
55
+ .prepare("SELECT COUNT(*) AS n FROM memories WHERE last_recalled_at IS NOT NULL")
56
+ .get();
57
+ const avgRecallScore = totalMemories > 0 ? recalledRow.n / totalMemories : 0;
58
+ const topStableMemories = topRows.map((r) => ({
59
+ id: r.id,
60
+ text: r.text ?? "",
61
+ recallCount: r.recallCount ?? 0,
62
+ lastRecalledAt: r.lastRecalledAt ?? null,
63
+ }));
64
+ return { totalMemories, memoriesInLast30Days, topStableMemories, avgRecallScore };
65
+ }
66
+ catch {
67
+ // Non-fatal: return zeros rather than propagating an error.
68
+ return {
69
+ totalMemories: 0,
70
+ memoriesInLast30Days: 0,
71
+ topStableMemories: [],
72
+ avgRecallScore: 0,
73
+ };
74
+ }
75
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * prefix-break.ts — S53A prefix-break classification.
3
+ *
4
+ * Classifies WHY the provider cache prefix broke on a given turn by comparing
5
+ * the break timestamp against the three candidate event timestamps with a
6
+ * configurable tolerance window.
7
+ *
8
+ * The classification is best-effort (non-fatal): null timestamps are absent,
9
+ * and a break outside all tolerance windows is classified as 'other'.
10
+ *
11
+ * Pi-agnostic: no pi runtime types, no network.
12
+ */
13
+ /**
14
+ * Classify why the provider cache prefix broke at `breakTimestamp`.
15
+ *
16
+ * @param breakTimestamp Wall-clock ms of the cache hit % drop sample.
17
+ * @param events Recent event timestamps from the runtime.
18
+ * @param toleranceMs Max ms gap to consider a match (default 2000).
19
+ * @returns Classification result with cause + confidence.
20
+ *
21
+ * Priority: recall > compaction > inject. Confidence = 1 − (gap / (2·tolerance)),
22
+ * capped at [0.5, 1.0]. Returns 'other' when no event is within tolerance.
23
+ */
24
+ export function classifyPrefixBreak(breakTimestamp, events, toleranceMs = 2000) {
25
+ // Ordered by priority (recall wins if multiple match).
26
+ const candidates = [
27
+ { cause: "recall", ts: events.lastRecallAt },
28
+ { cause: "compaction", ts: events.lastCompactAt },
29
+ { cause: "inject", ts: events.lastInjectAt },
30
+ ];
31
+ for (const { cause, ts } of candidates) {
32
+ if (ts == null)
33
+ continue;
34
+ const gap = Math.abs(breakTimestamp - ts);
35
+ if (gap <= toleranceMs) {
36
+ // confidence = 1 − (gap / (2·tolerance)) ∈ [0.5, 1.0]
37
+ // exact match (gap=0) → 1.0; at boundary (gap=tolerance) → 0.5
38
+ const confidence = Math.max(0.5, 1 - gap / (2 * toleranceMs));
39
+ return { cause, confidence };
40
+ }
41
+ }
42
+ return { cause: "other", confidence: 0 };
43
+ }
@@ -34,7 +34,7 @@ export const MODEL_INPUT_RATES = {
34
34
  "gemini-2.0-flash": 0.1e-6,
35
35
  "deepseek-chat": 0.14e-6,
36
36
  "deepseek-reasoner": 0.55e-6,
37
- "codestral": 0.3e-6,
37
+ codestral: 0.3e-6,
38
38
  "mistral-large": 2e-6,
39
39
  "llama-3.1-405b": 1.33e-6,
40
40
  "llama-3.1-70b": 0.8e-6,
@@ -21,6 +21,7 @@ export const PERF_KINDS = [
21
21
  "provider_latency_ms",
22
22
  "tps",
23
23
  "cache_hit_pct",
24
+ "prefix_break",
24
25
  "rss_mb",
25
26
  "heap_mb",
26
27
  "cpu_user_ms",
@@ -80,20 +81,39 @@ export function readPerfSamples(stateDir = getStateDir(), sinceTs = 0, kind) {
80
81
  return out;
81
82
  }
82
83
  /**
83
- * Read lifetime provider prompt cache aggregates from `perf_samples`.
84
- *
85
- * Aggregates `cache_creation_input_tokens` / `cache_read_input_tokens` from
86
- * the `meta` JSON column of `cache_hit_pct` samples (`json_extract`,
87
- * available in node:sqlite ≥22.13 / SQLite ≥3.38). Returns a zeroed
88
- * `ProviderCacheLifetime` when no samples exist (never undefined).
84
+ * Extract a model label from parsed meta (modelName or modelId), or null.
89
85
  */
90
- export function readProviderCacheLifetime(stateDir = getStateDir()) {
91
- const db = openStore(stateDir);
92
- const rows = db
93
- .prepare(`SELECT ts, meta FROM perf_samples
94
- WHERE kind = ?
95
- ORDER BY ts ASC`)
96
- .all("cache_hit_pct");
86
+ function extractModelLabel(meta) {
87
+ if (typeof meta.modelName === "string" && meta.modelName.length > 0)
88
+ return meta.modelName;
89
+ if (typeof meta.modelId === "string" && meta.modelId.length > 0)
90
+ return meta.modelId;
91
+ return null;
92
+ }
93
+ /**
94
+ * Build sorted byModel array from the model aggregation map.
95
+ */
96
+ function byModelFromMap(map) {
97
+ const out = [];
98
+ for (const [model, m] of map) {
99
+ out.push({
100
+ model,
101
+ hitPct: m.hitCount > 0 ? m.sumHitPct / m.hitCount : 0,
102
+ totalCacheRead: m.sumCr,
103
+ totalCacheWrite: m.sumCw,
104
+ sampleCount: m.samples,
105
+ });
106
+ }
107
+ out.sort((a, b) => b.sampleCount - a.sampleCount);
108
+ return out;
109
+ }
110
+ /**
111
+ * Aggregate `cache_hit_pct` rows (ts + parsed meta) into a
112
+ * `ProviderCacheLifetime`. Shared by the lifetime and window readers so both
113
+ * use the identical aggregation formula. `meta` is parsed defensively
114
+ * (PREVENT-001 null-safe). Rows must be ascending by ts.
115
+ */
116
+ function aggregateCacheRows(rows) {
97
117
  if (rows.length === 0) {
98
118
  return {
99
119
  sampleCount: 0,
@@ -103,6 +123,7 @@ export function readProviderCacheLifetime(stateDir = getStateDir()) {
103
123
  totalInput: 0,
104
124
  firstSampleAt: null,
105
125
  latestSampleAt: null,
126
+ byModel: [],
106
127
  };
107
128
  }
108
129
  let totalCacheRead = 0;
@@ -110,6 +131,7 @@ export function readProviderCacheLifetime(stateDir = getStateDir()) {
110
131
  let totalInput = 0;
111
132
  let sumHitPct = 0;
112
133
  let hitPctCount = 0;
134
+ const modelMap = new Map();
113
135
  for (const r of rows) {
114
136
  if (r.meta != null) {
115
137
  let meta = null;
@@ -140,6 +162,31 @@ export function readProviderCacheLifetime(stateDir = getStateDir()) {
140
162
  sumHitPct += (cr / denom) * 100;
141
163
  hitPctCount++;
142
164
  }
165
+ // Per-model grouping (F4)
166
+ const modelLabel = extractModelLabel(meta);
167
+ if (modelLabel != null) {
168
+ let m = modelMap.get(modelLabel);
169
+ if (!m) {
170
+ m = {
171
+ sumCr: 0,
172
+ sumCw: 0,
173
+ sumInp: 0,
174
+ sumHitPct: 0,
175
+ hitCount: 0,
176
+ samples: 0,
177
+ };
178
+ modelMap.set(modelLabel, m);
179
+ }
180
+ m.sumCr += cr;
181
+ m.sumCw += cw;
182
+ m.sumInp += inp;
183
+ m.samples++;
184
+ if (denom > 0) {
185
+ m.sumHitPct += (cr / denom) * 100;
186
+ m.hitCount++;
187
+ }
188
+ }
189
+ // Untagged samples are omitted from byModel
143
190
  }
144
191
  }
145
192
  }
@@ -151,8 +198,43 @@ export function readProviderCacheLifetime(stateDir = getStateDir()) {
151
198
  totalInput,
152
199
  firstSampleAt: new Date(rows[0].ts).toISOString(),
153
200
  latestSampleAt: new Date(rows[rows.length - 1].ts).toISOString(),
201
+ byModel: byModelFromMap(modelMap),
154
202
  };
155
203
  }
204
+ /**
205
+ * Read lifetime provider prompt cache aggregates from `perf_samples`.
206
+ *
207
+ * Aggregates `cache_creation_input_tokens` / `cache_read_input_tokens` from
208
+ * the `meta` JSON column of `cache_hit_pct` samples (`json_extract`,
209
+ * available in node:sqlite >=22.13 / SQLite >=3.38). Returns a zeroed
210
+ * `ProviderCacheLifetime` when no samples exist (never undefined).
211
+ */
212
+ export function readProviderCacheLifetime(stateDir = getStateDir()) {
213
+ const db = openStore(stateDir);
214
+ const rows = db
215
+ .prepare(`SELECT ts, meta FROM perf_samples
216
+ WHERE kind = ?
217
+ ORDER BY ts ASC`)
218
+ .all("cache_hit_pct");
219
+ return aggregateCacheRows(rows);
220
+ }
221
+ /**
222
+ * Read provider prompt cache aggregates over a trailing window of `minutes`
223
+ * (samples with ts >= now - minutes*60_000). Same aggregate shape as
224
+ * `readProviderCacheLifetime`; the existing lifetime reader is untouched.
225
+ * Returns a zeroed `ProviderCacheLifetime` when no samples fall in the window.
226
+ */
227
+ export function readProviderCacheWindow(stateDir = getStateDir(), minutes) {
228
+ const db = openStore(stateDir);
229
+ const sinceTs = Date.now() - minutes * 60_000;
230
+ const rows = db
231
+ .prepare(`SELECT ts, meta FROM perf_samples
232
+ WHERE kind = ? AND ts >= ?
233
+ ORDER BY ts ASC`)
234
+ .all("cache_hit_pct", sinceTs);
235
+ return aggregateCacheRows(rows);
236
+ }
237
+ // ... keep every function below unchanged ...
156
238
  /**
157
239
  * Read the most recent `cache_hit_pct` value from `perf_samples`.
158
240
  *
@@ -164,9 +246,9 @@ export function readLatestCacheHitPct(stateDir = getStateDir()) {
164
246
  const db = openStore(stateDir);
165
247
  const row = db
166
248
  .prepare(`SELECT value FROM perf_samples
167
- WHERE kind = ?
168
- ORDER BY ts DESC, id DESC
169
- LIMIT 1`)
249
+ WHERE kind = ?
250
+ ORDER BY ts DESC, id DESC
251
+ LIMIT 1`)
170
252
  .get("cache_hit_pct");
171
253
  if (!row || typeof row.value !== "number" || !Number.isFinite(row.value))
172
254
  return 0;
@@ -183,3 +265,49 @@ export function readLatestCacheHitPct(stateDir = getStateDir()) {
183
265
  export function readProviderCacheForRepo(stateDir) {
184
266
  return readProviderCacheLifetime(stateDir);
185
267
  }
268
+ /**
269
+ * Read `prefix_break` samples in a time window.
270
+ *
271
+ * @param stateDir State directory for this repo.
272
+ * @param sinceTs Lower bound (epoch ms). Pass 0 for no lower bound.
273
+ * @param untilTs Upper bound (epoch ms). Pass 0 for no upper bound.
274
+ * Rows with ts > untilTs are excluded.
275
+ * @returns Sorted ascending array of PrefixBreak rows.
276
+ *
277
+ * SQL is fully parameterized (PREVENT-002). `meta` is parsed defensively
278
+ * (PREVENT-001 null-safe). When no rows exist the return is an empty array.
279
+ */
280
+ export function readPrefixBreaks(stateDir = getStateDir(), sinceTs = 0, untilTs = 0) {
281
+ const db = openStore(stateDir);
282
+ let rows;
283
+ if (untilTs > 0) {
284
+ const sql = `SELECT id, ts, value, meta FROM perf_samples
285
+ WHERE kind = ? AND ts >= ? AND ts <= ? ORDER BY ts ASC`;
286
+ rows = db.prepare(sql).all("prefix_break", sinceTs, untilTs);
287
+ }
288
+ else {
289
+ const sql = `SELECT id, ts, value, meta FROM perf_samples
290
+ WHERE kind = ? AND ts >= ? ORDER BY ts ASC`;
291
+ rows = db.prepare(sql).all("prefix_break", sinceTs);
292
+ }
293
+ const out = [];
294
+ for (const r of rows) {
295
+ let meta = null;
296
+ if (r.meta != null) {
297
+ try {
298
+ meta = JSON.parse(r.meta);
299
+ }
300
+ catch {
301
+ meta = null;
302
+ }
303
+ }
304
+ out.push({
305
+ id: r.id,
306
+ ts: r.ts,
307
+ kind: "prefix_break",
308
+ value: r.value,
309
+ meta: meta,
310
+ });
311
+ }
312
+ return out;
313
+ }
@@ -7,6 +7,7 @@
7
7
  * PREVENT-PI-004: no network. PREVENT-002: no SQL here.
8
8
  */
9
9
  import { randomBytes } from "node:crypto";
10
+ import { DuplicateTurnError, } from "./types.js";
10
11
  function newConversationId() {
11
12
  return `conv_${randomBytes(8).toString("hex")}`;
12
13
  }
@@ -134,6 +135,13 @@ export class InMemoryTurnStore {
134
135
  }
135
136
  // ── TurnWriter ──────────────────────────────────────────────
136
137
  appendTurn(entry) {
138
+ // Check for duplicate (conversationId, turnIndex)
139
+ for (const [, row] of this.turns) {
140
+ if (row.entry.conversationId === entry.conversationId &&
141
+ row.entry.turnIndex === entry.turnIndex) {
142
+ throw new DuplicateTurnError(entry.conversationId, entry.turnIndex);
143
+ }
144
+ }
137
145
  const id = allocId();
138
146
  const sid = normalizeSessionId(entry.sessionId);
139
147
  const normalized = { ...entry, sessionId: sid };
@@ -13,6 +13,7 @@ import { randomBytes } from "node:crypto";
13
13
  import { statSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { openTurnDb, closeTurnDb } from "./connection.js";
16
+ import { DuplicateTurnError } from "./types.js";
16
17
  // ─── Helpers ──────────────────────────────────────────────────────
17
18
  function newConversationId() {
18
19
  return `conv_${randomBytes(8).toString("hex")}`;
@@ -172,11 +173,21 @@ export class SqliteTurnStore {
172
173
  appendTurn(entry) {
173
174
  const sid = normalizeSessionId(entry.sessionId);
174
175
  this.persistSessionConv(sid, entry.conversationId);
175
- this.db
176
- .prepare(`INSERT INTO turns (conversation_id, session_id, turn_index, role, ended_at,
176
+ try {
177
+ this.db
178
+ .prepare(`INSERT INTO turns (conversation_id, session_id, turn_index, role, ended_at,
177
179
  ctx_tokens, ctx_percent, pressure_band, model)
178
180
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
179
- .run(entry.conversationId, sid, entry.turnIndex, entry.role, entry.endedAt, entry.ctxTokens ?? null, entry.ctxPercent ?? null, entry.pressureBand ?? null, entry.model ?? null);
181
+ .run(entry.conversationId, sid, entry.turnIndex, entry.role, entry.endedAt, entry.ctxTokens ?? null, entry.ctxPercent ?? null, entry.pressureBand ?? null, entry.model ?? null);
182
+ }
183
+ catch (e) {
184
+ if (e instanceof Error &&
185
+ e.message.includes("UNIQUE constraint") &&
186
+ e.code === "ERR_SQLITE_ERROR") {
187
+ throw new DuplicateTurnError(entry.conversationId, entry.turnIndex);
188
+ }
189
+ throw e;
190
+ }
180
191
  const row = this.db
181
192
  .prepare("SELECT id FROM turns WHERE conversation_id = ? AND turn_index = ?")
182
193
  .get(entry.conversationId, entry.turnIndex);
@@ -13,4 +13,21 @@
13
13
  *
14
14
  * PREVENT-PI-004: zero network. PREVENT-002: no SQL here (private to impls).
15
15
  */
16
- export {};
16
+ // ─── Domain types ───────────────────────────────────────────────────
17
+ /**
18
+ * Thrown when appendTurn is called with a (conversationId, turnIndex) pair
19
+ * that already exists. Both backends must throw this in lieu of a raw
20
+ * SQLite error (SqliteTurnStore) or silent success (InMemoryTurnStore).
21
+ */
22
+ export class DuplicateTurnError extends Error {
23
+ /** The conversation that already has this turn index. */
24
+ conversationId;
25
+ /** The duplicate turn index. */
26
+ turnIndex;
27
+ constructor(conversationId, turnIndex) {
28
+ super(`Duplicate turn: conversation "${conversationId}" already has turnIndex ${turnIndex}`);
29
+ this.name = "DuplicateTurnError";
30
+ this.conversationId = conversationId;
31
+ this.turnIndex = turnIndex;
32
+ }
33
+ }
@@ -241,14 +241,35 @@ export async function searchAsync(query, opts = {}) {
241
241
  return [];
242
242
  }
243
243
  }
244
+ /** Default cap for the WASM close (PGlite can stall ~40 min on teardown). */
245
+ export const DEFAULT_CLOSE_TIMEOUT_MS = 5_000;
246
+ function closeTimeoutMs() {
247
+ const raw = process.env.MEGACOMPACT_PGLITE_CLOSE_TIMEOUT_MS;
248
+ if (raw) {
249
+ const n = Number(raw);
250
+ if (Number.isFinite(n) && n > 0)
251
+ return n;
252
+ }
253
+ return DEFAULT_CLOSE_TIMEOUT_MS;
254
+ }
255
+ /**
256
+ * Race a promise against a timeout. If the inner promise never settles, the
257
+ * timer fires, resolves with `undefined`, and the caller proceeds. This mirrors
258
+ * `withOpenTimeout` (pgOpenGuard.ts) but is local to the close path.
259
+ */
260
+ function withCloseTimeout(promise, timeoutMs) {
261
+ return Promise.race([
262
+ promise,
263
+ new Promise((resolve) => setTimeout(() => resolve(undefined), timeoutMs)),
264
+ ]);
265
+ }
244
266
  /** Close the index (test teardown / shutdown). Safe to call when unopened. */
245
267
  export async function closeVectorIndex() {
246
268
  if (db) {
247
- try {
248
- await db.close();
249
- }
250
- catch {
251
- /* ignore */
269
+ const closePromise = db.close().catch(() => { });
270
+ const result = await withCloseTimeout(closePromise, closeTimeoutMs());
271
+ if (result === undefined) {
272
+ logWarn(`close timed out after ${closeTimeoutMs()}ms (WASM teardown stalled; OS will reclaim)`);
252
273
  }
253
274
  }
254
275
  db = undefined;
@@ -1,2 +1,2 @@
1
- import{r as o,j as n}from"./index-BX0bcgLu.js";function x(s){try{return new Date(s*1e3).toLocaleString()}catch{return"—"}}function f({achievements:s}){const i=o.useRef(0),[r,a]=o.useState(null),l=s.reduce((e,t)=>Math.max(e,t.unlocked_at??0),0),c=i.current;o.useEffect(()=>{l>i.current&&(i.current=l)},[l]),o.useEffect(()=>{if(c>0&&l>c){const e=s.filter(t=>t.unlocked_at!=null&&(t.unlocked_at??0)>c);if(e.length>0){const t=e.map(d=>`${d.icon??""} ${d.title}`.trim()).join(", ")+" unlocked!";a(t);const h=setTimeout(()=>a(null),4e3);return()=>clearTimeout(h)}}},[s,l,c]);const u=s.filter(e=>!(e.hidden===1&&e.unlocked_at==null));return n.jsxs("div",{className:"ach-section",children:[r!=null&&n.jsx("div",{className:"ach-toast show",children:r}),n.jsxs("div",{className:"ach-tiles",children:[u.length===0&&n.jsx("span",{className:"repo-none",children:"no achievements yet"}),u.map(e=>{if(e.unlocked_at!=null){const t=c>0&&(e.unlocked_at??0)>c;return n.jsxs("div",{className:`ach-tile unlocked${t?" just-unlocked":""}`,children:[e.icon??""," ",e.title,n.jsxs("span",{className:"ach-detail",children:["unlocked ",x(e.unlocked_at)]})]},e.id)}return n.jsxs("div",{className:"ach-tile locked",children:["??? ",e.title]},e.id)})]})]})}export{f as A};
2
- //# sourceMappingURL=AchievementTiles-vEo2MtE2.js.map
1
+ import{r as o,j as n}from"./index-4gQ7auad.js";function x(s){try{return new Date(s*1e3).toLocaleString()}catch{return"—"}}function f({achievements:s}){const i=o.useRef(0),[r,a]=o.useState(null),l=s.reduce((e,t)=>Math.max(e,t.unlocked_at??0),0),c=i.current;o.useEffect(()=>{l>i.current&&(i.current=l)},[l]),o.useEffect(()=>{if(c>0&&l>c){const e=s.filter(t=>t.unlocked_at!=null&&(t.unlocked_at??0)>c);if(e.length>0){const t=e.map(d=>`${d.icon??""} ${d.title}`.trim()).join(", ")+" unlocked!";a(t);const h=setTimeout(()=>a(null),4e3);return()=>clearTimeout(h)}}},[s,l,c]);const u=s.filter(e=>!(e.hidden===1&&e.unlocked_at==null));return n.jsxs("div",{className:"ach-section",children:[r!=null&&n.jsx("div",{className:"ach-toast show",children:r}),n.jsxs("div",{className:"ach-tiles",children:[u.length===0&&n.jsx("span",{className:"repo-none",children:"no achievements yet"}),u.map(e=>{if(e.unlocked_at!=null){const t=c>0&&(e.unlocked_at??0)>c;return n.jsxs("div",{className:`ach-tile unlocked${t?" just-unlocked":""}`,children:[e.icon??""," ",e.title,n.jsxs("span",{className:"ach-detail",children:["unlocked ",x(e.unlocked_at)]})]},e.id)}return n.jsxs("div",{className:"ach-tile locked",children:["??? ",e.title]},e.id)})]})]})}export{f as A};
2
+ //# sourceMappingURL=AchievementTiles-BoL7qBOJ.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AchievementTiles-vEo2MtE2.js","sources":["../../src/components/AchievementTiles.tsx"],"sourcesContent":["/**\n * dashboard-client/src/components/AchievementTiles.tsx — shared achievement grid.\n *\n * Renders a grid of achievement tiles from fetchAchievements() data.\n * Tile states: unlocked (icon+title+date), visible-locked (??? teaser),\n * hidden+locked (not rendered). Just-unlocked tiles get a pulse animation.\n * Toast notification fires for newly-unlocked achievements.\n *\n * Used by both GameTab (sub-section) and AchievementsTab (standalone).\n */\n\nimport type React from \"react\";\nimport { useRef, useState, useEffect } from \"react\";\nimport type { AchievementRow } from \"@contracts\";\n\nexport interface AchievementTilesProps {\n\t/** Achievement rows from /api/achievements. */\n\tachievements: AchievementRow[];\n}\n\n/** Format unlock timestamp (seconds → locale string). */\nfunction fmtUnlockDate(ts: number): string {\n\ttry {\n\t\treturn new Date(ts * 1000).toLocaleString();\n\t} catch {\n\t\treturn \"\\u2014\";\n\t}\n}\n\nexport function AchievementTiles({\n\tachievements,\n}: AchievementTilesProps): React.ReactElement {\n\tconst lastMaxTsRef = useRef(0);\n\tconst [toastText, setToastText] = useState<string | null>(null);\n\n\tconst maxTs = achievements.reduce(\n\t\t(mx, a) => Math.max(mx, a.unlocked_at ?? 0),\n\t\t0,\n\t);\n\tconst prevMaxTs = lastMaxTsRef.current;\n\n\t/* Update ref after render so next poll sees previous baseline. */\n\tuseEffect(() => {\n\t\tif (maxTs > lastMaxTsRef.current) {\n\t\t\tlastMaxTsRef.current = maxTs;\n\t\t}\n\t}, [maxTs]);\n\n\t/* Toast for newly-unlocked achievements. */\n\tuseEffect(() => {\n\t\tif (prevMaxTs > 0 && maxTs > prevMaxTs) {\n\t\t\tconst newly = achievements.filter(\n\t\t\t\t(a) => a.unlocked_at != null && (a.unlocked_at ?? 0) > prevMaxTs,\n\t\t\t);\n\t\t\tif (newly.length > 0) {\n\t\t\t\tconst text =\n\t\t\t\t\tnewly\n\t\t\t\t\t\t.map((a) => `${a.icon ?? \"\"} ${a.title}`.trim())\n\t\t\t\t\t\t.join(\", \") + \" unlocked!\";\n\t\t\t\tsetToastText(text);\n\t\t\t\tconst t = setTimeout(() => setToastText(null), 4000);\n\t\t\t\treturn (): void => clearTimeout(t);\n\t\t\t}\n\t\t}\n\t}, [achievements, maxTs, prevMaxTs]);\n\n\t/* Visible tiles: skip hidden+locked (hidden === 1 && unlocked_at == null). */\n\tconst tiles = achievements.filter(\n\t\t(a) => !(a.hidden === 1 && a.unlocked_at == null),\n\t);\n\n\treturn (\n\t\t<div className=\"ach-section\">\n\t\t\t{toastText != null && (\n\t\t\t\t<div className=\"ach-toast show\">{toastText}</div>\n\t\t\t)}\n\t\t\t<div className=\"ach-tiles\">\n\t\t\t\t{tiles.length === 0 && (\n\t\t\t\t\t<span className=\"repo-none\">no achievements yet</span>\n\t\t\t\t)}\n\t\t\t\t{tiles.map((a) => {\n\t\t\t\t\tif (a.unlocked_at != null) {\n\t\t\t\t\t\tconst isNew = prevMaxTs > 0 && (a.unlocked_at ?? 0) > prevMaxTs;\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={a.id}\n\t\t\t\t\t\t\t\tclassName={`ach-tile unlocked${isNew ? \" just-unlocked\" : \"\"}`}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{a.icon ?? \"\"} {a.title}\n\t\t\t\t\t\t\t\t<span className=\"ach-detail\">\n\t\t\t\t\t\t\t\t\tunlocked {fmtUnlockDate(a.unlocked_at)}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<div key={a.id} className=\"ach-tile locked\">\n\t\t\t\t\t\t\t??? {a.title}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"],"names":["fmtUnlockDate","ts","AchievementTiles","achievements","lastMaxTsRef","useRef","toastText","setToastText","useState","maxTs","mx","a","prevMaxTs","useEffect","newly","text","t","tiles","jsxs","jsx","isNew"],"mappings":"+CAqBA,SAASA,EAAcC,EAAoB,CAC1C,GAAI,CACH,OAAO,IAAI,KAAKA,EAAK,GAAI,EAAE,eAAA,CAC5B,MAAQ,CACP,MAAO,GACR,CACD,CAEO,SAASC,EAAiB,CAChC,aAAAC,CACD,EAA8C,CAC7C,MAAMC,EAAeC,EAAAA,OAAO,CAAC,EACvB,CAACC,EAAWC,CAAY,EAAIC,EAAAA,SAAwB,IAAI,EAExDC,EAAQN,EAAa,OAC1B,CAACO,EAAIC,IAAM,KAAK,IAAID,EAAIC,EAAE,aAAe,CAAC,EAC1C,CAAA,EAEKC,EAAYR,EAAa,QAG/BS,EAAAA,UAAU,IAAM,CACXJ,EAAQL,EAAa,UACxBA,EAAa,QAAUK,EAEzB,EAAG,CAACA,CAAK,CAAC,EAGVI,EAAAA,UAAU,IAAM,CACf,GAAID,EAAY,GAAKH,EAAQG,EAAW,CACvC,MAAME,EAAQX,EAAa,OACzBQ,GAAMA,EAAE,aAAe,OAASA,EAAE,aAAe,GAAKC,CAAA,EAExD,GAAIE,EAAM,OAAS,EAAG,CACrB,MAAMC,EACLD,EACE,IAAKH,GAAM,GAAGA,EAAE,MAAQ,EAAE,IAAIA,EAAE,KAAK,GAAG,KAAA,CAAM,EAC9C,KAAK,IAAI,EAAI,aAChBJ,EAAaQ,CAAI,EACjB,MAAMC,EAAI,WAAW,IAAMT,EAAa,IAAI,EAAG,GAAI,EACnD,MAAO,IAAY,aAAaS,CAAC,CAClC,CACD,CACD,EAAG,CAACb,EAAcM,EAAOG,CAAS,CAAC,EAGnC,MAAMK,EAAQd,EAAa,OACzBQ,GAAM,EAAEA,EAAE,SAAW,GAAKA,EAAE,aAAe,KAAA,EAG7C,OACCO,EAAAA,KAAC,MAAA,CAAI,UAAU,cACb,SAAA,CAAAZ,GAAa,MACba,EAAAA,IAAC,MAAA,CAAI,UAAU,iBAAkB,SAAAb,EAAU,EAE5CY,EAAAA,KAAC,MAAA,CAAI,UAAU,YACb,SAAA,CAAAD,EAAM,SAAW,GACjBE,MAAC,OAAA,CAAK,UAAU,YAAY,SAAA,sBAAmB,EAE/CF,EAAM,IAAKN,GAAM,CACjB,GAAIA,EAAE,aAAe,KAAM,CAC1B,MAAMS,EAAQR,EAAY,IAAMD,EAAE,aAAe,GAAKC,EACtD,OACCM,EAAAA,KAAC,MAAA,CAEA,UAAW,oBAAoBE,EAAQ,iBAAmB,EAAE,GAE3D,SAAA,CAAAT,EAAE,MAAQ,GAAG,IAAEA,EAAE,MAClBO,EAAAA,KAAC,OAAA,CAAK,UAAU,aAAa,SAAA,CAAA,YAClBlB,EAAcW,EAAE,WAAW,CAAA,CAAA,CACtC,CAAA,CAAA,EANKA,EAAE,EAAA,CASV,CACA,OACCO,EAAAA,KAAC,MAAA,CAAe,UAAU,kBAAkB,SAAA,CAAA,OACtCP,EAAE,KAAA,CAAA,EADEA,EAAE,EAEZ,CAEF,CAAC,CAAA,CAAA,CACF,CAAA,EACD,CAEF"}
1
+ {"version":3,"file":"AchievementTiles-BoL7qBOJ.js","sources":["../../src/components/AchievementTiles.tsx"],"sourcesContent":["/**\n * dashboard-client/src/components/AchievementTiles.tsx — shared achievement grid.\n *\n * Renders a grid of achievement tiles from fetchAchievements() data.\n * Tile states: unlocked (icon+title+date), visible-locked (??? teaser),\n * hidden+locked (not rendered). Just-unlocked tiles get a pulse animation.\n * Toast notification fires for newly-unlocked achievements.\n *\n * Used by both GameTab (sub-section) and AchievementsTab (standalone).\n */\n\nimport type React from \"react\";\nimport { useRef, useState, useEffect } from \"react\";\nimport type { AchievementRow } from \"@contracts\";\n\nexport interface AchievementTilesProps {\n\t/** Achievement rows from /api/achievements. */\n\tachievements: AchievementRow[];\n}\n\n/** Format unlock timestamp (seconds → locale string). */\nfunction fmtUnlockDate(ts: number): string {\n\ttry {\n\t\treturn new Date(ts * 1000).toLocaleString();\n\t} catch {\n\t\treturn \"\\u2014\";\n\t}\n}\n\nexport function AchievementTiles({\n\tachievements,\n}: AchievementTilesProps): React.ReactElement {\n\tconst lastMaxTsRef = useRef(0);\n\tconst [toastText, setToastText] = useState<string | null>(null);\n\n\tconst maxTs = achievements.reduce(\n\t\t(mx, a) => Math.max(mx, a.unlocked_at ?? 0),\n\t\t0,\n\t);\n\tconst prevMaxTs = lastMaxTsRef.current;\n\n\t/* Update ref after render so next poll sees previous baseline. */\n\tuseEffect(() => {\n\t\tif (maxTs > lastMaxTsRef.current) {\n\t\t\tlastMaxTsRef.current = maxTs;\n\t\t}\n\t}, [maxTs]);\n\n\t/* Toast for newly-unlocked achievements. */\n\tuseEffect(() => {\n\t\tif (prevMaxTs > 0 && maxTs > prevMaxTs) {\n\t\t\tconst newly = achievements.filter(\n\t\t\t\t(a) => a.unlocked_at != null && (a.unlocked_at ?? 0) > prevMaxTs,\n\t\t\t);\n\t\t\tif (newly.length > 0) {\n\t\t\t\tconst text =\n\t\t\t\t\tnewly\n\t\t\t\t\t\t.map((a) => `${a.icon ?? \"\"} ${a.title}`.trim())\n\t\t\t\t\t\t.join(\", \") + \" unlocked!\";\n\t\t\t\tsetToastText(text);\n\t\t\t\tconst t = setTimeout(() => setToastText(null), 4000);\n\t\t\t\treturn (): void => clearTimeout(t);\n\t\t\t}\n\t\t}\n\t}, [achievements, maxTs, prevMaxTs]);\n\n\t/* Visible tiles: skip hidden+locked (hidden === 1 && unlocked_at == null). */\n\tconst tiles = achievements.filter(\n\t\t(a) => !(a.hidden === 1 && a.unlocked_at == null),\n\t);\n\n\treturn (\n\t\t<div className=\"ach-section\">\n\t\t\t{toastText != null && (\n\t\t\t\t<div className=\"ach-toast show\">{toastText}</div>\n\t\t\t)}\n\t\t\t<div className=\"ach-tiles\">\n\t\t\t\t{tiles.length === 0 && (\n\t\t\t\t\t<span className=\"repo-none\">no achievements yet</span>\n\t\t\t\t)}\n\t\t\t\t{tiles.map((a) => {\n\t\t\t\t\tif (a.unlocked_at != null) {\n\t\t\t\t\t\tconst isNew = prevMaxTs > 0 && (a.unlocked_at ?? 0) > prevMaxTs;\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={a.id}\n\t\t\t\t\t\t\t\tclassName={`ach-tile unlocked${isNew ? \" just-unlocked\" : \"\"}`}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{a.icon ?? \"\"} {a.title}\n\t\t\t\t\t\t\t\t<span className=\"ach-detail\">\n\t\t\t\t\t\t\t\t\tunlocked {fmtUnlockDate(a.unlocked_at)}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<div key={a.id} className=\"ach-tile locked\">\n\t\t\t\t\t\t\t??? {a.title}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"],"names":["fmtUnlockDate","ts","AchievementTiles","achievements","lastMaxTsRef","useRef","toastText","setToastText","useState","maxTs","mx","a","prevMaxTs","useEffect","newly","text","t","tiles","jsxs","jsx","isNew"],"mappings":"+CAqBA,SAASA,EAAcC,EAAoB,CAC1C,GAAI,CACH,OAAO,IAAI,KAAKA,EAAK,GAAI,EAAE,eAAA,CAC5B,MAAQ,CACP,MAAO,GACR,CACD,CAEO,SAASC,EAAiB,CAChC,aAAAC,CACD,EAA8C,CAC7C,MAAMC,EAAeC,EAAAA,OAAO,CAAC,EACvB,CAACC,EAAWC,CAAY,EAAIC,EAAAA,SAAwB,IAAI,EAExDC,EAAQN,EAAa,OAC1B,CAACO,EAAIC,IAAM,KAAK,IAAID,EAAIC,EAAE,aAAe,CAAC,EAC1C,CAAA,EAEKC,EAAYR,EAAa,QAG/BS,EAAAA,UAAU,IAAM,CACXJ,EAAQL,EAAa,UACxBA,EAAa,QAAUK,EAEzB,EAAG,CAACA,CAAK,CAAC,EAGVI,EAAAA,UAAU,IAAM,CACf,GAAID,EAAY,GAAKH,EAAQG,EAAW,CACvC,MAAME,EAAQX,EAAa,OACzBQ,GAAMA,EAAE,aAAe,OAASA,EAAE,aAAe,GAAKC,CAAA,EAExD,GAAIE,EAAM,OAAS,EAAG,CACrB,MAAMC,EACLD,EACE,IAAKH,GAAM,GAAGA,EAAE,MAAQ,EAAE,IAAIA,EAAE,KAAK,GAAG,KAAA,CAAM,EAC9C,KAAK,IAAI,EAAI,aAChBJ,EAAaQ,CAAI,EACjB,MAAMC,EAAI,WAAW,IAAMT,EAAa,IAAI,EAAG,GAAI,EACnD,MAAO,IAAY,aAAaS,CAAC,CAClC,CACD,CACD,EAAG,CAACb,EAAcM,EAAOG,CAAS,CAAC,EAGnC,MAAMK,EAAQd,EAAa,OACzBQ,GAAM,EAAEA,EAAE,SAAW,GAAKA,EAAE,aAAe,KAAA,EAG7C,OACCO,EAAAA,KAAC,MAAA,CAAI,UAAU,cACb,SAAA,CAAAZ,GAAa,MACba,EAAAA,IAAC,MAAA,CAAI,UAAU,iBAAkB,SAAAb,EAAU,EAE5CY,EAAAA,KAAC,MAAA,CAAI,UAAU,YACb,SAAA,CAAAD,EAAM,SAAW,GACjBE,MAAC,OAAA,CAAK,UAAU,YAAY,SAAA,sBAAmB,EAE/CF,EAAM,IAAKN,GAAM,CACjB,GAAIA,EAAE,aAAe,KAAM,CAC1B,MAAMS,EAAQR,EAAY,IAAMD,EAAE,aAAe,GAAKC,EACtD,OACCM,EAAAA,KAAC,MAAA,CAEA,UAAW,oBAAoBE,EAAQ,iBAAmB,EAAE,GAE3D,SAAA,CAAAT,EAAE,MAAQ,GAAG,IAAEA,EAAE,MAClBO,EAAAA,KAAC,OAAA,CAAK,UAAU,aAAa,SAAA,CAAA,YAClBlB,EAAcW,EAAE,WAAW,CAAA,CAAA,CACtC,CAAA,CAAA,EANKA,EAAE,EAAA,CASV,CACA,OACCO,EAAAA,KAAC,MAAA,CAAe,UAAU,kBAAkB,SAAA,CAAA,OACtCP,EAAE,KAAA,CAAA,EADEA,EAAE,EAEZ,CAEF,CAAC,CAAA,CAAA,CACF,CAAA,EACD,CAEF"}
@@ -0,0 +1,2 @@
1
+ import{r as t,l as m,j as s}from"./index-4gQ7auad.js";import{A as v}from"./AchievementTiles-BoL7qBOJ.js";function x(){const[r,o]=t.useState([]),[l,u]=t.useState(!0),[a,n]=t.useState(null),c=t.useCallback(async()=>{try{const e=await m();o(e),n(null)}catch(e){n(e instanceof Error?e:new Error(String(e)))}finally{u(!1)}},[]);return t.useEffect(()=>{let e=!0;const i=async()=>{e&&await c()};i();const h=setInterval(()=>void i(),15e3);return()=>{e=!1,clearInterval(h)}},[c]),l&&r.length===0?s.jsx("div",{className:"tab-stub",children:"Loading achievements…"}):a&&r.length===0?s.jsxs("div",{className:"tab-stub",children:["Error: ",a.message]}):s.jsxs("div",{className:"achievements-tab",children:[s.jsx("h2",{children:"Achievements"}),s.jsx(v,{achievements:r})]})}export{x as default};
2
+ //# sourceMappingURL=AchievementsTab-BFDE3HYc.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AchievementsTab-DjxfYrDc.js","sources":["../../src/tabs/AchievementsTab.tsx"],"sourcesContent":["/**\n * dashboard-client/src/tabs/AchievementsTab.tsx — Achievements tab (NEW).\n *\n * Renders the shared AchievementTiles component standalone, plus a toast area.\n * Polls /api/achievements every 15s.\n */\n\nimport type React from \"react\";\nimport { useState, useEffect, useCallback } from \"react\";\nimport type { AchievementRow } from \"@contracts\";\nimport { fetchAchievements } from \"../api/client\";\nimport { AchievementTiles } from \"../components/AchievementTiles\";\n\nexport default function AchievementsTab(): React.ReactElement {\n\tconst [achievements, setAchievements] = useState<AchievementRow[]>([]);\n\tconst [loading, setLoading] = useState(true);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\tconst doFetch = useCallback(async (): Promise<void> => {\n\t\ttry {\n\t\t\tconst rows = await fetchAchievements();\n\t\t\tsetAchievements(rows);\n\t\t\tsetError(null);\n\t\t} catch (e) {\n\t\t\tsetError(e instanceof Error ? e : new Error(String(e)));\n\t\t} finally {\n\t\t\tsetLoading(false);\n\t\t}\n\t}, []);\n\n\tuseEffect(() => {\n\t\tlet active = true;\n\t\tconst run = async (): Promise<void> => {\n\t\t\tif (active) await doFetch();\n\t\t};\n\t\tvoid run();\n\t\tconst timer = setInterval(() => void run(), 15000);\n\t\treturn () => {\n\t\t\tactive = false;\n\t\t\tclearInterval(timer);\n\t\t};\n\t}, [doFetch]);\n\n\tif (loading && achievements.length === 0)\n\t\treturn <div className=\"tab-stub\">Loading achievements…</div>;\n\tif (error && achievements.length === 0)\n\t\treturn <div className=\"tab-stub\">Error: {error.message}</div>;\n\n\treturn (\n\t\t<div className=\"achievements-tab\">\n\t\t\t<h2>Achievements</h2>\n\t\t\t<AchievementTiles achievements={achievements} />\n\t\t</div>\n\t);\n}\n"],"names":["AchievementsTab","achievements","setAchievements","useState","loading","setLoading","error","setError","doFetch","useCallback","rows","fetchAchievements","useEffect","active","run","timer","jsx","jsxs","AchievementTiles"],"mappings":"yGAaA,SAAwBA,GAAsC,CAC7D,KAAM,CAACC,EAAcC,CAAe,EAAIC,EAAAA,SAA2B,CAAA,CAAE,EAC/D,CAACC,EAASC,CAAU,EAAIF,EAAAA,SAAS,EAAI,EACrC,CAACG,EAAOC,CAAQ,EAAIJ,EAAAA,SAAuB,IAAI,EAE/CK,EAAUC,EAAAA,YAAY,SAA2B,CACtD,GAAI,CACH,MAAMC,EAAO,MAAMC,EAAA,EACnBT,EAAgBQ,CAAI,EACpBH,EAAS,IAAI,CACd,OAAS,EAAG,CACXA,EAAS,aAAa,MAAQ,EAAI,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,CACvD,QAAA,CACCF,EAAW,EAAK,CACjB,CACD,EAAG,CAAA,CAAE,EAeL,OAbAO,EAAAA,UAAU,IAAM,CACf,IAAIC,EAAS,GACb,MAAMC,EAAM,SAA2B,CAClCD,SAAcL,EAAA,CACnB,EACKM,EAAA,EACL,MAAMC,EAAQ,YAAY,IAAM,KAAKD,EAAA,EAAO,IAAK,EACjD,MAAO,IAAM,CACZD,EAAS,GACT,cAAcE,CAAK,CACpB,CACD,EAAG,CAACP,CAAO,CAAC,EAERJ,GAAWH,EAAa,SAAW,EAC/Be,EAAAA,IAAC,MAAA,CAAI,UAAU,WAAW,SAAA,wBAAqB,EACnDV,GAASL,EAAa,SAAW,EAC7BgB,EAAAA,KAAC,MAAA,CAAI,UAAU,WAAW,SAAA,CAAA,UAAQX,EAAM,OAAA,EAAQ,EAGvDW,EAAAA,KAAC,MAAA,CAAI,UAAU,mBACd,SAAA,CAAAD,EAAAA,IAAC,MAAG,SAAA,cAAA,CAAY,EAChBA,MAACE,GAAiB,aAAAjB,CAAA,CAA4B,CAAA,EAC/C,CAEF"}
1
+ {"version":3,"file":"AchievementsTab-BFDE3HYc.js","sources":["../../src/tabs/AchievementsTab.tsx"],"sourcesContent":["/**\n * dashboard-client/src/tabs/AchievementsTab.tsx — Achievements tab (NEW).\n *\n * Renders the shared AchievementTiles component standalone, plus a toast area.\n * Polls /api/achievements every 15s.\n */\n\nimport type React from \"react\";\nimport { useState, useEffect, useCallback } from \"react\";\nimport type { AchievementRow } from \"@contracts\";\nimport { fetchAchievements } from \"../api/client\";\nimport { AchievementTiles } from \"../components/AchievementTiles\";\n\nexport default function AchievementsTab(): React.ReactElement {\n\tconst [achievements, setAchievements] = useState<AchievementRow[]>([]);\n\tconst [loading, setLoading] = useState(true);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\tconst doFetch = useCallback(async (): Promise<void> => {\n\t\ttry {\n\t\t\tconst rows = await fetchAchievements();\n\t\t\tsetAchievements(rows);\n\t\t\tsetError(null);\n\t\t} catch (e) {\n\t\t\tsetError(e instanceof Error ? e : new Error(String(e)));\n\t\t} finally {\n\t\t\tsetLoading(false);\n\t\t}\n\t}, []);\n\n\tuseEffect(() => {\n\t\tlet active = true;\n\t\tconst run = async (): Promise<void> => {\n\t\t\tif (active) await doFetch();\n\t\t};\n\t\tvoid run();\n\t\tconst timer = setInterval(() => void run(), 15000);\n\t\treturn () => {\n\t\t\tactive = false;\n\t\t\tclearInterval(timer);\n\t\t};\n\t}, [doFetch]);\n\n\tif (loading && achievements.length === 0)\n\t\treturn <div className=\"tab-stub\">Loading achievements…</div>;\n\tif (error && achievements.length === 0)\n\t\treturn <div className=\"tab-stub\">Error: {error.message}</div>;\n\n\treturn (\n\t\t<div className=\"achievements-tab\">\n\t\t\t<h2>Achievements</h2>\n\t\t\t<AchievementTiles achievements={achievements} />\n\t\t</div>\n\t);\n}\n"],"names":["AchievementsTab","achievements","setAchievements","useState","loading","setLoading","error","setError","doFetch","useCallback","rows","fetchAchievements","useEffect","active","run","timer","jsx","jsxs","AchievementTiles"],"mappings":"yGAaA,SAAwBA,GAAsC,CAC7D,KAAM,CAACC,EAAcC,CAAe,EAAIC,EAAAA,SAA2B,CAAA,CAAE,EAC/D,CAACC,EAASC,CAAU,EAAIF,EAAAA,SAAS,EAAI,EACrC,CAACG,EAAOC,CAAQ,EAAIJ,EAAAA,SAAuB,IAAI,EAE/CK,EAAUC,EAAAA,YAAY,SAA2B,CACtD,GAAI,CACH,MAAMC,EAAO,MAAMC,EAAA,EACnBT,EAAgBQ,CAAI,EACpBH,EAAS,IAAI,CACd,OAAS,EAAG,CACXA,EAAS,aAAa,MAAQ,EAAI,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,CACvD,QAAA,CACCF,EAAW,EAAK,CACjB,CACD,EAAG,CAAA,CAAE,EAeL,OAbAO,EAAAA,UAAU,IAAM,CACf,IAAIC,EAAS,GACb,MAAMC,EAAM,SAA2B,CAClCD,SAAcL,EAAA,CACnB,EACKM,EAAA,EACL,MAAMC,EAAQ,YAAY,IAAM,KAAKD,EAAA,EAAO,IAAK,EACjD,MAAO,IAAM,CACZD,EAAS,GACT,cAAcE,CAAK,CACpB,CACD,EAAG,CAACP,CAAO,CAAC,EAERJ,GAAWH,EAAa,SAAW,EAC/Be,EAAAA,IAAC,MAAA,CAAI,UAAU,WAAW,SAAA,wBAAqB,EACnDV,GAASL,EAAa,SAAW,EAC7BgB,EAAAA,KAAC,MAAA,CAAI,UAAU,WAAW,SAAA,CAAA,UAAQX,EAAM,OAAA,EAAQ,EAGvDW,EAAAA,KAAC,MAAA,CAAI,UAAU,mBACd,SAAA,CAAAD,EAAAA,IAAC,MAAG,SAAA,cAAA,CAAY,EAChBA,MAACE,GAAiB,aAAAjB,CAAA,CAA4B,CAAA,EAC/C,CAEF"}
@@ -0,0 +1,2 @@
1
+ import{j as a,u as v,r as m,i as b,c as j}from"./index-4gQ7auad.js";import{C,T as S}from"./TimeSavedCard-Cw1dJ4e9.js";import{b as n,c as o,d as g,e as T}from"./format-COjDIVGj.js";import{l as f}from"./pricing-CvE4NllQ.js";function x(e){return e>=80?"ov-stat-value-green":e>=50?"ov-stat-value-yellow":"ov-stat-value-red"}function p(e){return e==null?"":e>=0?"ov-stat-value-green":"ov-stat-value-red"}function t({label:e,value:l,className:s,title:c}){return a.jsxs("div",{className:"ov-stat-row",children:[a.jsx("span",{className:"ov-stat-label",title:c,children:e}),a.jsx("span",{className:`ov-stat-value${s?` ${s}`:""}`,children:l})]})}function R(e){return a.jsxs("div",{className:"card provider-cache-card",children:[a.jsx(t,{label:"Cache Hit Rate",value:`${e.hitPct.toFixed(1)}%`,className:x(e.hitPct)}),a.jsx(t,{label:"Turns Recorded",value:String(e.turnCount)}),a.jsx(t,{label:"Cache Read Tokens",value:n(e.totalCacheRead)}),a.jsx(t,{label:"Cache Write Tokens",value:n(e.totalCacheWrite)}),a.jsx(t,{label:"Input Tokens",value:n(e.totalInput)}),a.jsx(t,{label:"$ Saved (reads)",value:o(e.cacheReadSaved)}),a.jsx(t,{label:"Write Investment",value:o(e.cacheWriteCost)}),a.jsx(t,{label:"Net Saved",value:o(e.netSaved),className:p(e.netSaved)}),a.jsx(t,{label:"Model",value:e.modelLabel??"—"}),a.jsx(t,{label:"Tracked Since",value:g(e.firstTurnAt)}),a.jsx(t,{label:"Last Updated",value:T(e.latestTurnAt)}),e.byModel.length>0&&a.jsxs(a.Fragment,{children:[a.jsx("div",{className:"ov-stat-section-label",children:"Per Model"}),e.byModel.map(l=>{const s=f(l.model),c=s!=null?l.totalCacheRead/1e6*s:null,r=s!=null?l.totalCacheWrite/1e6*s:null;return a.jsxs("div",{className:"ov-stat-group",children:[a.jsx(t,{label:"Model",value:l.model}),a.jsx(t,{label:"Hit Rate",value:`${l.hitPct.toFixed(1)}%`,className:x(l.hitPct)}),a.jsx(t,{label:"Cache Read",value:n(l.totalCacheRead)}),a.jsx(t,{label:"Cache Write",value:n(l.totalCacheWrite)}),a.jsx(t,{label:"Samples",value:String(l.sampleCount)}),a.jsx(t,{label:"$ Saved (reads)",value:c!=null?o(c):"—"}),a.jsx(t,{label:"Write Investment",value:r!=null?o(r):"—"})]},l.model)})]})]})}function H(){const{data:e,loading:l,error:s}=v(m.useCallback(()=>b(),[]),{pollInterval:5e3}),{data:c,loading:r,error:u}=v(m.useCallback(()=>j(),[]),{pollInterval:5e3});return a.jsxs("div",{className:"cache-tab",children:[a.jsx("h2",{children:"Provider Prompt Cache"}),l?a.jsx("p",{className:"tab-stub",children:"Loading…"}):s?a.jsxs("p",{className:"tab-stub",children:["Provider cache unavailable: ",s.message]}):e?a.jsx(R,{hitPct:e.cache.avgHitPct,turnCount:e.cache.turnCount,totalCacheRead:e.cache.totalCacheRead,totalCacheWrite:e.cache.totalCacheWrite,totalInput:e.cache.totalInput,cacheReadSaved:e.savings?.cacheReadSaved??null,cacheWriteCost:e.savings?.cacheWriteCost??null,netSaved:e.savings?.netSaved??null,modelLabel:e.savings?.model??null,firstTurnAt:e.cache.firstTurnAt,latestTurnAt:e.cache.latestTurnAt,byModel:e.cache.byModel??[]}):null,a.jsx("h2",{children:"Mega-Compact Dedup Cache"}),r?a.jsx("p",{className:"tab-stub",children:"Loading…"}):u?a.jsxs("p",{className:"tab-stub",children:["Snapshot unavailable: ",u.message]}):c&&(()=>{const{cacheHits:i,compacts:h,timeSaved:d}=c;return a.jsxs("div",{className:"card-grid overview-card-grid",children:[a.jsx(C,{cacheHitsSession:i.session,cacheHitsTotal:i.total,tokensSavedSession:i.sessionTokensSaved,tokensSavedTotal:i.totalTokensSaved,compactionsSession:h.session,compactionsTotal:h.total}),a.jsx(S,{compactSessionSec:d.compact.sessionSec,compactTotalSec:d.compact.totalSec,cacheHitSessionSec:d.cacheHit.sessionSec,cacheHitTotalSec:d.cacheHit.totalSec})]})})()]})}export{H as default};
2
+ //# sourceMappingURL=CacheTab-eKVc-NV1.js.map