@animalabs/agent-framework 0.32.0 → 0.34.0

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 (368) hide show
  1. package/README.md +340 -22
  2. package/dist/src/admission/admit.d.ts +18 -0
  3. package/dist/src/admission/admit.d.ts.map +1 -1
  4. package/dist/src/admission/admit.js +16 -3
  5. package/dist/src/admission/admit.js.map +1 -1
  6. package/dist/src/agent.d.ts +208 -80
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +345 -113
  9. package/dist/src/agent.js.map +1 -1
  10. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  11. package/dist/src/api/mcp-server-core.js +50 -12
  12. package/dist/src/api/mcp-server-core.js.map +1 -1
  13. package/dist/src/api/server.d.ts.map +1 -1
  14. package/dist/src/api/server.js +4 -20
  15. package/dist/src/api/server.js.map +1 -1
  16. package/dist/src/append-all.d.ts +5 -0
  17. package/dist/src/append-all.d.ts.map +1 -0
  18. package/dist/src/append-all.js +8 -0
  19. package/dist/src/append-all.js.map +1 -0
  20. package/dist/src/code-execution/isolate/scripts.d.ts +12 -8
  21. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  22. package/dist/src/code-execution/isolate/scripts.js +24 -0
  23. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  24. package/dist/src/config-checks.d.ts +7 -0
  25. package/dist/src/config-checks.d.ts.map +1 -1
  26. package/dist/src/config-checks.js +14 -0
  27. package/dist/src/config-checks.js.map +1 -1
  28. package/dist/src/context-manager-errors.d.ts +25 -0
  29. package/dist/src/context-manager-errors.d.ts.map +1 -0
  30. package/dist/src/context-manager-errors.js +31 -0
  31. package/dist/src/context-manager-errors.js.map +1 -0
  32. package/dist/src/context-options.d.ts +70 -0
  33. package/dist/src/context-options.d.ts.map +1 -0
  34. package/dist/src/context-options.js +90 -0
  35. package/dist/src/context-options.js.map +1 -0
  36. package/dist/src/cut-off-round.js +4 -4
  37. package/dist/src/deferred-import.d.ts +93 -0
  38. package/dist/src/deferred-import.d.ts.map +1 -0
  39. package/dist/src/deferred-import.js +177 -0
  40. package/dist/src/deferred-import.js.map +1 -0
  41. package/dist/src/dendrite/audience.d.ts +223 -0
  42. package/dist/src/dendrite/audience.d.ts.map +1 -0
  43. package/dist/src/dendrite/audience.js +202 -0
  44. package/dist/src/dendrite/audience.js.map +1 -0
  45. package/dist/src/dendrite/index.d.ts +5 -3
  46. package/dist/src/dendrite/index.d.ts.map +1 -1
  47. package/dist/src/dendrite/index.js +2 -1
  48. package/dist/src/dendrite/index.js.map +1 -1
  49. package/dist/src/dendrite/presets.d.ts +10 -5
  50. package/dist/src/dendrite/presets.d.ts.map +1 -1
  51. package/dist/src/dendrite/presets.js +17 -5
  52. package/dist/src/dendrite/presets.js.map +1 -1
  53. package/dist/src/dendrite/registry.d.ts +62 -7
  54. package/dist/src/dendrite/registry.d.ts.map +1 -1
  55. package/dist/src/dendrite/registry.js +183 -12
  56. package/dist/src/dendrite/registry.js.map +1 -1
  57. package/dist/src/dendrite/types.d.ts +91 -8
  58. package/dist/src/dendrite/types.d.ts.map +1 -1
  59. package/dist/src/failure-text.d.ts +36 -0
  60. package/dist/src/failure-text.d.ts.map +1 -0
  61. package/dist/src/failure-text.js +89 -0
  62. package/dist/src/failure-text.js.map +1 -0
  63. package/dist/src/foreign-rounds.d.ts +69 -0
  64. package/dist/src/foreign-rounds.d.ts.map +1 -0
  65. package/dist/src/foreign-rounds.js +152 -0
  66. package/dist/src/foreign-rounds.js.map +1 -0
  67. package/dist/src/framework.d.ts +2064 -842
  68. package/dist/src/framework.d.ts.map +1 -1
  69. package/dist/src/framework.js +8586 -3739
  70. package/dist/src/framework.js.map +1 -1
  71. package/dist/src/gate/event-gate.d.ts +125 -21
  72. package/dist/src/gate/event-gate.d.ts.map +1 -1
  73. package/dist/src/gate/event-gate.js +487 -72
  74. package/dist/src/gate/event-gate.js.map +1 -1
  75. package/dist/src/gate/index.d.ts +1 -1
  76. package/dist/src/gate/index.d.ts.map +1 -1
  77. package/dist/src/gate/index.js.map +1 -1
  78. package/dist/src/gate/reply-judge.d.ts +4 -0
  79. package/dist/src/gate/reply-judge.d.ts.map +1 -1
  80. package/dist/src/gate/reply-judge.js +12 -3
  81. package/dist/src/gate/reply-judge.js.map +1 -1
  82. package/dist/src/gate/rule-scope.d.ts +30 -0
  83. package/dist/src/gate/rule-scope.d.ts.map +1 -0
  84. package/dist/src/gate/rule-scope.js +41 -0
  85. package/dist/src/gate/rule-scope.js.map +1 -0
  86. package/dist/src/gate/suppress-script-worker.d.ts +2 -0
  87. package/dist/src/gate/suppress-script-worker.d.ts.map +1 -0
  88. package/dist/src/gate/suppress-script-worker.js +103 -0
  89. package/dist/src/gate/suppress-script-worker.js.map +1 -0
  90. package/dist/src/gate/suppress-scripts.d.ts +105 -0
  91. package/dist/src/gate/suppress-scripts.d.ts.map +1 -0
  92. package/dist/src/gate/suppress-scripts.js +403 -0
  93. package/dist/src/gate/suppress-scripts.js.map +1 -0
  94. package/dist/src/gate/suppression-review.d.ts +143 -0
  95. package/dist/src/gate/suppression-review.d.ts.map +1 -0
  96. package/dist/src/gate/suppression-review.js +463 -0
  97. package/dist/src/gate/suppression-review.js.map +1 -0
  98. package/dist/src/gate/suppression-stats.d.ts +125 -0
  99. package/dist/src/gate/suppression-stats.d.ts.map +1 -0
  100. package/dist/src/gate/suppression-stats.js +214 -0
  101. package/dist/src/gate/suppression-stats.js.map +1 -0
  102. package/dist/src/gate/types.d.ts +142 -7
  103. package/dist/src/gate/types.d.ts.map +1 -1
  104. package/dist/src/gate/wake-onboarding.d.ts +119 -0
  105. package/dist/src/gate/wake-onboarding.d.ts.map +1 -0
  106. package/dist/src/gate/wake-onboarding.js +277 -0
  107. package/dist/src/gate/wake-onboarding.js.map +1 -0
  108. package/dist/src/heartbeat/heartbeat.js +1 -1
  109. package/dist/src/heartbeat/heartbeat.js.map +1 -1
  110. package/dist/src/held-writes.d.ts +31 -23
  111. package/dist/src/held-writes.d.ts.map +1 -1
  112. package/dist/src/held-writes.js +44 -59
  113. package/dist/src/held-writes.js.map +1 -1
  114. package/dist/src/index.d.ts +6 -2
  115. package/dist/src/index.d.ts.map +1 -1
  116. package/dist/src/index.js +4 -0
  117. package/dist/src/index.js.map +1 -1
  118. package/dist/src/inference-bounds.d.ts +2 -1
  119. package/dist/src/inference-bounds.d.ts.map +1 -1
  120. package/dist/src/inference-bounds.js +9 -6
  121. package/dist/src/inference-bounds.js.map +1 -1
  122. package/dist/src/jsonrpc-envelope.d.ts +69 -0
  123. package/dist/src/jsonrpc-envelope.d.ts.map +1 -0
  124. package/dist/src/jsonrpc-envelope.js +66 -0
  125. package/dist/src/jsonrpc-envelope.js.map +1 -0
  126. package/dist/src/mcpl/channel-registry.d.ts +188 -28
  127. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  128. package/dist/src/mcpl/channel-registry.js +426 -70
  129. package/dist/src/mcpl/channel-registry.js.map +1 -1
  130. package/dist/src/mcpl/checkpoint-manager.d.ts +2 -4
  131. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  132. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  133. package/dist/src/mcpl/feature-set-manager.d.ts +2 -4
  134. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  135. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  136. package/dist/src/mcpl/inbound-source.d.ts +2 -1
  137. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  138. package/dist/src/mcpl/inbound-source.js +2 -1
  139. package/dist/src/mcpl/inbound-source.js.map +1 -1
  140. package/dist/src/mcpl/inference-router.d.ts +6 -6
  141. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  142. package/dist/src/mcpl/inference-router.js.map +1 -1
  143. package/dist/src/mcpl/modern-connection.d.ts +109 -15
  144. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  145. package/dist/src/mcpl/modern-connection.js +593 -86
  146. package/dist/src/mcpl/modern-connection.js.map +1 -1
  147. package/dist/src/mcpl/protocol-family.d.ts +3 -1
  148. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  149. package/dist/src/mcpl/protocol-family.js +15 -9
  150. package/dist/src/mcpl/protocol-family.js.map +1 -1
  151. package/dist/src/mcpl/push-coalescer.d.ts +132 -22
  152. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  153. package/dist/src/mcpl/push-coalescer.js +105 -15
  154. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  155. package/dist/src/mcpl/push-handler.d.ts +9 -7
  156. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  157. package/dist/src/mcpl/push-handler.js.map +1 -1
  158. package/dist/src/mcpl/server-connection.d.ts +50 -28
  159. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  160. package/dist/src/mcpl/server-connection.js +165 -72
  161. package/dist/src/mcpl/server-connection.js.map +1 -1
  162. package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
  163. package/dist/src/mcpl/tool-classes.js +3 -0
  164. package/dist/src/mcpl/tool-classes.js.map +1 -1
  165. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  166. package/dist/src/mcpl/tool-presence.js +10 -6
  167. package/dist/src/mcpl/tool-presence.js.map +1 -1
  168. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  169. package/dist/src/mcpl/toolset-host.js +7 -6
  170. package/dist/src/mcpl/toolset-host.js.map +1 -1
  171. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  172. package/dist/src/mcpl/toolsets-module.js +2 -1
  173. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  174. package/dist/src/mcpl/types.d.ts +33 -6
  175. package/dist/src/mcpl/types.d.ts.map +1 -1
  176. package/dist/src/mcpl/types.js.map +1 -1
  177. package/dist/src/module-registry.d.ts +24 -1
  178. package/dist/src/module-registry.d.ts.map +1 -1
  179. package/dist/src/module-registry.js +68 -18
  180. package/dist/src/module-registry.js.map +1 -1
  181. package/dist/src/modules/discord/index.d.ts +45 -8
  182. package/dist/src/modules/discord/index.d.ts.map +1 -1
  183. package/dist/src/modules/discord/index.js +238 -87
  184. package/dist/src/modules/discord/index.js.map +1 -1
  185. package/dist/src/modules/health/index.js +2 -2
  186. package/dist/src/modules/health/index.js.map +1 -1
  187. package/dist/src/modules/history/index.d.ts +59 -0
  188. package/dist/src/modules/history/index.d.ts.map +1 -1
  189. package/dist/src/modules/history/index.js +432 -67
  190. package/dist/src/modules/history/index.js.map +1 -1
  191. package/dist/src/modules/history/semantic.d.ts +6 -5
  192. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  193. package/dist/src/modules/history/semantic.js +18 -25
  194. package/dist/src/modules/history/semantic.js.map +1 -1
  195. package/dist/src/modules/workspace/disk-agreement.d.ts +1 -1
  196. package/dist/src/modules/workspace/disk-agreement.js +1 -1
  197. package/dist/src/modules/workspace/index.d.ts +28 -7
  198. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  199. package/dist/src/modules/workspace/index.js +80 -25
  200. package/dist/src/modules/workspace/index.js.map +1 -1
  201. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  202. package/dist/src/modules/workspace/observe.js +2 -1
  203. package/dist/src/modules/workspace/observe.js.map +1 -1
  204. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  205. package/dist/src/modules/workspace/reconcile.js +3 -2
  206. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  207. package/dist/src/notice-tags.d.ts +20 -9
  208. package/dist/src/notice-tags.d.ts.map +1 -1
  209. package/dist/src/notice-tags.js +20 -9
  210. package/dist/src/notice-tags.js.map +1 -1
  211. package/dist/src/operator-log.d.ts +36 -3
  212. package/dist/src/operator-log.d.ts.map +1 -1
  213. package/dist/src/operator-log.js +2 -2
  214. package/dist/src/operator-log.js.map +1 -1
  215. package/dist/src/presentation.d.ts +747 -0
  216. package/dist/src/presentation.d.ts.map +1 -0
  217. package/dist/src/presentation.js +1437 -0
  218. package/dist/src/presentation.js.map +1 -0
  219. package/dist/src/prose-drafts.d.ts +15 -11
  220. package/dist/src/prose-drafts.d.ts.map +1 -1
  221. package/dist/src/prose-drafts.js +16 -12
  222. package/dist/src/prose-drafts.js.map +1 -1
  223. package/dist/src/read-marks.d.ts +192 -0
  224. package/dist/src/read-marks.d.ts.map +1 -0
  225. package/dist/src/read-marks.js +242 -0
  226. package/dist/src/read-marks.js.map +1 -0
  227. package/dist/src/reasoning-loss.d.ts +5 -0
  228. package/dist/src/reasoning-loss.d.ts.map +1 -0
  229. package/dist/src/reasoning-loss.js +8 -0
  230. package/dist/src/reasoning-loss.js.map +1 -0
  231. package/dist/src/recovery/offline-branch.d.ts +91 -6
  232. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  233. package/dist/src/recovery/offline-branch.js +277 -32
  234. package/dist/src/recovery/offline-branch.js.map +1 -1
  235. package/dist/src/recovery/recover-cli.js +24 -4
  236. package/dist/src/recovery/recover-cli.js.map +1 -1
  237. package/dist/src/send-tools.d.ts +5 -9
  238. package/dist/src/send-tools.d.ts.map +1 -1
  239. package/dist/src/send-tools.js +40 -7
  240. package/dist/src/send-tools.js.map +1 -1
  241. package/dist/src/sent-rows.js +1 -1
  242. package/dist/src/stored-injections.d.ts +2 -2
  243. package/dist/src/stored-injections.js +2 -2
  244. package/dist/src/stored-removal.d.ts +17 -4
  245. package/dist/src/stored-removal.d.ts.map +1 -1
  246. package/dist/src/stored-removal.js +29 -5
  247. package/dist/src/stored-removal.js.map +1 -1
  248. package/dist/src/supplied-text.d.ts +1 -1
  249. package/dist/src/supplied-text.js +3 -3
  250. package/dist/src/supplied-text.js.map +1 -1
  251. package/dist/src/thinking-drops.d.ts +2 -0
  252. package/dist/src/thinking-drops.d.ts.map +1 -1
  253. package/dist/src/thinking-drops.js.map +1 -1
  254. package/dist/src/tool-presentation.d.ts.map +1 -1
  255. package/dist/src/tool-presentation.js +38 -9
  256. package/dist/src/tool-presentation.js.map +1 -1
  257. package/dist/src/tool-result-guard.d.ts +12 -1
  258. package/dist/src/tool-result-guard.d.ts.map +1 -1
  259. package/dist/src/tool-result-guard.js +42 -8
  260. package/dist/src/tool-result-guard.js.map +1 -1
  261. package/dist/src/tune-out/coordinator.d.ts +1170 -81
  262. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  263. package/dist/src/tune-out/coordinator.js +3732 -369
  264. package/dist/src/tune-out/coordinator.js.map +1 -1
  265. package/dist/src/tune-out/tools.d.ts +114 -17
  266. package/dist/src/tune-out/tools.d.ts.map +1 -1
  267. package/dist/src/tune-out/tools.js +89 -28
  268. package/dist/src/tune-out/tools.js.map +1 -1
  269. package/dist/src/tune-out/window-anchor.d.ts +143 -0
  270. package/dist/src/tune-out/window-anchor.d.ts.map +1 -0
  271. package/dist/src/tune-out/window-anchor.js +202 -0
  272. package/dist/src/tune-out/window-anchor.js.map +1 -0
  273. package/dist/src/types/agent.d.ts +121 -25
  274. package/dist/src/types/agent.d.ts.map +1 -1
  275. package/dist/src/types/events.d.ts +2 -0
  276. package/dist/src/types/events.d.ts.map +1 -1
  277. package/dist/src/types/framework.d.ts +72 -17
  278. package/dist/src/types/framework.d.ts.map +1 -1
  279. package/dist/src/types/inference-log.d.ts +1 -0
  280. package/dist/src/types/inference-log.d.ts.map +1 -1
  281. package/dist/src/types/module.d.ts +180 -69
  282. package/dist/src/types/module.d.ts.map +1 -1
  283. package/dist/src/types/trace.d.ts +573 -39
  284. package/dist/src/types/trace.d.ts.map +1 -1
  285. package/package.json +4 -4
  286. package/src/admission/admit.ts +32 -3
  287. package/src/agent.ts +392 -132
  288. package/src/api/mcp-server-core.ts +54 -17
  289. package/src/api/server.ts +4 -23
  290. package/src/append-all.ts +6 -0
  291. package/src/code-execution/isolate/scripts.ts +28 -3
  292. package/src/config-checks.ts +14 -0
  293. package/src/context-manager-errors.ts +37 -0
  294. package/src/context-options.ts +106 -0
  295. package/src/cut-off-round.ts +4 -4
  296. package/src/deferred-import.ts +239 -0
  297. package/src/dendrite/audience.ts +315 -0
  298. package/src/dendrite/index.ts +14 -2
  299. package/src/dendrite/presets.ts +18 -6
  300. package/src/dendrite/registry.ts +213 -14
  301. package/src/dendrite/types.ts +92 -8
  302. package/src/failure-text.ts +88 -0
  303. package/src/foreign-rounds.ts +199 -0
  304. package/src/framework.ts +8796 -3813
  305. package/src/gate/event-gate.ts +518 -91
  306. package/src/gate/index.ts +3 -0
  307. package/src/gate/reply-judge.ts +12 -3
  308. package/src/gate/rule-scope.ts +45 -0
  309. package/src/gate/suppress-script-worker.ts +89 -0
  310. package/src/gate/suppress-scripts.ts +414 -0
  311. package/src/gate/suppression-review.ts +517 -0
  312. package/src/gate/suppression-stats.ts +314 -0
  313. package/src/gate/types.ts +148 -7
  314. package/src/gate/wake-onboarding.ts +309 -0
  315. package/src/heartbeat/heartbeat.ts +1 -1
  316. package/src/held-writes.ts +49 -60
  317. package/src/index.ts +10 -2
  318. package/src/inference-bounds.ts +11 -6
  319. package/src/jsonrpc-envelope.ts +81 -0
  320. package/src/mcpl/channel-registry.ts +513 -116
  321. package/src/mcpl/checkpoint-manager.ts +3 -2
  322. package/src/mcpl/feature-set-manager.ts +2 -1
  323. package/src/mcpl/inbound-source.ts +2 -1
  324. package/src/mcpl/inference-router.ts +7 -4
  325. package/src/mcpl/modern-connection.ts +629 -91
  326. package/src/mcpl/protocol-family.ts +15 -9
  327. package/src/mcpl/push-coalescer.ts +193 -34
  328. package/src/mcpl/push-handler.ts +10 -5
  329. package/src/mcpl/server-connection.ts +169 -75
  330. package/src/mcpl/tool-classes.ts +3 -0
  331. package/src/mcpl/tool-presence.ts +10 -5
  332. package/src/mcpl/toolset-host.ts +7 -6
  333. package/src/mcpl/toolsets-module.ts +2 -1
  334. package/src/mcpl/types.ts +33 -6
  335. package/src/module-registry.ts +73 -17
  336. package/src/modules/discord/index.ts +230 -82
  337. package/src/modules/health/index.ts +2 -2
  338. package/src/modules/history/index.ts +439 -70
  339. package/src/modules/history/semantic.ts +18 -24
  340. package/src/modules/workspace/disk-agreement.ts +1 -1
  341. package/src/modules/workspace/index.ts +89 -26
  342. package/src/modules/workspace/observe.ts +2 -1
  343. package/src/modules/workspace/reconcile.ts +3 -2
  344. package/src/notice-tags.ts +21 -9
  345. package/src/operator-log.ts +36 -3
  346. package/src/presentation.ts +1678 -0
  347. package/src/prose-drafts.ts +16 -12
  348. package/src/read-marks.ts +328 -0
  349. package/src/reasoning-loss.ts +7 -0
  350. package/src/recovery/offline-branch.ts +350 -35
  351. package/src/recovery/recover-cli.ts +23 -4
  352. package/src/send-tools.ts +40 -7
  353. package/src/sent-rows.ts +1 -1
  354. package/src/stored-injections.ts +2 -2
  355. package/src/stored-removal.ts +31 -7
  356. package/src/supplied-text.ts +3 -3
  357. package/src/thinking-drops.ts +2 -0
  358. package/src/tool-presentation.ts +34 -9
  359. package/src/tool-result-guard.ts +41 -7
  360. package/src/tune-out/coordinator.ts +4121 -472
  361. package/src/tune-out/tools.ts +162 -44
  362. package/src/tune-out/window-anchor.ts +277 -0
  363. package/src/types/agent.ts +122 -25
  364. package/src/types/events.ts +2 -0
  365. package/src/types/framework.ts +73 -17
  366. package/src/types/inference-log.ts +1 -0
  367. package/src/types/module.ts +181 -68
  368. package/src/types/trace.ts +607 -43
