pi-mega-compact 0.11.4 → 0.11.6

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 (381) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/cache-stripes.js +8 -0
  2. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +32 -0
  3. package/dist/extensions/dashboard-server/api-contracts/memory-map.js +1 -0
  4. package/dist/extensions/dashboard-server/api-contracts/memory.js +8 -0
  5. package/dist/extensions/dashboard-server/api-contracts/provider-cache.js +1 -0
  6. package/dist/extensions/dashboard-server/api-contracts/setup.js +10 -0
  7. package/dist/extensions/dashboard-server/index-reader.js +6 -2
  8. package/dist/extensions/dashboard-server/routes-cache.js +135 -47
  9. package/dist/extensions/dashboard-server/routes-game.js +3 -0
  10. package/dist/extensions/dashboard-server/routes-memory-map.js +50 -0
  11. package/dist/extensions/dashboard-server/routes-memory.js +51 -0
  12. package/dist/extensions/dashboard-server/routes-repo.js +2 -2
  13. package/dist/extensions/dashboard-server/routes-setup.js +267 -0
  14. package/dist/extensions/dashboard-server/routes.js +4 -0
  15. package/dist/extensions/dashboard-server/server.js +13 -1
  16. package/dist/extensions/mega-commands.js +189 -1
  17. package/dist/extensions/mega-compact.js +5 -0
  18. package/dist/extensions/mega-config.js +7 -0
  19. package/dist/extensions/mega-events/agent-handlers.js +19 -4
  20. package/dist/extensions/mega-events/compact-handlers.js +1 -0
  21. package/dist/extensions/mega-events/context-handler.js +45 -92
  22. package/dist/extensions/mega-events/context-hwm.js +51 -0
  23. package/dist/extensions/mega-events/error-classifier.js +16 -5
  24. package/dist/extensions/mega-events/mirror-append.js +107 -0
  25. package/dist/extensions/mega-events/outage-advisor.js +7 -1
  26. package/dist/extensions/mega-events/perf-handler.js +134 -2
  27. package/dist/extensions/mega-events/recall-tail.js +59 -0
  28. package/dist/extensions/mega-events/separated-prompt.js +248 -0
  29. package/dist/extensions/mega-events/session-handlers.js +15 -8
  30. package/dist/extensions/mega-runtime/env-loader.js +56 -0
  31. package/dist/extensions/mega-runtime/reset-runtime.js +6 -0
  32. package/dist/extensions/mega-runtime/runtime-snapshot.js +1 -0
  33. package/dist/extensions/mega-runtime/runtime.js +7 -0
  34. package/dist/extensions/mega-runtime/snapshot.js +1 -0
  35. package/dist/extensions/mega-runtime/widget.js +5 -0
  36. package/dist/src/cache-stripe-impl.js +288 -0
  37. package/dist/src/cache-stripe.js +11 -0
  38. package/dist/src/config/stopwords.js +24 -0
  39. package/dist/src/config.js +17 -0
  40. package/dist/src/embedder.js +41 -2
  41. package/dist/src/memoryGraph.js +299 -0
  42. package/dist/src/memoryStats.js +75 -0
  43. package/dist/src/monitoring.js +13 -0
  44. package/dist/src/prefix-break.js +43 -0
  45. package/dist/src/pricing.js +1 -1
  46. package/dist/src/queryExpansion.js +163 -0
  47. package/dist/src/queryReformulation/cache.js +123 -0
  48. package/dist/src/queryReformulation/rrf.js +29 -0
  49. package/dist/src/queryReformulation/tfidf.js +61 -0
  50. package/dist/src/queryReformulation/vagueness.js +33 -0
  51. package/dist/src/queryReformulation.js +225 -0
  52. package/dist/src/recallMetrics.js +258 -0
  53. package/dist/src/store/sqlite/cache-stripes.js +133 -0
  54. package/dist/src/store/sqlite/fts5-search.js +68 -0
  55. package/dist/src/store/sqlite/perf-samples.js +145 -16
  56. package/dist/src/store/sqlite/schema.js +60 -1
  57. package/dist/src/store/sqlite.js +1 -0
  58. package/dist/src/store/turns/memory-store.js +8 -0
  59. package/dist/src/store/turns/sqlite-store.js +14 -3
  60. package/dist/src/store/turns/types.js +18 -1
  61. package/dist/src/store/vectorIndex.js +55 -10
  62. package/dist/src/tiered-router/types.js +18 -0
  63. package/dist/src/tieredRouter.js +421 -0
  64. package/dist/src/vectorStore.js +8 -6
  65. package/extensions/dashboard-client/dist/assets/{AchievementTiles-vEo2MtE2.js → AchievementTiles-DR9cLGDL.js} +2 -2
  66. package/extensions/dashboard-client/dist/assets/{AchievementTiles-vEo2MtE2.js.map → AchievementTiles-DR9cLGDL.js.map} +1 -1
  67. package/extensions/dashboard-client/dist/assets/AchievementsTab-svNUgJ1e.js +2 -0
  68. package/extensions/dashboard-client/dist/assets/{AchievementsTab-DjxfYrDc.js.map → AchievementsTab-svNUgJ1e.js.map} +1 -1
  69. package/extensions/dashboard-client/dist/assets/CacheTab-ASwhOMjR.js +2 -0
  70. package/extensions/dashboard-client/dist/assets/CacheTab-ASwhOMjR.js.map +1 -0
  71. package/extensions/dashboard-client/dist/assets/{ConfigTab-Cm6ymbmx.js → ConfigTab-CmmWheuW.js} +2 -2
  72. package/extensions/dashboard-client/dist/assets/{ConfigTab-Cm6ymbmx.js.map → ConfigTab-CmmWheuW.js.map} +1 -1
  73. package/extensions/dashboard-client/dist/assets/{EventsTab-DXWl_YzI.js → EventsTab-CFE7jLsa.js} +2 -2
  74. package/extensions/dashboard-client/dist/assets/{EventsTab-DXWl_YzI.js.map → EventsTab-CFE7jLsa.js.map} +1 -1
  75. package/extensions/dashboard-client/dist/assets/GameTab-CJAX_th_.js +2 -0
  76. package/extensions/dashboard-client/dist/assets/{GameTab-BLDudcO5.js.map → GameTab-CJAX_th_.js.map} +1 -1
  77. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DLkKCztn.js → MaintenanceTab-P4SUsvMf.js} +2 -2
  78. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DLkKCztn.js.map → MaintenanceTab-P4SUsvMf.js.map} +1 -1
  79. package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js +2 -0
  80. package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js.map +1 -0
  81. package/extensions/dashboard-client/dist/assets/{MetricsTab-Cq4u-rZF.js → MetricsTab-CHQscixy.js} +2 -2
  82. package/extensions/dashboard-client/dist/assets/{MetricsTab-Cq4u-rZF.js.map → MetricsTab-CHQscixy.js.map} +1 -1
  83. package/extensions/dashboard-client/dist/assets/OverviewTab-DBs4DJdd.js +2 -0
  84. package/extensions/dashboard-client/dist/assets/{OverviewTab-D4kGvsiQ.js.map → OverviewTab-DBs4DJdd.js.map} +1 -1
  85. package/extensions/dashboard-client/dist/assets/ReposTab-DeFz76L5.js +2 -0
  86. package/extensions/dashboard-client/dist/assets/ReposTab-DeFz76L5.js.map +1 -0
  87. package/extensions/dashboard-client/dist/assets/{SessionsTab-DZC2-GCn.js → SessionsTab-BWNsj1OB.js} +2 -2
  88. package/extensions/dashboard-client/dist/assets/{SessionsTab-DZC2-GCn.js.map → SessionsTab-BWNsj1OB.js.map} +1 -1
  89. package/extensions/dashboard-client/dist/assets/SetupTab-uS6A0xmw.js +2 -0
  90. package/extensions/dashboard-client/dist/assets/SetupTab-uS6A0xmw.js.map +1 -0
  91. package/extensions/dashboard-client/dist/assets/TimeSavedCard-CiqCOQuL.js +2 -0
  92. package/extensions/dashboard-client/dist/assets/TimeSavedCard-CiqCOQuL.js.map +1 -0
  93. package/extensions/dashboard-client/dist/assets/{TopicsTab-01G71hv1.js → TopicsTab-BWWQxltv.js} +2 -2
  94. package/extensions/dashboard-client/dist/assets/{TopicsTab-01G71hv1.js.map → TopicsTab-BWWQxltv.js.map} +1 -1
  95. package/extensions/dashboard-client/dist/assets/{TurnsTab-DLRg0LZT.js → TurnsTab-CVz4f4-o.js} +2 -2
  96. package/extensions/dashboard-client/dist/assets/{TurnsTab-DLRg0LZT.js.map → TurnsTab-CVz4f4-o.js.map} +1 -1
  97. package/extensions/dashboard-client/dist/assets/format-COjDIVGj.js +2 -0
  98. package/extensions/dashboard-client/dist/assets/format-COjDIVGj.js.map +1 -0
  99. package/extensions/dashboard-client/dist/assets/index-CUNn22iV.css +1 -0
  100. package/extensions/dashboard-client/dist/assets/index-DrktNOoT.js +42 -0
  101. package/extensions/dashboard-client/dist/assets/index-DrktNOoT.js.map +1 -0
  102. package/extensions/dashboard-client/dist/assets/pricing-CvE4NllQ.js +2 -0
  103. package/extensions/dashboard-client/dist/assets/pricing-CvE4NllQ.js.map +1 -0
  104. package/extensions/dashboard-client/dist/assets/{useSSE-BBYt5UPB.js → useSSE-Bf5LOLSi.js} +2 -2
  105. package/extensions/dashboard-client/dist/assets/{useSSE-BBYt5UPB.js.map → useSSE-Bf5LOLSi.js.map} +1 -1
  106. package/extensions/dashboard-client/dist/index.html +2 -2
  107. package/extensions/dashboard-client/src/App.tsx +28 -8
  108. package/extensions/dashboard-client/src/api/client.ts +29 -0
  109. package/extensions/dashboard-client/src/components/ActiveReposTable.tsx +21 -12
  110. package/extensions/dashboard-client/src/components/CacheHitRateTrendCard.tsx +100 -0
  111. package/extensions/dashboard-client/src/components/ProviderCacheCard.tsx +56 -0
  112. package/extensions/dashboard-client/src/components/SavingsByModelTable.tsx +38 -57
  113. package/extensions/dashboard-client/src/components/StripeDistributionCard.tsx +110 -0
  114. package/extensions/dashboard-client/src/components/TabBar.tsx +49 -4
  115. package/extensions/dashboard-client/src/main.tsx +1 -0
  116. package/extensions/dashboard-client/src/styles/base.css +30 -0
  117. package/extensions/dashboard-client/src/styles/cache.css +167 -0
  118. package/extensions/dashboard-client/src/tabs/CacheTab.tsx +75 -12
  119. package/extensions/dashboard-client/src/tabs/MemoryMapTab.tsx +440 -0
  120. package/extensions/dashboard-client/src/tabs/SetupTab.tsx +418 -0
  121. package/extensions/dashboard-client/tsconfig.json +3 -2
  122. package/extensions/dashboard-client/vite.config.ts +2 -0
  123. package/extensions/dashboard-server/api-contracts/cache-stripes.ts +49 -0
  124. package/extensions/dashboard-server/api-contracts/endpoints.ts +66 -3
  125. package/extensions/dashboard-server/api-contracts/index.ts +22 -0
  126. package/extensions/dashboard-server/api-contracts/memory-map.ts +50 -0
  127. package/extensions/dashboard-server/api-contracts/memory.ts +27 -0
  128. package/extensions/dashboard-server/api-contracts/multi-repo.ts +231 -231
  129. package/extensions/dashboard-server/api-contracts/provider-cache.ts +37 -0
  130. package/extensions/dashboard-server/api-contracts/setup.ts +80 -0
  131. package/extensions/dashboard-server/index-reader.ts +138 -121
  132. package/extensions/dashboard-server/routes-cache.ts +186 -75
  133. package/extensions/dashboard-server/routes-game.ts +3 -0
  134. package/extensions/dashboard-server/routes-memory-map.ts +105 -0
  135. package/extensions/dashboard-server/routes-memory.ts +63 -0
  136. package/extensions/dashboard-server/routes-repo.ts +36 -36
  137. package/extensions/dashboard-server/routes-setup.ts +293 -0
  138. package/extensions/dashboard-server/routes.ts +4 -0
  139. package/extensions/dashboard-server/server.ts +12 -0
  140. package/extensions/mega-commands.ts +211 -1
  141. package/extensions/mega-compact.ts +5 -0
  142. package/extensions/mega-config.ts +27 -0
  143. package/extensions/mega-events/agent-handlers.ts +20 -4
  144. package/extensions/mega-events/compact-handlers.ts +1 -0
  145. package/extensions/mega-events/context-handler.ts +53 -110
  146. package/extensions/mega-events/context-hwm.ts +85 -0
  147. package/extensions/mega-events/error-classifier.ts +15 -4
  148. package/extensions/mega-events/mirror-append.ts +155 -0
  149. package/extensions/mega-events/outage-advisor.ts +11 -5
  150. package/extensions/mega-events/perf-handler.ts +190 -6
  151. package/extensions/mega-events/recall-tail.ts +88 -0
  152. package/extensions/mega-events/separated-prompt.ts +329 -0
  153. package/extensions/mega-events/session-handlers.ts +14 -7
  154. package/extensions/mega-runtime/env-loader.ts +55 -0
  155. package/extensions/mega-runtime/helpers.ts +14 -0
  156. package/extensions/mega-runtime/reset-runtime.ts +6 -0
  157. package/extensions/mega-runtime/runtime-snapshot.ts +1 -0
  158. package/extensions/mega-runtime/runtime.ts +7 -0
  159. package/extensions/mega-runtime/snapshot.ts +6 -0
  160. package/extensions/mega-runtime/widget-types.ts +3 -0
  161. package/extensions/mega-runtime/widget.ts +5 -0
  162. package/package.json +5 -2
  163. package/src/cache-stripe-impl.ts +385 -0
  164. package/src/cache-stripe.ts +18 -0
  165. package/src/config/stopwords.ts +25 -0
  166. package/src/config.ts +26 -0
  167. package/src/embedder.ts +42 -2
  168. package/src/memoryGraph.ts +438 -0
  169. package/src/memoryStats.ts +107 -0
  170. package/src/monitoring.ts +35 -0
  171. package/src/prefix-break.ts +83 -0
  172. package/src/pricing.ts +2 -2
  173. package/src/queryExpansion.ts +206 -0
  174. package/src/queryReformulation/cache.ts +169 -0
  175. package/src/queryReformulation/rrf.ts +51 -0
  176. package/src/queryReformulation/tfidf.ts +79 -0
  177. package/src/queryReformulation/vagueness.ts +38 -0
  178. package/src/queryReformulation.ts +355 -0
  179. package/src/recallMetrics.ts +388 -0
  180. package/src/store/sqlite/cache-stripes.ts +196 -0
  181. package/src/store/sqlite/fts5-search.ts +90 -0
  182. package/src/store/sqlite/perf-samples.ts +218 -23
  183. package/src/store/sqlite/schema.ts +60 -1
  184. package/src/store/sqlite.ts +1 -0
  185. package/src/store/turns/memory-store.ts +10 -1
  186. package/src/store/turns/sqlite-store.ts +27 -15
  187. package/src/store/turns/types.ts +21 -0
  188. package/src/store/vectorIndex.ts +71 -11
  189. package/src/tiered-router/types.ts +64 -0
  190. package/src/tieredRouter.ts +489 -0
  191. package/src/vectorStore.ts +9 -5
  192. package/dist/extensions/conflict-scan.test.js +0 -115
  193. package/dist/extensions/dashboard-server/api-contracts.test.js +0 -884
  194. package/dist/extensions/dashboard-server/html.test.js +0 -52
  195. package/dist/extensions/dashboard-server/perf-server.test.js +0 -80
  196. package/dist/extensions/dashboard-server/routes-cache.test.js +0 -248
  197. package/dist/extensions/dashboard-server/server.test.js +0 -120
  198. package/dist/extensions/dashboard-server-s32.test.js +0 -181
  199. package/dist/extensions/dashboard-server.test.js +0 -339
  200. package/dist/extensions/mega-cache-replay.test.js +0 -400
  201. package/dist/extensions/mega-compact-s38.test.js +0 -1383
  202. package/dist/extensions/mega-compact.test.js +0 -1112
  203. package/dist/extensions/mega-events.test.js +0 -47
  204. package/dist/extensions/mega-game-cmds.test.js +0 -163
  205. package/dist/extensions/mega-runtime/snapshot.test.js +0 -127
  206. package/dist/extensions/mega-runtime/state.test.js +0 -266
  207. package/dist/extensions/mega-runtime/widget.test.js +0 -343
  208. package/dist/extensions/mega-shutdown-widget.test.js +0 -121
  209. package/dist/extensions/mega-teamrun.test.js +0 -156
  210. package/dist/extensions/mega-trim.test.js +0 -58
  211. package/dist/extensions/mega-turn-store.test.js +0 -68
  212. package/dist/src/boundary.test.js +0 -170
  213. package/dist/src/compact.test.js +0 -78
  214. package/dist/src/config/themes.test.js +0 -94
  215. package/dist/src/dedup/dedup.test.js +0 -41
  216. package/dist/src/dedup/l1.test.js +0 -50
  217. package/dist/src/dedup/raptor/buildHistory.test.js +0 -292
  218. package/dist/src/dedup/raptor/multilevel-serve.test.js +0 -229
  219. package/dist/src/dedup/raptor/multilevel.test.js +0 -238
  220. package/dist/src/dedup/raptor/promote.test.js +0 -69
  221. package/dist/src/dedup/raptor/raptor.test.js +0 -248
  222. package/dist/src/dedup/raptor/retrieval.test.js +0 -95
  223. package/dist/src/dedup/raptor/serve-gate.test.js +0 -298
  224. package/dist/src/dedup/sprint12.test.js +0 -219
  225. package/dist/src/dedup-engine.test.js +0 -472
  226. package/dist/src/driftDetection.test.js +0 -87
  227. package/dist/src/e2e.test.js +0 -698
  228. package/dist/src/engine.test.js +0 -111
  229. package/dist/src/extractive.test.js +0 -147
  230. package/dist/src/fork.test.js +0 -81
  231. package/dist/src/game/scoring.test.js +0 -98
  232. package/dist/src/httpEmbedder.test.js +0 -277
  233. package/dist/src/importance.test.js +0 -372
  234. package/dist/src/intent.test.js +0 -78
  235. package/dist/src/log.test.js +0 -42
  236. package/dist/src/mechanical-fix.test.js +0 -65
  237. package/dist/src/memory.test.js +0 -70
  238. package/dist/src/memoryConsolidate.test.js +0 -38
  239. package/dist/src/memoryOps.test.js +0 -129
  240. package/dist/src/memoryRecall.test.js +0 -171
  241. package/dist/src/memoryRoundtrip.test.js +0 -137
  242. package/dist/src/metrics/turns.test.js +0 -152
  243. package/dist/src/mirror/mirror.test.js +0 -185
  244. package/dist/src/raptor-inject-summaries.test.js +0 -162
  245. package/dist/src/ratio.bench.test.js +0 -897
  246. package/dist/src/recall.integration.test.js +0 -77
  247. package/dist/src/recall.test.js +0 -379
  248. package/dist/src/sprint14.test.js +0 -219
  249. package/dist/src/sprint4x-rag-verification.test.js +0 -93
  250. package/dist/src/store/compression.test.js +0 -139
  251. package/dist/src/store/memoryIndex.test.js +0 -51
  252. package/dist/src/store/migrate.test.js +0 -139
  253. package/dist/src/store/pgOpenGuard.test.js +0 -74
  254. package/dist/src/store/phase01.test.js +0 -81
  255. package/dist/src/store/phase04.test.js +0 -54
  256. package/dist/src/store/sprint10.test.js +0 -186
  257. package/dist/src/store/sqlite/game-achievements.test.js +0 -67
  258. package/dist/src/store/sqlite/game-scores.test.js +0 -106
  259. package/dist/src/store/sqlite/game-state.test.js +0 -76
  260. package/dist/src/store/sqlite/global-index.test.js +0 -136
  261. package/dist/src/store/sqlite/mechanical-fix.test.js +0 -146
  262. package/dist/src/store/sqlite/perf-samples.test.js +0 -146
  263. package/dist/src/store/sqlite/raptor.test.js +0 -106
  264. package/dist/src/store/sqlite/turns.test.js +0 -183
  265. package/dist/src/store/sqlite.cachehit.test.js +0 -55
  266. package/dist/src/store/sqlite.dbmaint.test.js +0 -158
  267. package/dist/src/store/sqlite.dbmirror.test.js +0 -184
  268. package/dist/src/store/turns/contract-compliance.test.js +0 -428
  269. package/dist/src/store/turns/memory-store.test.js +0 -9
  270. package/dist/src/store/turns/migrations.test.js +0 -138
  271. package/dist/src/store/turns/sqlite-store.test.js +0 -144
  272. package/dist/src/store/vectorIndex.test.js +0 -123
  273. package/dist/src/store.test.js +0 -161
  274. package/dist/src/supersede.test.js +0 -36
  275. package/dist/src/topics/cluster.test.js +0 -150
  276. package/dist/src/topics/store.test.js +0 -117
  277. package/dist/src/types.test.js +0 -9
  278. package/dist/src/vector-search-cache.test.js +0 -157
  279. package/dist/src/vectorStore.test.js +0 -479
  280. package/dist/src/wiki.test.js +0 -171
  281. package/extensions/conflict-scan.test.ts +0 -129
  282. package/extensions/dashboard-client/dist/assets/AchievementsTab-DjxfYrDc.js +0 -2
  283. package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js +0 -2
  284. package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js.map +0 -1
  285. package/extensions/dashboard-client/dist/assets/GameTab-BLDudcO5.js +0 -2
  286. package/extensions/dashboard-client/dist/assets/OverviewTab-D4kGvsiQ.js +0 -2
  287. package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js +0 -2
  288. package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js.map +0 -1
  289. package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js +0 -2
  290. package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js.map +0 -1
  291. package/extensions/dashboard-client/dist/assets/index-BEdKlpAB.css +0 -1
  292. package/extensions/dashboard-client/dist/assets/index-BX0bcgLu.js +0 -42
  293. package/extensions/dashboard-client/dist/assets/index-BX0bcgLu.js.map +0 -1
  294. package/extensions/dashboard-server/api-contracts.test.ts +0 -1065
  295. package/extensions/dashboard-server/html.test.ts +0 -62
  296. package/extensions/dashboard-server/perf-server.test.ts +0 -101
  297. package/extensions/dashboard-server/routes-cache.test.ts +0 -302
  298. package/extensions/dashboard-server/server.test.ts +0 -131
  299. package/extensions/dashboard-server-s32.test.ts +0 -195
  300. package/extensions/dashboard-server.test.ts +0 -382
  301. package/extensions/mega-cache-replay.test.ts +0 -526
  302. package/extensions/mega-compact-s38.test.ts +0 -1433
  303. package/extensions/mega-compact.test.ts +0 -1509
  304. package/extensions/mega-events.test.ts +0 -55
  305. package/extensions/mega-game-cmds.test.ts +0 -197
  306. package/extensions/mega-runtime/snapshot.test.ts +0 -139
  307. package/extensions/mega-runtime/state.test.ts +0 -262
  308. package/extensions/mega-runtime/widget.test.ts +0 -468
  309. package/extensions/mega-shutdown-widget.test.ts +0 -141
  310. package/extensions/mega-teamrun.test.ts +0 -176
  311. package/extensions/mega-trim.test.ts +0 -64
  312. package/extensions/mega-turn-store.test.ts +0 -108
  313. package/src/boundary.test.ts +0 -187
  314. package/src/compact.test.ts +0 -99
  315. package/src/config/themes.test.ts +0 -116
  316. package/src/dedup/dedup.test.ts +0 -46
  317. package/src/dedup/l1.test.ts +0 -57
  318. package/src/dedup/raptor/buildHistory.test.ts +0 -353
  319. package/src/dedup/raptor/multilevel-serve.test.ts +0 -273
  320. package/src/dedup/raptor/multilevel.test.ts +0 -325
  321. package/src/dedup/raptor/promote.test.ts +0 -82
  322. package/src/dedup/raptor/raptor.test.ts +0 -297
  323. package/src/dedup/raptor/retrieval.test.ts +0 -118
  324. package/src/dedup/raptor/serve-gate.test.ts +0 -348
  325. package/src/dedup/sprint12.test.ts +0 -242
  326. package/src/dedup-engine.test.ts +0 -670
  327. package/src/driftDetection.test.ts +0 -100
  328. package/src/e2e.test.ts +0 -843
  329. package/src/engine.test.ts +0 -123
  330. package/src/extractive.test.ts +0 -174
  331. package/src/fork.test.ts +0 -94
  332. package/src/game/scoring.test.ts +0 -103
  333. package/src/httpEmbedder.test.ts +0 -286
  334. package/src/importance.test.ts +0 -538
  335. package/src/intent.test.ts +0 -87
  336. package/src/log.test.ts +0 -47
  337. package/src/mechanical-fix.test.ts +0 -70
  338. package/src/memory.test.ts +0 -92
  339. package/src/memoryConsolidate.test.ts +0 -47
  340. package/src/memoryOps.test.ts +0 -148
  341. package/src/memoryRecall.test.ts +0 -194
  342. package/src/memoryRoundtrip.test.ts +0 -155
  343. package/src/metrics/turns.test.ts +0 -165
  344. package/src/mirror/mirror.test.ts +0 -240
  345. package/src/raptor-inject-summaries.test.ts +0 -228
  346. package/src/ratio.bench.test.ts +0 -1316
  347. package/src/recall.integration.test.ts +0 -96
  348. package/src/recall.test.ts +0 -467
  349. package/src/sprint14.test.ts +0 -245
  350. package/src/sprint4x-rag-verification.test.ts +0 -119
  351. package/src/store/compression.test.ts +0 -168
  352. package/src/store/memoryIndex.test.ts +0 -61
  353. package/src/store/migrate.test.ts +0 -158
  354. package/src/store/pgOpenGuard.test.ts +0 -89
  355. package/src/store/phase01.test.ts +0 -88
  356. package/src/store/phase04.test.ts +0 -58
  357. package/src/store/sprint10.test.ts +0 -214
  358. package/src/store/sqlite/game-achievements.test.ts +0 -80
  359. package/src/store/sqlite/game-scores.test.ts +0 -132
  360. package/src/store/sqlite/game-state.test.ts +0 -89
  361. package/src/store/sqlite/global-index.test.ts +0 -222
  362. package/src/store/sqlite/mechanical-fix.test.ts +0 -162
  363. package/src/store/sqlite/perf-samples.test.ts +0 -169
  364. package/src/store/sqlite/raptor.test.ts +0 -139
  365. package/src/store/sqlite/turns.test.ts +0 -218
  366. package/src/store/sqlite.cachehit.test.ts +0 -68
  367. package/src/store/sqlite.dbmaint.test.ts +0 -193
  368. package/src/store/sqlite.dbmirror.test.ts +0 -229
  369. package/src/store/turns/contract-compliance.test.ts +0 -562
  370. package/src/store/turns/memory-store.test.ts +0 -15
  371. package/src/store/turns/migrations.test.ts +0 -161
  372. package/src/store/turns/sqlite-store.test.ts +0 -161
  373. package/src/store/vectorIndex.test.ts +0 -140
  374. package/src/store.test.ts +0 -191
  375. package/src/supersede.test.ts +0 -42
  376. package/src/topics/cluster.test.ts +0 -176
  377. package/src/topics/store.test.ts +0 -132
  378. package/src/types.test.ts +0 -10
  379. package/src/vector-search-cache.test.ts +0 -190
  380. package/src/vectorStore.test.ts +0 -533
  381. package/src/wiki.test.ts +0 -217
