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
@@ -1,1112 +0,0 @@
1
- /**
2
- * mega-compact.extension.test.ts — end-to-end drive of the REAL extension
3
- * entry (extensions/mega-compact.ts) through a faithful mock pi.
4
- *
5
- * This is the closest we get to "a live pi session" without a model: it
6
- * loads the compiled extension, captures its event/command handlers, and
7
- * fires them with mock ctx objects — proving the three compact layers
8
- * (auto-trigger -> compactSession) AND the three recall entries all
9
- * route through the real code, not just the unit-tested src/ modules.
10
- *
11
- * Uses a per-test isolated state dir (process.env.MEGACOMPACT_STATE_DIR)
12
- * so concurrent node --test runs do not collide on disk.
13
- */
14
- import { test } from "node:test";
15
- import assert from "node:assert/strict";
16
- import { mkdtempSync, rmSync } from "node:fs";
17
- import { tmpdir } from "node:os";
18
- import { join } from "node:path";
19
- import { createRequire } from "node:module";
20
- import { closeVectorIndex } from "../src/store/vectorIndex.js";
21
- const require = createRequire(import.meta.url);
22
- const baseTmp = mkdtempSync(join(tmpdir(), "mc-ext-"));
23
- // Isolate the machine-wide repo index so test runs (which call bindRepo ->
24
- // upsertRepoRegistry) never pollute the developer's real ~/.mega-compact-index.
25
- process.env.MEGACOMPACT_INDEX_DIR = join(baseTmp, "index");
26
- let counter = 0;
27
- /** Build a mock pi + ctx and load the extension into them. */
28
- function harness(opts = {}) {
29
- const stateDir = join(baseTmp, `run-${counter++}`);
30
- process.env.MEGACOMPACT_STATE_DIR = stateDir;
31
- process.env.MEGACOMPACT_DEBUG = "true";
32
- // Low threshold so the auto-trigger gate trips on our small mock context.
33
- // Tier tests opt out (keepTier/keepThreshold) so they can drive the real
34
- // tier resolution instead of the forced 50-token threshold.
35
- if (!opts.keepThreshold)
36
- process.env.MEGACOMPACT_THRESHOLD_TOKENS = "50";
37
- if (!opts.keepTier)
38
- delete process.env.MEGACOMPACT_TIER;
39
- process.env.MEGACOMPACT_FAST_GATE_PCT = "1";
40
- const handlers = {};
41
- const commands = {};
42
- const appended = [];
43
- let statusKey;
44
- let statusText;
45
- const notifies = [];
46
- const compactCalls = [];
47
- const sendUserMessages = [];
48
- // Minimal AgentMessage factory for the session we project into the extension.
49
- function msg(role, text, toolName) {
50
- if (role === "assistant" && toolName) {
51
- return {
52
- role: "assistant",
53
- content: [
54
- { type: "toolCall", name: toolName, id: "c1", arguments: {} },
55
- ],
56
- api: "anthropic-messages",
57
- provider: "anthropic",
58
- model: "m",
59
- usage: {
60
- inputTokens: 1,
61
- outputTokens: 1,
62
- cacheReadTokens: 0,
63
- cacheWriteTokens: 0,
64
- },
65
- stopReason: "tool_use",
66
- timestamp: 0,
67
- };
68
- }
69
- if (role === "toolResult" && toolName) {
70
- return {
71
- role: "toolResult",
72
- toolCallId: "c1",
73
- toolName,
74
- content: [{ type: "text", text }],
75
- isError: false,
76
- timestamp: 0,
77
- };
78
- }
79
- return {
80
- role: "user",
81
- content: text,
82
- timestamp: 0,
83
- };
84
- }
85
- const session = [
86
- msg("user", "read src/vec.ts and understand the index"),
87
- msg("assistant", "ok", "Read"),
88
- msg("user", "edit src/vec.ts to add a cosine helper"),
89
- msg("assistant", "ok", "Edit"),
90
- msg("user", "now fix the dedupe bug in store.ts"),
91
- msg("assistant", "ok", "Edit"),
92
- msg("user", "actually we should add recall sorting too"),
93
- msg("assistant", "ok", "Edit"),
94
- ];
95
- // Mirror the REAL SessionManager: getEntries() returns SessionEntry objects,
96
- // which the extension projects to messages via the SDK's
97
- // sessionEntryToContextMessages(entry). The harness must use the same shape
98
- // (type:"message" with a .message) or recentUserQuery() silently queries "".
99
- const toEntry = (m, i) => ({
100
- type: "message",
101
- id: `e${i}`,
102
- parentId: null,
103
- timestamp: String(i),
104
- message: m,
105
- });
106
- const sessionManager = {
107
- getSessionId: () => "sess_ext_001",
108
- getEntries: () => session.map(toEntry),
109
- // Faithful mock: getBranch() returns the current branch's entries, which
110
- // piCompactWouldNoop() reads to predict whether ctx.compact() would no-op.
111
- getBranch: () => session.map(toEntry),
112
- };
113
- function makeCtx(over = {}) {
114
- return {
115
- ui: {
116
- setStatus: (k, t) => {
117
- statusKey = k;
118
- statusText = t;
119
- },
120
- notify: (s) => notifies.push(s),
121
- select: () => { },
122
- confirm: async () => true,
123
- input: async () => "",
124
- setWidget: () => { },
125
- },
126
- mode: "tui",
127
- hasUI: true,
128
- cwd: stateDir,
129
- sessionManager,
130
- modelRegistry: {},
131
- model: undefined,
132
- isIdle: () => true,
133
- isProjectTrusted: () => true,
134
- signal: undefined,
135
- abort: () => { },
136
- hasPendingMessages: () => false,
137
- shutdown: () => { },
138
- getContextUsage: () => ({
139
- tokens: 200000,
140
- contextWindow: 200000,
141
- percent: 100,
142
- }),
143
- // Faithful mock: ctx.compact() starts pi's flow, which fires the
144
- // session_before_compact handler (where WE supply the durable trim).
145
- compact: (opts) => {
146
- compactCalls.push(opts);
147
- const _sbc = handlers["session_before_compact"];
148
- if (_sbc && _sbc.length) {
149
- return _sbc[0]({
150
- type: "session_before_compact",
151
- reason: "threshold",
152
- willRetry: false,
153
- signal: undefined,
154
- // pi computed the cut honoring anchor floor + tool-pair (PREVENT-PI-002);
155
- // our handler reuses it as firstKeptEntryId.
156
- preparation: {
157
- firstKeptEntryId: "e2",
158
- messagesToSummarize: session.slice(0, 2),
159
- tokensBefore: 500,
160
- },
161
- }, makeCtx());
162
- }
163
- return undefined;
164
- },
165
- getSystemPrompt: () => "system base",
166
- ...over,
167
- };
168
- }
169
- const pi = {
170
- on: (ev, h) => {
171
- if (!handlers[ev])
172
- handlers[ev] = [];
173
- handlers[ev].push(h);
174
- },
175
- registerCommand: (name, opts) => {
176
- commands[name] = opts;
177
- },
178
- registerTool: () => { },
179
- registerShortcut: () => { },
180
- registerFlag: () => { },
181
- getFlag: () => undefined,
182
- registerMessageRenderer: () => { },
183
- registerEntryRenderer: () => { },
184
- sendMessage: (_m) => { },
185
- sendUserMessage: (m) => {
186
- sendUserMessages.push(m);
187
- },
188
- appendEntry: (t, d) => appended.push({ t, d }),
189
- setSessionName: () => { },
190
- getSessionName: () => undefined,
191
- setLabel: () => { },
192
- exec: async () => ({ stdout: "", stderr: "", code: 0 }),
193
- getActiveTools: () => [],
194
- getAllTools: () => [],
195
- setActiveTools: () => { },
196
- getCommands: () => [],
197
- setModel: async () => false,
198
- getThinkingLevel: () => "off",
199
- setThinkingLevel: () => { },
200
- };
201
- // Import the compiled extension (same dist/extensions dir as this test).
202
- const mod = require("./mega-compact.js");
203
- mod.default(pi);
204
- return {
205
- stateDir,
206
- handlers,
207
- commands,
208
- appended,
209
- get status() {
210
- return { statusKey, statusText };
211
- },
212
- notifies,
213
- compactCalls,
214
- sendUserMessages,
215
- fire: async (ev, event, ctx) => { let r; for (const h of handlers[ev] || [])
216
- r = await h(event, ctx); return r; },
217
- ctx: makeCtx,
218
- session,
219
- };
220
- }
221
- test("auto-trigger (legacy): past threshold persists a chkpt and starts a durable trim via ctx.compact", async () => {
222
- const h = harness();
223
- const messages = h.session;
224
- // The mock session is tiny (~100 tokens). piCompactWouldNoop() would skip
225
- // ctx.compact() for a transcript under pi's keepRecentTokens budget — so
226
- // lower the floor to 0 to simulate a transcript large enough that pi WOULD
227
- // compact (the positive path this test exercises).
228
- // S16: this is the LEGACY path — the default no longer calls ctx.compact()
229
- // (it returns a live-trimmed view instead). Set the legacy flag to exercise
230
- // the v0.4.28 ctx.compact durable-trim flow this test asserts.
231
- process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR = "0";
232
- process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM = "true";
233
- try {
234
- const ctx = h.ctx({
235
- getContextUsage: () => ({
236
- tokens: 200000,
237
- contextWindow: 200000,
238
- percent: 100,
239
- }),
240
- });
241
- const res = await h.fire("context", { type: "context", messages }, ctx);
242
- // S38.5: the strict race guard (default) defers ctx.compact() via a
243
- // setTimeout(500) re-check. The synchronous return is still undefined
244
- // (no local drop), but ctx.compact() only lands after the timer — wait
245
- // for it before asserting the durable-trim call count.
246
- await new Promise((r) => setTimeout(r, 700));
247
- // L1->L4 ran: a checkpoint was persisted to the SQLite store + a marker entry written.
248
- const { listCheckpoints } = await import("../src/store/sqlite.js");
249
- assert.ok(listCheckpoints("sess_ext_001", h.stateDir).length > 0, "checkpoint persisted to local vector db");
250
- assert.equal(h.appended.some((a) => a.t === "mega-compact-marker"), true, "marker sentinel appended");
251
- // The legacy context handler triggers pi's compaction flow (ctx.compact),
252
- // which calls our session_before_compact handler to supply the DURABLE trim.
253
- assert.equal(res, undefined, "legacy context handler returns nothing (no local drop)");
254
- assert.equal(h.compactCalls.length, 1, "ctx.compact() called to start durable trim (legacy path)");
255
- // The durable trim was supplied (summary + firstKeptEntryId from pi's prep).
256
- assert.ok(h.compactCalls[0] !== undefined, "compaction flow executed");
257
- }
258
- finally {
259
- delete process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
260
- delete process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM;
261
- }
262
- });
263
- test("auto-trigger: skips ctx.compact() when pi would no-op (session too small, legacy path)", async () => {
264
- const h = harness();
265
- const messages = h.session;
266
- // Default floor (20000): the tiny mock transcript is below pi's
267
- // keepRecentTokens budget, so piCompactWouldNoop() must skip ctx.compact()
268
- // rather than surface pi's "Nothing to compact (session too small)" throw.
269
- // S16: exercised under the legacy flag (the default path never calls ctx.compact).
270
- delete process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
271
- process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM = "true";
272
- try {
273
- const ctx = h.ctx({
274
- getContextUsage: () => ({
275
- tokens: 200000,
276
- contextWindow: 200000,
277
- percent: 100,
278
- }),
279
- });
280
- const res = await h.fire("context", { type: "context", messages }, ctx);
281
- assert.equal(res, undefined, "legacy context handler returns nothing (no local drop)");
282
- assert.equal(h.compactCalls.length, 0, "ctx.compact() NOT called — pi would no-op");
283
- // Our recall checkpoint still persisted (Path A) — the durable trim is the
284
- // only thing skipped; recall is independent of it.
285
- const { listCheckpoints } = await import("../src/store/sqlite.js");
286
- assert.ok(listCheckpoints("sess_ext_001", h.stateDir).length > 0, "recall checkpoint still persisted");
287
- assert.equal(h.appended.some((a) => a.t === "mega-compact-marker"), true, "marker sentinel still appended");
288
- }
289
- finally {
290
- delete process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM;
291
- }
292
- });
293
- test("auto-trigger (S16): trims the live view and does NOT call ctx.compact()", async () => {
294
- const h = harness();
295
- const messages = h.session;
296
- // S16 default: live context-event trim. No legacy flag. Lower the anchor floor
297
- // so the trimmed recent window (4 messages, 2 user) clears the anchor check
298
- // and the live trim actually fires — mirrors how the legacy test lowers the
299
- // durable floor to exercise its positive path.
300
- delete process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM;
301
- delete process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
302
- process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES = "1";
303
- try {
304
- const ctx = h.ctx({
305
- getContextUsage: () => ({
306
- tokens: 200000,
307
- contextWindow: 200000,
308
- percent: 100,
309
- }),
310
- });
311
- const res = await h.fire("context", { type: "context", messages }, ctx);
312
- // S16: context handler returns a TRIMMED messages array (live trim), not undefined.
313
- assert.ok(res && typeof res === "object", "context handler returns a result object (live trim)");
314
- assert.ok(Array.isArray(res.messages), "result has a trimmed messages array");
315
- // The trimmed view starts with the compacted summary (user-role) + is shorter.
316
- assert.ok(res.messages.length < messages.length, "trimmed view is shorter than the full session");
317
- // S16: ctx.compact() is NEVER called (it would stop the agent).
318
- assert.equal(h.compactCalls.length, 0, "ctx.compact() NOT called — compact-and-continue");
319
- // The recall checkpoint is still persisted (the durable value).
320
- const { listCheckpoints } = await import("../src/store/sqlite.js");
321
- assert.ok(listCheckpoints("sess_ext_001", h.stateDir).length > 0, "recall checkpoint persisted under live trim");
322
- }
323
- finally {
324
- delete process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES;
325
- }
326
- });
327
- test("auto-trigger (S16): does not trim when below the anchor floor (returns undefined, no ctx.compact)", async () => {
328
- const h = harness();
329
- // A session so short that buildLiveTrimmedView's anchor floor can't hold — the
330
- // live trim skips this call (returns undefined, the next context event retries).
331
- delete process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM;
332
- delete process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
333
- const shortSession = [h.session[0], h.session[1]]; // one user + one assistant
334
- const ctx = h.ctx({
335
- getContextUsage: () => ({
336
- tokens: 200000,
337
- contextWindow: 200000,
338
- percent: 100,
339
- }),
340
- });
341
- const res = await h.fire("context", { type: "context", messages: shortSession }, ctx);
342
- // Either it skipped (undefined) or trimmed safely — but it must never call ctx.compact.
343
- assert.equal(h.compactCalls.length, 0, "ctx.compact() NOT called under live trim (short session)");
344
- if (res === undefined) {
345
- // skipped path is fine
346
- assert.ok(true, "below anchor floor → no trim this call (retries next event)");
347
- }
348
- });
349
- test("auto-trigger (S16): sendUserMessage resume nudge fires only when idle + queued + not already nudged", async () => {
350
- const h = harness();
351
- // No queued messages → the nudge must NOT fire (the guard prevents busy-loops).
352
- // We assert the extension did not throw and did not push a spurious resume.
353
- const ctx = h.ctx({ isIdle: () => true, hasPendingMessages: () => false });
354
- await h.fire("agent_end", { type: "agent_end", messages: [] }, ctx);
355
- // No throw + no spurious nudge side-effect is the contract; appended stays
356
- // free of any auto "continue" marker when there is no queued work.
357
- assert.equal(h.appended.some((a) => a.t && /continue/i.test(String(a.d ?? ""))), false, "no spurious continue when no queued work");
358
- });
359
- test("auto-trigger (S16): durable trim still happens via pi native auto-compaction (session_before_compact)", async () => {
360
- const h = harness();
361
- // pi's native auto-compaction fires at agent-end with reason "threshold" (the
362
- // CONTINUING path). Our session_before_compact handler must still supply the
363
- // durable trim summary — independent of the live context-event trim.
364
- const prep = {
365
- firstKeptEntryId: "e2",
366
- messagesToSummarize: h.session.slice(0, 4),
367
- tokensBefore: 500,
368
- };
369
- const res = await h.fire("session_before_compact", {
370
- type: "session_before_compact",
371
- reason: "threshold",
372
- willRetry: false,
373
- signal: undefined,
374
- preparation: prep,
375
- }, h.ctx());
376
- assert.ok(res?.compaction, "we supply a durable compaction result to pi's native path");
377
- assert.ok(res.compaction.firstKeptEntryId === "e2", "reuses pi's boundary (PREVENT-PI-002)");
378
- assert.ok(res.compaction.summary.length > 0, "summary is non-empty");
379
- });
380
- test("session_before_compact supplies our durable trim (not pi's summary)", async () => {
381
- const h = harness();
382
- // pi fires session_before_compact with its own computed preparation.
383
- const res = await h.fire("session_before_compact", {
384
- type: "session_before_compact",
385
- reason: "overflow",
386
- willRetry: true,
387
- preparation: {
388
- firstKeptEntryId: "e2",
389
- messagesToSummarize: h.session.slice(0, 2),
390
- tokensBefore: 500,
391
- },
392
- signal: undefined,
393
- }, h.ctx());
394
- assert.ok(res && res.compaction, "returns a compaction result");
395
- assert.equal(res.compaction.firstKeptEntryId, "e2", "reuses pi's cut boundary (PREVENT-PI-002 safe)");
396
- assert.ok(typeof res.compaction.summary === "string" &&
397
- res.compaction.summary.length > 0, "our summary supplied");
398
- assert.ok(res.compaction.tokensBefore >= 0, "tokensBefore reported");
399
- });
400
- test("session_before_compact supplies a fallback summary when nothing to summarize", async () => {
401
- const h = harness();
402
- // Empty preparation → no messages to summarize (anchor floor protects
403
- // everything). We MUST still supply a compaction (never {}), otherwise pi
404
- // runs its own compact() which throws "Nothing to compact (session too
405
- // small)" and leaves the session stuck with no resume context. The fallback
406
- // records a minimal resume summary so the session always resumes.
407
- const res = await h.fire("session_before_compact", {
408
- type: "session_before_compact",
409
- reason: "threshold",
410
- willRetry: false,
411
- preparation: {
412
- firstKeptEntryId: "e0",
413
- messagesToSummarize: [],
414
- tokensBefore: 0,
415
- },
416
- signal: undefined,
417
- }, h.ctx());
418
- assert.ok(res && res.compaction, "fallback compaction supplied (never {})");
419
- assert.ok(res.compaction.summary.includes("context compacted"), "fallback summary injected so the session resumes");
420
- assert.equal(res.compaction.firstKeptEntryId, "e0", "keeps pi's cut point");
421
- });
422
- test("resume auto-inline stages recall into the system prompt", async () => {
423
- const h = harness();
424
- // Seed a checkpoint first (simulate a prior session that compacted).
425
- await h.fire("context", { type: "context", messages: h.session }, h.ctx({
426
- getContextUsage: () => ({
427
- tokens: 200000,
428
- contextWindow: 200000,
429
- percent: 100,
430
- }),
431
- }));
432
- // Fresh resume: session_start with reason "resume".
433
- const ctx = h.ctx();
434
- await h.fire("session_start", {
435
- type: "session_start",
436
- reason: "resume",
437
- previousSessionFile: undefined,
438
- }, ctx);
439
- // The next before_agent_start must prepend the recalled block.
440
- const res = await h.fire("before_agent_start", {
441
- type: "before_agent_start",
442
- prompt: "base system",
443
- images: undefined,
444
- systemPrompt: "base system",
445
- systemPromptOptions: {},
446
- }, ctx);
447
- assert.ok(res && typeof res.systemPrompt === "string", "before_agent_start returns a systemPrompt");
448
- assert.ok(res.systemPrompt.includes("Recalled context"), "recalled block injected into system prompt");
449
- });
450
- test("/recall-context reports and stages the top checkpoint", async () => {
451
- const h = harness();
452
- await h.fire("context", { type: "context", messages: h.session }, h.ctx({
453
- getContextUsage: () => ({
454
- tokens: 200000,
455
- contextWindow: 200000,
456
- percent: 100,
457
- }),
458
- }));
459
- const ctx = h.ctx();
460
- await h.commands["mega-recall"].handler("dedupe bug store.ts", ctx);
461
- assert.ok(h.notifies.some((n) => n.includes("recall staged")), "command reports staged checkpoints");
462
- assert.ok(h.notifies.some((n) => n.includes("chkpt_")), "command names the checkpoint");
463
- });
464
- test("/megacompact-status reports live store stats", async () => {
465
- const h = harness();
466
- await h.fire("context", { type: "context", messages: h.session }, h.ctx({
467
- getContextUsage: () => ({
468
- tokens: 200000,
469
- contextWindow: 200000,
470
- percent: 100,
471
- }),
472
- }));
473
- const ctx = h.ctx({
474
- getContextUsage: () => ({
475
- tokens: 50000,
476
- contextWindow: 200000,
477
- percent: 25,
478
- }),
479
- });
480
- await h.commands["mega-status"].handler("", ctx);
481
- assert.ok(h.notifies.some((n) => n.includes("store:") && n.includes("chkpt")), "status shows checkpoint count");
482
- });
483
- // ---- Model/provider capture (Phase 5b model_snapshots) ----------------------
484
- test("model_select captures model + provider into SQL", async () => {
485
- const h = harness();
486
- const modelCtx = h.ctx({
487
- model: {
488
- id: "claude-opus-4-8",
489
- name: "Claude Opus 4.8",
490
- provider: "anthropic",
491
- contextWindow: 200000,
492
- maxTokens: 32000,
493
- reasoning: false,
494
- cost: { input: 0.000015, output: 0.000075 },
495
- },
496
- modelRegistry: {
497
- getProviderDisplayName: (p) => p === "anthropic" ? "Anthropic" : p,
498
- },
499
- });
500
- await h.fire("model_select", {}, modelCtx);
501
- const { latestModelSnapshot } = await import("../src/store/sqlite.js");
502
- const snap = latestModelSnapshot(h.stateDir);
503
- assert.ok(snap, "model_snapshots row persisted");
504
- assert.equal(snap.modelId, "claude-opus-4-8", "correct model id captured");
505
- assert.equal(snap.provider, "anthropic", "correct provider captured");
506
- assert.equal(snap.providerName, "Anthropic", "provider display name resolved");
507
- assert.equal(snap.inputRate, 0.000015, "input rate captured");
508
- });
509
- test("/mega-status surfaces the captured model + provider", async () => {
510
- const h = harness();
511
- const modelCtx = h.ctx({
512
- model: {
513
- id: "claude-opus-4-8",
514
- name: "Claude Opus 4.8",
515
- provider: "anthropic",
516
- contextWindow: 200000,
517
- maxTokens: 32000,
518
- reasoning: false,
519
- cost: { input: 0.000015, output: 0.000075 },
520
- },
521
- modelRegistry: { getProviderDisplayName: () => "Anthropic" },
522
- });
523
- await h.fire("model_select", {}, modelCtx);
524
- await h.fire("context", { type: "context", messages: h.session }, h.ctx({
525
- getContextUsage: () => ({
526
- tokens: 200000,
527
- contextWindow: 200000,
528
- percent: 100,
529
- }),
530
- }));
531
- const ctx = h.ctx({
532
- getContextUsage: () => ({
533
- tokens: 50000,
534
- contextWindow: 200000,
535
- percent: 25,
536
- }),
537
- });
538
- await h.commands["mega-status"].handler("", ctx);
539
- assert.ok(h.notifies.some((n) => n.includes("🤖 model:") &&
540
- n.includes("Claude Opus 4.8") &&
541
- n.includes("Anthropic")), "status surfaces captured model + provider");
542
- });
543
- // ---- Named compaction tiers -------------------------------------------------
544
- // low=50k, medium=100k, high=200k, ultra=1M, mega=10M. Driven through the REAL
545
- // loadConfig()/status path by setting MEGACOMPACT_TIER before loading the ext.
546
- // Percentage-based thresholds: tierPct × the model context window. The harness
547
- // getContextUsage below reports contextWindow=2_000_000, so each tier resolves to
548
- // tierPct × 2_000_000 — which fires BELOW pi's native ~80% auto-compaction for
549
- // ANY model size (200k or 1M). Driven through the REAL loadConfig()/status path
550
- // by setting MEGACOMPACT_TIER before loading the ext.
551
- const TIER_CASES = [
552
- ["low", 1_000_000], // 0.50 × 2_000_000
553
- ["medium", 1_200_000], // 0.60 × 2_000_000
554
- ["high", 1_400_000], // 0.70 × 2_000_000
555
- ["ultra", 1_400_000], // 0.70 × 2_000_000
556
- ["mega", 1_500_000], // 0.75 × 2_000_000
557
- ];
558
- for (const [tier, threshold] of TIER_CASES) {
559
- test(`tier "${tier}" resolves to a ${threshold.toLocaleString()}-token threshold (tierPct × 2M window; live band shown separately)`, async () => {
560
- // Keep tier + keep threshold UNSET so the tier (not an explicit number)
561
- // drives the threshold. harness() would otherwise reset the threshold.
562
- delete process.env.MEGACOMPACT_THRESHOLD_TOKENS;
563
- process.env.MEGACOMPACT_TIER = tier;
564
- const h = harness({ keepTier: true, keepThreshold: true });
565
- // tokens=1 against a 2M window → near-zero pressure → live band "low".
566
- const ctx = h.ctx({
567
- getContextUsage: () => ({
568
- tokens: 1,
569
- contextWindow: 2_000_000,
570
- percent: 0.01,
571
- }),
572
- });
573
- await h.commands["mega-status"].handler("", ctx);
574
- delete process.env.MEGACOMPACT_TIER;
575
- // /mega-status renders threshold with toLocaleString() (thousands commas).
576
- assert.ok(h.notifies.some((n) => n.includes(`preset=${tier}`) &&
577
- n.includes(`threshold=${threshold.toLocaleString()}`)), `status should report preset=${tier} threshold=${threshold.toLocaleString()} (tierPct × 2M window)`);
578
- // S24: the headline tier is the LIVE pressure band, shown as "tier=low (live)".
579
- assert.ok(h.notifies.some((n) => n.includes("tier=low (live)")), "live band reported (low at near-zero pressure)");
580
- });
581
- }
582
- test("explicit MEGACOMPACT_THRESHOLD_TOKENS overrides the tier", async () => {
583
- process.env.MEGACOMPACT_TIER = "mega";
584
- process.env.MEGACOMPACT_THRESHOLD_TOKENS = "777";
585
- const h = harness({ keepTier: true, keepThreshold: true });
586
- const ctx = h.ctx({
587
- getContextUsage: () => ({
588
- tokens: 1,
589
- contextWindow: 2_000_000,
590
- percent: 0.01,
591
- }),
592
- });
593
- await h.commands["mega-status"].handler("", ctx);
594
- delete process.env.MEGACOMPACT_TIER;
595
- assert.ok(h.notifies.some((n) => n.includes("preset=custom") && n.includes("threshold=777")), "explicit threshold wins over tier (preset=custom)");
596
- });
597
- // ---- S24: memory review tied to pressure / compaction -----------------------
598
- // Build a decision-bearing session large enough to guarantee a real (non-skipped,
599
- // non-deduped) compaction. Each user turn contains a decision phrase
600
- // (/\bactually\b/i, /\bwe (?:use|decided)\b/i) so reviewConversation yields ops.
601
- function decisionSession() {
602
- const out = [];
603
- for (let i = 0; i < 14; i++) {
604
- out.push({
605
- role: "user",
606
- content: `actually we decided to use approach ${i} for module ${i}`,
607
- timestamp: i,
608
- });
609
- out.push({
610
- role: "assistant",
611
- content: [{ type: "toolCall", name: "Edit", id: `c${i}`, arguments: {} }],
612
- api: "anthropic-messages",
613
- provider: "anthropic",
614
- model: "m",
615
- usage: {
616
- inputTokens: 1,
617
- outputTokens: 1,
618
- cacheReadTokens: 0,
619
- cacheWriteTokens: 0,
620
- },
621
- stopReason: "tool_use",
622
- timestamp: i,
623
- });
624
- out.push({
625
- role: "toolResult",
626
- content: [{ type: "text", text: `edited module ${i}` }],
627
- toolCallId: `c${i}`,
628
- toolName: "Edit",
629
- isError: false,
630
- timestamp: i,
631
- });
632
- }
633
- return out;
634
- }
635
- test("S24: high pressure triggers a memory review on compaction", async () => {
636
- const h = harness();
637
- // Force a real (non-legacy) compaction at full pressure → pressureBand "mega",
638
- // which must fire the shared runMemoryReview on compact (review-on-compact).
639
- process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM = "false";
640
- try {
641
- const messages = decisionSession();
642
- const ctx = h.ctx({
643
- getContextUsage: () => ({
644
- tokens: 200000,
645
- contextWindow: 200000,
646
- percent: 100,
647
- }),
648
- });
649
- await h.fire("context", { type: "context", messages }, ctx);
650
- // review-on-compact runs as a fire-and-forget async (doCompact is sync), so
651
- // let the microtask/macrotask queue drain before asserting the side effect.
652
- await new Promise((r) => setTimeout(r, 20));
653
- const { listMemories, listCheckpoints } = await import("../src/store/sqlite.js");
654
- // A checkpoint must have been persisted (proves compaction ran, not skipped).
655
- assert.ok(listCheckpoints("sess_ext_001", h.stateDir).length > 0, "checkpoint persisted to local vector db");
656
- // The just-compacted region is worth remembering, so durable memories must
657
- // have been written to the SQLite store (review-on-compact path).
658
- const mem = listMemories(null, 50, h.stateDir);
659
- assert.ok(mem.length > 0, "memory review wrote durable memories on compact");
660
- }
661
- finally {
662
- delete process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM;
663
- }
664
- });
665
- test("S24: /mega-status reports the live pressure band + %", async () => {
666
- const h = harness();
667
- // Populate the runtime's live context first (a context event sets
668
- // lastCtxTokens/lastCtxPercent), then read /mega-status. At 100% usage the live
669
- // band must read "mega" and pressure must report 100%.
670
- const ctx = h.ctx({
671
- getContextUsage: () => ({
672
- tokens: 200000,
673
- contextWindow: 200000,
674
- percent: 100,
675
- }),
676
- });
677
- await h.fire("context", { type: "context", messages: h.session }, ctx);
678
- await h.commands["mega-status"].handler("", ctx);
679
- assert.ok(h.notifies.some((n) => n.includes("tier=mega (live)")), "live band reported as mega at 100% pressure");
680
- assert.ok(h.notifies.some((n) => n.includes("pressure=100%")), "live pressure % reported");
681
- });
682
- // ---- /dashboard commands ----------------------------------------------------
683
- test("/dashboard-status reports no server when pid file missing", async () => {
684
- // Private base so this asserts "no server" on a range nothing else uses,
685
- // not the machine-global 9320 family (which may hold a leftover/production server).
686
- process.env.MEGACOMPACT_DASHBOARD_PORT = "49320";
687
- try {
688
- const h = harness();
689
- const ctx = h.ctx();
690
- await h.commands["mega-dashboard-status"].handler("", ctx);
691
- assert.ok(h.notifies.some((n) => n.includes("not running")), "reports no server running");
692
- }
693
- finally {
694
- delete process.env.MEGACOMPACT_DASHBOARD_PORT;
695
- }
696
- });
697
- test("/dashboard-stop reports no server when pid file missing", async () => {
698
- const h = harness();
699
- const ctx = h.ctx();
700
- await h.commands["mega-dashboard-stop"].handler("", ctx);
701
- assert.ok(h.notifies.some((n) => n.includes("no dashboard server running")), "reports no server");
702
- });
703
- // Skipped: creates a real localhost HTTP server + 10-port scan that hangs the
704
- // isolated test runner (open handle keeps the event loop alive). The two
705
- // /dashboard-*-status/stop tests above cover the no-server paths; the
706
- // positive spawn path is covered by dashboard-server.test.js.
707
- test.skip("/dashboard skips server spawn when already running", async () => {
708
- // Use a private dashboard port base for THIS test's harness + fake server so
709
- // it never races the (parallel, hard-coded-9320) dashboard-server.test.js or
710
- // a leftover production server. Set BEFORE harness() so registerDashboardCommands
711
- // reads our base for findLivePort().
712
- process.env.MEGACOMPACT_DASHBOARD_PORT = "29320";
713
- const h = harness();
714
- const confirms = [];
715
- const livPort = 29320; // inside the harness's private scan range (29320–29329)
716
- const { createServer } = await import("node:http");
717
- const server = createServer((_req, res) => {
718
- res.writeHead(200, { "Content-Type": "application/json" });
719
- res.end(JSON.stringify({
720
- updatedAt: new Date().toISOString(),
721
- tier: "test",
722
- version: 1,
723
- config: {},
724
- session: {},
725
- context: {},
726
- trigger: {},
727
- store: {},
728
- }));
729
- });
730
- await new Promise((r) => server.listen(livPort, "127.0.0.1", r));
731
- const { join: j } = await import("node:path");
732
- const { writeFileSync: wf } = await import("node:fs");
733
- wf(j(h.stateDir, "port.pid"), JSON.stringify({ port: livPort, pid: process.pid }));
734
- const ctx = h.ctx({
735
- ui: {
736
- setStatus: () => { },
737
- notify: (s) => {
738
- h.notifies.push(s);
739
- },
740
- select: () => { },
741
- confirm: async () => {
742
- confirms.push(true);
743
- return true;
744
- },
745
- input: async () => "",
746
- },
747
- });
748
- await h.commands["mega-dashboard"].handler("", ctx);
749
- assert.ok(h.notifies.some((n) => n.includes("already running")), "reports already running");
750
- assert.ok(confirms.length > 0, "confirm dialog was shown");
751
- await new Promise((r) => server.close(() => r()));
752
- delete process.env.MEGACOMPACT_DASHBOARD_PORT;
753
- });
754
- test("state snapshot writes dashboard.json after compaction", async () => {
755
- const h = harness();
756
- const ctx = h.ctx({
757
- getContextUsage: () => ({
758
- tokens: 200000,
759
- contextWindow: 200000,
760
- percent: 100,
761
- }),
762
- });
763
- // Fire auto-trigger compaction (context event above 80% threshold)
764
- await h.fire("context", { type: "context", messages: h.session }, ctx);
765
- const { existsSync: ex, readFileSync: rf } = await import("node:fs");
766
- const { join: j } = await import("node:path");
767
- const snapPath = j(h.stateDir, "dashboard.json");
768
- assert.ok(ex(snapPath), "dashboard.json written after compaction");
769
- const snap = JSON.parse(rf(snapPath, "utf-8"));
770
- // Item B: the honest token model is wired — the original dropped region was
771
- // captured (originalTokens > 0), and the saved amount never exceeds the
772
- // original (saved = max(0, original − stored) ≤ original). For this tiny
773
- // harness session the summary can be ≥ the region, so saved may be 0; the
774
- // positive "saved > 0" case with a large region is covered by the
775
- // vectorStore unit tests.
776
- assert.ok(snap.store.originalTokens > 0, "snapshot.store.originalTokens captured after compaction");
777
- assert.ok(snap.store.originalTokens >= snap.store.tokensSaved, "model invariant: original region >= tokens saved");
778
- // Item A: crew (live agent) block is present in the dashboard snapshot.
779
- assert.ok(snap.crew && typeof snap.crew.activeAgents === "number", "snapshot.crew.activeAgents present");
780
- });
781
- test("events.log receives compaction events", async () => {
782
- const h = harness();
783
- const ctx = h.ctx({
784
- getContextUsage: () => ({
785
- tokens: 200000,
786
- contextWindow: 200000,
787
- percent: 100,
788
- }),
789
- });
790
- // Fire auto-trigger compaction twice (first fires compaction, second also fires)
791
- await h.fire("context", { type: "context", messages: h.session }, ctx);
792
- const { readFileSync: rf, existsSync: ex } = await import("node:fs");
793
- const { join: j } = await import("node:path");
794
- const logPath = j(h.stateDir, "events.log");
795
- if (ex(logPath)) {
796
- const content = rf(logPath, "utf-8").trim();
797
- // At minimum, we expect at least one event logged
798
- assert.ok(content.length > 0, "events.log is non-empty after compaction");
799
- }
800
- else {
801
- // events.log may not exist if the DashboardEmitter path differs from stateDir;
802
- // verify dashboard.json was written (proves the post-compact path executed)
803
- assert.ok(ex(j(h.stateDir, "dashboard.json")), "dashboard.json proves post-compact ran");
804
- }
805
- });
806
- test("S28: length-stop auto-continue nudges once, no ctx.compact on low-pressure length path", async () => {
807
- const h = harness();
808
- // Force a low-pressure context so the durable-trim branch (which calls
809
- // ctx.compact()) is NOT taken; only the length-stop nudge should fire.
810
- const lowPressureCtx = h.ctx({
811
- isIdle: () => true,
812
- hasPendingMessages: () => false,
813
- getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }),
814
- });
815
- // 1) Normal stop: no length flag armed → no nudge.
816
- await h.fire("turn_end", {
817
- type: "turn_end",
818
- turnIndex: 1,
819
- message: { role: "assistant", stopReason: "stop" },
820
- }, lowPressureCtx);
821
- await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
822
- assert.equal(h.sendUserMessages.length, 0, "normal stop: no nudge");
823
- assert.equal(h.compactCalls.length, 0, "normal stop: no ctx.compact");
824
- // 2) Length stop: arms the flag, agent_end fires exactly one continue nudge
825
- // that references the output-token truncation (not a compaction).
826
- await h.fire("turn_end", {
827
- type: "turn_end",
828
- turnIndex: 2,
829
- message: { role: "assistant", stopReason: "length" },
830
- }, lowPressureCtx);
831
- await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
832
- assert.equal(h.sendUserMessages.length, 1, "length stop: exactly one nudge");
833
- assert.match(h.sendUserMessages[0], /output-token cap/, "length stop: nudge references the output-token truncation");
834
- assert.equal(h.compactCalls.length, 0, "length path: ctx.compact() NOT called (low pressure)");
835
- // 3) One-shot: a second agent_end without a new length stop must NOT re-nudge.
836
- await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
837
- assert.equal(h.sendUserMessages.length, 1, "one-shot: no second nudge without a new length stop");
838
- });
839
- test("S28: length-stop auto-continue fires even when config.auto === false (autoContinueLengthStop is the sole gate)", async () => {
840
- // Disable auto (durable-trim + queued-resume) but keep the length-stop flag on.
841
- // Set BEFORE harness() loads the compiled extension so loadConfig() picks it up.
842
- const prevAuto = process.env.MEGACOMPACT_AUTO;
843
- process.env.MEGACOMPACT_AUTO = "false";
844
- try {
845
- // Re-load the extension with the new env so config.auto is false but
846
- // autoContinueLengthStop stays true (default).
847
- const h2 = harness();
848
- const lowPressureCtx = h2.ctx({
849
- isIdle: () => true,
850
- hasPendingMessages: () => false,
851
- getContextUsage: () => ({
852
- tokens: 100,
853
- contextWindow: 200000,
854
- percent: 0,
855
- }),
856
- });
857
- // Length stop arms the flag; agent_end must still nudge despite auto=false.
858
- await h2.fire("turn_end", {
859
- type: "turn_end",
860
- turnIndex: 1,
861
- message: { role: "assistant", stopReason: "length" },
862
- }, lowPressureCtx);
863
- await h2.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
864
- assert.equal(h2.sendUserMessages.length, 1, "auto=false: length stop still nudges");
865
- assert.match(h2.sendUserMessages[0], /output-token cap/, "auto=false: nudge references the output-token truncation");
866
- assert.equal(h2.compactCalls.length, 0, "auto=false: ctx.compact() NOT called (auto gates durable-trim)");
867
- }
868
- finally {
869
- if (prevAuto === undefined)
870
- delete process.env.MEGACOMPACT_AUTO;
871
- else
872
- process.env.MEGACOMPACT_AUTO = prevAuto;
873
- }
874
- });
875
- // Helper: read <stateDir>/events.log JSONL and return the list of event `type`s.
876
- // Dashboard.event (extensions/mega-dashboard.ts) appends `{ ts, type, ...data }`
877
- // per line. Used to assert the S28 length_stop / length_stop_continue dashboard
878
- // events fire on the right paths (spec acceptance #7; OPEN issue #3).
879
- function eventTypes(stateDir) {
880
- const { readFileSync: rf, existsSync: ex } = require("node:fs");
881
- const { join: j } = require("node:path");
882
- const logPath = j(stateDir, "events.log");
883
- if (!ex(logPath))
884
- return [];
885
- const content = rf(logPath, "utf-8").trim();
886
- if (content.length === 0)
887
- return [];
888
- return content
889
- .split("\n")
890
- .map((line) => {
891
- try {
892
- return JSON.parse(line).type;
893
- }
894
- catch {
895
- return undefined;
896
- }
897
- })
898
- .filter((t) => typeof t === "string");
899
- }
900
- test("S28: length_stop + length_stop_continue dashboard events fire on the right paths", async () => {
901
- const h = harness();
902
- const lowPressureCtx = h.ctx({
903
- isIdle: () => true,
904
- hasPendingMessages: () => false,
905
- getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }),
906
- });
907
- // Normal stop: no length_stop event, no nudge, no length_stop_continue.
908
- await h.fire("turn_end", {
909
- type: "turn_end",
910
- turnIndex: 1,
911
- message: { role: "assistant", stopReason: "stop" },
912
- }, lowPressureCtx);
913
- await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
914
- const afterNormal = eventTypes(h.stateDir);
915
- assert.ok(!afterNormal.includes("length_stop"), "normal stop: no length_stop dashboard event");
916
- assert.ok(!afterNormal.includes("length_stop_continue"), "normal stop: no length_stop_continue dashboard event");
917
- assert.equal(h.sendUserMessages.length, 0, "normal stop: no nudge");
918
- // Length stop: length_stop fires on turn_end, length_stop_continue on agent_end.
919
- await h.fire("turn_end", {
920
- type: "turn_end",
921
- turnIndex: 2,
922
- message: { role: "assistant", stopReason: "length" },
923
- }, lowPressureCtx);
924
- const afterTurnEnd = eventTypes(h.stateDir);
925
- assert.ok(afterTurnEnd.includes("length_stop"), "length stop: length_stop dashboard event fired on turn_end");
926
- await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
927
- const afterAgentEnd = eventTypes(h.stateDir);
928
- assert.ok(afterAgentEnd.includes("length_stop_continue"), "length stop: length_stop_continue dashboard event fired on agent_end");
929
- assert.equal(h.sendUserMessages.length, 1, "length stop: exactly one nudge");
930
- });
931
- test("S28: non-length stopReasons do not arm the length-stop flag (no length_stop event); S38 owns error/aborted retries", async () => {
932
- const h = harness();
933
- const lowPressureCtx = h.ctx({
934
- isIdle: () => true,
935
- hasPendingMessages: () => false,
936
- getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }),
937
- });
938
- // S28 contract: every non-length StopReason must leave lengthStopPending unset,
939
- // so no length_stop event fires and no length-stop resume nudge is armed.
940
- // (S38 separately owns error/aborted — it WILL fire error-retry nudges for those;
941
- // this test only asserts the S28 length-stop flag is not armed.)
942
- let transientRetries = 0;
943
- for (const stopReason of ["tool_use", "error", "aborted"]) {
944
- const before = h.sendUserMessages.length;
945
- await h.fire("turn_end", {
946
- type: "turn_end",
947
- turnIndex: 1,
948
- message: { role: "assistant", stopReason },
949
- }, lowPressureCtx);
950
- await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
951
- // tool_use is success (0 nudges); error/aborted are transient under S38 (1 each).
952
- if (stopReason === "tool_use") {
953
- assert.equal(h.sendUserMessages.length - before, 0, "tool_use (success): no nudge");
954
- }
955
- else {
956
- transientRetries += h.sendUserMessages.length - before;
957
- }
958
- }
959
- // error + aborted each produced one S38 transient retry nudge (2 total).
960
- assert.equal(transientRetries, 2, "S38 owns error/aborted: 1 transient retry each");
961
- assert.ok(!eventTypes(h.stateDir).includes("length_stop"), "non-length stopReasons: no length_stop dashboard event");
962
- });
963
- // ---- S29: percent-based auto-compact trigger (gate on context %, not tokens) -
964
- // The context-handler gate now fires on pct/100 >= (autoPctTrigger ?? tierPct)
965
- // for tiered configs, with a token FALLBACK when pct is null. `custom` keeps the
966
- // absolute token gate. These are the first tests to drive a `context` event
967
- // on a tiered config (the default harness forces custom via THRESHOLD_TOKENS=50).
968
- /** S29 tiered-config helper: tiered (not custom), low tier (tierPct 0.5), with
969
- * the legacy durable-trim flag off + anchor floor lowered so the live trim
970
- * returns a trimmed view (mirrors the S16 live-trim test setup at ~line 329). */
971
- function s29TieredCtx(h, usage) {
972
- delete process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM;
973
- delete process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
974
- process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES = "1";
975
- return h.ctx({
976
- isIdle: () => true,
977
- hasPendingMessages: () => false,
978
- getContextUsage: () => usage,
979
- });
980
- }
981
- test("S29: percent gate fires when tokens under-report (tiered low, percent 55, tokens 10)", async () => {
982
- process.env.MEGACOMPACT_TIER = "low";
983
- delete process.env.MEGACOMPACT_THRESHOLD_TOKENS;
984
- delete process.env.MEGACOMPACT_AUTO_PCT_TRIGGER;
985
- try {
986
- const h = harness({ keepTier: true, keepThreshold: true });
987
- // tokens=10 (under the 0.5×10000=5000 token gate), percent=55 (>= 0.5).
988
- // The OLD token-only gate would return (10 < 5000) → no trim. The S29
989
- // percent gate (0.55 >= 0.5) fires → live trim returns a trimmed view.
990
- const ctx = s29TieredCtx(h, {
991
- tokens: 10,
992
- contextWindow: 10000,
993
- percent: 55,
994
- });
995
- const res = await h.fire("context", { type: "context", messages: h.session }, ctx);
996
- assert.ok(res && typeof res === "object", "percent gate: live trim returned a result object");
997
- assert.ok(Array.isArray(res.messages), "percent gate: result has a trimmed messages array");
998
- assert.ok(res.messages.length < h.session.length, "percent gate: trimmed view is shorter than the full session");
999
- assert.equal(h.compactCalls.length, 0, "percent gate: live trim, no ctx.compact()");
1000
- // Control: percent 40 (< 0.5) → no trim, even with the same under-reported tokens.
1001
- const h2 = harness({ keepTier: true, keepThreshold: true });
1002
- const ctx2 = s29TieredCtx(h2, {
1003
- tokens: 10,
1004
- contextWindow: 10000,
1005
- percent: 40,
1006
- });
1007
- const res2 = await h2.fire("context", { type: "context", messages: h2.session }, ctx2);
1008
- assert.ok(!(res2 &&
1009
- typeof res2 === "object" &&
1010
- Array.isArray(res2.messages)), "percent below fire point: no trim (token count 10 is also below the token gate)");
1011
- }
1012
- finally {
1013
- delete process.env.MEGACOMPACT_TIER;
1014
- delete process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES;
1015
- }
1016
- });
1017
- test("S29: MEGACOMPACT_AUTO_PCT_TRIGGER overrides the tier fire point (0.85)", async () => {
1018
- process.env.MEGACOMPACT_TIER = "low"; // tierPct 0.5
1019
- process.env.MEGACOMPACT_AUTO_PCT_TRIGGER = "0.85";
1020
- delete process.env.MEGACOMPACT_THRESHOLD_TOKENS;
1021
- try {
1022
- // percent 80 < 0.85 → no trim.
1023
- const h = harness({ keepTier: true, keepThreshold: true });
1024
- const ctx80 = s29TieredCtx(h, {
1025
- tokens: 10,
1026
- contextWindow: 10000,
1027
- percent: 80,
1028
- });
1029
- const res80 = await h.fire("context", { type: "context", messages: h.session }, ctx80);
1030
- assert.ok(!(res80 &&
1031
- typeof res80 === "object" &&
1032
- Array.isArray(res80.messages)), "override 0.85: percent 80 does NOT trim (below the override fire point)");
1033
- // percent 90 >= 0.85 → trim fires (despite the tier's own 0.5 fire point).
1034
- const h2 = harness({ keepTier: true, keepThreshold: true });
1035
- const ctx90 = s29TieredCtx(h2, {
1036
- tokens: 10,
1037
- contextWindow: 10000,
1038
- percent: 90,
1039
- });
1040
- const res90 = await h2.fire("context", { type: "context", messages: h2.session }, ctx90);
1041
- assert.ok(res90 &&
1042
- Array.isArray(res90.messages) &&
1043
- res90.messages.length < h2.session.length, "override 0.85: percent 90 DOES trim (above the override fire point)");
1044
- }
1045
- finally {
1046
- delete process.env.MEGACOMPACT_TIER;
1047
- delete process.env.MEGACOMPACT_AUTO_PCT_TRIGGER;
1048
- delete process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES;
1049
- }
1050
- });
1051
- test("S29: custom tier keeps the absolute token gate (percent 40 but tokens 100 >= 50)", async () => {
1052
- // MEGACOMPACT_THRESHOLD_TOKENS → custom (tierPct null) → token gate, percent ignored.
1053
- process.env.MEGACOMPACT_THRESHOLD_TOKENS = "50";
1054
- delete process.env.MEGACOMPACT_TIER;
1055
- delete process.env.MEGACOMPACT_AUTO_PCT_TRIGGER;
1056
- try {
1057
- const h = harness({ keepTier: true, keepThreshold: true });
1058
- // percent 40 (low) BUT tokens 100 >= 50 threshold → custom token gate fires.
1059
- const ctx = s29TieredCtx(h, {
1060
- tokens: 100,
1061
- contextWindow: 10000,
1062
- percent: 40,
1063
- });
1064
- const res = await h.fire("context", { type: "context", messages: h.session }, ctx);
1065
- assert.ok(res &&
1066
- Array.isArray(res.messages) &&
1067
- res.messages.length < h.session.length, "custom tier: token gate fires (tokens 100 >= 50) despite low percent 40");
1068
- }
1069
- finally {
1070
- delete process.env.MEGACOMPACT_THRESHOLD_TOKENS;
1071
- delete process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES;
1072
- }
1073
- });
1074
- test("S29: tiered config with pct==null falls back to the token gate (not skipped)", async () => {
1075
- // The regression guard for the audit finding: a percent-ONLY gate would skip
1076
- // compaction when percent is unreported. S29 falls back to the token gate
1077
- // (S27 boot-fallback guarantee). tiered low: effectiveThreshold = 0.5×10000 = 5000;
1078
- // tokens 6000 >= 5000 → token fallback fires.
1079
- process.env.MEGACOMPACT_TIER = "low";
1080
- delete process.env.MEGACOMPACT_THRESHOLD_TOKENS;
1081
- delete process.env.MEGACOMPACT_AUTO_PCT_TRIGGER;
1082
- try {
1083
- const h = harness({ keepTier: true, keepThreshold: true });
1084
- const ctx = s29TieredCtx(h, {
1085
- tokens: 6000,
1086
- contextWindow: 10000,
1087
- percent: null,
1088
- });
1089
- const res = await h.fire("context", { type: "context", messages: h.session }, ctx);
1090
- assert.ok(res &&
1091
- Array.isArray(res.messages) &&
1092
- res.messages.length < h.session.length, "pct==null on tiered: token fallback fires (NOT skipped) — S27 boot-fallback preserved");
1093
- }
1094
- finally {
1095
- delete process.env.MEGACOMPACT_TIER;
1096
- delete process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES;
1097
- }
1098
- });
1099
- test("cleanup", async () => {
1100
- // Terminate the global PGlite cross-repo index (WASM worker thread) so the
1101
- // test process can exit. Without this, node --test never returns even though
1102
- // every test passed — the leaked worker keeps the event loop alive.
1103
- // Race with a timeout to prevent 40-min hangs if PGlite WASM close stalls.
1104
- try {
1105
- await Promise.race([
1106
- closeVectorIndex(),
1107
- new Promise((r) => setTimeout(r, 3000)),
1108
- ]);
1109
- }
1110
- catch { /* ignore */ }
1111
- rmSync(baseTmp, { recursive: true, force: true });
1112
- });