@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
@@ -15,10 +15,16 @@ import { cutToFit, quotedJson, quotedValue, suppliedValue } from '../supplied-te
15
15
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
16
16
  import { basename, dirname, join } from 'node:path';
17
17
  import { GateScript } from './gate-script.js';
18
+ import { SuppressScripts } from './suppress-scripts.js';
19
+ import { firstCatchAll, namesSomething } from './rule-scope.js';
18
20
  import { canonical, duplicateName, lastRecipePath, noChanges, reconcileLine, reconcileWithRecipe } from './recipe-reconcile.js';
19
21
  import { closestNames, isNearMiss } from '../closest-names.js';
20
22
  import { MAX_TIMER_MS } from '../deadline-timer.js';
21
23
  import { safeSlice } from '../safe-slice.js';
24
+ import { failureWords } from '../failure-text.js';
25
+ import { JudgeFailure } from './reply-judge.js';
26
+ import { isSystemNotice } from '../mcpl/tags.js';
27
+ import { appendAll } from '../append-all.js';
22
28
  // ---------------------------------------------------------------------------
23
29
  // Constants
24
30
  // ---------------------------------------------------------------------------
@@ -102,15 +108,25 @@ function isMetadataTruthy(value) {
102
108
  return Boolean(value);
103
109
  }
104
110
  /**
105
- * Provenance of a batched wake: the newest ADDRESSED channel-bearing event
106
- * wins (mention / reply / DM outranks newer ambient chatter — the framework's
107
- * own locus rule); else the newest channel-bearing event. Channel, author and
108
- * addressed all come from that one event, so they can never disagree.
111
+ * Provenance of a batched wake: the newest ADDRESSED event that names a
112
+ * channel or an author wins (mention / reply / DM outranks newer ambient
113
+ * chatter — the framework's own locus rule); else the newest event that
114
+ * names either. An addressed event's resolved route counts as its channel;
115
+ * an ambient one's doesn't. Channel, author and addressed all come from
116
+ * that one event, so they can never disagree. A `chat:system` notice is
117
+ * never chosen for the batch's route, channel or addressed flag, even one
118
+ * addressed by its place (a pin in a DM), since a notice never decides where
119
+ * the agent's speech goes; a coalesced render whose latest occurrence is a
120
+ * notice isn't taken as one (GateEventInfo.folds). A batch with no such event
121
+ * carries only its `batchChannels` when it has them; else its chosen event is
122
+ * its newest notice that has an author, of which it takes only the author
123
+ * and time.
109
124
  */