package/src/gate/index.ts CHANGED
@@ -17,5 +17,8 @@ export type {
17
17
  JudgeVerdict,
18
18
  ReplyJudgeOptions,
19
19
  UpcomingWake,
20
+ UpcomingWakeKind,
21
+ SuppressConfig,
22
+ SuppressionReviewOptions,
20
23
  } from './types.js';
21
24
  export { ReplyJudge, DEFAULT_JUDGE_MODEL, DEFAULT_JUDGE_MAX_INPUT_TOKENS, MIN_JUDGE_INPUT_TOKENS, type ReplyJudgeDeps } from './reply-judge.js';
@@ -29,6 +29,15 @@ import { NativeFormatter } from '@animalabs/membrane';
29
29
  import type { ContentBlock, DetailedUsage, Membrane, NormalizedMessage, NormalizedRequest, NormalizedResponse } from '@animalabs/membrane';
30
30
  import type { JudgeEventSummary, JudgeFn, JudgeRequest, JudgeVerdict, ReplyJudgeOptions, UpcomingWake } from './types.js';
31
31
 
32
+ export class JudgeFailure extends Error {
33
+ readonly detail?: string;
34
+ constructor(message: string, detail?: string) {
35
+ super(message);
36
+ this.name = 'JudgeFailure';
37
+ if (detail !== undefined) this.detail = detail;
38
+ }
39
+ }
40
+
32
41
  /** The judge's model when the host names none. */