@@ -1,1383 +0,0 @@
1
- /**
2
- * mega-compact.s38-error-retry.test.ts — S38 error-retry safety net tests.
3
- *
4
- * Extracted from mega-compact.test.ts so the error-retry feature has a focused,
5
- * fast-isolated test file. Exercises the real extension entry through a mock pi:
6
- * - classifyError classifier (transient / permanent / compaction-noop / null)
7
- * - turn_end retry nudges (max 5 transient, max 1 permanent)
8
- * - compaction-noop DOES NOT retry (pi race guard)
9
- * - MAX=0 disables transient retries
10
- * - mid-response errors (stream died without a stopReason)
11
- * - S38.5 race-guard (strict deferred vs synchronous)
12
- *
13
- * The harness is duplicated from mega-compact.test.ts (per guardrails guidance to
14
- * keep each test file self-contained; the harness is ~200 lines and does not
15
- * warrant a shared helper module that would itself exceed the 500-line target).
16
- */
17
- import { test } from "node:test";
18
- import assert from "node:assert/strict";
19
- import { mkdtempSync, rmSync } from "node:fs";
20
- import { tmpdir } from "node:os";
21
- import { join } from "node:path";
22
- import { createRequire } from "node:module";
23
- import { closeVectorIndex } from "../src/store/vectorIndex.js";
24
- const require = createRequire(import.meta.url);
25
- const baseTmp = mkdtempSync(join(tmpdir(), "mc-s38-"));
26
- // Isolate the machine-wide repo index so test runs never pollute the real one.
27
- process.env.MEGACOMPACT_INDEX_DIR = join(baseTmp, "index");
28
- let counter = 0;
29
- /** Read events.log and return an array of event type strings. */
30
- function eventTypes(stateDir) {
31
- const { readFileSync: rf, existsSync: ex } = require("node:fs");
32
- const { join: j } = require("node:path");
33
- const logPath = j(stateDir, "events.log");
34
- if (!ex(logPath))
35
- return [];
36
- const content = rf(logPath, "utf-8").trim();
37
- if (content.length === 0)
38
- return [];
39
- return content
40
- .split("\n")
41
- .map((line) => { try {
42
- return JSON.parse(line).type;
43
- }
44
- catch {
45
- return undefined;
46
- } })
47
- .filter((t) => typeof t === "string");
48
- }
49
- /** R11: read events.log and return full JSON payloads for a given type. */
50
- function eventPayloads(stateDir, type) {
51
- const { readFileSync: rf, existsSync: ex } = require("node:fs");
52
- const { join: j } = require("node:path");
53
- const logPath = j(stateDir, "events.log");
54
- if (!ex(logPath))
55
- return [];
56
- const content = rf(logPath, "utf-8").trim();
57
- if (content.length === 0)
58
- return [];
59
- return content
60
- .split("\n")
61
- .map((line) => { try {
62
- return JSON.parse(line);
63
- }
64
- catch {
65
- return undefined;
66
- } })
67
- .filter((p) => typeof p === "object" && p !== null && p.type === type);
68
- }
69
- /** Build a mock pi + ctx and load the extension into them. */
70
- function harness(opts = {}) {
71
- const stateDir = join(baseTmp, `run-${counter++}`);
72
- process.env.MEGACOMPACT_STATE_DIR = stateDir;
73
- process.env.MEGACOMPACT_DEBUG = "true";
74
- if (!opts.keepThreshold)
75
- process.env.MEGACOMPACT_THRESHOLD_TOKENS = "50";
76
- if (!opts.keepTier)
77
- delete process.env.MEGACOMPACT_TIER;
78
- process.env.MEGACOMPACT_FAST_GATE_PCT = "1";
79
- const handlers = {};
80
- const appended = [];
81
- const sendUserMessages = [];
82
- const compactCalls = [];
83
- const session = [
84
- { role: "user", content: "read src/vec.ts and understand the index", timestamp: 0 },
85
- { role: "assistant", content: [{ type: "toolCall", name: "Read", id: "c1", arguments: {} }], api: "anthropic-messages", provider: "anthropic", model: "m", usage: { inputTokens: 1, outputTokens: 1, cacheReadTokens: 0, cacheWriteTokens: 0 }, stopReason: "tool_use", timestamp: 0 },
86
- { role: "user", content: "edit src/vec.ts to add a cosine helper", timestamp: 0 },
87
- { role: "assistant", content: [{ type: "toolCall", name: "Edit", id: "c1", arguments: {} }], api: "anthropic-messages", provider: "anthropic", model: "m", usage: { inputTokens: 1, outputTokens: 1, cacheReadTokens: 0, cacheWriteTokens: 0 }, stopReason: "tool_use", timestamp: 0 },
88
- ];
89
- const toEntry = (m, i) => ({ type: "message", id: `e${i}`, parentId: null, timestamp: String(i), message: m });
90
- const sessionManager = { getSessionId: () => "sess_ext_001", getEntries: () => session.map(toEntry), getBranch: () => session.map(toEntry) };
91
- function makeCtx(over = {}) {
92
- return {
93
- ui: { setStatus: () => { }, notify: () => { }, select: () => { }, confirm: async () => true, input: async () => "", setWidget: () => { } },
94
- mode: "tui", hasUI: true, cwd: stateDir, sessionManager,
95
- modelRegistry: {}, model: undefined,
96
- isIdle: () => true, isProjectTrusted: () => true,
97
- signal: undefined, abort: () => { }, hasPendingMessages: () => false, shutdown: () => { },
98
- getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }),
99
- compact: (opts) => {
100
- compactCalls.push(opts);
101
- const _sbc = handlers["session_before_compact"];
102
- if (_sbc && _sbc.length)
103
- return _sbc[0]({ type: "session_before_compact", reason: "threshold", willRetry: false, signal: undefined, preparation: { firstKeptEntryId: "e2", messagesToSummarize: session.slice(0, 2), tokensBefore: 500 } }, makeCtx());
104
- return undefined;
105
- },
106
- getSystemPrompt: () => "system base", ...over,
107
- };
108
- }
109
- const pi = {
110
- on: (ev, h) => { if (!handlers[ev])
111
- handlers[ev] = []; handlers[ev].push(h); },
112
- registerCommand: () => { }, registerTool: () => { }, registerShortcut: () => { },
113
- registerFlag: () => { }, getFlag: () => undefined,
114
- registerMessageRenderer: () => { }, registerEntryRenderer: () => { },
115
- sendMessage: () => { }, sendUserMessage: (m) => { sendUserMessages.push(m); },
116
- appendEntry: (t, d) => appended.push({ t, d }),
117
- setSessionName: () => { }, getSessionName: () => undefined, setLabel: () => { },
118
- exec: async () => ({ stdout: "", stderr: "", code: 0 }),
119
- getActiveTools: () => [], getAllTools: () => { }, setActiveTools: () => { },
120
- getCommands: () => [], setModel: async () => false,
121
- getThinkingLevel: () => "off", setThinkingLevel: () => { },
122
- };
123
- const mod = require("./mega-compact.js");
124
- mod.default(pi);
125
- return {
126
- stateDir, handlers, appended, compactCalls, sendUserMessages,
127
- fire: async (ev, event, ctx) => { let r; for (const h of handlers[ev] || [])
128
- r = await h(event, ctx); return r; },
129
- ctx: makeCtx, session,
130
- };
131
- }
132
- const { classifyError: classifyErrorFn, classifyErrorDetailed: classifyErrorDetailedFn, extractErrorSignature: extractErrorSignatureFn } = require("./mega-events.js");
133
- /** S38 helper: fire a turn_end with a given stopReason + optional text, using a
134
- * low-pressure ctx so the durable-trim branch (ctx.compact) does NOT fire. */
135
- async function s38TurnEnd(h, stopReason, text) {
136
- const lowCtx = h.ctx({ isIdle: () => true, hasPendingMessages: () => false, getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }) });
137
- const message = { role: "assistant" };
138
- if (stopReason !== undefined)
139
- message.stopReason = stopReason;
140
- if (text)
141
- message.content = text;
142
- await h.fire("turn_end", { type: "turn_end", turnIndex: 1, message }, lowCtx);
143
- }
144
- /** R3 helper: like s38TurnEnd but attaches a `usage` object so the classifier's
145
- * 0-token poisoned-context signal is exercised (usage PRESENT with 0 tokens).
146
- * Pass `tokens` > 0 to simulate a turn that reached the model. */
147
- async function s38TurnEndUsage(h, stopReason, text, tokens) {
148
- const lowCtx = h.ctx({ isIdle: () => true, hasPendingMessages: () => false, getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }) });
149
- const message = { role: "assistant", usage: { inputTokens: tokens, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 } };
150
- if (stopReason !== undefined)
151
- message.stopReason = stopReason;
152
- if (text)
153
- message.content = text;
154
- await h.fire("turn_end", { type: "turn_end", turnIndex: 1, message }, lowCtx);
155
- }
156
- // ---- classifier unit tests (no extension harness needed) ----
157
- test("S38: classifyError returns 'transient' for error stopReason", () => {
158
- assert.equal(classifyErrorFn({ stopReason: "error" }), "transient");
159
- });
160
- test("S38: classifyError returns 'cancelled' for aborted stopReason (ESC/Ctrl-C)", () => {
161
- assert.equal(classifyErrorFn({ stopReason: "aborted" }), "cancelled");
162
- assert.equal(classifyErrorFn({ stopReason: "aborted", errorMessage: "Operation aborted" }), "cancelled");
163
- assert.equal(classifyErrorFn({ stopReason: "aborted", errorMessage: "Aborted after 3 retry attempts" }), "cancelled");
164
- });
165
- test("S38: classifyError does NOT return 'cancelled' for error message containing 'aborted' with non-aborted stopReason", () => {
166
- // Defense: the old text-based s.includes('aborted') path is gone,
167
- // but verify that an error message mentioning 'aborted' with stopReason
168
- // 'error' (not 'aborted') still classifies as transient, not cancelled.
169
- assert.equal(classifyErrorFn({ stopReason: "error", errorMessage: "Connection aborted" }), "transient");
170
- });
171
- test("S38: classifyError returns 'transient' for max-output-token text", () => {
172
- assert.equal(classifyErrorFn({ stopReason: "error", content: "reached the maximum output token limit" }), "transient");
173
- assert.equal(classifyErrorFn("max output token exceeded"), "transient");
174
- });
175
- test("S38: classifyError returns 'permanent' for auth/unauthorized text", () => {
176
- assert.equal(classifyErrorFn("unauthorized: invalid api key"), "permanent");
177
- assert.equal(classifyErrorFn("permission denied"), "permanent");
178
- });
179
- test("S38: classifyError returns null for stop/toolUse stopReasons (success)", () => {
180
- assert.equal(classifyErrorFn({ stopReason: "stop" }), null);
181
- assert.equal(classifyErrorFn({ stopReason: "toolUse" }), null);
182
- assert.equal(classifyErrorFn({ stopReason: "tool_use" }), null);
183
- });
184
- test("S38: classifyError returns null for 'length' stopReason (S28 guard)", () => {
185
- assert.equal(classifyErrorFn({ stopReason: "length" }), null);
186
- });
187
- test("S38: classifyError returns 'transient' for mid-response errors with no stopReason", () => {
188
- assert.equal(classifyErrorFn({}), "transient");
189
- assert.equal(classifyErrorFn({ content: [] }), "transient");
190
- assert.equal(classifyErrorFn({ stopReason: undefined }), "transient");
191
- assert.equal(classifyErrorFn({ stopReason: "" }), "transient");
192
- });
193
- test("S38: classifyError returns 'transient' for error objects with message field", () => {
194
- assert.equal(classifyErrorFn({ error: { message: "Stream interrupted" } }), "transient");
195
- assert.equal(classifyErrorFn({ error: { message: "Connection lost" } }), "transient");
196
- assert.equal(classifyErrorFn({ error: { message: "500 Internal Server Error" } }), "transient");
197
- assert.equal(classifyErrorFn({ error: "Connection lost" }), "transient");
198
- });
199
- test("S38: classifyError returns 'transient' for mid-response stream failures in content", () => {
200
- assert.equal(classifyErrorFn({ content: [{ type: "text", text: "Processing... Error: connection reset" }] }), "transient");
201
- assert.equal(classifyErrorFn({ content: [{ type: "text", text: "Here is the answer..." }], stopReason: "error" }), "transient");
202
- });
203
- test("S38: classifyError returns 'transient' for partial content with NO stopReason (stream died after emitting text)", () => {
204
- // The mid-response disconnect case: provider streamed partial content then
205
- // died without a stopReason. MUST be transient (retryable), NOT null.
206
- assert.equal(classifyErrorFn({ role: "assistant", content: [{ type: "text", text: "partial response..." }], stopReason: undefined }), "transient");
207
- assert.equal(classifyErrorFn({ role: "assistant", content: [{ type: "text", text: "Here is the start of the answer" }] }), "transient");
208
- assert.equal(classifyErrorFn({ role: "assistant", content: "partial response..." }), "transient");
209
- });
210
- test("S38: classifyError returns null for success stopReasons even with empty content", () => {
211
- assert.equal(classifyErrorFn({ stopReason: "stop", content: [] }), null);
212
- assert.equal(classifyErrorFn({ stopReason: "tool_use", content: [] }), null);
213
- });
214
- test("S38: classifyError returns 'compaction-noop' for 'Already compacted' text", () => {
215
- assert.equal(classifyErrorFn("Error: Already compacted"), "compaction-noop");
216
- });
217
- test("S38: classifyError returns 'compaction-noop' for 'Nothing to compact' text", () => {
218
- assert.equal(classifyErrorFn("Nothing to compact (session too small)"), "compaction-noop");
219
- });
220
- test("S38: classifyError returns 'compaction-noop' for 'Auto compaction failed' text", () => {
221
- assert.equal(classifyErrorFn("Auto compaction failed"), "compaction-noop");
222
- assert.equal(classifyErrorFn("Auto-compaction failed"), "compaction-noop");
223
- });
224
- // ---- context-overflow classifier (S38.8: 400 "too long... even after compaction") ----
225
- test("S38: classifyError returns 'context-overflow' for the literal user 400 string", () => {
226
- assert.equal(classifyErrorFn("Your conversation is too long for this model's context window even after compaction. Reduce the conversation length or enable/allow compaction."), "context-overflow");
227
- });
228
- test("S38: classifyError returns 'context-overflow' for 'All targets failed' wrapper", () => {
229
- assert.equal(classifyErrorFn("All targets failed: ... Last error: ... too long ... context window even after compaction"), "context-overflow");
230
- });
231
- test("S38: classifyError returns 'context-overflow' for invalid_request_error JSON shape", () => {
232
- assert.equal(classifyErrorFn('{"type":"invalid_request_error","message":"... too long for context window ..."}'), "context-overflow");
233
- });
234
- test("S38: classifyError returns 'context-overflow' for OpenRouter 'All targets failed' max-context 400 (FAIL-20260725)", () => {
235
- // Regression: the OpenAI/OpenRouter provider-side phrasing
236
- // "maximum context length is N tokens ... requires at least M tokens ...
237
- // reduce your input or max_tokens" does NOT contain "too long" / "context
238
- // window" / "reduce the conversation", so it slipped past the original S38.8
239
- // regex and fell through to the generic transient branch, firing 5 blind
240
- // retry nudges that re-submitted the same oversized prompt -> re-400 ->
241
- // busy-loop. The exact user-facing wrapper string from the router:
242
- assert.equal(classifyErrorFn('Error: 400: {"message":"All targets failed: neuralwatt/glm-5.2-short. Last error: This model\'s maximum context length is 200000 tokens. Your request requires at least 201070 tokens (201070 prompt + 0 max_tokens). Please reduce your input or max_tokens.","type":"invalid_request_error"}'), "context-overflow");
243
- });
244
- test("S38: classifyError returns 'context-overflow' for bare 'maximum context length' phrasing", () => {
245
- assert.equal(classifyErrorFn("This model's maximum context length is 128000 tokens. Your request requires at least 130000 tokens. Please reduce your input or max_tokens."), "context-overflow");
246
- });
247
- test("S38: classifyError returns 'context-overflow' for 'context length exceeded' phrasing", () => {
248
- assert.equal(classifyErrorFn("This model's context length exceeded: 200000 tokens"), "context-overflow");
249
- });
250
- test("S38: context-overflow fires NO blind retry nudge and logs 'context_overflow'", async () => {
251
- const h = harness();
252
- await s38TurnEnd(h, "error", "Your conversation is too long for this model's context window even after compaction. Reduce the conversation length or enable/allow compaction.");
253
- const ev = eventTypes(h.stateDir);
254
- assert.equal(h.sendUserMessages.length, 0, "context-overflow: NO blind retry nudge fired");
255
- assert.ok(ev.includes("context_overflow"), "context-overflow: 'context_overflow' event logged");
256
- });
257
- test("S38: ESC-abort (stopReason='aborted') fires NO retry nudge and logs 'error_retry_cancelled'", async () => {
258
- const h = harness();
259
- await s38TurnEnd(h, "aborted", "Operation aborted");
260
- const ev = eventTypes(h.stateDir);
261
- assert.equal(h.sendUserMessages.length, 0, "cancelled: NO retry nudge fired after ESC abort");
262
- assert.ok(ev.includes("error_retry_cancelled"), "cancelled: 'error_retry_cancelled' event logged");
263
- });
264
- test("S38: ESC-abort resets errorRetryCount and consecutiveErrors so next transient retry starts fresh", async () => {
265
- const h = harness();
266
- // First: simulate an ESC abort (should reset both counters to 0)
267
- await s38TurnEnd(h, "aborted", "Operation aborted");
268
- assert.equal(h.sendUserMessages.length, 0, "cancelled: no nudge on abort");
269
- // Then: simulate a real transient error — should fire a fresh retry from count=1
270
- await s38TurnEnd(h, "error", "500 Internal Server Error");
271
- assert.equal(h.sendUserMessages.length, 1, "transient after cancel: retry fires from fresh count");
272
- });
273
- // ---- integration tests (fire turn_end through the real extension) ----
274
- test("S38: compaction-noop logs 'compaction_noop_diagnostic' + resets counter + no retry fired", async () => {
275
- const h = harness();
276
- await s38TurnEnd(h, "error", "Already compacted");
277
- const ev = eventTypes(h.stateDir);
278
- assert.ok(ev.includes("compaction_noop_diagnostic"), "compaction-noop: diagnostic event logged");
279
- assert.equal(h.sendUserMessages.length, 0, "compaction-noop: NO retry nudge fired");
280
- });
281
- test("S38: compaction-noop does NOT fire pi.sendUserMessage (NOT retryable)", async () => {
282
- const h = harness();
283
- await s38TurnEnd(h, "error", "Nothing to compact");
284
- assert.equal(h.sendUserMessages.length, 0, "compaction-noop: no sendUserMessage");
285
- assert.ok(eventTypes(h.stateDir).includes("compaction_noop_diagnostic"));
286
- });
287
- test("S38: R1 burst of immediate transient errors fires 1 nudge (dedup), rest suppressed by retryNudgePending", async () => {
288
- // R1 redesign: a burst of immediate error turn_ends (no turn_start between)
289
- // produces ONE nudge — the rest are suppressed by retryNudgePending because
290
- // the queued nudge (deliverAs:'followUp') has not been consumed by a new
291
- // agent turn. errorRetryCount still advances for each error turn, so the
292
- // per-burst max + circuit breaker still bound the burst.
293
- // R10: disable outage advisory so this test isolates nudge dedup only.
294
- const prevOutage = process.env.MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD;
295
- process.env.MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD = "0";
296
- try {
297
- const h = harness();
298
- for (let i = 0; i < 5; i++)
299
- await s38TurnEnd(h, "error", `internal server error ${i}`);
300
- assert.equal(h.sendUserMessages.length, 1, "R1 dedup: 1 nudge in burst (rest suppressed)");
301
- // The 6th turn reaches count=6 > max=5 → exhausted (count advances even for dedup'd turns).
302
- await s38TurnEnd(h, "error", "internal server error 5");
303
- assert.equal(h.sendUserMessages.length, 1, "exhausted: still 1 nudge (no 6th)");
304
- assert.ok(eventTypes(h.stateDir).includes("error_retry_exhausted"), "exhausted event logged on max+1");
305
- assert.ok(eventTypes(h.stateDir).includes("error_retry_dedup_skip"), "dedup_skip events logged for suppressed turns");
306
- }
307
- finally {
308
- if (prevOutage === undefined)
309
- delete process.env.MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD;
310
- else
311
- process.env.MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD = prevOutage;
312
- }
313
- });
314
- test("S38: retry fires 1x for permanent errors then stops", async () => {
315
- const h = harness();
316
- await s38TurnEnd(h, "error", "invalid api key");
317
- assert.equal(h.sendUserMessages.length, 1, "permanent: 1 retry nudge (<= max 1)");
318
- await s38TurnEnd(h, "error", "invalid api key");
319
- assert.equal(h.sendUserMessages.length, 1, "permanent: exhausted -> no 2nd nudge");
320
- assert.ok(eventTypes(h.stateDir).includes("error_retry_exhausted"), "permanent exhausted logged");
321
- });
322
- test("S38: successful turn (stop/toolUse) resets the retry counter", async () => {
323
- // R1: backoff is now gating, so use a tiny backoff + small wait to let the
324
- // second nudge fire after the success reset clears retryNudgePending (R4).
325
- const prev = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
326
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
327
- try {
328
- const h = harness();
329
- await s38TurnEnd(h, "error", "5xx server error 0");
330
- assert.equal(h.sendUserMessages.length, 1, "first transient: 1 nudge");
331
- await s38TurnEnd(h, "stop");
332
- assert.equal(h.sendUserMessages.length, 1, "success: no nudge, resets pending (R4)");
333
- await new Promise((r) => setTimeout(r, 5)); // let backoff elapse
334
- await s38TurnEnd(h, "error", "5xx server error 1");
335
- assert.equal(h.sendUserMessages.length, 2, "success reset -> transient fires again from count=1");
336
- }
337
- finally {
338
- if (prev === undefined)
339
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
340
- else
341
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = prev;
342
- }
343
- });
344
- test("S38: error_retry_exhausted event logged when max exceeded", async () => {
345
- // R3: "malformed bad request" is now poisoned-context (not permanent), so
346
- // use an auth-derived permanent error to exercise the per-burst exhausted path.
347
- const h = harness();
348
- await s38TurnEnd(h, "error", "unauthorized: invalid api key");
349
- await s38TurnEnd(h, "error", "unauthorized: invalid api key");
350
- assert.ok(eventTypes(h.stateDir).includes("error_retry_exhausted"), "error_retry_exhausted logged");
351
- });
352
- test("S38: MEGACOMPACT_AUTO_RETRY_TRANSIENT_MAX=0 disables transient retries cleanly", async () => {
353
- const prev = process.env.MEGACOMPACT_AUTO_RETRY_TRANSIENT_MAX;
354
- process.env.MEGACOMPACT_AUTO_RETRY_TRANSIENT_MAX = "0";
355
- try {
356
- const h = harness();
357
- await s38TurnEnd(h, "error", "network timeout");
358
- assert.equal(h.sendUserMessages.length, 0, "max=0: no transient retry nudge");
359
- assert.ok(!eventTypes(h.stateDir).includes("error_retry"), "max=0: no error_retry event");
360
- }
361
- finally {
362
- if (prev === undefined)
363
- delete process.env.MEGACOMPACT_AUTO_RETRY_TRANSIENT_MAX;
364
- else
365
- process.env.MEGACOMPACT_AUTO_RETRY_TRANSIENT_MAX = prev;
366
- }
367
- });
368
- test("S38: retry fires for mid-response errors (no stopReason — stream died silently)", async () => {
369
- const h = harness();
370
- const lowCtx = h.ctx({ isIdle: () => true, hasPendingMessages: () => false, getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }) });
371
- await h.fire("turn_end", { type: "turn_end", turnIndex: 1, message: { role: "assistant" } }, lowCtx);
372
- assert.equal(h.sendUserMessages.length, 1, "mid-response silent failure: 1 retry nudge fired");
373
- assert.ok(eventTypes(h.stateDir).includes("error_retry"), "error_retry event logged for mid-response failure");
374
- });
375
- test("S38: retry fires for error objects with message field", async () => {
376
- const h = harness();
377
- const lowCtx = h.ctx({ isIdle: () => true, hasPendingMessages: () => false, getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }) });
378
- await h.fire("turn_end", { type: "turn_end", turnIndex: 1, message: { role: "assistant", error: { message: "Connection reset by peer" } } }, lowCtx);
379
- assert.equal(h.sendUserMessages.length, 1, "error object with message: 1 retry nudge fired");
380
- });
381
- test("S38: retry fires for partial content with no stopReason (disconnect after emitting text)", async () => {
382
- // The post-resume disconnect case: provider streamed partial content then
383
- // died mid-stream with NO stopReason. Must fire a retry nudge.
384
- const h = harness();
385
- const lowCtx = h.ctx({ isIdle: () => true, hasPendingMessages: () => false, getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }) });
386
- await h.fire("turn_end", {
387
- type: "turn_end",
388
- turnIndex: 1,
389
- message: { role: "assistant", content: [{ type: "text", text: "Here is the start of the answer" }], stopReason: undefined },
390
- }, lowCtx);
391
- assert.equal(h.sendUserMessages.length, 1, "partial-content mid-response failure: 1 retry nudge fired");
392
- assert.ok(eventTypes(h.stateDir).includes("error_retry"), "error_retry event logged for partial-content failure");
393
- });
394
- // ---- S38.5: race-guard strengthening (cooldown 10s->30s + deferred re-check) ---
395
- test("S38.5: MEGACOMPACT_RACE_GUARD_STRICT=false reverts to synchronous 10s guard", async () => {
396
- const prev = process.env.MEGACOMPACT_RACE_GUARD_STRICT;
397
- const prevFloor = process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
398
- process.env.MEGACOMPACT_RACE_GUARD_STRICT = "false";
399
- process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR = "0";
400
- try {
401
- const h = harness();
402
- const hiCtx = h.ctx({ isIdle: () => true, hasPendingMessages: () => false, getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }) });
403
- await h.fire("agent_end", { type: "agent_end", messages: [] }, hiCtx);
404
- assert.ok(h.compactCalls.length >= 1, "non-strict: synchronous ctx.compact() fired");
405
- }
406
- finally {
407
- if (prev === undefined)
408
- delete process.env.MEGACOMPACT_RACE_GUARD_STRICT;
409
- else
410
- process.env.MEGACOMPACT_RACE_GUARD_STRICT = prev;
411
- if (prevFloor === undefined)
412
- delete process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
413
- else
414
- process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR = prevFloor;
415
- }
416
- });
417
- test("S38.5: strict (default) defers ctx.compact() via setTimeout re-check", async () => {
418
- const prevFloor = process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
419
- process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR = "0";
420
- try {
421
- const h = harness();
422
- const hiCtx = h.ctx({ isIdle: () => true, hasPendingMessages: () => false, getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }) });
423
- await h.fire("agent_end", { type: "agent_end", messages: [] }, hiCtx);
424
- assert.equal(h.compactCalls.length, 0, "strict: ctx.compact() NOT called synchronously (deferred)");
425
- await new Promise((r) => setTimeout(r, 700));
426
- assert.ok(h.compactCalls.length >= 1, "strict: deferred ctx.compact() fired after re-check");
427
- }
428
- finally {
429
- if (prevFloor === undefined)
430
- delete process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
431
- else
432
- process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR = prevFloor;
433
- }
434
- });
435
- // ---- R3 classifier unit tests: poisoned-context signals ----
436
- test("R9 classifier: bare 0-token generic error (usage present, 0 tokens) → transient (corroboration required)", () => {
437
- // R9: The 2026-07-30 incidents proved that 0-token ≠ deterministic rejection
438
- // when a router fronts the provider (the request never reached any model).
439
- // Signal 3 now returns 'transient'; the repeat detector in agent-handlers.ts
440
- // becomes the corroboration mechanism. A bare 0-token error that repeats
441
- // ≥ poisonedContextRepeatThreshold (default 3) upgrades to poisoned.
442
- assert.equal(classifyErrorFn({ stopReason: "error", usage: { inputTokens: 0, outputTokens: 0 } }), "transient");
443
- // Bare stopReason 'error' with NO usage field stays transient (unknown tokens
444
- // — conservative; preserves the pre-R3 mid-response/partial-content behavior).
445
- assert.equal(classifyErrorFn({ stopReason: "error" }), "transient");
446
- });
447
- test("R3 classifier: ECONNRESET (0-token) → transient (network failures stay transient)", () => {
448
- // R3: network failures must stay transient even with 0 tokens.
449
- assert.equal(classifyErrorFn({ stopReason: "error", content: "ECONNRESET", usage: { inputTokens: 0, outputTokens: 0 } }), "transient");
450
- assert.equal(classifyErrorFn({ stopReason: "error", content: "connection reset by peer", usage: { inputTokens: 0, outputTokens: 0 } }), "transient");
451
- assert.equal(classifyErrorFn({ stopReason: "error", content: "timeout", usage: { inputTokens: 0, outputTokens: 0 } }), "transient");
452
- assert.equal(classifyErrorFn({ stopReason: "error", content: "503 service unavailable", usage: { inputTokens: 0, outputTokens: 0 } }), "transient");
453
- });
454
- test("R3 classifier: 'request failed' generic (no transient marker) → poisoned-context", () => {
455
- // The exact incident phrasing: "Request failed — please retry." with no
456
- // specific transient cause → deterministic rejection.
457
- assert.equal(classifyErrorFn({ stopReason: "error", content: "Request failed — please retry.", usage: { inputTokens: 0, outputTokens: 0 } }), "poisoned-context");
458
- assert.equal(classifyErrorFn("request failed"), "poisoned-context");
459
- });
460
- test("R3 classifier: orphaned-tool-result 400 (non-overflow) → poisoned-context", () => {
461
- // Provider request-validation 400 that is NOT context-overflow: orphaned
462
- // tool result / malformed message structure. Previously 'permanent' (1
463
- // retry), now 'poisoned-context' (retry re-submits the same malformed shape).
464
- assert.equal(classifyErrorFn('{"type":"invalid_request_error","message":"orphaned tool result: tooluse ids mismatch"}'), "poisoned-context");
465
- assert.equal(classifyErrorFn("invalid request: unexpected role ordering"), "poisoned-context");
466
- assert.equal(classifyErrorFn("malformed message structure"), "poisoned-context");
467
- });
468
- test("R3 classifier: context-overflow phrasing still context-overflow (not poisoned)", () => {
469
- // Regression guard: the context-overflow check runs BEFORE the poisoned
470
- // signals, so a 400 "too long" stays context-overflow (forced re-compact),
471
- // not poisoned (advise + compact).
472
- assert.equal(classifyErrorFn({ stopReason: "error", content: "Your conversation is too long for this model's context window even after compaction.", usage: { inputTokens: 0, outputTokens: 0 } }), "context-overflow");
473
- assert.equal(classifyErrorFn('{"type":"invalid_request_error","message":"maximum context length is 200000 tokens. requires at least 201070 tokens."}'), "context-overflow");
474
- });
475
- test("R3 classifier: auth/permission stays permanent (not poisoned)", () => {
476
- // Auth errors are retryable-once (permanent), not poisoned — the user can
477
- // fix the key and retry.
478
- assert.equal(classifyErrorFn("unauthorized: invalid api key"), "permanent");
479
- assert.equal(classifyErrorFn("permission denied"), "permanent");
480
- });
481
- // ---- R6 integration tests (retry redesign) ----
482
- test("R6(a): 10 consecutive identical 0-token transient failures produce at most errorRetrySessionMax nudges", async () => {
483
- // Use TRANSIENT 0-token failures (network text so they're transient, not
484
- // poisoned) + turn_start + tiny backoff between each so the nudge is
485
- // consumed and the next turn can fire. sessionMax default = 3. Repeat
486
- // threshold raised to disable the stateful poisoned upgrade so this
487
- // exercises the SESSION CAP, not the repeat signal.
488
- const prevBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
489
- const prevRepeat = process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD;
490
- const prevOutage = process.env.MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD;
491
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
492
- process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD = "999";
493
- // R10: disable the outage advisory so this test isolates the session cap only.
494
- process.env.MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD = "0";
495
- try {
496
- const h = harness();
497
- for (let i = 0; i < 10; i++) {
498
- // 0-token transient: usage present with 0 tokens + "connection reset" (network marker).
499
- await s38TurnEndUsage(h, "error", "connection reset", 0);
500
- await h.fire("turn_start", { type: "turn_start", turnIndex: i + 2 }, h.ctx());
501
- await new Promise((r) => setTimeout(r, 3));
502
- }
503
- assert.ok(h.sendUserMessages.length <= 3, `R6(a): at most sessionMax (3) nudges, got ${h.sendUserMessages.length}`);
504
- assert.ok(eventTypes(h.stateDir).includes("error_retry_session_exhausted"), "session_exhausted event logged");
505
- }
506
- finally {
507
- if (prevBackoff === undefined)
508
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
509
- else
510
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = prevBackoff;
511
- if (prevRepeat === undefined)
512
- delete process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD;
513
- else
514
- process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD = prevRepeat;
515
- if (prevOutage === undefined)
516
- delete process.env.MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD;
517
- else
518
- process.env.MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD = prevOutage;
519
- }
520
- });
521
- test("R6(b): poisoned-context fires zero retry nudges and exactly one advise message", async () => {
522
- // auto=false so the guarded compact attempt (R3c) is skipped — this test
523
- // focuses on the advise + no-retry behavior. The compact path is the same
524
- // race-guarded deferred mechanism already covered by the context-overflow tests.
525
- const prevAuto = process.env.MEGACOMPACT_AUTO;
526
- process.env.MEGACOMPACT_AUTO = "false";
527
- try {
528
- const h = harness();
529
- // 0-token generic "request failed" (no transient marker) → poisoned.
530
- await s38TurnEndUsage(h, "error", "Request failed — please retry.", 0);
531
- assert.equal(h.sendUserMessages.length, 1, "poisoned: exactly one advise message");
532
- assert.ok(h.sendUserMessages[0].includes("/clear") || h.sendUserMessages[0].includes("/new"), "advise mentions /clear or /new");
533
- assert.ok(eventTypes(h.stateDir).includes("poisoned_context"), "poisoned_context event logged");
534
- assert.ok(!eventTypes(h.stateDir).includes("error_retry"), "poisoned: zero retry nudges (no error_retry event)");
535
- // Second poisoned turn: advise throttled to one per session.
536
- await s38TurnEndUsage(h, "error", "Request failed — please retry.", 0);
537
- assert.equal(h.sendUserMessages.length, 1, "poisoned: advise throttled (one per session)");
538
- assert.ok(eventTypes(h.stateDir).filter((t) => t === "poisoned_context").length >= 2, "poisoned_context logged each turn");
539
- }
540
- finally {
541
- if (prevAuto === undefined)
542
- delete process.env.MEGACOMPACT_AUTO;
543
- else
544
- process.env.MEGACOMPACT_AUTO = prevAuto;
545
- }
546
- });
547
- test("R6(c): transient burst retries with backoff gating — second immediate nudge suppressed while one pending", async () => {
548
- const prevBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
549
- const prevSession = process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX;
550
- const prevRepeat = process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD;
551
- const prevOutage = process.env.MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD;
552
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
553
- process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX = "999"; // don't let session cap bind
554
- process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD = "999"; // don't let repeat upgrade bind
555
- // R10: disable the outage advisory so this test isolates backoff gating only.
556
- process.env.MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD = "0";
557
- try {
558
- const h = harness();
559
- // Turn 1: transient → nudge 1 fires (pending=true, backoff=1ms).
560
- await s38TurnEnd(h, "error", "internal server error 0");
561
- assert.equal(h.sendUserMessages.length, 1, "first transient: 1 nudge");
562
- // Turn 2: immediate (no turn_start) → suppressed by retryNudgePending.
563
- await s38TurnEnd(h, "error", "internal server error 1");
564
- assert.equal(h.sendUserMessages.length, 1, "second immediate nudge suppressed (retryNudgePending)");
565
- // turn_start consumes the pending nudge (resets pending + count).
566
- await h.fire("turn_start", { type: "turn_start", turnIndex: 2 }, h.ctx());
567
- await new Promise((r) => setTimeout(r, 5)); // let backoff elapse
568
- // Turn 3: transient → nudge 2 fires (pending cleared, backoff elapsed).
569
- await s38TurnEnd(h, "error", "internal server error 2");
570
- assert.equal(h.sendUserMessages.length, 2, "after turn_start + backoff: nudge fires");
571
- }
572
- finally {
573
- if (prevBackoff === undefined)
574
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
575
- else
576
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = prevBackoff;
577
- if (prevSession === undefined)
578
- delete process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX;
579
- else
580
- process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX = prevSession;
581
- if (prevRepeat === undefined)
582
- delete process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD;
583
- else
584
- process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD = prevRepeat;
585
- if (prevOutage === undefined)
586
- delete process.env.MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD;
587
- else
588
- process.env.MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD = prevOutage;
589
- }
590
- });
591
- test("R6(d): user abort (stopReason aborted) never nudges, even across repeated aborts", async () => {
592
- const h = harness();
593
- await s38TurnEnd(h, "aborted", "Operation aborted");
594
- assert.equal(h.sendUserMessages.length, 0, "aborted: no nudge");
595
- await s38TurnEnd(h, "aborted", "Aborted after 3 retry attempts");
596
- assert.equal(h.sendUserMessages.length, 0, "aborted: still no nudge after repeated aborts");
597
- assert.ok(eventTypes(h.stateDir).includes("error_retry_cancelled"), "cancelled event logged");
598
- });
599
- test("R6(e): success resets retry-nudge-pending state", async () => {
600
- const prevBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
601
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
602
- try {
603
- const h = harness();
604
- // Turn 1: transient → nudge fires, pending=true.
605
- await s38TurnEnd(h, "error", "internal server error 0");
606
- assert.equal(h.sendUserMessages.length, 1, "first transient: 1 nudge");
607
- // Turn 2: immediate transient → suppressed by pending (no turn_start).
608
- await s38TurnEnd(h, "error", "internal server error 1");
609
- assert.equal(h.sendUserMessages.length, 1, "second immediate suppressed by pending");
610
- // Turn 3: success (stop) → resets pending (R4), no nudge.
611
- await s38TurnEnd(h, "stop");
612
- assert.equal(h.sendUserMessages.length, 1, "success: no nudge, resets pending");
613
- await new Promise((r) => setTimeout(r, 5)); // let backoff elapse
614
- // Turn 4: transient → nudge fires again (pending was reset by success).
615
- await s38TurnEnd(h, "error", "internal server error 2");
616
- assert.equal(h.sendUserMessages.length, 2, "after success reset pending: transient nudge fires");
617
- }
618
- finally {
619
- if (prevBackoff === undefined)
620
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
621
- else
622
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = prevBackoff;
623
- }
624
- });
625
- test("R3: repeated identical transient error text upgrades to poisoned-context at threshold", async () => {
626
- // The stateful repeat signal: 3 consecutive identical transient errors
627
- // (default threshold) upgrade to poisoned. Uses "5xx server error" —
628
- // deliberately NO known-retryable marker ("5xx" is not /5\d\d/, and
629
- // "server error" alone matches nothing): the classifier returns transient
630
- // via the generic 'error' fallthrough, then the repeat tracker upgrades it.
631
- // auto=false to skip the compact attempt.
632
- const prevAuto = process.env.MEGACOMPACT_AUTO;
633
- const prevBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
634
- process.env.MEGACOMPACT_AUTO = "false";
635
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
636
- try {
637
- const h = harness();
638
- // Turns 1-2: transient (repeatCount 1, 2) → nudges fire.
639
- await s38TurnEnd(h, "error", "5xx server error");
640
- await h.fire("turn_start", { type: "turn_start", turnIndex: 2 }, h.ctx());
641
- await new Promise((r) => setTimeout(r, 3));
642
- await s38TurnEnd(h, "error", "5xx server error");
643
- await h.fire("turn_start", { type: "turn_start", turnIndex: 3 }, h.ctx());
644
- await new Promise((r) => setTimeout(r, 3));
645
- // Turn 3: repeatCount=3 ≥ threshold → upgraded to poisoned. No nudge, advise fires.
646
- await s38TurnEnd(h, "error", "5xx server error");
647
- assert.ok(eventTypes(h.stateDir).includes("poisoned_context"), "repeat threshold reached: poisoned_context event logged");
648
- assert.ok(h.sendUserMessages.some((m) => m.includes("/clear") || m.includes("/new")), "repeat threshold: advise message fired");
649
- }
650
- finally {
651
- if (prevAuto === undefined)
652
- delete process.env.MEGACOMPACT_AUTO;
653
- else
654
- process.env.MEGACOMPACT_AUTO = prevAuto;
655
- if (prevBackoff === undefined)
656
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
657
- else
658
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = prevBackoff;
659
- }
660
- });
661
- test("R9: extractErrorSignature fallback — bare 0-token error -> 'bare-0-token-error'", () => {
662
- // The repeat detector needs a non-empty signature to track bare 0-token errors.
663
- assert.equal(extractErrorSignatureFn({ stopReason: "error", usage: { inputTokens: 0, outputTokens: 0 } }), "bare-0-token-error");
664
- // Without usage, stopReason alone does NOT get the fallback (mid-response deaths stay out of repeat tracking).
665
- assert.equal(extractErrorSignatureFn({ stopReason: "error" }), "");
666
- // Normal text content is returned as-is.
667
- assert.equal(extractErrorSignatureFn({ stopReason: "error", content: "x" }), "x");
668
- });
669
- // ---- R7 regression tests: network/throughput errors must NEVER upgrade to ----
670
- // ---- poisoned-context (2026-07-30 false-alarm incident) ----
671
- /** R7 helper: fire the same transient error text `count` times with turn_start
672
- * + elapsed backoff between turns so each turn's nudge can fire. */
673
- async function r7RepeatTurns(h, text, count) {
674
- for (let i = 0; i < count; i++) {
675
- await s38TurnEnd(h, "error", text);
676
- await h.fire("turn_start", { type: "turn_start", turnIndex: i + 2 }, h.ctx());
677
- await new Promise((r) => setTimeout(r, 3));
678
- }
679
- }
680
- /** R7 helper: shared assertions for "stays transient" — no poisoned upgrade,
681
- * no /clear advise, and the transient retry path actually ran. */
682
- function assertStaysTransient(h, label) {
683
- assert.ok(!eventTypes(h.stateDir).includes("poisoned_context"), `${label}: no poisoned_context event (network errors never upgrade)`);
684
- assert.ok(!h.sendUserMessages.some((m) => m.includes("/clear")), `${label}: no /clear advise message`);
685
- assert.ok(eventTypes(h.stateDir).includes("error_retry"), `${label}: transient retry path ran (error_retry event)`);
686
- }
687
- test("R7(a): repeated 'timed out' phrasing (2026-07-30 incident text) stays transient — no poisoned upgrade", async () => {
688
- // The incident error text was "Request timed out or failed. Try again" —
689
- // "timed out" (two words) does NOT match the old guard's /timeout/, so the
690
- // 3rd repeat fired the /clear poisoned advise. Must stay transient.
691
- const prevAuto = process.env.MEGACOMPACT_AUTO;
692
- const prevBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
693
- process.env.MEGACOMPACT_AUTO = "false";
694
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
695
- try {
696
- const h = harness();
697
- await r7RepeatTurns(h, "Request timed out or failed. Try again", 3);
698
- assertStaysTransient(h, "timed out x3");
699
- }
700
- finally {
701
- if (prevAuto === undefined)
702
- delete process.env.MEGACOMPACT_AUTO;
703
- else
704
- process.env.MEGACOMPACT_AUTO = prevAuto;
705
- if (prevBackoff === undefined)
706
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
707
- else
708
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = prevBackoff;
709
- }
710
- });
711
- test("R7(b): repeated ETIMEDOUT errno stays transient", async () => {
712
- // Node's timeout errno lowercases to "etimedout" — does NOT contain the
713
- // substring "timeout". Slipped through the old guard.
714
- const prevAuto = process.env.MEGACOMPACT_AUTO;
715
- const prevBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
716
- process.env.MEGACOMPACT_AUTO = "false";
717
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
718
- try {
719
- const h = harness();
720
- await r7RepeatTurns(h, "connect ETIMEDOUT 142.250.80.46:443", 3);
721
- assertStaysTransient(h, "ETIMEDOUT x3");
722
- }
723
- finally {
724
- if (prevAuto === undefined)
725
- delete process.env.MEGACOMPACT_AUTO;
726
- else
727
- process.env.MEGACOMPACT_AUTO = prevAuto;
728
- if (prevBackoff === undefined)
729
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
730
- else
731
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = prevBackoff;
732
- }
733
- });
734
- test("R7(c): repeated 'socket hang up' stays transient", async () => {
735
- // Node surfaces ECONNRESET as the message "socket hang up" (the errno lives
736
- // in error.code, which extractErrorSignature never sees).
737
- const prevAuto = process.env.MEGACOMPACT_AUTO;
738
- const prevBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
739
- process.env.MEGACOMPACT_AUTO = "false";
740
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
741
- try {
742
- const h = harness();
743
- await r7RepeatTurns(h, "socket hang up", 3);
744
- assertStaysTransient(h, "socket hang up x3");
745
- }
746
- finally {
747
- if (prevAuto === undefined)
748
- delete process.env.MEGACOMPACT_AUTO;
749
- else
750
- process.env.MEGACOMPACT_AUTO = prevAuto;
751
- if (prevBackoff === undefined)
752
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
753
- else
754
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = prevBackoff;
755
- }
756
- });
757
- test("R7(d): repeated 429 rate-limit stays transient", async () => {
758
- // /clear cannot fix a rate limit — the classifier explicitly keeps 429
759
- // transient, so the repeat-upgrade must not override that verdict.
760
- const prevAuto = process.env.MEGACOMPACT_AUTO;
761
- const prevBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
762
- process.env.MEGACOMPACT_AUTO = "false";
763
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
764
- try {
765
- const h = harness();
766
- await r7RepeatTurns(h, "429 Too Many Requests: rate limit exceeded", 3);
767
- assertStaysTransient(h, "429 x3");
768
- }
769
- finally {
770
- if (prevAuto === undefined)
771
- delete process.env.MEGACOMPACT_AUTO;
772
- else
773
- process.env.MEGACOMPACT_AUTO = prevAuto;
774
- if (prevBackoff === undefined)
775
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
776
- else
777
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = prevBackoff;
778
- }
779
- });
780
- test("R7(e): 0-token 'timed out' turn is NOT poisoned on first occurrence", async () => {
781
- // Deeper gap: with usage PRESENT at 0 tokens, the classifier's 0-token
782
- // poisoned signal fires on turn ONE for "timed out" phrasing (the stopReason
783
- // 'error' is in the text blob and no transient marker matched). The network
784
- // markers must be checked before that signal so this returns transient.
785
- const prevAuto = process.env.MEGACOMPACT_AUTO;
786
- process.env.MEGACOMPACT_AUTO = "false";
787
- try {
788
- const h = harness();
789
- await s38TurnEndUsage(h, "error", "Request timed out or failed. Try again", 0);
790
- assert.ok(!eventTypes(h.stateDir).includes("poisoned_context"), "0-token timed out: no poisoned_context on first turn");
791
- assert.ok(!h.sendUserMessages.some((m) => m.includes("/clear")), "0-token timed out: no /clear advise on first turn");
792
- }
793
- finally {
794
- if (prevAuto === undefined)
795
- delete process.env.MEGACOMPACT_AUTO;
796
- else
797
- process.env.MEGACOMPACT_AUTO = prevAuto;
798
- }
799
- });
800
- test("R7(f): repeated non-network transient still upgrades to poisoned-context (control)", async () => {
801
- // True-positive control: an error with NO known-retryable marker that
802
- // repeats identically must still upgrade (the R3 feature itself).
803
- const prevAuto = process.env.MEGACOMPACT_AUTO;
804
- const prevBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
805
- process.env.MEGACOMPACT_AUTO = "false";
806
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
807
- try {
808
- const h = harness();
809
- await r7RepeatTurns(h, "upstream rejected the request", 3);
810
- assert.ok(eventTypes(h.stateDir).includes("poisoned_context"), "non-network repeat: poisoned_context event logged");
811
- assert.ok(h.sendUserMessages.some((m) => m.includes("/clear")), "non-network repeat: /clear advise fired");
812
- }
813
- finally {
814
- if (prevAuto === undefined)
815
- delete process.env.MEGACOMPACT_AUTO;
816
- else
817
- process.env.MEGACOMPACT_AUTO = prevAuto;
818
- if (prevBackoff === undefined)
819
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
820
- else
821
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = prevBackoff;
822
- }
823
- });
824
- test("R7(g): 'Request failed — please retry.' 0-token stays poisoned-context (2026-07-28 control)", async () => {
825
- // The deterministic-rejection incident that motivated R3 must remain
826
- // poisoned: the shared marker pattern must NOT match this text.
827
- const prevAuto = process.env.MEGACOMPACT_AUTO;
828
- process.env.MEGACOMPACT_AUTO = "false";
829
- try {
830
- const h = harness();
831
- await s38TurnEndUsage(h, "error", "Request failed — please retry.", 0);
832
- assert.ok(eventTypes(h.stateDir).includes("poisoned_context"), "deterministic rejection: poisoned_context event logged");
833
- assert.ok(h.sendUserMessages.some((m) => m.includes("/clear")), "deterministic rejection: /clear advise fired");
834
- }
835
- finally {
836
- if (prevAuto === undefined)
837
- delete process.env.MEGACOMPACT_AUTO;
838
- else
839
- process.env.MEGACOMPACT_AUTO = prevAuto;
840
- }
841
- });
842
- test("R7 classifier: 0-token 'timed out' error → transient (not poisoned)", () => {
843
- // Unit-level pin of the R7(e) gap.
844
- assert.equal(classifyErrorFn({
845
- stopReason: "error",
846
- content: "Request timed out or failed. Try again",
847
- usage: { inputTokens: 0, outputTokens: 0 },
848
- }), "transient");
849
- });
850
- test("R7 classifier: bare network phrasings → transient", () => {
851
- assert.equal(classifyErrorFn("socket hang up"), "transient");
852
- assert.equal(classifyErrorFn("the operation timed out"), "transient");
853
- assert.equal(classifyErrorFn("connect ETIMEDOUT 10.0.0.1:443"), "transient");
854
- });
855
- test("R7 classifier: 429/rate-limit → transient (control)", () => {
856
- assert.equal(classifyErrorFn("429 Too Many Requests: rate limit exceeded"), "transient");
857
- });
858
- // ---- R8 regression tests: router-wrapped infra errors (2026-07-30 incident #2) ----
859
- // pi's console "Error: 500:" prefix is NOT part of the delivered message body,
860
- // so router phrasings ("All targets failed", "No healthy target selected",
861
- // "Too many concurrent requests") matched NO marker and the 0-token signal
862
- // poisoned the session on the FIRST turn.
863
- /** The exact error bodies from the 2026-07-30 incident (GLM router flapping). */
864
- const R8_NO_HEALTHY_TARGET = '{"message":"No healthy target selected for alias \'hf:zai-org/GLM-4.7\'","type":"api_error"}';
865
- const R8_SOCKET_CLOSED = "All targets failed: modal/zai-org/GLM-5.1-FP8. Last error: The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()"; // guardrails-allow PREVENT-PI-004: verbatim 2026-07-30 incident error text (string fixture, not a network call)
866
- const R8_TOO_MANY_CONCURRENT = 'All targets failed: modal/zai-org/GLM-5.1-FP8. Last error: {"error": "Too many concurrent requests for this model"}';
867
- test("R8 classifier: router phrasings → transient, even at 0 tokens", () => {
868
- for (const text of [R8_NO_HEALTHY_TARGET, R8_SOCKET_CLOSED, R8_TOO_MANY_CONCURRENT]) {
869
- assert.equal(classifyErrorFn({ stopReason: "error", content: text, usage: { inputTokens: 0, outputTokens: 0 } }), "transient", `0-token router error must be transient: ${text.slice(0, 60)}`);
870
- }
871
- });
872
- test("R8 classifier: structured status field wins over phrasing", () => {
873
- // 5xx → transient even with zero recognizable text.
874
- assert.equal(classifyErrorFn({ stopReason: "error", error: { status: 502, message: "???" }, usage: { inputTokens: 0, outputTokens: 0 } }), "transient");
875
- // 429 structured → transient.
876
- assert.equal(classifyErrorFn({ stopReason: "error", error: { statusCode: 429, message: "???" }, usage: { inputTokens: 0, outputTokens: 0 } }), "transient");
877
- // 401 → permanent (not transient, not poisoned).
878
- assert.equal(classifyErrorFn({ stopReason: "error", error: { status: 401, message: "???" } }), "permanent");
879
- // 400 with deterministic-rejection text → still poisoned (text rules 4xx).
880
- assert.equal(classifyErrorFn({ stopReason: "error", error: { status: 400, message: "orphaned tool result: tooluse ids mismatch" }, usage: { inputTokens: 0, outputTokens: 0 } }), "poisoned-context");
881
- });
882
- test("R8(a): 0-token 'No healthy target' turn is NOT poisoned on first occurrence", async () => {
883
- const prevAuto = process.env.MEGACOMPACT_AUTO;
884
- process.env.MEGACOMPACT_AUTO = "false";
885
- try {
886
- const h = harness();
887
- await s38TurnEndUsage(h, "error", R8_NO_HEALTHY_TARGET, 0);
888
- assert.ok(!eventTypes(h.stateDir).includes("poisoned_context"), "no poisoned_context on first turn");
889
- assert.ok(!h.sendUserMessages.some((m) => m.includes("/clear")), "no /clear advise");
890
- }
891
- finally {
892
- if (prevAuto === undefined)
893
- delete process.env.MEGACOMPACT_AUTO;
894
- else
895
- process.env.MEGACOMPACT_AUTO = prevAuto;
896
- }
897
- });
898
- test("R8(b): repeated 'No healthy target' x3 stays transient", async () => {
899
- const prevAuto = process.env.MEGACOMPACT_AUTO;
900
- const prevBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
901
- process.env.MEGACOMPACT_AUTO = "false";
902
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
903
- try {
904
- const h = harness();
905
- await r7RepeatTurns(h, R8_NO_HEALTHY_TARGET, 3);
906
- assertStaysTransient(h, "no-healthy-target x3");
907
- }
908
- finally {
909
- if (prevAuto === undefined)
910
- delete process.env.MEGACOMPACT_AUTO;
911
- else
912
- process.env.MEGACOMPACT_AUTO = prevAuto;
913
- if (prevBackoff === undefined)
914
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
915
- else
916
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = prevBackoff;
917
- }
918
- });
919
- test("R8(c): repeated 'All targets failed / socket closed' x3 stays transient", async () => {
920
- const prevAuto = process.env.MEGACOMPACT_AUTO;
921
- const prevBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
922
- process.env.MEGACOMPACT_AUTO = "false";
923
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
924
- try {
925
- const h = harness();
926
- await r7RepeatTurns(h, R8_SOCKET_CLOSED, 3);
927
- assertStaysTransient(h, "all-targets-failed x3");
928
- }
929
- finally {
930
- if (prevAuto === undefined)
931
- delete process.env.MEGACOMPACT_AUTO;
932
- else
933
- process.env.MEGACOMPACT_AUTO = prevAuto;
934
- if (prevBackoff === undefined)
935
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
936
- else
937
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = prevBackoff;
938
- }
939
- });
940
- test("R9 handler: bare 0-token error turn x1 is transient (no poisoned, no advise)", async () => {
941
- const h = harness();
942
- const origBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
943
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
944
- process.env.MEGACOMPACT_AUTO = "false";
945
- try {
946
- // Single bare 0-token error turn
947
- await s38TurnEndUsage(h, "error", undefined, 0);
948
- await h.fire("turn_start", { type: "turn_start", turnIndex: 2 }, h.ctx());
949
- await new Promise((r) => setTimeout(r, 3));
950
- // No poisoned_context event should be logged
951
- assert.ok(!eventTypes(h.stateDir).includes("poisoned_context"), "bare 0-token single turn must not log poisoned_context");
952
- // No /clear advise message
953
- assert.equal(h.sendUserMessages.filter((m) => m.includes("/clear")).length, 0, "bare 0-token single turn must not /clear");
954
- // An error_retry event should be emitted
955
- assert.ok(eventTypes(h.stateDir).includes("error_retry"), "bare 0-token single turn must trigger error_retry");
956
- }
957
- finally {
958
- if (origBackoff === undefined)
959
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
960
- else
961
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = origBackoff;
962
- delete process.env.MEGACOMPACT_AUTO;
963
- }
964
- });
965
- test("R9 handler: bare 0-token error x3 upgrades to poisoned at threshold (corroborated)", async () => {
966
- const h = harness();
967
- const origBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
968
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
969
- process.env.MEGACOMPACT_AUTO = "false";
970
- try {
971
- // Three consecutive bare 0-token error turns
972
- for (let i = 0; i < 3; i++) {
973
- await s38TurnEndUsage(h, "error", undefined, 0);
974
- await h.fire("turn_start", { type: "turn_start", turnIndex: i + 2 }, h.ctx());
975
- await new Promise((r) => setTimeout(r, 3));
976
- }
977
- // poisoned_context event should eventually be logged
978
- assert.ok(eventTypes(h.stateDir).includes("poisoned_context"), "bare 0-token x3 must log poisoned_context");
979
- // Exactly one /clear advise total
980
- assert.equal(h.sendUserMessages.filter((m) => m.includes("/clear")).length, 1, "bare 0-token x3 must produce exactly one /clear advise");
981
- }
982
- finally {
983
- if (origBackoff === undefined)
984
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
985
- else
986
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = origBackoff;
987
- delete process.env.MEGACOMPACT_AUTO;
988
- }
989
- });
990
- // ---- R10 tests: provider outage advisory (2026-07-30) ----
991
- test("R10: 3 consecutive transient failures fire one provider-outage advisory (no /clear)", async () => {
992
- const origBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
993
- const origSession = process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX;
994
- const origRepeat = process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD;
995
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
996
- process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX = "999";
997
- process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD = "999";
998
- try {
999
- const h = harness();
1000
- for (let i = 0; i < 3; i++) {
1001
- await s38TurnEnd(h, "error", "socket hang up");
1002
- await h.fire("turn_start", { type: "turn_start", turnIndex: i + 2 }, h.ctx());
1003
- await new Promise((r) => setTimeout(r, 3));
1004
- }
1005
- const providerMessages = h.sendUserMessages.filter((m) => m.includes("provider is having issues"));
1006
- assert.equal(providerMessages.length, 1, "exactly one provider-outage advisory");
1007
- const clearMessages = h.sendUserMessages.filter((m) => m.includes("/clear"));
1008
- assert.equal(clearMessages.length, 0, "no /clear in outage advisory");
1009
- assert.ok(eventTypes(h.stateDir).includes("error_retry"), "error_retry events present");
1010
- }
1011
- finally {
1012
- if (origBackoff === undefined)
1013
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
1014
- else
1015
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = origBackoff;
1016
- if (origSession === undefined)
1017
- delete process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX;
1018
- else
1019
- process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX = origSession;
1020
- if (origRepeat === undefined)
1021
- delete process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD;
1022
- else
1023
- process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD = origRepeat;
1024
- }
1025
- });
1026
- test("R10: advisory fires once per outage episode", async () => {
1027
- const origBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
1028
- const origSession = process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX;
1029
- const origRepeat = process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD;
1030
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
1031
- process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX = "999";
1032
- process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD = "999";
1033
- try {
1034
- const h = harness();
1035
- // Episode 1: 5 consecutive transient failures → 1 advisory at consecutiveErrors=3.
1036
- for (let i = 0; i < 5; i++) {
1037
- await s38TurnEnd(h, "error", "socket hang up");
1038
- await h.fire("turn_start", { type: "turn_start", turnIndex: i + 2 }, h.ctx());
1039
- await new Promise((r) => setTimeout(r, 3));
1040
- }
1041
- assert.equal(h.sendUserMessages.filter((m) => m.includes("provider is having issues")).length, 1, "episode 1: 1 advisory after 5 transient turns");
1042
- // Success turn: resets consecutiveErrors + providerOutageAdvised.
1043
- await s38TurnEnd(h, "stop");
1044
- await h.fire("turn_start", { type: "turn_start", turnIndex: 8 }, h.ctx());
1045
- await new Promise((r) => setTimeout(r, 3));
1046
- // Episode 2: 3 more transient failures → second advisory.
1047
- for (let i = 0; i < 3; i++) {
1048
- await s38TurnEnd(h, "error", "socket hang up");
1049
- await h.fire("turn_start", { type: "turn_start", turnIndex: 9 + i }, h.ctx());
1050
- await new Promise((r) => setTimeout(r, 3));
1051
- }
1052
- assert.equal(h.sendUserMessages.filter((m) => m.includes("provider is having issues")).length, 2, "episode 2: exactly 2 advisories total");
1053
- }
1054
- finally {
1055
- if (origBackoff === undefined)
1056
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
1057
- else
1058
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = origBackoff;
1059
- if (origSession === undefined)
1060
- delete process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX;
1061
- else
1062
- process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX = origSession;
1063
- if (origRepeat === undefined)
1064
- delete process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD;
1065
- else
1066
- process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD = origRepeat;
1067
- }
1068
- });
1069
- test("R10: MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD=0 disables the advisory", async () => {
1070
- const origBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
1071
- const origSession = process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX;
1072
- const origRepeat = process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD;
1073
- const origThreshold = process.env.MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD;
1074
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
1075
- process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX = "999";
1076
- process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD = "999";
1077
- process.env.MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD = "0";
1078
- try {
1079
- const h = harness();
1080
- for (let i = 0; i < 5; i++) {
1081
- await s38TurnEnd(h, "error", "socket hang up");
1082
- await h.fire("turn_start", { type: "turn_start", turnIndex: i + 2 }, h.ctx());
1083
- await new Promise((r) => setTimeout(r, 3));
1084
- }
1085
- const providerMessages = h.sendUserMessages.filter((m) => m.includes("provider is having issues"));
1086
- assert.equal(providerMessages.length, 0, "threshold=0: zero advisory messages");
1087
- }
1088
- finally {
1089
- if (origBackoff === undefined)
1090
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
1091
- else
1092
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = origBackoff;
1093
- if (origSession === undefined)
1094
- delete process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX;
1095
- else
1096
- process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX = origSession;
1097
- if (origRepeat === undefined)
1098
- delete process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD;
1099
- else
1100
- process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD = origRepeat;
1101
- if (origThreshold === undefined)
1102
- delete process.env.MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD;
1103
- else
1104
- process.env.MEGACOMPACT_PROVIDER_OUTAGE_THRESHOLD = origThreshold;
1105
- }
1106
- });
1107
- test("R10: poisoned-context path does NOT fire the outage advisory", async () => {
1108
- // Single "Request failed — please retry." 0-token turn: poisoned on turn 1
1109
- // (auto=false to skip the compact attempt).
1110
- const origBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
1111
- const origAuto = process.env.MEGACOMPACT_AUTO;
1112
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
1113
- process.env.MEGACOMPACT_AUTO = "false";
1114
- try {
1115
- const h = harness();
1116
- await s38TurnEndUsage(h, "error", "Request failed — please retry.", 0);
1117
- // /clear advise should fire.
1118
- assert.ok(h.sendUserMessages.some((m) => m.includes("/clear")), "poisoned: /clear advise fires");
1119
- // No provider-outage advisory.
1120
- assert.equal(h.sendUserMessages.filter((m) => m.includes("provider is having issues")).length, 0, "poisoned path: no outage advisory");
1121
- }
1122
- finally {
1123
- if (origBackoff === undefined)
1124
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
1125
- else
1126
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = origBackoff;
1127
- if (origAuto === undefined)
1128
- delete process.env.MEGACOMPACT_AUTO;
1129
- else
1130
- process.env.MEGACOMPACT_AUTO = origAuto;
1131
- }
1132
- });
1133
- // ── R11: diagnostic signal + rawText logging ──────────────────────────────
1134
- test("R11 classifier: classifyErrorDetailed signal tags", () => {
1135
- // length → signal 'length-guard', category null
1136
- const len = classifyErrorDetailedFn({ stopReason: "length" });
1137
- assert.equal(len.signal, "length-guard");
1138
- assert.equal(len.category, null);
1139
- // aborted → 'cancelled'
1140
- const abort = classifyErrorDetailedFn({ stopReason: "aborted" });
1141
- assert.equal(abort.signal, "cancelled");
1142
- assert.equal(abort.category, "cancelled");
1143
- // stop → 'success'
1144
- assert.equal(classifyErrorDetailedFn({ stopReason: "stop" }).signal, "success");
1145
- // compaction-noop
1146
- const compact = classifyErrorDetailedFn("Error: Already compacted");
1147
- assert.equal(compact.signal, "compaction-noop");
1148
- assert.equal(compact.category, "compaction-noop");
1149
- // context-overflow
1150
- const overflow = classifyErrorDetailedFn("too long for this model");
1151
- assert.equal(overflow.signal, "context-overflow");
1152
- assert.equal(overflow.category, "context-overflow");
1153
- // transient-marker (text-matched)
1154
- const tm = classifyErrorDetailedFn("socket hang up");
1155
- assert.equal(tm.signal, "transient-marker");
1156
- assert.equal(tm.category, "transient");
1157
- // transient-status (HTTP status-matched) — use outer-level status + non-5xx-matching error text
1158
- // so extractHttpStatus finds it but the text doesn't contain "5xx" digits
1159
- const ts = classifyErrorDetailedFn({ stopReason: "error", error: { type: "upstream_error", message: "request aborted" }, status: 502 });
1160
- assert.equal(ts.signal, "transient-status");
1161
- assert.equal(ts.category, "transient");
1162
- assert.equal(ts.httpStatus, 502);
1163
- // permanent-status
1164
- const ps = classifyErrorDetailedFn({ stopReason: "error", error: { type: "auth_error", message: "session expired" }, status: 401 });
1165
- assert.equal(ps.signal, "permanent-status");
1166
- assert.equal(ps.category, "permanent");
1167
- assert.equal(ps.httpStatus, 401);
1168
- // poisoned-invalid-request (needs stopReason so sr is non-empty)
1169
- const pir = classifyErrorDetailedFn({ stopReason: "error", error: { type: "invalid_request_error", message: "bad request" } });
1170
- assert.equal(pir.signal, "poisoned-invalid-request");
1171
- assert.equal(pir.category, "poisoned-context");
1172
- // poisoned-request-failed (from R9/R3)
1173
- const prf = classifyErrorDetailedFn("Request failed — please retry.");
1174
- assert.equal(prf.signal, "poisoned-request-failed");
1175
- assert.equal(prf.category, "poisoned-context");
1176
- // bare-0-token (R9) — usage present with 0 tokens, stopReason error
1177
- const b0 = classifyErrorDetailedFn({ stopReason: "error", usage: { inputTokens: 0, outputTokens: 0 } });
1178
- assert.equal(b0.signal, "bare-0-token");
1179
- assert.equal(b0.category, "transient");
1180
- // generic-error
1181
- const ge = classifyErrorDetailedFn({ stopReason: "error", content: "something odd" });
1182
- assert.equal(ge.signal, "generic-error");
1183
- assert.equal(ge.category, "transient");
1184
- // permanent-auth
1185
- const pa = classifyErrorDetailedFn("unauthorized: invalid api key");
1186
- assert.equal(pa.signal, "permanent-auth");
1187
- assert.equal(pa.category, "permanent");
1188
- // unknown (plain string, no markers)
1189
- const unk = classifyErrorDetailedFn("some random text with no pattern");
1190
- assert.equal(unk.signal, "unknown");
1191
- assert.equal(unk.category, null);
1192
- });
1193
- test("R11: poisoned_context event carries signal + rawText", async () => {
1194
- const prevAuto = process.env.MEGACOMPACT_AUTO;
1195
- process.env.MEGACOMPACT_AUTO = "false";
1196
- try {
1197
- const h = harness();
1198
- await s38TurnEndUsage(h, "error", "Request failed — please retry.", 0);
1199
- const payloads = eventPayloads(h.stateDir, "poisoned_context");
1200
- assert.ok(payloads.length >= 1, "expected a poisoned_context event");
1201
- const last = payloads[payloads.length - 1];
1202
- assert.equal(last.signal, "poisoned-request-failed", "signal field present and correct");
1203
- assert.ok(typeof last.rawText === "string" && last.rawText.toLowerCase().includes("request failed"), "rawText contains error text");
1204
- }
1205
- finally {
1206
- if (prevAuto === undefined)
1207
- delete process.env.MEGACOMPACT_AUTO;
1208
- else
1209
- process.env.MEGACOMPACT_AUTO = prevAuto;
1210
- }
1211
- });
1212
- test("R11: provider outage advisory payload carries signal + rawText", async () => {
1213
- const origBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
1214
- const origSession = process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX;
1215
- const origRepeat = process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD;
1216
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
1217
- process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX = "999";
1218
- process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD = "999";
1219
- try {
1220
- const h = harness();
1221
- for (let i = 0; i < 3; i++) {
1222
- await s38TurnEnd(h, "error", "socket hang up");
1223
- await h.fire("turn_start", { type: "turn_start", turnIndex: i + 2 }, h.ctx());
1224
- await new Promise((r) => setTimeout(r, 3));
1225
- }
1226
- const payloads = eventPayloads(h.stateDir, "provider_outage_advised");
1227
- assert.ok(payloads.length >= 1, "expected provider_outage_advised event");
1228
- const last = payloads[payloads.length - 1];
1229
- assert.equal(last.signal, "transient-marker", "outage event carries signal");
1230
- assert.ok(typeof last.rawText === "string" && last.rawText.includes("socket hang up"), "outage event carries rawText");
1231
- }
1232
- finally {
1233
- if (origBackoff === undefined)
1234
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
1235
- else
1236
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = origBackoff;
1237
- if (origSession === undefined)
1238
- delete process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX;
1239
- else
1240
- process.env.MEGACOMPACT_ERROR_RETRY_SESSION_MAX = origSession;
1241
- if (origRepeat === undefined)
1242
- delete process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD;
1243
- else
1244
- process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD = origRepeat;
1245
- }
1246
- });
1247
- test("R11: repeat-upgrade-declined signal is transient-marker for retryable marker", () => {
1248
- // The repeat-upgrade-declined log fires when isKnownRetryable(signature) is true
1249
- // but the count hasn't reached the threshold yet (decline path). Since the
1250
- // test harness doesn't expose internal logger output, we unit-test the signal
1251
- // tag that the handler would log.
1252
- const detail = classifyErrorDetailedFn("Request timed out or failed. Try again");
1253
- assert.equal(detail.signal, "transient-marker", "repeat-guard text gets transient-marker signal");
1254
- assert.equal(detail.category, "transient");
1255
- });
1256
- // ── R12: normalize volatile tokens in extractErrorSignature ──────────────
1257
- test("R12: extractErrorSignature normalizes volatile tokens", () => {
1258
- // Model/provider paths → <model>
1259
- const sigA = extractErrorSignatureFn("All targets failed: modal/zai-org/GLM-5.1-FP8. Last error: boom");
1260
- const sigB = extractErrorSignatureFn("All targets failed: hf/other-org/GLM-4.7. Last error: boom");
1261
- assert.equal(sigA, sigB, "different model paths must normalize to the same signature");
1262
- assert.ok(sigA.includes("<model>"), "normalized sig should contain <model>");
1263
- assert.ok(!sigA.includes("modal"), "original model alias must be replaced");
1264
- assert.ok(!sigA.includes("hf/other-org"), "original model alias must be replaced");
1265
- // IP:port → <ip>
1266
- const ipA = extractErrorSignatureFn("connect ETIMEDOUT 10.0.0.1:443");
1267
- const ipB = extractErrorSignatureFn("connect ETIMEDOUT 192.168.0.2:8443");
1268
- assert.equal(ipA, ipB, "different IPs must normalize to the same signature");
1269
- assert.ok(ipA.includes("<ip>"), "normalized sig should contain <ip>");
1270
- assert.ok(!ipA.includes("10.0.0.1"), "original IP must be replaced");
1271
- // Hex ids (8+ chars) → <hex>
1272
- const hexA = extractErrorSignatureFn("request id a1b2c3d4e5f6 failed");
1273
- const hexB = extractErrorSignatureFn("request id 9f8e7d6c5b4a failed");
1274
- assert.equal(hexA, hexB, "different hex ids must normalize to the same signature");
1275
- assert.ok(hexA.includes("<hex>"), "normalized sig should contain <hex>");
1276
- // "after N attempts" → "after <n> attempts"
1277
- const rA = extractErrorSignatureFn("Retry failed after 3 attempts: boom");
1278
- const rB = extractErrorSignatureFn("Retry failed after 5 attempts: boom");
1279
- assert.equal(rA, rB, "different attempt counts must normalize to the same signature");
1280
- assert.ok(rA.includes("after <n> attempts"), "normalized sig should contain 'after <n> attempts'");
1281
- // Status codes (3-digit) survive — NOT merged by the 4+ digit rule
1282
- const s500 = extractErrorSignatureFn("error 500 now");
1283
- const s502 = extractErrorSignatureFn("error 502 now");
1284
- assert.notEqual(s500, s502, "3-digit status codes must NOT merge");
1285
- // Empty + bare 0-token error object still returns "bare-0-token-error" (R9 preserved)
1286
- assert.equal(extractErrorSignatureFn({ stopReason: "error", usage: { inputTokens: 0, outputTokens: 0 } }), "bare-0-token-error", "R9 bare-0-token fallback must survive normalization change");
1287
- // Empty content with no usage keeps returning ""
1288
- assert.equal(extractErrorSignatureFn({ stopReason: "error" }), "", "bare error with no usage must return empty");
1289
- });
1290
- test("R12: alternating-but-equivalent marker-less errors upgrade at threshold", async () => {
1291
- const origBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
1292
- const origRepeat = process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD;
1293
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
1294
- process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD = "3";
1295
- process.env.MEGACOMPACT_AUTO = "false";
1296
- try {
1297
- const h = harness();
1298
- // Two texts that differ ONLY in model alias — no known-retryable markers.
1299
- // "upstream rejected alias modal/a-b/X-1" has no network/timeout/socket/
1300
- // 429/5xx markers in KNOWN_RETRYABLE_TRANSIENT_PATTERN.
1301
- const textA = "upstream rejected alias modal/a-b/X-1";
1302
- const textB = "upstream rejected alias hf/c-d/Y-2";
1303
- // Turn 1 (textA): count → 1
1304
- await s38TurnEnd(h, "error", textA);
1305
- await h.fire("turn_start", { type: "turn_start", turnIndex: 2 }, h.ctx());
1306
- await new Promise((r) => setTimeout(r, 3));
1307
- // Turn 2 (textB): normalized matches → count → 2
1308
- await s38TurnEnd(h, "error", textB);
1309
- await h.fire("turn_start", { type: "turn_start", turnIndex: 3 }, h.ctx());
1310
- await new Promise((r) => setTimeout(r, 3));
1311
- // Turn 3 (textA again): normalized matches → count → 3 → upgrade
1312
- await s38TurnEnd(h, "error", textA);
1313
- await h.fire("turn_start", { type: "turn_start", turnIndex: 4 }, h.ctx());
1314
- await new Promise((r) => setTimeout(r, 3));
1315
- assert.ok(eventTypes(h.stateDir).includes("poisoned_context"), "equivalent alternating errors must upgrade to poisoned at threshold");
1316
- assert.equal(h.sendUserMessages.filter((m) => m.includes("/clear")).length, 1, "exactly one /clear advise");
1317
- }
1318
- finally {
1319
- if (origBackoff === undefined)
1320
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
1321
- else
1322
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = origBackoff;
1323
- if (origRepeat === undefined)
1324
- delete process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD;
1325
- else
1326
- process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD = origRepeat;
1327
- delete process.env.MEGACOMPACT_AUTO;
1328
- }
1329
- });
1330
- test("R12: genuinely different marker-less errors do NOT merge", async () => {
1331
- // Characterization/control test: two truly distinct error messages must NOT
1332
- // merge under normalization — their signatures stay different and the repeat
1333
- // counter never reaches threshold. This should pass both before and after
1334
- // the R12 normalization change (guard against over-normalization).
1335
- const origBackoff = process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
1336
- const origRepeat = process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD;
1337
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = "1";
1338
- process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD = "3";
1339
- process.env.MEGACOMPACT_AUTO = "false";
1340
- try {
1341
- const h = harness();
1342
- const textA = "upstream rejected the request";
1343
- const textB = "provider returned an empty response";
1344
- // Alternate a,b,a,b — counter never exceeds 1.
1345
- for (let i = 0; i < 4; i++) {
1346
- await s38TurnEnd(h, "error", i % 2 === 0 ? textA : textB);
1347
- await h.fire("turn_start", { type: "turn_start", turnIndex: i + 2 }, h.ctx());
1348
- await new Promise((r) => setTimeout(r, 3));
1349
- }
1350
- assert.ok(!eventTypes(h.stateDir).includes("poisoned_context"), "truly different errors must NOT trigger poisoned_context");
1351
- assert.equal(h.sendUserMessages.filter((m) => m.includes("/clear")).length, 0, "no /clear for non-repeating errors");
1352
- }
1353
- finally {
1354
- if (origBackoff === undefined)
1355
- delete process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS;
1356
- else
1357
- process.env.MEGACOMPACT_ERROR_RETRY_BACKOFF_MS = origBackoff;
1358
- if (origRepeat === undefined)
1359
- delete process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD;
1360
- else
1361
- process.env.MEGACOMPACT_POISONED_REPEAT_THRESHOLD = origRepeat;
1362
- delete process.env.MEGACOMPACT_AUTO;
1363
- }
1364
- });
1365
- test("cleanup", async () => {
1366
- // PGlite WASM close can hang; race with a timeout to prevent 40-min hangs.
1367
- try {
1368
- await Promise.race([closeVectorIndex(), new Promise((r) => setTimeout(r, 3000))]);
1369
- }
1370
- catch { /* ignore */ }
1371
- rmSync(baseTmp, { recursive: true, force: true });
1372
- // Force-exit: each harness() creates a MegaRuntime with an fs.watch
1373
- // game-state watcher that is never disposed (no session_shutdown in tests).
1374
- // Those handles keep the event loop alive indefinitely after all tests
1375
- // complete, so `node --test` (without --test-force-exit) would hang.
1376
- // Drain stdout/stderr, then defer the exit: a bare process.exit() discards
1377
- // unflushed pipe buffers — observed 2026-07-30: when piped, the trailing
1378
- // tests' results and the run summary silently vanished from the report.
1379
- await new Promise((r) => process.stdout.write("", r));
1380
- await new Promise((r) => process.stderr.write("", r));
1381
- await new Promise((r) => setTimeout(r, 1500));
1382
- process.exit(0);
1383
- });