110
125
  export function wakeProvenance(events) {
126
+ const said = events.filter((e) => !e.systemNotice);
111
127
  let newest;
112
128
  let newestAddressed;
113
- for (const e of events) {
129
+ for (const e of said) {
114
130
  // A resolved route counts as naming a channel only for an ADDRESSED
115
131
  // event: a push whose origin declares only `mcplChannelId` (no raw
116
132
  // channel id, no author) is still a routable addressed message, and
@@ -126,22 +142,40 @@ export function wakeProvenance(events) {
126
142
  newestAddressed = e;
127
143
  }
128
144
  const pick = newestAddressed ?? newest;
129
- if (!pick)
130
- return undefined;
145
+ const channels = said.some((e) => e.addressed) ? [] : batchRoutes(said);
146
+ if (!pick && channels.length === 0) {
147
+ const notice = events.reduce((n, e) => (e.systemNotice && e.authorId && (!n || e.timestamp >= n.timestamp) ? e : n), undefined);
148
+ return notice ? { counterparty: counterpartyOf(notice), at: notice.timestamp } : undefined;
149
+ }
131
150
  // push-event channel ids are the adapter's raw ids (a Discord snowflake):
132
151
  // not a composite channel id, so not reported as one
133
- const channelId = pick.channelId && pick.eventType !== 'mcpl:push-event' ? pick.channelId : undefined;
134
- const ns = pick.serverId || (pick.channelId ? pick.channelId.split(':')[0] : '') || 'channel';
152
+ const channelId = pick?.channelId && pick.eventType !== 'mcpl:push-event' ? pick.channelId : undefined;
135
153
  return {
136
154
  ...(channelId ? { channelId } : {}),
137
- ...(pick.authorId ? { counterparty: `${ns}:user:${pick.authorId}` } : {}),
138
- ...(pick.addressed ? { addressed: true } : {}),
139
- ...(pick.addressed && pick.routeChannelId
155
+ ...(pick?.authorId ? { counterparty: counterpartyOf(pick) } : {}),
156
+ ...(pick?.addressed ? { addressed: true } : {}),
157
+ ...(pick?.addressed && pick.routeChannelId
140
158
  ? { routeChannelId: pick.routeChannelId, ...(pick.serverId ? { routeServerId: pick.serverId } : {}) }
141
159
  : {}),
142
- at: pick.timestamp,
160
+ ...(channels.length > 0 ? { batchChannels: channels } : {}),
161
+ ...(pick ? { at: pick.timestamp } : {}),
143
162
  };
144
163
  }
164
+ function counterpartyOf(e) {
165
+ const ns = e.serverId || (e.channelId ? e.channelId.split(':')[0] : '') || 'channel';
166
+ return `${ns}:user:${e.authorId}`;
167
+ }
168
+ function batchRoutes(events) {
169
+ const routes = [];
170
+ for (const e of events) {
171
+ if (!e.routeChannelId)
172
+ continue;
173
+ if (routes.some((r) => r.channelId === e.routeChannelId && r.serverId === e.serverId))
174
+ continue;
175
+ routes.push({ channelId: e.routeChannelId, ...(e.serverId ? { serverId: e.serverId } : {}) });
176
+ }
177
+ return routes;
178
+ }
145
179
  /** A `[Gate: …]` line's reference to messages already in the window: how
146
180
  * many, in which channel, and how long ago the oldest arrived, as of `now`. */
147
181
  function referenceLine(count, channel, oldest, now) {
@@ -252,7 +286,7 @@ function behaviorFingerprint(b) {
252
286
  // file for a null or a "" (a passthrough, a behavior's kind, gate.json's
253
287
  // default), it now loads without it.
254
288
  const CONFIG_FIELDS = ['default', 'policies'];
255
- const POLICY_FIELDS = ['name', 'match', 'behavior', 'resets', 'passthrough'];
289
+ const POLICY_FIELDS = ['name', 'match', 'behavior', 'resets', 'passthrough', 'suppress'];
256
290
  const MATCH_FIELDS = [
257
291
  'scope', 'source', 'channel', 'mount', 'pathGlob', 'tagsAny', 'tagsAll', 'tagsNone', 'metadataTrue', 'filter',
258
292
  ];
@@ -261,6 +295,7 @@ const BEHAVIOR_KINDS = ['debounce', 'rate_limit', 'passive_sample', 'judge'];
261
295
  const RATE_LIMIT_FIELDS = ['tokens', 'refillIntervalMs', 'keyBy'];
262
296
  const PASSIVE_SAMPLE_FIELDS = ['every', 'keyBy'];
263
297
  const JUDGE_FIELDS = ['debounceMs', 'maxWaitMs', 'instructions'];
298
+ const SUPPRESS_FIELDS = ['description', 'script'];
264
299
  function isPlainObject(value) {
265
300
  return value !== null && typeof value === 'object' && !Array.isArray(value);
266
301
  }
@@ -650,6 +685,7 @@ function readRule(raw, unnamed, file = 'gate.json', named = asGateNames) {
650
685
  const match = readMatch(r, obj.match);
651
686
  const resets = readResets(r, obj.resets);
652
687
  const passthrough = readPassthrough(r, obj.passthrough, kind);
688
+ const suppress = readSuppress(r, obj.suppress, obj.behavior, match, obj.match);
653
689
  const reading = {
654
690
  where: r.where, problems: r.problems, notes: r.notes, quiet: kind === 'skip' || kind === 'defer' || r.noTokens === true,
655
691
  };
@@ -659,6 +695,8 @@ function readRule(raw, unnamed, file = 'gate.json', named = asGateNames) {
659
695
  policy.resets = resets;
660
696
  if (passthrough)
661
697
  policy.passthrough = true;
698
+ if (suppress)
699
+ policy.suppress = suppress;
662
700
  reading.policy = policy;
663
701
  if (r.matchesAll)
664
702
  reading.matchesAll = true;
@@ -1071,8 +1109,7 @@ function readMatch(r, value) {
1071
1109
  // Left with nothing that narrows it once something in it was named, it
1072
1110
  // matches every event. Empty lists and nulls alone always meant no
1073
1111
  // constraint, and name nothing.
1074
- const narrows = Object.entries(match).some(([field, v]) => !(field === 'scope' && Array.isArray(v) && v.length === 0));
1075
- if (!narrows && r.problems.length + r.notes.length > named)
1112
+ if (!matchNarrows(match) && r.problems.length + r.notes.length > named)
1076
1113
  r.matchesAll = true;
1077
1114
  return match;
1078
1115
  }
@@ -1120,6 +1157,137 @@ function readPassthrough(r, value, kind) {
1120
1157
  });
1121
1158
  return false;
1122
1159
  }
1160
+ /** Whether a match narrows what it matches: anything but an empty scope. */
1161
+ export function matchNarrows(match) {
1162
+ return Object.entries(match).some(([field, v]) => !(field === 'scope' && Array.isArray(v) && v.length === 0));
1163
+ }
1164
+ /**
1165
+ * Whether a suppress rule's match names what it hides: an event type, or a
1166
+ * source, channel, mount, path or tag pattern that isn't only wildcards
1167
+ * (namesSomething), a metadata flag, or a content filter. A pattern of
1168
+ * wildcards alone, such as `source: "*"`, or only `tagsNone`, takes every
1169
+ * event, which a suppress rule may not.
1170
+ */
1171
+ function suppressMatchNarrows(match) {
1172
+ if ((match.scope ?? []).length > 0 || (match.metadataTrue ?? []).length > 0 || match.filter !== undefined)
1173
+ return true;
1174
+ if ([match.source, match.channel, match.mount, match.pathGlob].some(namesSomething))
1175
+ return true;
1176
+ return [...(match.tagsAny ?? []), ...(match.tagsAll ?? [])].some(namesSomething);
1177
+ }
1178
+ /** Longest suppress description, in characters: the review sends it with every rule it judges. */
1179
+ export const MAX_SUPPRESS_DESCRIPTION = 2_000;
1180
+ /** Longest suppress script, in characters. */
1181
+ export const MAX_SUPPRESS_SCRIPT = 20_000;
1182
+ const SUPPRESS_EXAMPLE = '{"description": "the eye going on and off"}';
1183
+ /**
1184
+ * A rule's `suppress`. Every problem with it is read as a rule that doesn't
1185
+ * suppress, which is how a release without suppression reads the rule:
1186
+ * its behavior then decides the event, and the event is rendered. The
1187
+ * behavior has to be written out as "defer" (or "skip"), not left to
1188
+ * default, because such a release reads a rule with no behavior as
1189
+ * "always" and would wake the agent on every event the rule was meant to
1190
+ * hide.
1191
+ */
1192
+ function readSuppress(r, value, behavior, match, rawMatch) {
1193
+ if (value === undefined || value === false)
1194
+ return undefined;
1195
+ const without = "so the rule doesn't suppress, and its behavior decides its events as for any other rule";
1196
+ if (!isPlainObject(value)) {
1197
+ r.problems.push({ text: `suppress is ${shownValue(value)}, where an object such as ${SUPPRESS_EXAMPLE} is needed`, read: without });
1198
+ return undefined;
1199
+ }
1200
+ const before = r.problems.length;
1201
+ // A script given as null or "" is a script that's missing, not one left
1202
+ // out: read as left out, the rule would hide every event its match takes.
1203
+ const { script: _givenScript, ...rest } = value;
1204
+ const emptyScript = Object.hasOwn(value, 'script') && (value.script === null || value.script === '');
1205
+ if (emptyScript) {
1206
+ r.problems.push({ text: `suppress.script is ${shownValue(value.script)}, where the source of a function such as "(event, state) => false" is needed`, read: without });
1207
+ }
1208
+ const given = withoutEmpty(`${r.where}: suppress`, emptyScript ? rest : value, r.notes);
1209
+ const reported = r.problems.length;
1210
+ readFieldNames(r, 'suppress', given, SUPPRESS_FIELDS);
1211
+ // A field it doesn't read can't be left aside here, as elsewhere in a
1212
+ // rule: one meant as a condition, left out, would hide every event the
1213
+ // match takes.
1214
+ const unread = Object.keys(given).filter((key) => !SUPPRESS_FIELDS.includes(key));
1215
+ if (unread.length > 0 && r.problems.length === reported) {
1216
+ r.problems.push({ text: `suppress has ${quotedList(unread)}, and a suppress rule reads only "description" and "script"`, read: without });
1217
+ }
1218
+ const config = { description: '' };
1219
+ if (typeof given.description !== 'string' || given.description.trim() === '') {
1220
+ r.problems.push({
1221
+ text: `suppress.description is ${given.description === undefined ? 'missing' : shownValue(given.description)}, where the rule's `
1222
+ + 'purpose in a few words is needed: the review checks what the rule hides against it',
1223
+ read: without,
1224
+ });
1225
+ }
1226
+ else if ([...given.description].length > MAX_SUPPRESS_DESCRIPTION) {
1227
+ r.problems.push({
1228
+ text: `suppress.description is ${[...given.description].length.toLocaleString('en-US')} characters, over the `
1229
+ + `${MAX_SUPPRESS_DESCRIPTION.toLocaleString('en-US')} allowed`,
1230
+ read: without,
1231
+ });
1232
+ }
1233
+ else {
1234
+ config.description = given.description.trim();
1235
+ }
1236
+ if (given.script !== undefined) {
1237
+ if (typeof given.script !== 'string' || given.script.trim() === '') {
1238
+ r.problems.push({ text: `suppress.script is ${shownValue(given.script)}, where the source of a function such as "(event, state) => false" is needed`, read: without });
1239
+ }
1240
+ else if ([...given.script].length > MAX_SUPPRESS_SCRIPT) {
1241
+ r.problems.push({
1242
+ text: `suppress.script is ${[...given.script].length.toLocaleString('en-US')} characters, over the ${MAX_SUPPRESS_SCRIPT.toLocaleString('en-US')} allowed`,
1243
+ read: without,
1244
+ });
1245
+ }
1246
+ else {
1247
+ // Parsed only, never run here: the script runs on its worker.
1248
+ try {
1249
+ new Function(`"use strict";\nreturn (${given.script}\n);`);
1250
+ config.script = given.script;
1251
+ }
1252
+ catch (e) {
1253
+ r.problems.push({ text: `suppress.script doesn't parse (${quotedValue(errorText(e))})`, read: without });
1254
+ }
1255
+ }
1256
+ }
1257
+ if (behavior !== 'defer' && behavior !== 'skip') {
1258
+ r.problems.push({
1259
+ text: `suppress is given, and a rule that suppresses needs "behavior": "defer" written out, so a release that `
1260
+ + `doesn't know "suppress" defers its events instead of waking you on them; it has `
1261
+ + (behavior === undefined ? 'no behavior' : `behavior ${shownValue(behavior)}`),
1262
+ read: without,
1263
+ });
1264
+ }
1265
+ // A match field the gate doesn't read would, left aside, hide more than was
1266
+ // written: the condition it was meant to add is gone.
1267
+ if (isPlainObject(rawMatch)) {
1268
+ const rawFilter = isPlainObject(rawMatch.filter) ? Object.keys(rawMatch.filter).filter((key) => !FILTER_FIELDS.includes(key)).map((key) => `filter.${key}`) : [];
1269
+ const unreadMatch = [...Object.keys(rawMatch).filter((key) => !MATCH_FIELDS.includes(key)), ...rawFilter];
1270
+ if (unreadMatch.length > 0) {
1271
+ r.problems.push({ text: `suppress is given, and match has ${quotedList(unreadMatch)}, which the gate doesn't read, so the rule would hide more than was written`, read: without });
1272
+ }
1273
+ }
1274
+ if (!suppressMatchNarrows(match)) {
1275
+ r.problems.push({
1276
+ text: 'suppress is given, and the rule\'s match names nothing it hides: a suppress rule needs a match that names '
1277
+ + 'a scope, a source, a channel, tags, a mount or path, metadata or a filter (not a pattern of wildcards alone, or tagsNone alone), '
1278
+ + 'so it can\'t hide everything',
1279
+ read: without,
1280
+ });
1281
+ }
1282
+ // Only a rule read without any problem suppresses: one read past a problem
1283
+ // (a slip of a field, a match read without a part of it) matches more, or
1284
+ // decides otherwise, than its author wrote, and hiding is what can't be
1285
+ // seen to go wrong. Its behavior decides its events instead.
1286
+ if (before > 0) {
1287
+ r.problems.push({ text: 'suppress is given on a rule with problems, and a rule suppresses only when it has none', read: without });
1288
+ }
1289
+ return r.problems.length === 0 ? config : undefined;
1290
+ }
1123
1291
  /**
1124
1292
  * A rule with no problem is in force as written. One with a problem is read
1125
1293
  * past it, the way earlier releases read it, so it wakes the agent wherever
@@ -1266,7 +1434,7 @@ function readConfig(raw, file, start) {
1266
1434
  }
1267
1435
  }
1268
1436
  for (const rule of rules)
1269
- sentences.push(...readingSentences(rule.reading, rule.outcome));
1437
+ appendAll(sentences, readingSentences(rule.reading, rule.outcome));
1270
1438
  // A list with no rules, or none the gate reads, where no sentence above said it has none.
1271
1439
  if (notUsed !== undefined && !sentences.some((said) => said.includes(notUsed))) {
1272
1440
  sentences.push(`${File} has no rules${rules.length > 0 ? ' but ones the gate leaves out' : ''}, so ${notUsedWhy}.`);
@@ -1613,7 +1781,9 @@ export function findShadowedPolicies(policies, eventTypesFrom) {
1613
1781
  const out = [];
1614
1782
  for (let i = 0; i < policies.length; i++) {
1615
1783
  const earlier = policies[i];
1616
- if (earlier.passthrough)
1784
+ // A suppress rule lets addressed events and the heartbeat's by, so it
1785
+ // can't be said to take every event of a later rule.
1786
+ if (earlier.passthrough || earlier.suppress)
1617
1787
  continue;
1618
1788
  for (let j = i + 1; j < policies.length; j++) {
1619
1789
  const found = subsumedEventTypes(earlier, policies[j], eventTypesFrom);
@@ -1645,15 +1815,53 @@ export function formatShadowWarning(w) {
1645
1815
  + `Nothing to do unless ${later} is meant for ${types} events; then ${move}. `
1646
1816
  + `A scope on ${later} naming the event types it's for clears this note.`;
1647
1817
  }
1818
+ function judgeFailureWords(err) {
1819
+ if (err instanceof JudgeFailure)
1820
+ return err.message;
1821
+ return err instanceof Error ? failureWords(err, 'the call failed') : 'the call failed';
1822
+ }
1648
1823
  /** The text of a thrown value, for a log line. It never throws itself, whatever was thrown. */
1649
1824
  function errorText(err) {
1650
1825
  try {
1826
+ if (err instanceof JudgeFailure && err.detail !== undefined)
1827
+ return `${err.message} (${err.detail})`;
1651
1828
  return err instanceof Error ? err.message : String(err);
1652
1829
  }
1653
1830
  catch {
1654
1831
  return 'a value that could not be printed';
1655
1832
  }
1656
1833
  }
1834
+ class PartialDelivery extends Error {
1835
+ woke;
1836
+ notWoken;
1837
+ constructor(woke, notWoken, cause) {
1838
+ super(errorText(cause), { cause });
1839
+ this.woke = woke;
1840
+ this.notWoken = notWoken;
1841
+ }
1842
+ }
1843
+ /**
1844
+ * Runs a batch's delivery to `woken`, which pushes each agent it wakes to
1845
+ * `woke`: a throw once any is woken is a PartialDelivery naming them and the
1846
+ * rest; one before any wake is thrown as it is.
1847
+ */
1848
+ function deliveredTracking(woke, woken, deliver) {
1849
+ try {
1850
+ deliver();
1851
+ }
1852
+ catch (err) {
1853
+ if (woke.length === 0)
1854
+ throw err;
1855
+ throw new PartialDelivery(woke, woken.filter((name) => !woke.includes(name)), err);
1856
+ }
1857
+ }
1858
+ function deliveredInPart(err) {
1859
+ if (!(err instanceof PartialDelivery))
1860
+ return undefined;
1861
+ const one = err.notWoken.length === 1;
1862
+ return `woke ${joinedList(err.woke)}, and delivering it threw (${err.message}), `
1863
+ + `so it's dropped for ${joinedList(err.notWoken)}, who ${one ? "isn't" : "aren't"} woken`;
1864
+ }
1657
1865
  /**
1658
1866
  * Run a caller's callback once the change it's told of is made and traced. A
1659
1867
  * throw from it is logged, not thrown, so the call answers as the change it
@@ -1766,6 +1974,8 @@ export class EventGate {
1766
1974
  droppedBufferedEvents = 0;
1767
1975
  // Per-policy stats
1768
1976
  stats = new Map();
1977
+ /** Suppress rules' scripts and their state (suppress-scripts.ts), kept beside gate.json. */
1978
+ suppressScripts;
1769
1979
  // The shadow lint's last findings (see refreshShadowWarnings), recomputed
1770
1980
  // on every config change and fingerprinted so an unchanged set doesn't
1771
1981
  // re-trace.
@@ -1823,6 +2033,11 @@ export class EventGate {
1823
2033
  now;
1824
2034
  constructor(opts) {
1825
2035
  this.configPath = opts.configPath;
2036
+ // Before the rules are read: a start's merge can clear a rule's state, its script's included.
2037
+ this.suppressScripts = new SuppressScripts({
2038
+ statePath: join(dirname(opts.configPath), 'suppress-state.json'),
2039
+ ...(opts.suppressScriptTimeoutMs !== undefined ? { timeoutMs: opts.suppressScriptTimeoutMs } : {}),
2040
+ });
1826
2041
  this.privilegedUsersPath = opts.privilegedUsersPath;
1827
2042
  this.emitTrace = opts.emitTrace;
1828
2043
  this.addMessageFn = opts.addMessage;
@@ -2108,7 +2323,6 @@ export class EventGate {
2108
2323
  ...changes,
2109
2324
  // Every name the start added to gate.json, as an append's trace gave.
2110
2325
  addedPolicies: names,
2111
- timestamp: Date.now(),
2112
2326
  });
2113
2327
  }
2114
2328
  if (!appended && (!last.recipe || canonical({ rules: last.recipe.rules, default: last.recipe.default }) !== canonical({ rules: starting.rules.map((rule) => rule.raw), default: given.default }))) {
@@ -2208,7 +2422,6 @@ export class EventGate {
2208
2422
  type: 'gate:config-error',
2209
2423
  error: why,
2210
2424
  configPath: this.configPath,
2211
- timestamp: Date.now(),
2212
2425
  });
2213
2426
  }
2214
2427
  this.configError = problem;
@@ -2358,8 +2571,11 @@ export class EventGate {
2358
2571
  clearStateFor(was, next) {
2359
2572
  const behaviorsByName = (config) => {
2360
2573
  const byName = new Map();
2361
- for (const p of config.policies)
2362
- byName.set(p.name, [...(byName.get(p.name) ?? []), behaviorFingerprint(p.behavior)]);
2574
+ for (const p of config.policies) {
2575
+ // A suppress rule's script is part of what it is: a changed script starts from a fresh state.
2576
+ const fingerprint = behaviorFingerprint(p.behavior) + (p.suppress ? ` suppress:${p.suppress.script ?? ''}` : '');
2577
+ byName.set(p.name, [...(byName.get(p.name) ?? []), fingerprint]);
2578
+ }
2363
2579
  return new Map([...byName].map(([name, behaviors]) => [name, JSON.stringify(behaviors)]));
2364
2580
  };
2365
2581
  const before = behaviorsByName(was);
@@ -2412,7 +2628,6 @@ export class EventGate {
2412
2628
  policyCount: newConfig.policies.length,
2413
2629
  removedPolicies: removedPolicies.length > 0 ? removedPolicies : undefined,
2414
2630
  stateClearedPolicies: stateClearedPolicies.length > 0 ? stateClearedPolicies : undefined,
2415
- timestamp: now,
2416
2631
  });
2417
2632
  }
2418
2633
  catch {
@@ -2523,7 +2738,6 @@ export class EventGate {
2523
2738
  configPath: this.configPath,
2524
2739
  policyName: policy.name,
2525
2740
  replaced: idx >= 0,
2526
- timestamp: Date.now(),
2527
2741
  });
2528
2742
  if (ignored.length > 0) {
2529
2743
  // A caller that doesn't take the notes still gets them said, in the log.
@@ -2570,7 +2784,6 @@ export class EventGate {
2570
2784
  type: 'gate:policy-removed',
2571
2785
  configPath: this.configPath,
2572
2786
  policyName: name,
2573
- timestamp: Date.now(),
2574
2787
  });
2575
2788
  return true;
2576
2789
  }
@@ -2585,6 +2798,15 @@ export class EventGate {
2585
2798
  const current = read.state === 'loaded' ? read.config : this.config;
2586
2799
  return current.policies.map((p) => p.name);
2587
2800
  }
2801
+ /** The rules in force, in order (copies): what the last decision was made by. */
2802
+ policiesInForce() {
2803
+ return this.config.policies.map((p) => structuredClone(p));
2804
+ }
2805
+ /** The rule in force by this name (a copy), or undefined. */
2806
+ policyInForce(name) {
2807
+ const p = this.config.policies.find((x) => x.name === name);
2808
+ return p ? structuredClone(p) : undefined;
2809
+ }
2588
2810
  /** @internal For `wake_add_rule`'s and `wake_remove_rule`'s refusals; not a host API. */
2589
2811
  ruleNamesForChange() {
2590
2812
  const edit = this.rulesForChange();
@@ -2763,7 +2985,6 @@ export class EventGate {
2763
2985
  matchedPolicy: 'asleep',
2764
2986
  trigger: false,
2765
2987
  behavior: 'skip',
2766
- timestamp: this.now(),
2767
2988
  });
2768
2989
  return { trigger: false, policyName: 'asleep', behavior: 'skip' };
2769
2990
  }
@@ -2786,6 +3007,12 @@ export class EventGate {
2786
3007
  }
2787
3008
  else {
2788
3009
  for (const compiled of this.compiledPolicies) {
3010
+ // A suppress rule doesn't decide a wake for an event suppression was
3011
+ // asked about: one it takes was suppressed before the gate was asked,
3012
+ // and one that reaches here is one it let by. Where suppression wasn't
3013
+ // asked (no view to hide it from), it defers, as its behavior says.
3014
+ if (compiled.policy.suppress && this.passesOverSuppress(info))
3015
+ continue;
2789
3016
  if (!this.compiledMatches(compiled, info))
2790
3017
  continue;
2791
3018
  const candidate = this.computeDecision(compiled.policy, info);
@@ -2822,7 +3049,6 @@ export class EventGate {
2822
3049
  trigger: decision.trigger,
2823
3050
  behavior: formatBehavior(decision.behavior),
2824
3051
  ...(observed.length > 0 ? { observed } : {}),
2825
- timestamp: this.now(),
2826
3052
  });
2827
3053
  return decision;
2828
3054
  }
@@ -2843,7 +3069,6 @@ export class EventGate {
2843
3069
  behavior: formatBehavior(policy.behavior),
2844
3070
  eventType: info.eventType,
2845
3071
  source: info.serverId || undefined,
2846
- timestamp: this.now(),
2847
3072
  });
2848
3073
  if (policy.behavior === 'defer' || policy.behavior === 'skip') {
2849
3074
  // Report the configured name faithfully ('defer' or legacy 'skip').
@@ -2975,6 +3200,7 @@ export class EventGate {
2975
3200
  const debounce = this.debounceTimers.get(name);
2976
3201
  if (debounce)
2977
3202
  clearTimeout(debounce.timer);
3203
+ this.suppressScripts.forget(name);
2978
3204
  this.rateLimitBuckets.delete(name);
2979
3205
  this.passiveSampleCounters.delete(name);
2980
3206
  this.rateLimitDenied.delete(name);
@@ -3032,6 +3258,8 @@ export class EventGate {
3032
3258
  */
3033
3259
  probe(info) {
3034
3260
  for (const compiled of this.compiledPolicies) {
3261
+ if (compiled.policy.suppress && this.passesOverSuppress(info))
3262
+ continue; // as evaluate does
3035
3263
  if (!this.compiledMatches(compiled, info))
3036
3264
  continue;
3037
3265
  const trigger = this.wouldTrigger(compiled.policy, info);
@@ -3042,6 +3270,140 @@ export class EventGate {
3042
3270
  const trigger = (this.config.default ?? 'always') === 'always';
3043
3271
  return { trigger, policyName: null, behavior: this.config.default ?? 'always' };
3044
3272
  }
3273
+ /**
3274
+ * The suppress rule that takes this event, or null when none does.
3275
+ * Side-effect-free, like probe: no counter moves, no batch forms, nothing
3276
+ * is traced, and a rule's script runs on a copy of its state, which is
3277
+ * then dropped. (A closure a script builds for itself is its own, and a
3278
+ * dry run's timeout counts toward setting a script aside, as it costs the same.) The probe table and other dry runs ask this; the
3279
+ * framework asks decideSuppression instead, once per event.
3280
+ *
3281
+ * It reads the rules as evaluate does, first match deciding: a rule ahead
3282
+ * of the suppress rule that matches the event decides it instead (it isn't
3283
+ * suppressed), unless it's a passthrough rule that wouldn't fire. A rule
3284
+ * with a script (`suppress.script`) takes an event its match takes only
3285
+ * when the script says to hide it; otherwise the event goes on to the rules
3286
+ * after it, as one the rule doesn't match does. Sleep and gate.js aren't
3287
+ * consulted: they decide wakes, and suppression decides what's rendered.
3288
+ *
3289
+ * Never suppressed, whatever the rules say: an event addressed to the
3290
+ * agent (tagged `chat:addressed`: a mention, a reply, a DM) and the
3291
+ * heartbeat's (the agent's own clock). A suppress rule lets them by to the
3292
+ * rules after it, and its script isn't asked. The framework itself never
3293
+ * asks about console or API input.
3294
+ */
3295
+ suppressionFor(info) {
3296
+ return this.walkSuppression(info, false);
3297
+ }
3298
+ /**
3299
+ * suppressionFor, for an event that is arriving now: each script asked
3300
+ * keeps the state it leaves, whatever later rules do with the event, and a
3301
+ * suppressed event counts in the passthrough rules ahead of its rule. Ask
3302
+ * once per event.
3303
+ */
3304
+ decideSuppression(info) {
3305
+ return this.walkSuppression(info, true);
3306
+ }
3307
+ walkSuppression(info, commit) {
3308
+ this.reloadIfChanged();
3309
+ this.syncSuppressScripts();
3310
+ if (info.serverId === 'heartbeat')
3311
+ return null;
3312
+ const addressed = Array.isArray(info.tags) && info.tags.includes('chat:addressed');
3313
+ let event;
3314
+ const observers = [];
3315
+ for (const compiled of this.compiledPolicies) {
3316
+ if (!this.compiledMatches(compiled, info))
3317
+ continue;
3318
+ const suppress = compiled.policy.suppress;
3319
+ if (suppress) {
3320
+ if (addressed)
3321
+ continue;
3322
+ if (suppress.script !== undefined) {
3323
+ event ??= this.suppressScriptEvent(info);
3324
+ const ran = this.suppressScripts.run(compiled.policy.name, event, commit);
3325
+ // A script that failed shows the event: it goes on to the rules after.
3326
+ if (!ran || !('hide' in ran) || !ran.hide)
3327
+ continue;
3328
+ }
3329
+ // A suppressed event never reaches evaluate, so the passthrough rules
3330
+ // ahead of this one count it here, as evaluate would have: none of
3331
+ // them fires for it, and each fires on its schedule. A dry run counts
3332
+ // nothing.
3333
+ if (commit)
3334
+ for (const observer of observers)
3335
+ this.computeDecision(observer, info);
3336
+ return compiled.policy.name;
3337
+ }
3338
+ if (compiled.policy.passthrough && !this.wouldTrigger(compiled.policy, info)) {
3339
+ observers.push(compiled.policy);
3340
+ continue;
3341
+ }
3342
+ return null;
3343
+ }
3344
+ return null;
3345
+ }
3346
+ /** Whether evaluate and probe pass over a suppress rule for this event: suppression was asked about it, or it's one suppression always lets by. */
3347
+ passesOverSuppress(info) {
3348
+ return info.suppressionAsked === true || info.serverId === 'heartbeat'
3349
+ || (Array.isArray(info.tags) && info.tags.includes('chat:addressed'));
3350
+ }
3351
+ /** The scripts in step with the rules in force (SuppressScripts.sync: only changed ones are compiled). */
3352
+ syncSuppressScripts() {
3353
+ this.suppressScripts.sync(this.config.policies.map((p) => ({ name: p.name, ...(p.suppress?.script !== undefined ? { script: p.suppress.script } : {}) })));
3354
+ }
3355
+ /** What a suppress script is given for an event: plain JSON, its metadata included where it can be. */
3356
+ suppressScriptEvent(info) {
3357
+ let metadata = {};
3358
+ try {
3359
+ metadata = JSON.parse(JSON.stringify(info.metadata ?? {}));
3360
+ }
3361
+ catch {
3362
+ metadata = {};
3363
+ }
3364
+ const channel = this.routeChannelFor(info) ?? (info.channelId || undefined);
3365
+ const author = this.extractAuthorId(info.metadata);
3366
+ return {
3367
+ eventType: info.eventType,
3368
+ source: info.serverId,
3369
+ ...(channel ? { channel } : {}),
3370
+ ...(author ? { author } : {}),
3371
+ tags: [...(info.tags ?? [])],
3372
+ text: info.content,
3373
+ at: this.now(),
3374
+ metadata,
3375
+ };
3376
+ }
3377
+ /**
3378
+ * Whether a suppress script compiles to a function, on the scripts' worker:
3379
+ * an error's text, or null. wake_add_rule asks before it saves a rule.
3380
+ */
3381
+ checkSuppressScript(script) {
3382
+ return this.suppressScripts.check(script);
3383
+ }
3384
+ /** A suppress rule's script state as it stands (a copy), for a host or a test. */
3385
+ suppressScriptState(name) {
3386
+ this.syncSuppressScripts();
3387
+ return this.suppressScripts.stateOf(name);
3388
+ }
3389
+ /**
3390
+ * Count a row the framework suppressed by `name` (suppressionFor's
3391
+ * answer) once it's written. The rule's stats count it as a match, so
3392
+ * gate_status shows how many events each suppress rule took since start.
3393
+ */
3394
+ noteSuppressed(name, info) {
3395
+ const policyStats = this.stats.get(name) ?? { matchCount: 0, lastMatchTimestamp: null };
3396
+ policyStats.matchCount++;
3397
+ policyStats.lastMatchTimestamp = this.now();
3398
+ this.stats.set(name, policyStats);
3399
+ this.emitTrace({
3400
+ type: 'gate:suppressed',
3401
+ policyName: name,
3402
+ eventType: info.eventType,
3403
+ ...(info.serverId ? { serverId: info.serverId } : {}),
3404
+ ...(info.channelId ? { channelId: info.channelId } : {}),
3405
+ });
3406
+ }
3045
3407
  /** The canonical synthetic events the probe table runs: chat shapes as MCPL delivers them, and a heartbeat. */
3046
3408
  probeEvents() {
3047
3409
  const chat = (label, eventType, metadata, tags) => ({
@@ -3100,7 +3462,12 @@ export class EventGate {
3100
3462
  */
3101
3463
  probeTable() {
3102
3464
  return this.probeEvents().map(({ label, info }) => {
3103
- const d = this.probe(info);
3465
+ const suppressedBy = this.suppressionFor(info);
3466
+ if (suppressedBy !== null) {
3467
+ return { probe: label, eventType: info.eventType, policy: suppressedBy, behavior: 'suppress', wouldWake: false };
3468
+ }
3469
+ // As admission asks: suppression first, so a script that says show lets the probe on.
3470
+ const d = this.probe({ ...info, suppressionAsked: true });
3104
3471
  return {
3105
3472
  probe: label,
3106
3473
  eventType: info.eventType,
@@ -3147,7 +3514,6 @@ export class EventGate {
3147
3514
  type: 'gate:shadow-warnings',
3148
3515
  reason,
3149
3516
  warnings: warnings.map(formatShadowWarning),
3150
- timestamp: this.now(),
3151
3517
  });
3152
3518
  }
3153
3519
  }
@@ -3201,9 +3567,11 @@ export class EventGate {
3201
3567
  authorId: this.extractAuthorId(info.metadata) ?? undefined,
3202
3568
  serverId: info.serverId || undefined,
3203
3569
  addressed: Array.isArray(info.tags) && info.tags.includes('chat:addressed'),
3570
+ ...(isSystemNotice(info.tags) && !info.folds ? { systemNotice: true } : {}),
3204
3571
  routeChannelId: this.routeChannelFor(info),
3205
3572
  ...(typeof info.metadata?.messageId === 'string' ? { messageId: info.metadata.messageId } : {}),
3206
3573
  ...(typeof info.metadata?.eventId === 'string' ? { eventId: info.metadata.eventId } : {}),
3574
+ ...(info.recipients !== undefined ? { recipients: [...info.recipients] } : {}),
3207
3575
  };
3208
3576
  const existing = this.debounceTimers.get(policy.name);
3209
3577
  // A judge's maxWaitMs caps how far new events push the batch back: it's
@@ -3258,15 +3626,14 @@ export class EventGate {
3258
3626
  }
3259
3627
  catch (err) {
3260
3628
  const one = events.length === 1;
3261
- console.error(`[gate] Rule ${quotedValue(policyName)}: its pending batch of ${events.length} ${one ? 'event' : 'events'} couldn't be delivered `
3262
- + `(${errorText(err)}), so the batch is dropped`);
3629
+ console.error(`[gate] Rule ${quotedValue(policyName)}: its pending batch of ${events.length} ${one ? 'event' : 'events'} `
3630
+ + (deliveredInPart(err) ?? `couldn't be delivered (${errorText(err)}), so the batch is dropped`));
3263
3631
  return;
3264
3632
  }
3265
3633
  this.emitTrace({
3266
3634
  type: 'gate:debounce-delivered',
3267
3635
  policyName,
3268
3636
  eventCount: events.length,
3269
- timestamp: Date.now(),
3270
3637
  });
3271
3638
  }
3272
3639
  // =========================================================================
@@ -3286,11 +3653,37 @@ export class EventGate {
3286
3653
  type: 'gate:held-dropped',
3287
3654
  dropped: batched.length - events.length,
3288
3655
  channels: [...new Set(batched.filter((e) => !events.includes(e)).map(heldAt))],
3289
- timestamp: this.now(),
3290
3656
  });
3291
3657
  }
3292
3658
  if (events.length === 0)
3293
3659
  return;
3660
+ // Who each delivery woke, so that a throw after a wake is told as a
3661
+ // delivery in part (PartialDelivery), the batch dropped only for those
3662
+ // it didn't wake.
3663
+ const woke = [];
3664
+ if (events.every((e) => e.recipients === undefined)) {
3665
+ const names = this.getAgentNamesFn();
3666
+ deliveredTracking(woke, names, () => this.deliverBatch(events, names, undefined, woke));
3667
+ return;
3668
+ }
3669
+ const woken = new Set(events.flatMap((e) => e.recipients ?? []));
3670
+ if (events.some((e) => e.recipients === undefined))
3671
+ for (const name of this.getAgentNamesFn())
3672
+ woken.add(name);
3673
+ const groups = new Map();
3674
+ for (const agentName of woken) {
3675
+ const seen = events.filter((e) => e.recipients === undefined || e.recipients.includes(agentName));
3676
+ const key = seen.map((e) => events.indexOf(e)).join(',');
3677
+ const group = groups.get(key) ?? { agents: [], events: seen };
3678
+ group.agents.push(agentName);
3679
+ groups.set(key, group);
3680
+ }
3681
+ deliveredTracking(woke, [...woken], () => {
3682
+ for (const group of groups.values())
3683
+ this.deliverBatch(group.events, group.agents, group.agents, woke);
3684
+ });
3685
+ }
3686
+ deliverBatch(events, woken, audience, woke) {
3294
3687
  const policyNames = [...new Set(events.map(e => e.policyName))];
3295
3688
  // Events whose content the framework already stored at arrival (MCPL
3296
3689
  // channel-incoming / push-event) get a compact REFERENCE — re-quoting
@@ -3346,7 +3739,12 @@ export class EventGate {
3346
3739
  const text = `[${NOTICE_TAG.gate}: ${events.length} event${events.length > 1 ? 's' : ''} matched]\n\n${lines.join('\n')}`;
3347
3740
  const only = events.length === 1 && events[0].inContext ? events[0] : undefined;
3348
3741
  let line;
3349
- if (only && this.newestEntry(only)) {
3742
+ // Where a line stored now landed, for the delivery agent's wake, as the
3743
+ // self-wake's notice places its own (readsRowFor): a live turn that
3744
+ // carried and answered the line drops the wake. Other agents' wakes stay
3745
+ // plain by readsRowFor's rule.
3746
+ const placement = {};
3747
+ if (only && this.newestEntry(only, audience)) {
3350
3748
  const shownChannel = only.routeChannelId ?? only.channelId;
3351
3749
  line = {
3352
3750
  entry: entryRef(only),
@@ -3360,29 +3758,31 @@ export class EventGate {
3360
3758
  this.addMessageFn('user', [{ type: 'text', text }], {
3361
3759
  source: 'gate:debounce',
3362
3760
  policies: policyNames,
3363
- });
3761
+ }, undefined, placement, audience);
3364
3762
  }
3365
3763
  // Provenance of the batched wake (who/where woke the agent): the newest
3366
3764
  // ADDRESSED event first, else the newest event naming a channel or an
3367
- // author; channel + author + addressed from that ONE event. When that
3368
- // event addressed the agent and its registered channel is known,
3369
- // `routeChannelId` names the turn's speech locus too — the reply goes
3370
- // back to whoever addressed the agent, not to whichever channel last saw
3371
- // any traffic. Ambient-only batches set no locus. A batch whose line the
3372
- // gate left to the turn's start carries it too (`line`), one object
3373
- // shared by every agent the batch wakes, so it is written once.
3765
+ // author, `chat:system` notices aside; channel + author + addressed from
3766
+ // that ONE event. When that event addressed the agent and its registered
3767
+ // channel is known, `routeChannelId` names the turn's speech locus too —
3768
+ // the reply goes back to whoever addressed the agent, not to whichever
3769
+ // channel last saw any traffic. Ambient-only batches set no locus, and
3770
+ // name the channels they came from (`batchChannels`). A batch whose line
3771
+ // the gate left to the turn's start carries it too (`line`), one object
3772
+ // shared by every agent woken with these messages, so it is written once.
3374
3773
  const chosen = wakeProvenance(events);
3375
3774
  const provenance = line ? { ...chosen, line } : chosen;
3376
- for (const agentName of this.getAgentNamesFn()) {
3377
- this.requestInferenceFn(agentName, 'gate:debounce', 'gate', provenance);
3775
+ for (const agentName of woken) {
3776
+ this.requestInferenceFn(agentName, 'gate:debounce', 'gate', provenance, line ? undefined : placement);
3777
+ woke?.push(agentName);
3378
3778
  }
3379
3779
  }
3380
3780
  /** isNewestEntry for one event; a callback fault keeps the line. */
3381
- newestEntry(event) {
3781
+ newestEntry(event, recipients) {
3382
3782
  if (!this.isNewestEntryFn)
3383
3783
  return false;
3384
3784
  try {
3385
- return this.isNewestEntryFn(entryRef(event)) === true;
3785
+ return this.isNewestEntryFn(entryRef(event), recipients) === true;
3386
3786
  }
3387
3787
  catch {
3388
3788
  return false;
@@ -3405,7 +3805,7 @@ export class EventGate {
3405
3805
  return;
3406
3806
  const deciding = this.judging.get(policyName);
3407
3807
  if (deciding) {
3408
- deciding.queued.push(...events);
3808
+ appendAll(deciding.queued, events);
3409
3809
  this.capJudgeEvents(deciding.queued);
3410
3810
  return;
3411
3811
  }
@@ -3424,8 +3824,8 @@ export class EventGate {
3424
3824
  this.judging.set(policyName, state);
3425
3825
  const timedOut = new Promise((_, reject) => {
3426
3826
  state.timer = setTimeout(() => {
3827
+ reject(new JudgeFailure(`the judge took more than ${this.judgeTimeoutMs} ms`));
3427
3828
  abort.abort();
3428
- reject(new Error(`the judge took more than ${this.judgeTimeoutMs} ms`));
3429
3829
  }, this.judgeTimeoutMs);
3430
3830
  // Never keep the process alive on the judge's account.
3431
3831
  state.timer.unref?.();
@@ -3456,7 +3856,7 @@ export class EventGate {
3456
3856
  holdForJudge(policyName, events, judge) {
3457
3857
  const held = this.judgeHeld.get(policyName);
3458
3858
  if (held) {
3459
- held.events.push(...events);
3859
+ appendAll(held.events, events);
3460
3860
  held.judge = judge;
3461
3861
  this.capJudgeEvents(held.events);
3462
3862
  }
@@ -3496,7 +3896,8 @@ export class EventGate {
3496
3896
  * the judge to weigh what waiting costs: each agent's pending self-wake
3497
3897
  * (skip_reply's wake_in_seconds), the end of a sleep, and the pending
3498
3898
  * batches of rules other than `exceptPolicy` (a debounce's wakes the agent
3499
- * when it fires; a judge rule's goes to the judge, so it only may).
3899
+ * when it fires; a judge rule's goes to the judge, so it only may). A batch,
3900
+ * and a sleep with no agent named, reach the gate's recipients alone.
3500
3901
  */
3501
3902
  upcomingWakes(agentNames, exceptPolicy) {
3502
3903
  const wakes = [];
@@ -3504,20 +3905,22 @@ export class EventGate {
3504
3905
  for (const name of agentNames) {
3505
3906
  const due = this.selfWakeDue.get(name);
3506
3907
  if (due && this.selfWakeTimers.has(name)) {
3507
- wakes.push({ at: due.at, certain: true, what: `${agentNames.length > 1 ? `${name}'s` : 'its'} own ${due.source} timer` });
3908
+ wakes.push({ at: due.at, certain: true, kind: 'self-wake', what: `${agentNames.length > 1 ? `${name}'s` : 'its'} own ${due.source} timer` });
3508
3909
  }
3509
3910
  }
3510
- if (this.sleepUntil > now && (this.sleepAgent === undefined || agentNames.includes(this.sleepAgent))) {
3511
- wakes.push({ at: this.sleepUntil, certain: true, what: 'the end of its sleep' });
3911
+ const recipients = this.getAgentNamesFn();
3912
+ const recipient = agentNames.some((name) => recipients.includes(name));
3913
+ if (this.sleepUntil > now && (this.sleepAgent === undefined ? recipient : agentNames.includes(this.sleepAgent))) {
3914
+ wakes.push({ at: this.sleepUntil, certain: true, kind: 'sleep-end', what: 'the end of its sleep' });
3512
3915
  }
3513
3916
  for (const [name, state] of this.debounceTimers) {
3514
- if (name === exceptPolicy || state.events.length === 0)
3917
+ if (!recipient || name === exceptPolicy || state.events.length === 0)
3515
3918
  continue;
3516
3919
  const n = state.events.length;
3517
3920
  const batch = `${n} ${n === 1 ? 'event' : 'events'}`;
3518
3921
  wakes.push(state.judge
3519
- ? { at: state.dueAt, certain: false, what: `rule ${quotedValue(name)}'s batch of ${batch} goes to the judge` }
3520
- : { at: state.dueAt, certain: true, what: `rule ${quotedValue(name)}'s debounced batch of ${batch}` });
3922
+ ? { at: state.dueAt, certain: false, kind: 'judge-batch', what: `rule ${quotedValue(name)}'s batch of ${batch} goes to the judge` }
3923
+ : { at: state.dueAt, certain: true, kind: 'debounce', what: `rule ${quotedValue(name)}'s debounced batch of ${batch}` });
3521
3924
  }
3522
3925
  return wakes.sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
3523
3926
  }
@@ -3579,7 +3982,7 @@ export class EventGate {
3579
3982
  }
3580
3983
  const outcome = failed ? 'error' : verdict.reply ? 'reply' : 'wait';
3581
3984
  const reason = failed
3582
- ? (err !== undefined ? errorText(err) : 'the judge gave no verdict')
3985
+ ? (err !== undefined ? judgeFailureWords(err) : 'the judge gave no verdict')
3583
3986
  : typeof verdict.reason === 'string' ? verdict.reason : '';
3584
3987
  const record = this.judgeRecords.get(policyName) ?? { verdicts: { reply: 0, wait: 0, error: 0 } };
3585
3988
  record.verdicts[outcome]++;
@@ -3602,7 +4005,7 @@ export class EventGate {
3602
4005
  }
3603
4006
  const wake = outcome === 'reply' || (outcome === 'error' && this.judgeOnError === 'wake');
3604
4007
  if (failed) {
3605
- console.error(`[gate] Rule ${quotedValue(policyName)}: the reply judge failed (${reason}), so its batch of ${state.events.length} `
4008
+ console.error(`[gate] Rule ${quotedValue(policyName)}: the reply judge failed (${err !== undefined ? errorText(err) : reason}), so its batch of ${state.events.length} `
3606
4009
  + `${state.events.length === 1 ? 'event' : 'events'} ${wake ? 'wakes the agent' : "doesn't wake the agent"} (judge onError "${this.judgeOnError}")`);
3607
4010
  }
3608
4011
  this.emitTrace({
@@ -3616,7 +4019,6 @@ export class EventGate {
3616
4019
  ...(!failed && typeof verdict.inputTokens === 'number' ? { inputTokens: verdict.inputTokens } : {}),
3617
4020
  ...(!failed && typeof verdict.cacheReadTokens === 'number' ? { cacheReadTokens: verdict.cacheReadTokens } : {}),
3618
4021
  ...(!failed && typeof verdict.outputTokens === 'number' ? { outputTokens: verdict.outputTokens } : {}),
3619
- timestamp: Date.now(),
3620
4022
  });
3621
4023
  if (this.disposed)
3622
4024
  return;
@@ -3643,8 +4045,8 @@ export class EventGate {
3643
4045
  }
3644
4046
  catch (err) {
3645
4047
  const one = events.length === 1;
3646
- console.error(`[gate] a judged batch of ${events.length} ${one ? 'event' : 'events'} couldn't be delivered `
3647
- + `(${errorText(err)}), so the batch is dropped`);
4048
+ console.error(`[gate] a judged batch of ${events.length} ${one ? 'event' : 'events'} `
4049
+ + (deliveredInPart(err) ?? `couldn't be delivered (${errorText(err)}), so the batch is dropped`));
3648
4050
  }
3649
4051
  }
3650
4052
  changesJudgedContext(agentName) {
@@ -3713,8 +4115,8 @@ export class EventGate {
3713
4115
  * buffer through the same path a turn's end uses. Self-wake and sleep-expiry
3714
4116
  * timers deliberately do NOT route through this — their inference requests
3715
4117
  * are per-agent targeted pushes that the framework's quiesce wake gate parks
3716
- * and releases at resume, whereas the buffer's deliverEvents broadcasts a
3717
- * wake to every agent.
4118
+ * and releases at resume, whereas the buffer's deliverEvents wakes every
4119
+ * agent a batch is for.
3718
4120
  */
3719
4121
  setQuiesced(quiesced) {
3720
4122
  if (this.quiesced === quiesced)
@@ -3745,8 +4147,12 @@ export class EventGate {
3745
4147
  }
3746
4148
  catch (err) {
3747
4149
  const one = events.length === 1;
3748
- console.error(`[gate] ${events.length} ${one ? 'event' : 'events'} held for an agent's turn or the host's quiesce couldn't be `
3749
- + `delivered (${errorText(err)}), so ${one ? "it's" : "they're"} dropped`);
4150
+ const held = `${events.length} ${one ? 'event' : 'events'}`;
4151
+ const partly = deliveredInPart(err);
4152
+ console.error(partly
4153
+ ? `[gate] a batch of ${held}, held for an agent's turn or the host's quiesce, ${partly}`
4154
+ : `[gate] ${held} held for an agent's turn or the host's quiesce couldn't be delivered (${errorText(err)}), `
4155
+ + `so ${one ? "it's" : "they're"} dropped`);
3750
4156
  }
3751
4157
  });
3752
4158
  }
@@ -3815,7 +4221,6 @@ export class EventGate {
3815
4221
  matchedPolicy: 'asleep',
3816
4222
  trigger: false,
3817
4223
  behavior: 'skip',
3818
- timestamp: this.now(),
3819
4224
  });
3820
4225
  return { until: this.sleepUntil };
3821
4226
  }
@@ -3876,13 +4281,13 @@ export class EventGate {
3876
4281
  matchedPolicy: 'self-wake',
3877
4282
  trigger: true,
3878
4283
  behavior: 'always',
3879
- timestamp: this.now(),
3880
4284
  });
3881
4285
  // Compact in-window notice: without it the wake turn contains nothing
3882
4286
  // new, so the agent can't tell WHAT woke it (its own timer vs. a
3883
4287
  // heartbeat) or WHEN. One line, timestamped to the second. Added
3884
- // before the inference request so it rides the wake turn (addMessage's
3885
- // turn-alive guard defers it to that turn's start if needed).
4288
+ // before the inference request so it rides the wake turn: it is stored
4289
+ // at once, and while a turn of the agent's is alive it is kept out of
4290
+ // the window until the agent's next presentation point.
3886
4291
  const nowIso = new Date(this.now()).toISOString().slice(0, 19) + 'Z';
3887
4292
  // Deliver the notice into the WAKING agent's window — a self-wake
3888
4293
  // armed by a non-primary agent (e.g. a subconscious cadence) must not
@@ -3905,7 +4310,6 @@ export class EventGate {
3905
4310
  matchedPolicy: 'self-wake',
3906
4311
  trigger: false,
3907
4312
  behavior: 'skip',
3908
- timestamp: this.now(),
3909
4313
  });
3910
4314
  return { inMs: ms, armed: true };
3911
4315
  }
@@ -3953,7 +4357,6 @@ export class EventGate {
3953
4357
  matchedPolicy: 'asleep',
3954
4358
  trigger: true,
3955
4359
  behavior: 'always',
3956
- timestamp: this.now(),
3957
4360
  });
3958
4361
  const detail = note ? `${reason}: ${note}` : reason;
3959
4362
  const targets = agent ? [agent] : this.getAgentNamesFn();
@@ -4088,6 +4491,7 @@ export class EventGate {
4088
4491
  return { success: true, data: status };
4089
4492
  }
4090
4493
  getStatus() {
4494
+ this.syncSuppressScripts();
4091
4495
  const policies = this.config.policies.map(p => {
4092
4496
  const stats = this.stats.get(p.name);
4093
4497
  const debounce = this.debounceTimers.get(p.name);
@@ -4096,6 +4500,9 @@ export class EventGate {
4096
4500
  behavior: p.behavior,
4097
4501
  matchCount: stats?.matchCount ?? 0,
4098
4502
  lastMatchTimestamp: stats?.lastMatchTimestamp ?? null,
4503
+ ...(p.suppress
4504
+ ? { suppress: { ...p.suppress, ...(p.suppress.script !== undefined && this.suppressScripts.status(p.name) ? { scriptStatus: this.suppressScripts.status(p.name) } : {}) } }
4505
+ : {}),
4099
4506
  };
4100
4507
  if (typeof p.behavior === 'object' && 'debounce' in p.behavior) {
4101
4508
  result.debounceState = {
@@ -4157,6 +4564,13 @@ export class EventGate {
4157
4564
  // Fresh, not the cache: the sources the host knows (eventTypesFrom)
4158
4565
  // change as servers connect, with no change to the rules.
4159
4566
  shadowWarnings: this.lintShadows().map(formatShadowWarning),
4567
+ ...(() => {
4568
+ const rule = firstCatchAll(this.config.policies, { lettingBy: true });
4569
+ return rule ? { catchAll: {
4570
+ rule,
4571
+ note: `${JSON.stringify(rule)} lets MCPL events by without waking you and without naming a server, a channel or a tag, so a server connected later is handled by it until a rule for that server goes ahead of it.`,
4572
+ } } : {};
4573
+ })(),
4160
4574
  totalEvaluations: this.totalEvaluations,
4161
4575
  defaultDecisions: {
4162
4576
  triggered: this.defaultTriggered,
@@ -4192,6 +4606,7 @@ export class EventGate {
4192
4606
  // =========================================================================
4193
4607
  dispose() {
4194
4608
  this.gateScript.dispose();
4609
+ this.suppressScripts.dispose();
4195
4610
  for (const state of this.debounceTimers.values()) {
4196
4611
  clearTimeout(state.timer);
4197
4612
  }