33
42
  export const DEFAULT_JUDGE_MODEL = 'claude-haiku-5-5';
34
43
  /** The judge's input budget when the host names none: under Haiku 5.5's 100K price line, with margin. */
@@ -334,9 +343,9 @@ export class ReplyJudge {
334
343
  /** Build the judge's request for one batch: the system prompt, the document in chunks, the question. */
335
344
  async buildRequest(request: JudgeRequest, now = Date.now()): Promise<{ request: NormalizedRequest; agentName: string; chars: number }> {
336
345
  const agentName = this.chooseAgent(request.agentNames);
337
- if (!agentName) throw new Error('no agent to judge for');
346
+ if (!agentName) throw new JudgeFailure('no agent to judge for');
338
347
  const preview = await this.preview(agentName);
339
- if (request.signal.aborted) throw new Error('the judge was stopped before its request');
348
+ if (request.signal.aborted) throw new JudgeFailure('the judge was stopped before its request');
340
349
 
341
350
  let extra: UpcomingWake[] = [];
342
351
  try {
@@ -499,7 +508,7 @@ export class ReplyJudge {
499
508
  const verdict = parseVerdict(text);
500
509
  if (!verdict) {
501
510
  const stop = response.stopReason;
502
- throw new Error(`the judge's answer held no verdict (stop: ${stop}${text ? `, text: ${JSON.stringify(cut(text, 120))}` : ''})`);
511
+ throw new JudgeFailure("the judge's answer held no verdict", `stop: ${stop}${text ? `, text: ${JSON.stringify(cut(text, 120))}` : ''}`);
503
512
  }
504
513
  return {
505
514
  ...verdict,
@@ -0,0 +1,45 @@
1
+ /**
2
+ * What a gate rule decides about: whether its match names an event's server,
3
+ * channel or tags, or takes events without naming any of them (a catch-all).
4
+ * Shared by the gate (gate_status.catchAll) and wake onboarding, which treats
5
+ * a catch-all's let-by as no decision at all (MCPL RFC-001 §6, revision 4).
6
+ */
7
+ import type { GatePolicy } from './types.js';
8
+
9
+ /**
10
+ * Whether a glob names something: a pattern made only of wildcards and
11
+ * separators (`*`, `**`, `*:*`) matches everything, so it names nothing. A
12
+ * namespace glob such as `surface:*` names its namespace.
13
+ */
14
+ export const namesSomething = (pattern: unknown): boolean => typeof pattern === 'string' && /[^*?:]/.test(pattern);
15
+
16
+ /**
17
+ * Whether a rule's match is a decision about the events it takes: it names a
18
+ * source, a channel, or a tag (a tagsAny/tagsAll pattern that names
19
+ * something). Content filters and metadata flags (`metadataTrue`) don't: they
20
+ * take events from every server alike.
21
+ */
22
+ export function ruleDecides(policy: GatePolicy): boolean {
23
+ const m = policy.match ?? {};
24
+ if (namesSomething(m.source) || namesSomething(m.channel)) return true;
25
+ return [...(m.tagsAny ?? []), ...(m.tagsAll ?? [])].some(namesSomething);
26
+ }
27
+
28
+ /**
29
+ * The first rule that takes MCPL events without deciding about them, or null.
30
+ * A rule whose scope names no `mcpl:` event type never sees them. With
31
+ * `lettingBy`, only one that lets them by without waking (`defer`/`skip`):
32
+ * the rule that leaves a new server's events undecided.
33
+ */
34
+ export function firstCatchAll(policies: readonly GatePolicy[], opts: { lettingBy?: boolean } = {}): string | null {
35
+ for (const p of policies) {
36
+ // A suppress rule decides no wake: what it lets by goes on to the rules after it.
37
+ if (p.suppress) continue;
38
+ if (ruleDecides(p)) continue;
39
+ if (opts.lettingBy && p.behavior !== 'defer' && p.behavior !== 'skip') continue;
40
+ const scope = p.match?.scope;
41
+ if (scope && !scope.some((s) => s.startsWith('mcpl:'))) continue;
42
+ return p.name;
43
+ }
44
+ return null;
45
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Worker that runs agents' suppress-rule scripts (`suppress.script`, see
3
+ * suppress-scripts.ts).
4
+ *
5
+ * As with gate.js, the agent is trusted (it has shell and filesystem
6
+ * already), so this is a robustness boundary, not a security sandbox: on a
7
+ * thread of its own, a script that loops or hangs is timed out by the main
8
+ * thread (Atomics.wait) instead of freezing the agent's event loop.
9
+ *
10
+ * Protocol: the parent posts one message at a time and blocks in
11
+ * Atomics.wait until this worker writes the JSON answer into the shared
12
+ * buffer and flips the control word.
13
+ * - `{ type: 'reset' }` drops every script held: `{}`.
14
+ * - `{ type: 'load', name, script }` compiles one script (an expression
15
+ * that evaluates to a function) and keeps it under the rule's name:
16
+ * `{ error? }`.
17
+ * - `{ type: 'check', script }` compiles one without keeping it: `{ error? }`.
18
+ * - `{ type: 'eval', name, event, state }` calls the rule's function with
19
+ * the event and the state, and answers `{ hide, state }`: the state as
20
+ * the script left it, and `hide` true only where the script returned
21
+ * true. A script may be async.
22
+ */
23
+ import { parentPort, workerData } from 'node:worker_threads';
24
+
25
+ interface WorkerData {
26
+ sab: SharedArrayBuffer;
27
+ maxStateBytes: number;
28
+ }
29
+ const { sab, maxStateBytes } = workerData as WorkerData;
30
+ const ctrl = new Int32Array(sab, 0, 2); // [0]=status (0 pending, 1 done), [1]=answer byte length
31
+ const data = new Uint8Array(sab, 8);
32
+ const enc = new TextEncoder();
33
+
34
+ type SuppressFn = (event: unknown, state: Record<string, unknown>) => unknown;
35
+ const fns = new Map<string, SuppressFn>();
36
+
37
+ function answer(obj: unknown): void {
38
+ let bytes = enc.encode(JSON.stringify(obj));
39
+ if (bytes.length > data.length) bytes = enc.encode(JSON.stringify({ error: 'the answer was too large to pass back' }));
40
+ data.set(bytes);
41
+ Atomics.store(ctrl, 1, bytes.length);
42
+ Atomics.store(ctrl, 0, 1);
43
+ Atomics.notify(ctrl, 0);
44
+ }
45
+
46
+ const message = (e: unknown): string => (e instanceof Error ? e.message : String(e));
47
+
48
+ function compile(script: string): SuppressFn {
49
+ // An expression, as `(event, state) => …` or `function (event, state) { … }`.
50
+ const made = new Function(`"use strict";\nreturn (${script}\n);`)() as unknown;
51
+ if (typeof made !== 'function') throw new Error(`the script is ${made === null ? 'null' : typeof made}, not a function such as (event, state) => false`);
52
+ return made as SuppressFn;
53
+ }
54
+
55
+ parentPort?.on('message', (msg: { type?: string; script?: string; name?: string; event?: unknown; state?: unknown }) => {
56
+ if (msg?.type === 'reset') {
57
+ fns.clear();
58
+ answer({});
59
+ return;
60
+ }
61
+ if (msg?.type === 'load') {
62
+ const name = msg.name ?? '';
63
+ try { fns.set(name, compile(msg.script ?? '')); answer({}); } catch (e) { fns.delete(name); answer({ error: message(e) }); }
64
+ return;
65
+ }
66
+ if (msg?.type === 'check') {
67
+ try { compile(msg.script ?? ''); answer({}); } catch (e) { answer({ error: message(e) }); }
68
+ return;
69
+ }
70
+ if (msg?.type !== 'eval') return;
71
+ void (async () => {
72
+ const fn = fns.get(msg.name ?? '');
73
+ if (!fn) return answer({ error: 'the script is not loaded' });
74
+ const state = (msg.state && typeof msg.state === 'object' && !Array.isArray(msg.state) ? msg.state : {}) as Record<string, unknown>;
75
+ try {
76
+ let hide = fn(msg.event, state);
77
+ if (hide && typeof (hide as { then?: unknown }).then === 'function') hide = await hide;
78
+ let kept: string;
79
+ try { kept = JSON.stringify(state); } catch (e) { return answer({ error: `its state can't be kept as JSON: ${message(e)}` }); }
80
+ if (kept === undefined || enc.encode(kept).length > maxStateBytes) {
81
+ return answer({ error: `its state is over the ${maxStateBytes.toLocaleString('en-US')} bytes it may keep` });
82
+ }
83
+ // Only true hides: anything else, a truthy string included, shows the event.
84
+ answer({ hide: hide === true, state: JSON.parse(kept) });
85
+ } catch (e) {
86
+ answer({ error: message(e) });
87
+ }
88
+ })();
89
+ });
@@ -0,0 +1,414 @@
1
+ /**
2
+ * SuppressScripts — the agent's own logic for its suppress rules
3
+ * (`suppress.script`): a function `(event, state) => boolean` the rule's
4
+ * events are passed to, true hiding the event. `state` is the rule's own
5
+ * JSON object, which the script reads and changes as it likes, so a rule can
6
+ * hide what it has seen before, count, or keep a window of its own. The
7
+ * framework keeps each rule's state between events and across restarts, in
8
+ * `suppress-state.json` beside gate.json, and starts it afresh when the
9
+ * rule's script changes or the rule is removed.
10
+ *
11
+ * The scripts run on one worker thread, called synchronously from the gate
12
+ * (Atomics.wait) with a timeout, as gate.js is (gate-script.ts): a robustness
13
+ * boundary for code the agent wrote, not a security sandbox. Every failure
14
+ * lets the event go on to the rules after the script's (as a script that
15
+ * says show does): a script that throws, times out, isn't a function, or
16
+ * leaves a state that can't be kept. Its state then stays as it was, and the
17
+ * failure is counted and named in gate_status. A timeout stops the worker; a
18
+ * fresh one loads every script again, one at a time, so a script whose own
19
+ * loading hangs is set aside without the others. A script that times out
20
+ * MAX_TIMEOUTS_IN_WINDOW times in its last TIMEOUT_WINDOW runs, probes
21
+ * included, stops being run until its script changes: each timeout blocks the
22
+ * caller's thread for the timeout and a worker's restart, so neither a script
23
+ * that always hangs nor one that hangs now and then goes on costing that. A
24
+ * worker that fails to start or answer is tried again with a later event,
25
+ * each try later than the last, since a try can block for LOAD_TIMEOUT_MS.
26
+ *
27
+ * The worker starts with the first rule that has a script, so a gate with
28
+ * none runs no thread.
29
+ */
30
+ import { createHash } from 'node:crypto';
31
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
32
+ import { dirname, join } from 'node:path';
33
+ import { fileURLToPath } from 'node:url';
34
+ import { Worker } from 'node:worker_threads';
35
+
36
+ const WORKER_PATH = join(dirname(fileURLToPath(import.meta.url)), 'suppress-script-worker.js');
37
+ /** The most a rule's state may hold, as JSON (bytes). */
38
+ export const MAX_SUPPRESS_STATE_BYTES = 64 * 1024;
39
+ /** Room for an answer: the state, and the rest of the answer around it. */
40
+ const SAB_SIZE = 8 + 2 * MAX_SUPPRESS_STATE_BYTES + 16_384;
41
+ /** How long one event's call may take before the script is stopped and the event shown (ms). */
42
+ export const DEFAULT_SUPPRESS_SCRIPT_TIMEOUT_MS = 50;
43
+ /** How long compiling one script may take, the worker's start included (ms). */
44
+ const LOAD_TIMEOUT_MS = 3_000;
45
+ /** Timeouts within a script's last TIMEOUT_WINDOW runs after which it isn't run again until its script changes. */
46
+ export const MAX_TIMEOUTS_IN_WINDOW = 3;
47
+ export const TIMEOUT_WINDOW = 20;
48
+ /** How long after the worker failed to start or answer a later event first tries loading the scripts again (ms); doubled each failed try, up to the most. */
49
+ const WORKER_RETRY_MS = 5_000;
50
+ const WORKER_RETRY_MAX_MS = 5 * 60_000;
51
+ /** How soon after a state changes it's written to disk (ms), and how long a failed write waits to be tried again. */
52
+ const SAVE_DELAY_MS = 2_000;
53
+ const SAVE_RETRY_MS = 30_000;
54
+
55
+ /** What a script is given for each event. */
56
+ export interface SuppressScriptEvent {
57
+ eventType: string;
58
+ source: string;
59
+ channel?: string;
60
+ author?: string;
61
+ tags: string[];
62
+ /** The event's text. */
63
+ text: string;
64
+ /** When the event arrived (ms since the epoch). */
65
+ at: number;
66
+ /** The event's metadata, as JSON. */
67
+ metadata: Record<string, unknown>;
68
+ }
69
+
70
+ /** How a rule's script has run, for gate_status. */
71
+ export interface SuppressScriptStatus {
72
+ loaded: boolean;
73
+ /** Present when the script no longer runs until its rule changes: why. */
74
+ disabled?: string;
75
+ runs: number;
76
+ hidden: number;
77
+ errors: number;
78
+ timeouts: number;
79
+ lastError: string | null;
80
+ /** Its state's size as JSON (bytes). */
81
+ stateBytes: number;
82
+ }
83
+
84
+ interface RuleScript {
85
+ script: string;
86
+ hash: string;
87
+ state: Record<string, unknown>;
88
+ loaded: boolean;
89
+ disabled: string | null;
90
+ runs: number;
91
+ hidden: number;
92
+ errors: number;
93
+ timeouts: number;
94
+ /** Whether each of its last TIMEOUT_WINDOW runs timed out. */
95
+ recentRuns: boolean[];
96
+ lastError: string | null;
97
+ }
98
+
99
+ interface SavedStates {
100
+ version: 1;
101
+ rules: Record<string, { script: string; state: Record<string, unknown> }>;
102
+ }
103
+
104
+ type Answer = Record<string, unknown> | { timeout: true };
105
+
106
+ const hashOf = (script: string): string => createHash('sha256').update(script).digest('hex');
107
+ const message = (e: unknown): string => (e instanceof Error ? e.message : String(e));
108
+ const isRecord = (v: unknown): v is Record<string, unknown> => !!v && typeof v === 'object' && !Array.isArray(v);
109
+
110
+ /** Saved states as read from disk: entries that aren't a script hash and an object state are skipped and named; a file of another shape throws. */
111
+ function readSaved(raw: unknown, path: string): Map<string, { script: string; state: Record<string, unknown> }> {
112
+ const out = new Map<string, { script: string; state: Record<string, unknown> }>();
113
+ if (!isRecord(raw) || raw.version !== 1 || !isRecord(raw.rules)) throw new Error('it holds no version 1 states');
114
+ const skipped: string[] = [];
115
+ for (const [name, entry] of Object.entries(raw.rules)) {
116
+ if (isRecord(entry) && typeof entry.script === 'string' && isRecord(entry.state)) out.set(name, { script: entry.script, state: entry.state });
117
+ else skipped.push(name);
118
+ }
119
+ if (skipped.length > 0) console.error(`[gate] suppress-rule states in ${path}: ${skipped.map((n) => JSON.stringify(n)).join(', ')} can't be read, so those rules start afresh`);
120
+ return out;
121
+ }
122
+
123
+ export class SuppressScripts {
124
+ /** The worker and its own shared buffer: a worker stopped after a timeout may still write to the buffer it had. */
125
+ private channel?: { worker: Worker; ctrl: Int32Array; data: Uint8Array };
126
+ private readonly dec = new TextDecoder();
127
+ private readonly rules = new Map<string, RuleScript>();
128
+ /** States read from disk, by rule, kept until the rule with that script is synced or forgotten. */
129
+ private saved = new Map<string, { script: string; state: Record<string, unknown> }>();
130
+ private saveTimer: ReturnType<typeof setTimeout> | null = null;
131
+ private disposed = false;
132
+ /** Whether the first sync, with every rule in force, has run: saved states it didn't take are then retired. */
133
+ private synced = false;
134
+ /** When a later event may try loading again, after loading failed for want of a worker (not a script's own error). */
135
+ private workerRetryAt: number | null = null;
136
+ private workerRetryDelay = WORKER_RETRY_MS;
137
+
138
+ constructor(private readonly opts: { statePath?: string; timeoutMs?: number } = {}) {
139
+ if (opts.statePath && existsSync(opts.statePath)) {
140
+ try {
141
+ this.saved = readSaved(JSON.parse(readFileSync(opts.statePath, 'utf-8')), opts.statePath);
142
+ } catch (error) {
143
+ // Moved aside, so the next save doesn't write over a file a later release wrote.
144
+ const aside = `${opts.statePath}.unreadable`;
145
+ let moved = '';
146
+ try { renameSync(opts.statePath, aside); moved = ` It's kept as ${aside}.`; } catch { /* the save may yet fail too; the log says what was read */ }
147
+ console.error(`[gate] suppress-rule states in ${opts.statePath} can't be read, so every rule's state starts afresh.${moved}`, error);
148
+ }
149
+ }
150
+ }
151
+
152
+ private get timeoutMs(): number { return this.opts.timeoutMs ?? DEFAULT_SUPPRESS_SCRIPT_TIMEOUT_MS; }
153
+
154
+ /**
155
+ * Bring the scripts in step with the rules in force: compile new and
156
+ * changed ones, drop removed ones. A rule keeps its state while its script
157
+ * is the same text; a new or changed script starts from `{}`, or from the
158
+ * state saved for that very script before a restart.
159
+ */
160
+ sync(rules: ReadonlyArray<{ name: string; script?: string }>): void {
161
+ if (this.disposed) return;
162
+ const wanted = new Map(rules.filter((r): r is { name: string; script: string } => typeof r.script === 'string').map((r) => [r.name, r.script]));
163
+ let changed = false;
164
+ for (const name of [...this.rules.keys()]) {
165
+ if (wanted.get(name) !== this.rules.get(name)!.script) { this.rules.delete(name); changed = true; }
166
+ }
167
+ for (const [name, script] of wanted) {
168
+ if (this.rules.has(name)) continue;
169
+ const hash = hashOf(script);
170
+ const saved = this.saved.get(name);
171
+ this.saved.delete(name);
172
+ this.rules.set(name, {
173
+ script, hash, state: saved && saved.script === hash ? saved.state : {}, loaded: false, disabled: null,
174
+ runs: 0, hidden: 0, errors: 0, timeouts: 0, recentRuns: [], lastError: null,
175
+ });
176
+ changed = true;
177
+ }
178
+ // States saved for rules that weren't in force at the first sync are
179
+ // retired: the rule was removed while the host was down, and one made
180
+ // later under the name starts afresh.
181
+ if (!this.synced) {
182
+ this.synced = true;
183
+ if (this.saved.size > 0) { this.saved.clear(); changed = true; }
184
+ }
185
+ if (!changed) return;
186
+ this.scheduleSave();
187
+ if (this.rules.size === 0) { this.stopWorker(); return; }
188
+ this.load();
189
+ }
190
+
191
+ /** Forget a rule's script and state (a rule removed or redefined), a state saved for it before a restart included. */
192
+ forget(name: string): void {
193
+ const had = this.rules.delete(name) || this.saved.delete(name);
194
+ if (had) this.scheduleSave();
195
+ if (this.rules.size === 0) this.stopWorker();
196
+ }
197
+
198
+ /**
199
+ * Whether `script` compiles to a function, run on the worker: an error's
200
+ * text, or null. For wake_add_rule, before it saves a rule.
201
+ */
202
+ check(script: string): string | null {
203
+ const answer = this.call({ type: 'check', script }, LOAD_TIMEOUT_MS);
204
+ if ('timeout' in answer) {
205
+ this.load(); // the worker was stopped: the rules' scripts go to a fresh one
206
+ return `compiling it took over ${LOAD_TIMEOUT_MS / 1000} s`;
207
+ }
208
+ // A gate with no scripts runs no thread, this check's included.
209
+ if (this.rules.size === 0) this.stopWorker();
210
+ return typeof answer.error === 'string' ? answer.error : null;
211
+ }
212
+
213
+ /**
214
+ * Run a rule's script for one event. `commit` keeps the state it leaves and
215
+ * counts the run; without it (a probe) the state is as before. Undefined
216
+ * when the rule has no script; `{ error }` when the script failed, and the
217
+ * event goes on to the rules after it.
218
+ */
219
+ run(name: string, event: SuppressScriptEvent, commit: boolean): { hide: boolean } | { error: string } | undefined {
220
+ const rule = this.rules.get(name);
221
+ if (!rule) return undefined;
222
+ if (rule.disabled) return { error: rule.disabled };
223
+ // A load that failed for want of a worker is tried again, later each time.
224
+ if (!rule.loaded && this.workerRetryAt !== null && Date.now() >= this.workerRetryAt) this.load();
225
+ if (!rule.loaded) return { error: rule.lastError ?? 'the script is not loaded' };
226
+ const answer = this.call({ type: 'eval', name, event, state: rule.state }, this.timeoutMs);
227
+ if ('timeout' in answer) {
228
+ // The worker was stopped; every script goes to a fresh one. A probe's
229
+ // timeout counts toward setting the script aside too: it costs the same.
230
+ const error = `it took over ${this.timeoutMs} ms, so it was stopped`;
231
+ if (commit) rule.runs++;
232
+ rule.timeouts++;
233
+ rule.lastError = error;
234
+ if (this.noteRun(rule, true) >= MAX_TIMEOUTS_IN_WINDOW) {
235
+ rule.disabled = `it took over ${this.timeoutMs} ms ${MAX_TIMEOUTS_IN_WINDOW} times in its last ${TIMEOUT_WINDOW} runs, so it isn't run again until its script changes`;
236
+ rule.lastError = rule.disabled;
237
+ }
238
+ this.load();
239
+ return { error };
240
+ }
241
+ this.noteRun(rule, false);
242
+ if (!commit) return typeof answer.error === 'string' ? { error: answer.error } : { hide: answer.hide === true };
243
+ rule.runs++;
244
+ if (typeof answer.error === 'string') {
245
+ rule.errors++;
246
+ rule.lastError = answer.error;
247
+ return { error: answer.error };
248
+ }
249
+ const next = isRecord(answer.state) ? answer.state : {};
250
+ if (JSON.stringify(next) !== JSON.stringify(rule.state)) {
251
+ rule.state = next;
252
+ this.scheduleSave();
253
+ }
254
+ const hide = answer.hide === true;
255
+ if (hide) rule.hidden++;
256
+ return { hide };
257
+ }
258
+
259
+ /** One run in the rule's recent runs: how many of them timed out. */
260
+ private noteRun(rule: RuleScript, timedOut: boolean): number {
261
+ rule.recentRuns.push(timedOut);
262
+ if (rule.recentRuns.length > TIMEOUT_WINDOW) rule.recentRuns.shift();
263
+ return rule.recentRuns.filter(Boolean).length;
264
+ }
265
+
266
+ status(name: string): SuppressScriptStatus | undefined {
267
+ const rule = this.rules.get(name);
268
+ if (!rule) return undefined;
269
+ return {
270
+ loaded: rule.loaded, ...(rule.disabled ? { disabled: rule.disabled } : {}),
271
+ runs: rule.runs, hidden: rule.hidden, errors: rule.errors, timeouts: rule.timeouts,
272
+ lastError: rule.lastError, stateBytes: Buffer.byteLength(JSON.stringify(rule.state)),
273
+ };
274
+ }
275
+
276
+ /** A rule's state as its script left it (a copy). */
277
+ stateOf(name: string): Record<string, unknown> | undefined {
278
+ const rule = this.rules.get(name);
279
+ return rule ? structuredClone(rule.state) : undefined;
280
+ }
281
+
282
+ dispose(): void {
283
+ if (this.disposed) return;
284
+ this.flush();
285
+ this.disposed = true;
286
+ this.stopWorker();
287
+ }
288
+
289
+ /** Write the states now, if a write is due. */
290
+ flush(): void {
291
+ if (!this.saveTimer) return;
292
+ clearTimeout(this.saveTimer);
293
+ this.saveTimer = null;
294
+ this.save();
295
+ }
296
+
297
+ /**
298
+ * Load every script that isn't set aside into the worker, one at a time,
299
+ * on a clean slate. A script whose loading times out is set aside (it
300
+ * would stop the worker each time), and the rest are loaded again on the
301
+ * fresh worker that follows.
302
+ */
303
+ private load(): void {
304
+ if (this.rules.size === 0) return;
305
+ for (let attempt = 0; attempt <= this.rules.size; attempt++) {
306
+ const reset = this.call({ type: 'reset' }, LOAD_TIMEOUT_MS);
307
+ if ('timeout' in reset || typeof reset.error === 'string') {
308
+ const why = 'timeout' in reset ? `the script worker didn't answer within ${LOAD_TIMEOUT_MS / 1000} s` : reset.error as string;
309
+ for (const rule of this.rules.values()) { rule.loaded = false; rule.lastError = rule.disabled ?? `${why}; it's tried again with a later event`; }
310
+ this.workerRetryAt = Date.now() + this.workerRetryDelay;
311
+ this.workerRetryDelay = Math.min(WORKER_RETRY_MAX_MS, this.workerRetryDelay * 2);
312
+ return;
313
+ }
314
+ this.workerRetryAt = null;
315
+ this.workerRetryDelay = WORKER_RETRY_MS;
316
+ let restarted = false;
317
+ for (const [name, rule] of this.rules) {
318
+ if (rule.disabled) { rule.loaded = false; continue; }
319
+ const answer = this.call({ type: 'load', name, script: rule.script }, LOAD_TIMEOUT_MS);
320
+ if ('timeout' in answer) {
321
+ rule.loaded = false;
322
+ rule.disabled = `compiling it took over ${LOAD_TIMEOUT_MS / 1000} s, so it isn't run until its rule changes`;
323
+ rule.lastError = rule.disabled;
324
+ restarted = true;
325
+ break;
326
+ }
327
+ if (typeof answer.error === 'string') {
328
+ rule.loaded = false;
329
+ rule.lastError = `it doesn't compile: ${answer.error}`;
330
+ } else {
331
+ rule.loaded = true;
332
+ }
333
+ }
334
+ if (!restarted) return;
335
+ }
336
+ }
337
+
338
+ /** One message to the worker, waited for: its answer, or a timeout, after which the worker is stopped (the caller loads the scripts again). */
339
+ private call(msg: Record<string, unknown>, timeoutMs: number): Answer {
340
+ if (this.disposed) return { error: 'the gate is stopped' };
341
+ const channel = this.ensureWorker();
342
+ if (!channel) return { error: 'the script worker could not start' };
343
+ const { worker, ctrl, data } = channel;
344
+ Atomics.store(ctrl, 0, 0);
345
+ try {
346
+ worker.postMessage(msg);
347
+ } catch (e) {
348
+ return { error: message(e) };
349
+ }
350
+ if (Atomics.wait(ctrl, 0, 0, timeoutMs) === 'timed-out') {
351
+ this.stopWorker();
352
+ return { timeout: true };
353
+ }
354
+ const length = Atomics.load(ctrl, 1);
355
+ try {
356
+ return JSON.parse(this.dec.decode(data.slice(0, length))) as Record<string, unknown>;
357
+ } catch {
358
+ return { error: 'the script worker gave an unreadable answer' };
359
+ }
360
+ }
361
+
362
+ private ensureWorker(): { worker: Worker; ctrl: Int32Array; data: Uint8Array } | undefined {
363
+ if (this.channel) return this.channel;
364
+ try {
365
+ const sab = new SharedArrayBuffer(SAB_SIZE);
366
+ const worker = new Worker(WORKER_PATH, { workerData: { sab, maxStateBytes: MAX_SUPPRESS_STATE_BYTES } });
367
+ worker.on('error', (e) => { console.error('[gate] the suppress-script worker failed:', e); });
368
+ // A worker that dies on its own (a script's stray asynchronous throw) is
369
+ // no one's timeout: the next run loads the scripts on a fresh one.
370
+ worker.once('exit', () => {
371
+ if (this.channel?.worker !== worker) return;
372
+ this.channel = undefined;
373
+ for (const rule of this.rules.values()) rule.loaded = false;
374
+ this.workerRetryAt = 0;
375
+ });
376
+ worker.unref();
377
+ this.channel = { worker, ctrl: new Int32Array(sab, 0, 2), data: new Uint8Array(sab, 8) };
378
+ return this.channel;
379
+ } catch (error) {
380
+ console.error('[gate] the suppress-script worker could not start:', error);
381
+ return undefined;
382
+ }
383
+ }
384
+
385
+ private stopWorker(): void {
386
+ if (!this.channel) return;
387
+ void this.channel.worker.terminate().catch(() => {});
388
+ this.channel = undefined;
389
+ }
390
+
391
+ private scheduleSave(delayMs = SAVE_DELAY_MS): void {
392
+ if (!this.opts.statePath || this.saveTimer || this.disposed) return;
393
+ this.saveTimer = setTimeout(() => { this.saveTimer = null; this.save(); }, delayMs);
394
+ this.saveTimer.unref?.();
395
+ }
396
+
397
+ private save(): void {
398
+ const path = this.opts.statePath;
399
+ if (!path) return;
400
+ // A null-prototype object, so a rule named "__proto__" is an entry like any other.
401
+ const rules: SavedStates['rules'] = Object.create(null);
402
+ for (const [name, saved] of this.saved) rules[name] = saved;
403
+ for (const [name, rule] of this.rules) rules[name] = { script: rule.hash, state: rule.state };
404
+ try {
405
+ mkdirSync(dirname(path), { recursive: true });
406
+ const temp = `${path}.${process.pid}.tmp`;
407
+ writeFileSync(temp, `${JSON.stringify({ version: 1, rules }, null, 2)}\n`);
408
+ renameSync(temp, path);
409
+ } catch (error) {
410
+ console.error(`[gate] suppress-rule states couldn't be written to ${path}; they're kept in memory and written again shortly:`, error);
411
+ this.scheduleSave(SAVE_RETRY_MS);
412
+ }
413
+ }
414
+ }