@animalabs/agent-framework 0.31.0 → 0.33.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 (298) hide show
  1. package/README.md +204 -14
  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 +185 -78
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +279 -102
  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/code-execution/isolate/scripts.d.ts +12 -8
  17. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  18. package/dist/src/code-execution/isolate/scripts.js +24 -0
  19. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  20. package/dist/src/config-checks.d.ts +7 -0
  21. package/dist/src/config-checks.d.ts.map +1 -1
  22. package/dist/src/config-checks.js +14 -0
  23. package/dist/src/config-checks.js.map +1 -1
  24. package/dist/src/context-manager-errors.d.ts +25 -0
  25. package/dist/src/context-manager-errors.d.ts.map +1 -0
  26. package/dist/src/context-manager-errors.js +31 -0
  27. package/dist/src/context-manager-errors.js.map +1 -0
  28. package/dist/src/context-options.d.ts +67 -0
  29. package/dist/src/context-options.d.ts.map +1 -0
  30. package/dist/src/context-options.js +85 -0
  31. package/dist/src/context-options.js.map +1 -0
  32. package/dist/src/deferred-import.d.ts +93 -0
  33. package/dist/src/deferred-import.d.ts.map +1 -0
  34. package/dist/src/deferred-import.js +177 -0
  35. package/dist/src/deferred-import.js.map +1 -0
  36. package/dist/src/dendrite/audience.d.ts +223 -0
  37. package/dist/src/dendrite/audience.d.ts.map +1 -0
  38. package/dist/src/dendrite/audience.js +202 -0
  39. package/dist/src/dendrite/audience.js.map +1 -0
  40. package/dist/src/dendrite/index.d.ts +5 -3
  41. package/dist/src/dendrite/index.d.ts.map +1 -1
  42. package/dist/src/dendrite/index.js +2 -1
  43. package/dist/src/dendrite/index.js.map +1 -1
  44. package/dist/src/dendrite/presets.d.ts +10 -5
  45. package/dist/src/dendrite/presets.d.ts.map +1 -1
  46. package/dist/src/dendrite/presets.js +17 -5
  47. package/dist/src/dendrite/presets.js.map +1 -1
  48. package/dist/src/dendrite/registry.d.ts +62 -7
  49. package/dist/src/dendrite/registry.d.ts.map +1 -1
  50. package/dist/src/dendrite/registry.js +179 -10
  51. package/dist/src/dendrite/registry.js.map +1 -1
  52. package/dist/src/dendrite/types.d.ts +91 -8
  53. package/dist/src/dendrite/types.d.ts.map +1 -1
  54. package/dist/src/foreign-rounds.d.ts +69 -0
  55. package/dist/src/foreign-rounds.d.ts.map +1 -0
  56. package/dist/src/foreign-rounds.js +152 -0
  57. package/dist/src/foreign-rounds.js.map +1 -0
  58. package/dist/src/framework.d.ts +1977 -820
  59. package/dist/src/framework.d.ts.map +1 -1
  60. package/dist/src/framework.js +7571 -3457
  61. package/dist/src/framework.js.map +1 -1
  62. package/dist/src/gate/event-gate.d.ts +85 -15
  63. package/dist/src/gate/event-gate.d.ts.map +1 -1
  64. package/dist/src/gate/event-gate.js +421 -47
  65. package/dist/src/gate/event-gate.js.map +1 -1
  66. package/dist/src/gate/index.d.ts +1 -1
  67. package/dist/src/gate/index.d.ts.map +1 -1
  68. package/dist/src/gate/index.js.map +1 -1
  69. package/dist/src/gate/rule-scope.d.ts +30 -0
  70. package/dist/src/gate/rule-scope.d.ts.map +1 -0
  71. package/dist/src/gate/rule-scope.js +41 -0
  72. package/dist/src/gate/rule-scope.js.map +1 -0
  73. package/dist/src/gate/suppress-script-worker.d.ts +2 -0
  74. package/dist/src/gate/suppress-script-worker.d.ts.map +1 -0
  75. package/dist/src/gate/suppress-script-worker.js +103 -0
  76. package/dist/src/gate/suppress-script-worker.js.map +1 -0
  77. package/dist/src/gate/suppress-scripts.d.ts +105 -0
  78. package/dist/src/gate/suppress-scripts.d.ts.map +1 -0
  79. package/dist/src/gate/suppress-scripts.js +403 -0
  80. package/dist/src/gate/suppress-scripts.js.map +1 -0
  81. package/dist/src/gate/suppression-review.d.ts +143 -0
  82. package/dist/src/gate/suppression-review.d.ts.map +1 -0
  83. package/dist/src/gate/suppression-review.js +463 -0
  84. package/dist/src/gate/suppression-review.js.map +1 -0
  85. package/dist/src/gate/suppression-stats.d.ts +125 -0
  86. package/dist/src/gate/suppression-stats.d.ts.map +1 -0
  87. package/dist/src/gate/suppression-stats.js +214 -0
  88. package/dist/src/gate/suppression-stats.js.map +1 -0
  89. package/dist/src/gate/types.d.ts +135 -7
  90. package/dist/src/gate/types.d.ts.map +1 -1
  91. package/dist/src/gate/wake-onboarding.d.ts +119 -0
  92. package/dist/src/gate/wake-onboarding.d.ts.map +1 -0
  93. package/dist/src/gate/wake-onboarding.js +277 -0
  94. package/dist/src/gate/wake-onboarding.js.map +1 -0
  95. package/dist/src/held-writes.d.ts +31 -23
  96. package/dist/src/held-writes.d.ts.map +1 -1
  97. package/dist/src/held-writes.js +44 -59
  98. package/dist/src/held-writes.js.map +1 -1
  99. package/dist/src/index.d.ts +5 -2
  100. package/dist/src/index.d.ts.map +1 -1
  101. package/dist/src/index.js +3 -0
  102. package/dist/src/index.js.map +1 -1
  103. package/dist/src/jsonrpc-envelope.d.ts +69 -0
  104. package/dist/src/jsonrpc-envelope.d.ts.map +1 -0
  105. package/dist/src/jsonrpc-envelope.js +66 -0
  106. package/dist/src/jsonrpc-envelope.js.map +1 -0
  107. package/dist/src/mcpl/channel-registry.d.ts +131 -15
  108. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  109. package/dist/src/mcpl/channel-registry.js +290 -46
  110. package/dist/src/mcpl/channel-registry.js.map +1 -1
  111. package/dist/src/mcpl/checkpoint-manager.d.ts +2 -4
  112. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  113. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  114. package/dist/src/mcpl/feature-set-manager.d.ts +2 -4
  115. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  116. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  117. package/dist/src/mcpl/inbound-source.d.ts +2 -1
  118. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  119. package/dist/src/mcpl/inbound-source.js +2 -1
  120. package/dist/src/mcpl/inbound-source.js.map +1 -1
  121. package/dist/src/mcpl/inference-router.d.ts +6 -6
  122. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  123. package/dist/src/mcpl/inference-router.js.map +1 -1
  124. package/dist/src/mcpl/push-coalescer.d.ts +132 -22
  125. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  126. package/dist/src/mcpl/push-coalescer.js +105 -15
  127. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  128. package/dist/src/mcpl/push-handler.d.ts +9 -7
  129. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  130. package/dist/src/mcpl/push-handler.js.map +1 -1
  131. package/dist/src/mcpl/server-connection.d.ts +13 -10
  132. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  133. package/dist/src/mcpl/server-connection.js +134 -56
  134. package/dist/src/mcpl/server-connection.js.map +1 -1
  135. package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
  136. package/dist/src/mcpl/tool-classes.js +3 -0
  137. package/dist/src/mcpl/tool-classes.js.map +1 -1
  138. package/dist/src/mcpl/types.d.ts +8 -2
  139. package/dist/src/mcpl/types.d.ts.map +1 -1
  140. package/dist/src/mcpl/types.js.map +1 -1
  141. package/dist/src/module-registry.d.ts +24 -1
  142. package/dist/src/module-registry.d.ts.map +1 -1
  143. package/dist/src/module-registry.js +68 -18
  144. package/dist/src/module-registry.js.map +1 -1
  145. package/dist/src/modules/discord/index.d.ts +45 -8
  146. package/dist/src/modules/discord/index.d.ts.map +1 -1
  147. package/dist/src/modules/discord/index.js +237 -86
  148. package/dist/src/modules/discord/index.js.map +1 -1
  149. package/dist/src/modules/history/index.d.ts +59 -0
  150. package/dist/src/modules/history/index.d.ts.map +1 -1
  151. package/dist/src/modules/history/index.js +432 -67
  152. package/dist/src/modules/history/index.js.map +1 -1
  153. package/dist/src/modules/history/semantic.d.ts +6 -5
  154. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  155. package/dist/src/modules/history/semantic.js +15 -22
  156. package/dist/src/modules/history/semantic.js.map +1 -1
  157. package/dist/src/modules/workspace/disk-agreement.d.ts +1 -1
  158. package/dist/src/modules/workspace/disk-agreement.js +1 -1
  159. package/dist/src/modules/workspace/index.d.ts +28 -7
  160. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  161. package/dist/src/modules/workspace/index.js +76 -22
  162. package/dist/src/modules/workspace/index.js.map +1 -1
  163. package/dist/src/notice-tags.d.ts +10 -1
  164. package/dist/src/notice-tags.d.ts.map +1 -1
  165. package/dist/src/notice-tags.js +10 -1
  166. package/dist/src/notice-tags.js.map +1 -1
  167. package/dist/src/operator-log.d.ts +36 -3
  168. package/dist/src/operator-log.d.ts.map +1 -1
  169. package/dist/src/operator-log.js +2 -2
  170. package/dist/src/operator-log.js.map +1 -1
  171. package/dist/src/presentation.d.ts +746 -0
  172. package/dist/src/presentation.d.ts.map +1 -0
  173. package/dist/src/presentation.js +1435 -0
  174. package/dist/src/presentation.js.map +1 -0
  175. package/dist/src/prose-drafts.d.ts +15 -11
  176. package/dist/src/prose-drafts.d.ts.map +1 -1
  177. package/dist/src/prose-drafts.js +16 -12
  178. package/dist/src/prose-drafts.js.map +1 -1
  179. package/dist/src/read-marks.d.ts +192 -0
  180. package/dist/src/read-marks.d.ts.map +1 -0
  181. package/dist/src/read-marks.js +242 -0
  182. package/dist/src/read-marks.js.map +1 -0
  183. package/dist/src/recovery/offline-branch.d.ts +91 -6
  184. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  185. package/dist/src/recovery/offline-branch.js +273 -29
  186. package/dist/src/recovery/offline-branch.js.map +1 -1
  187. package/dist/src/recovery/recover-cli.js +22 -3
  188. package/dist/src/recovery/recover-cli.js.map +1 -1
  189. package/dist/src/send-tools.d.ts +5 -9
  190. package/dist/src/send-tools.d.ts.map +1 -1
  191. package/dist/src/send-tools.js +40 -7
  192. package/dist/src/send-tools.js.map +1 -1
  193. package/dist/src/stored-injections.d.ts +2 -2
  194. package/dist/src/stored-injections.js +2 -2
  195. package/dist/src/stored-removal.d.ts +17 -4
  196. package/dist/src/stored-removal.d.ts.map +1 -1
  197. package/dist/src/stored-removal.js +29 -5
  198. package/dist/src/stored-removal.js.map +1 -1
  199. package/dist/src/tool-result-guard.d.ts +12 -1
  200. package/dist/src/tool-result-guard.d.ts.map +1 -1
  201. package/dist/src/tool-result-guard.js +42 -8
  202. package/dist/src/tool-result-guard.js.map +1 -1
  203. package/dist/src/tune-out/coordinator.d.ts +1159 -79
  204. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  205. package/dist/src/tune-out/coordinator.js +3709 -363
  206. package/dist/src/tune-out/coordinator.js.map +1 -1
  207. package/dist/src/tune-out/tools.d.ts +114 -17
  208. package/dist/src/tune-out/tools.d.ts.map +1 -1
  209. package/dist/src/tune-out/tools.js +87 -27
  210. package/dist/src/tune-out/tools.js.map +1 -1
  211. package/dist/src/tune-out/window-anchor.d.ts +143 -0
  212. package/dist/src/tune-out/window-anchor.d.ts.map +1 -0
  213. package/dist/src/tune-out/window-anchor.js +202 -0
  214. package/dist/src/tune-out/window-anchor.js.map +1 -0
  215. package/dist/src/types/agent.d.ts +86 -31
  216. package/dist/src/types/agent.d.ts.map +1 -1
  217. package/dist/src/types/events.d.ts +2 -0
  218. package/dist/src/types/events.d.ts.map +1 -1
  219. package/dist/src/types/framework.d.ts +43 -21
  220. package/dist/src/types/framework.d.ts.map +1 -1
  221. package/dist/src/types/module.d.ts +180 -69
  222. package/dist/src/types/module.d.ts.map +1 -1
  223. package/dist/src/types/trace.d.ts +510 -36
  224. package/dist/src/types/trace.d.ts.map +1 -1
  225. package/package.json +3 -3
  226. package/src/admission/admit.ts +32 -3
  227. package/src/agent.ts +323 -120
  228. package/src/api/mcp-server-core.ts +54 -17
  229. package/src/api/server.ts +4 -23
  230. package/src/code-execution/isolate/scripts.ts +28 -3
  231. package/src/config-checks.ts +14 -0
  232. package/src/context-manager-errors.ts +37 -0
  233. package/src/context-options.ts +96 -0
  234. package/src/deferred-import.ts +239 -0
  235. package/src/dendrite/audience.ts +315 -0
  236. package/src/dendrite/index.ts +14 -2
  237. package/src/dendrite/presets.ts +18 -6
  238. package/src/dendrite/registry.ts +209 -12
  239. package/src/dendrite/types.ts +92 -8
  240. package/src/foreign-rounds.ts +199 -0
  241. package/src/framework.ts +7814 -3585
  242. package/src/gate/event-gate.ts +428 -65
  243. package/src/gate/index.ts +3 -0
  244. package/src/gate/rule-scope.ts +45 -0
  245. package/src/gate/suppress-script-worker.ts +89 -0
  246. package/src/gate/suppress-scripts.ts +414 -0
  247. package/src/gate/suppression-review.ts +517 -0
  248. package/src/gate/suppression-stats.ts +314 -0
  249. package/src/gate/types.ts +141 -7
  250. package/src/gate/wake-onboarding.ts +309 -0
  251. package/src/held-writes.ts +49 -60
  252. package/src/index.ts +9 -2
  253. package/src/jsonrpc-envelope.ts +81 -0
  254. package/src/mcpl/channel-registry.ts +353 -85
  255. package/src/mcpl/checkpoint-manager.ts +3 -2
  256. package/src/mcpl/feature-set-manager.ts +2 -1
  257. package/src/mcpl/inbound-source.ts +2 -1
  258. package/src/mcpl/inference-router.ts +7 -4
  259. package/src/mcpl/push-coalescer.ts +193 -34
  260. package/src/mcpl/push-handler.ts +10 -5
  261. package/src/mcpl/server-connection.ts +141 -60
  262. package/src/mcpl/tool-classes.ts +3 -0
  263. package/src/mcpl/types.ts +8 -2
  264. package/src/module-registry.ts +73 -17
  265. package/src/modules/discord/index.ts +229 -81
  266. package/src/modules/history/index.ts +439 -70
  267. package/src/modules/history/semantic.ts +15 -21
  268. package/src/modules/workspace/disk-agreement.ts +1 -1
  269. package/src/modules/workspace/index.ts +85 -23
  270. package/src/notice-tags.ts +10 -1
  271. package/src/operator-log.ts +36 -3
  272. package/src/presentation.ts +1676 -0
  273. package/src/prose-drafts.ts +16 -12
  274. package/src/read-marks.ts +328 -0
  275. package/src/recovery/offline-branch.ts +346 -32
  276. package/src/recovery/recover-cli.ts +21 -3
  277. package/src/send-tools.ts +40 -7
  278. package/src/stored-injections.ts +2 -2
  279. package/src/stored-removal.ts +31 -7
  280. package/src/tool-result-guard.ts +41 -7
  281. package/src/tune-out/coordinator.ts +4089 -457
  282. package/src/tune-out/tools.ts +160 -43
  283. package/src/tune-out/window-anchor.ts +277 -0
  284. package/src/types/agent.ts +90 -31
  285. package/src/types/events.ts +2 -0
  286. package/src/types/framework.ts +43 -21
  287. package/src/types/module.ts +181 -68
  288. package/src/types/trace.ts +542 -40
  289. package/dist/src/reasoning-loss-notice.d.ts +0 -23
  290. package/dist/src/reasoning-loss-notice.d.ts.map +0 -1
  291. package/dist/src/reasoning-loss-notice.js +0 -52
  292. package/dist/src/reasoning-loss-notice.js.map +0 -1
  293. package/dist/src/reasoning-loss.d.ts +0 -24
  294. package/dist/src/reasoning-loss.d.ts.map +0 -1
  295. package/dist/src/reasoning-loss.js +0 -102
  296. package/dist/src/reasoning-loss.js.map +0 -1
  297. package/src/reasoning-loss-notice.ts +0 -64
  298. package/src/reasoning-loss.ts +0 -120
@@ -15,6 +15,8 @@ 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';
@@ -252,7 +254,7 @@ function behaviorFingerprint(b) {
252
254
  // file for a null or a "" (a passthrough, a behavior's kind, gate.json's
253
255
  // default), it now loads without it.
254
256
  const CONFIG_FIELDS = ['default', 'policies'];
255
- const POLICY_FIELDS = ['name', 'match', 'behavior', 'resets', 'passthrough'];
257
+ const POLICY_FIELDS = ['name', 'match', 'behavior', 'resets', 'passthrough', 'suppress'];
256
258
  const MATCH_FIELDS = [
257
259
  'scope', 'source', 'channel', 'mount', 'pathGlob', 'tagsAny', 'tagsAll', 'tagsNone', 'metadataTrue', 'filter',
258
260
  ];
@@ -261,6 +263,7 @@ const BEHAVIOR_KINDS = ['debounce', 'rate_limit', 'passive_sample', 'judge'];
261
263
  const RATE_LIMIT_FIELDS = ['tokens', 'refillIntervalMs', 'keyBy'];
262
264
  const PASSIVE_SAMPLE_FIELDS = ['every', 'keyBy'];
263
265
  const JUDGE_FIELDS = ['debounceMs', 'maxWaitMs', 'instructions'];
266
+ const SUPPRESS_FIELDS = ['description', 'script'];
264
267
  function isPlainObject(value) {
265
268
  return value !== null && typeof value === 'object' && !Array.isArray(value);
266
269
  }
@@ -650,6 +653,7 @@ function readRule(raw, unnamed, file = 'gate.json', named = asGateNames) {
650
653
  const match = readMatch(r, obj.match);
651
654
  const resets = readResets(r, obj.resets);
652
655
  const passthrough = readPassthrough(r, obj.passthrough, kind);
656
+ const suppress = readSuppress(r, obj.suppress, obj.behavior, match, obj.match);
653
657
  const reading = {
654
658
  where: r.where, problems: r.problems, notes: r.notes, quiet: kind === 'skip' || kind === 'defer' || r.noTokens === true,
655
659
  };
@@ -659,6 +663,8 @@ function readRule(raw, unnamed, file = 'gate.json', named = asGateNames) {
659
663
  policy.resets = resets;
660
664
  if (passthrough)
661
665
  policy.passthrough = true;
666
+ if (suppress)
667
+ policy.suppress = suppress;
662
668
  reading.policy = policy;
663
669
  if (r.matchesAll)
664
670
  reading.matchesAll = true;
@@ -1071,8 +1077,7 @@ function readMatch(r, value) {
1071
1077
  // Left with nothing that narrows it once something in it was named, it
1072
1078
  // matches every event. Empty lists and nulls alone always meant no
1073
1079
  // 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)
1080
+ if (!matchNarrows(match) && r.problems.length + r.notes.length > named)
1076
1081
  r.matchesAll = true;
1077
1082
  return match;
1078
1083
  }
@@ -1120,6 +1125,137 @@ function readPassthrough(r, value, kind) {
1120
1125
  });
1121
1126
  return false;
1122
1127
  }
1128
+ /** Whether a match narrows what it matches: anything but an empty scope. */
1129
+ export function matchNarrows(match) {
1130
+ return Object.entries(match).some(([field, v]) => !(field === 'scope' && Array.isArray(v) && v.length === 0));
1131
+ }
1132
+ /**
1133
+ * Whether a suppress rule's match names what it hides: an event type, or a
1134
+ * source, channel, mount, path or tag pattern that isn't only wildcards
1135
+ * (namesSomething), a metadata flag, or a content filter. A pattern of
1136
+ * wildcards alone, such as `source: "*"`, or only `tagsNone`, takes every
1137
+ * event, which a suppress rule may not.
1138
+ */
1139
+ function suppressMatchNarrows(match) {
1140
+ if ((match.scope ?? []).length > 0 || (match.metadataTrue ?? []).length > 0 || match.filter !== undefined)
1141
+ return true;
1142
+ if ([match.source, match.channel, match.mount, match.pathGlob].some(namesSomething))
1143
+ return true;
1144
+ return [...(match.tagsAny ?? []), ...(match.tagsAll ?? [])].some(namesSomething);
1145
+ }
1146
+ /** Longest suppress description, in characters: the review sends it with every rule it judges. */
1147
+ export const MAX_SUPPRESS_DESCRIPTION = 2_000;
1148
+ /** Longest suppress script, in characters. */
1149
+ export const MAX_SUPPRESS_SCRIPT = 20_000;
1150
+ const SUPPRESS_EXAMPLE = '{"description": "the eye going on and off"}';
1151
+ /**
1152
+ * A rule's `suppress`. Every problem with it is read as a rule that doesn't
1153
+ * suppress, which is how a release without suppression reads the rule:
1154
+ * its behavior then decides the event, and the event is rendered. The
1155
+ * behavior has to be written out as "defer" (or "skip"), not left to
1156
+ * default, because such a release reads a rule with no behavior as
1157
+ * "always" and would wake the agent on every event the rule was meant to
1158
+ * hide.
1159
+ */
1160
+ function readSuppress(r, value, behavior, match, rawMatch) {
1161
+ if (value === undefined || value === false)
1162
+ return undefined;
1163
+ const without = "so the rule doesn't suppress, and its behavior decides its events as for any other rule";
1164
+ if (!isPlainObject(value)) {
1165
+ r.problems.push({ text: `suppress is ${shownValue(value)}, where an object such as ${SUPPRESS_EXAMPLE} is needed`, read: without });
1166
+ return undefined;
1167
+ }
1168
+ const before = r.problems.length;
1169
+ // A script given as null or "" is a script that's missing, not one left
1170
+ // out: read as left out, the rule would hide every event its match takes.
1171
+ const { script: _givenScript, ...rest } = value;
1172
+ const emptyScript = Object.hasOwn(value, 'script') && (value.script === null || value.script === '');
1173
+ if (emptyScript) {
1174
+ 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 });
1175
+ }
1176
+ const given = withoutEmpty(`${r.where}: suppress`, emptyScript ? rest : value, r.notes);
1177
+ const reported = r.problems.length;
1178
+ readFieldNames(r, 'suppress', given, SUPPRESS_FIELDS);
1179
+ // A field it doesn't read can't be left aside here, as elsewhere in a
1180
+ // rule: one meant as a condition, left out, would hide every event the
1181
+ // match takes.
1182
+ const unread = Object.keys(given).filter((key) => !SUPPRESS_FIELDS.includes(key));
1183
+ if (unread.length > 0 && r.problems.length === reported) {
1184
+ r.problems.push({ text: `suppress has ${quotedList(unread)}, and a suppress rule reads only "description" and "script"`, read: without });
1185
+ }
1186
+ const config = { description: '' };
1187
+ if (typeof given.description !== 'string' || given.description.trim() === '') {
1188
+ r.problems.push({
1189
+ text: `suppress.description is ${given.description === undefined ? 'missing' : shownValue(given.description)}, where the rule's `
1190
+ + 'purpose in a few words is needed: the review checks what the rule hides against it',
1191
+ read: without,
1192
+ });
1193
+ }
1194
+ else if ([...given.description].length > MAX_SUPPRESS_DESCRIPTION) {
1195
+ r.problems.push({
1196
+ text: `suppress.description is ${[...given.description].length.toLocaleString('en-US')} characters, over the `
1197
+ + `${MAX_SUPPRESS_DESCRIPTION.toLocaleString('en-US')} allowed`,
1198
+ read: without,
1199
+ });
1200
+ }
1201
+ else {
1202
+ config.description = given.description.trim();
1203
+ }
1204
+ if (given.script !== undefined) {
1205
+ if (typeof given.script !== 'string' || given.script.trim() === '') {
1206
+ 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 });
1207
+ }
1208
+ else if ([...given.script].length > MAX_SUPPRESS_SCRIPT) {
1209
+ r.problems.push({
1210
+ text: `suppress.script is ${[...given.script].length.toLocaleString('en-US')} characters, over the ${MAX_SUPPRESS_SCRIPT.toLocaleString('en-US')} allowed`,
1211
+ read: without,
1212
+ });
1213
+ }
1214
+ else {
1215
+ // Parsed only, never run here: the script runs on its worker.
1216
+ try {
1217
+ new Function(`"use strict";\nreturn (${given.script}\n);`);
1218
+ config.script = given.script;
1219
+ }
1220
+ catch (e) {
1221
+ r.problems.push({ text: `suppress.script doesn't parse (${quotedValue(errorText(e))})`, read: without });
1222
+ }
1223
+ }
1224
+ }
1225
+ if (behavior !== 'defer' && behavior !== 'skip') {
1226
+ r.problems.push({
1227
+ text: `suppress is given, and a rule that suppresses needs "behavior": "defer" written out, so a release that `
1228
+ + `doesn't know "suppress" defers its events instead of waking you on them; it has `
1229
+ + (behavior === undefined ? 'no behavior' : `behavior ${shownValue(behavior)}`),
1230
+ read: without,
1231
+ });
1232
+ }
1233
+ // A match field the gate doesn't read would, left aside, hide more than was
1234
+ // written: the condition it was meant to add is gone.
1235
+ if (isPlainObject(rawMatch)) {
1236
+ const rawFilter = isPlainObject(rawMatch.filter) ? Object.keys(rawMatch.filter).filter((key) => !FILTER_FIELDS.includes(key)).map((key) => `filter.${key}`) : [];
1237
+ const unreadMatch = [...Object.keys(rawMatch).filter((key) => !MATCH_FIELDS.includes(key)), ...rawFilter];
1238
+ if (unreadMatch.length > 0) {
1239
+ 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 });
1240
+ }
1241
+ }
1242
+ if (!suppressMatchNarrows(match)) {
1243
+ r.problems.push({
1244
+ text: 'suppress is given, and the rule\'s match names nothing it hides: a suppress rule needs a match that names '
1245
+ + 'a scope, a source, a channel, tags, a mount or path, metadata or a filter (not a pattern of wildcards alone, or tagsNone alone), '
1246
+ + 'so it can\'t hide everything',
1247
+ read: without,
1248
+ });
1249
+ }
1250
+ // Only a rule read without any problem suppresses: one read past a problem
1251
+ // (a slip of a field, a match read without a part of it) matches more, or
1252
+ // decides otherwise, than its author wrote, and hiding is what can't be
1253
+ // seen to go wrong. Its behavior decides its events instead.
1254
+ if (before > 0) {
1255
+ r.problems.push({ text: 'suppress is given on a rule with problems, and a rule suppresses only when it has none', read: without });
1256
+ }
1257
+ return r.problems.length === 0 ? config : undefined;
1258
+ }
1123
1259
  /**
1124
1260
  * A rule with no problem is in force as written. One with a problem is read
1125
1261
  * past it, the way earlier releases read it, so it wakes the agent wherever
@@ -1613,7 +1749,9 @@ export function findShadowedPolicies(policies, eventTypesFrom) {
1613
1749
  const out = [];
1614
1750
  for (let i = 0; i < policies.length; i++) {
1615
1751
  const earlier = policies[i];
1616
- if (earlier.passthrough)
1752
+ // A suppress rule lets addressed events and the heartbeat's by, so it
1753
+ // can't be said to take every event of a later rule.
1754
+ if (earlier.passthrough || earlier.suppress)
1617
1755
  continue;
1618
1756
  for (let j = i + 1; j < policies.length; j++) {
1619
1757
  const found = subsumedEventTypes(earlier, policies[j], eventTypesFrom);
@@ -1654,6 +1792,37 @@ function errorText(err) {
1654
1792
  return 'a value that could not be printed';
1655
1793
  }
1656
1794
  }
1795
+ class PartialDelivery extends Error {
1796
+ woke;
1797
+ notWoken;
1798
+ constructor(woke, notWoken, cause) {
1799
+ super(errorText(cause), { cause });
1800
+ this.woke = woke;
1801
+ this.notWoken = notWoken;
1802
+ }
1803
+ }
1804
+ /**
1805
+ * Runs a batch's delivery to `woken`, which pushes each agent it wakes to
1806
+ * `woke`: a throw once any is woken is a PartialDelivery naming them and the
1807
+ * rest; one before any wake is thrown as it is.
1808
+ */
1809
+ function deliveredTracking(woke, woken, deliver) {
1810
+ try {
1811
+ deliver();
1812
+ }
1813
+ catch (err) {
1814
+ if (woke.length === 0)
1815
+ throw err;
1816
+ throw new PartialDelivery(woke, woken.filter((name) => !woke.includes(name)), err);
1817
+ }
1818
+ }
1819
+ function deliveredInPart(err) {
1820
+ if (!(err instanceof PartialDelivery))
1821
+ return undefined;
1822
+ const one = err.notWoken.length === 1;
1823
+ return `woke ${joinedList(err.woke)}, and delivering it threw (${err.message}), `
1824
+ + `so it's dropped for ${joinedList(err.notWoken)}, who ${one ? "isn't" : "aren't"} woken`;
1825
+ }
1657
1826
  /**
1658
1827
  * Run a caller's callback once the change it's told of is made and traced. A
1659
1828
  * throw from it is logged, not thrown, so the call answers as the change it
@@ -1766,6 +1935,8 @@ export class EventGate {
1766
1935
  droppedBufferedEvents = 0;
1767
1936
  // Per-policy stats
1768
1937
  stats = new Map();
1938
+ /** Suppress rules' scripts and their state (suppress-scripts.ts), kept beside gate.json. */
1939
+ suppressScripts;
1769
1940
  // The shadow lint's last findings (see refreshShadowWarnings), recomputed
1770
1941
  // on every config change and fingerprinted so an unchanged set doesn't
1771
1942
  // re-trace.
@@ -1823,6 +1994,11 @@ export class EventGate {
1823
1994
  now;
1824
1995
  constructor(opts) {
1825
1996
  this.configPath = opts.configPath;
1997
+ // Before the rules are read: a start's merge can clear a rule's state, its script's included.
1998
+ this.suppressScripts = new SuppressScripts({
1999
+ statePath: join(dirname(opts.configPath), 'suppress-state.json'),
2000
+ ...(opts.suppressScriptTimeoutMs !== undefined ? { timeoutMs: opts.suppressScriptTimeoutMs } : {}),
2001
+ });
1826
2002
  this.privilegedUsersPath = opts.privilegedUsersPath;
1827
2003
  this.emitTrace = opts.emitTrace;
1828
2004
  this.addMessageFn = opts.addMessage;
@@ -2108,7 +2284,6 @@ export class EventGate {
2108
2284
  ...changes,
2109
2285
  // Every name the start added to gate.json, as an append's trace gave.
2110
2286
  addedPolicies: names,
2111
- timestamp: Date.now(),
2112
2287
  });
2113
2288
  }
2114
2289
  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 +2383,6 @@ export class EventGate {
2208
2383
  type: 'gate:config-error',
2209
2384
  error: why,
2210
2385
  configPath: this.configPath,
2211
- timestamp: Date.now(),
2212
2386
  });
2213
2387
  }
2214
2388
  this.configError = problem;
@@ -2358,8 +2532,11 @@ export class EventGate {
2358
2532
  clearStateFor(was, next) {
2359
2533
  const behaviorsByName = (config) => {
2360
2534
  const byName = new Map();
2361
- for (const p of config.policies)
2362
- byName.set(p.name, [...(byName.get(p.name) ?? []), behaviorFingerprint(p.behavior)]);
2535
+ for (const p of config.policies) {
2536
+ // A suppress rule's script is part of what it is: a changed script starts from a fresh state.
2537
+ const fingerprint = behaviorFingerprint(p.behavior) + (p.suppress ? ` suppress:${p.suppress.script ?? ''}` : '');
2538
+ byName.set(p.name, [...(byName.get(p.name) ?? []), fingerprint]);
2539
+ }
2363
2540
  return new Map([...byName].map(([name, behaviors]) => [name, JSON.stringify(behaviors)]));
2364
2541
  };
2365
2542
  const before = behaviorsByName(was);
@@ -2412,7 +2589,6 @@ export class EventGate {
2412
2589
  policyCount: newConfig.policies.length,
2413
2590
  removedPolicies: removedPolicies.length > 0 ? removedPolicies : undefined,
2414
2591
  stateClearedPolicies: stateClearedPolicies.length > 0 ? stateClearedPolicies : undefined,
2415
- timestamp: now,
2416
2592
  });
2417
2593
  }
2418
2594
  catch {
@@ -2523,7 +2699,6 @@ export class EventGate {
2523
2699
  configPath: this.configPath,
2524
2700
  policyName: policy.name,
2525
2701
  replaced: idx >= 0,
2526
- timestamp: Date.now(),
2527
2702
  });
2528
2703
  if (ignored.length > 0) {
2529
2704
  // A caller that doesn't take the notes still gets them said, in the log.
@@ -2570,7 +2745,6 @@ export class EventGate {
2570
2745
  type: 'gate:policy-removed',
2571
2746
  configPath: this.configPath,
2572
2747
  policyName: name,
2573
- timestamp: Date.now(),
2574
2748
  });
2575
2749
  return true;
2576
2750
  }
@@ -2585,6 +2759,15 @@ export class EventGate {
2585
2759
  const current = read.state === 'loaded' ? read.config : this.config;
2586
2760
  return current.policies.map((p) => p.name);
2587
2761
  }
2762
+ /** The rules in force, in order (copies): what the last decision was made by. */
2763
+ policiesInForce() {
2764
+ return this.config.policies.map((p) => structuredClone(p));
2765
+ }
2766
+ /** The rule in force by this name (a copy), or undefined. */
2767
+ policyInForce(name) {
2768
+ const p = this.config.policies.find((x) => x.name === name);
2769
+ return p ? structuredClone(p) : undefined;
2770
+ }
2588
2771
  /** @internal For `wake_add_rule`'s and `wake_remove_rule`'s refusals; not a host API. */
2589
2772
  ruleNamesForChange() {
2590
2773
  const edit = this.rulesForChange();
@@ -2763,7 +2946,6 @@ export class EventGate {
2763
2946
  matchedPolicy: 'asleep',
2764
2947
  trigger: false,
2765
2948
  behavior: 'skip',
2766
- timestamp: this.now(),
2767
2949
  });
2768
2950
  return { trigger: false, policyName: 'asleep', behavior: 'skip' };
2769
2951
  }
@@ -2786,6 +2968,12 @@ export class EventGate {
2786
2968
  }
2787
2969
  else {
2788
2970
  for (const compiled of this.compiledPolicies) {
2971
+ // A suppress rule doesn't decide a wake for an event suppression was
2972
+ // asked about: one it takes was suppressed before the gate was asked,
2973
+ // and one that reaches here is one it let by. Where suppression wasn't
2974
+ // asked (no view to hide it from), it defers, as its behavior says.
2975
+ if (compiled.policy.suppress && this.passesOverSuppress(info))
2976
+ continue;
2789
2977
  if (!this.compiledMatches(compiled, info))
2790
2978
  continue;
2791
2979
  const candidate = this.computeDecision(compiled.policy, info);
@@ -2822,7 +3010,6 @@ export class EventGate {
2822
3010
  trigger: decision.trigger,
2823
3011
  behavior: formatBehavior(decision.behavior),
2824
3012
  ...(observed.length > 0 ? { observed } : {}),
2825
- timestamp: this.now(),
2826
3013
  });
2827
3014
  return decision;
2828
3015
  }
@@ -2843,7 +3030,6 @@ export class EventGate {
2843
3030
  behavior: formatBehavior(policy.behavior),
2844
3031
  eventType: info.eventType,
2845
3032
  source: info.serverId || undefined,
2846
- timestamp: this.now(),
2847
3033
  });
2848
3034
  if (policy.behavior === 'defer' || policy.behavior === 'skip') {
2849
3035
  // Report the configured name faithfully ('defer' or legacy 'skip').
@@ -2975,6 +3161,7 @@ export class EventGate {
2975
3161
  const debounce = this.debounceTimers.get(name);
2976
3162
  if (debounce)
2977
3163
  clearTimeout(debounce.timer);
3164
+ this.suppressScripts.forget(name);
2978
3165
  this.rateLimitBuckets.delete(name);
2979
3166
  this.passiveSampleCounters.delete(name);
2980
3167
  this.rateLimitDenied.delete(name);
@@ -3032,6 +3219,8 @@ export class EventGate {
3032
3219
  */
3033
3220
  probe(info) {
3034
3221
  for (const compiled of this.compiledPolicies) {
3222
+ if (compiled.policy.suppress && this.passesOverSuppress(info))
3223
+ continue; // as evaluate does
3035
3224
  if (!this.compiledMatches(compiled, info))
3036
3225
  continue;
3037
3226
  const trigger = this.wouldTrigger(compiled.policy, info);
@@ -3042,6 +3231,140 @@ export class EventGate {
3042
3231
  const trigger = (this.config.default ?? 'always') === 'always';
3043
3232
  return { trigger, policyName: null, behavior: this.config.default ?? 'always' };
3044
3233
  }
3234
+ /**
3235
+ * The suppress rule that takes this event, or null when none does.
3236
+ * Side-effect-free, like probe: no counter moves, no batch forms, nothing
3237
+ * is traced, and a rule's script runs on a copy of its state, which is
3238
+ * then dropped. (A closure a script builds for itself is its own, and a
3239
+ * 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
3240
+ * framework asks decideSuppression instead, once per event.
3241
+ *
3242
+ * It reads the rules as evaluate does, first match deciding: a rule ahead
3243
+ * of the suppress rule that matches the event decides it instead (it isn't
3244
+ * suppressed), unless it's a passthrough rule that wouldn't fire. A rule
3245
+ * with a script (`suppress.script`) takes an event its match takes only
3246
+ * when the script says to hide it; otherwise the event goes on to the rules
3247
+ * after it, as one the rule doesn't match does. Sleep and gate.js aren't
3248
+ * consulted: they decide wakes, and suppression decides what's rendered.
3249
+ *
3250
+ * Never suppressed, whatever the rules say: an event addressed to the
3251
+ * agent (tagged `chat:addressed`: a mention, a reply, a DM) and the
3252
+ * heartbeat's (the agent's own clock). A suppress rule lets them by to the
3253
+ * rules after it, and its script isn't asked. The framework itself never
3254
+ * asks about console or API input.
3255
+ */
3256
+ suppressionFor(info) {
3257
+ return this.walkSuppression(info, false);
3258
+ }
3259
+ /**
3260
+ * suppressionFor, for an event that is arriving now: each script asked
3261
+ * keeps the state it leaves, whatever later rules do with the event, and a
3262
+ * suppressed event counts in the passthrough rules ahead of its rule. Ask
3263
+ * once per event.
3264
+ */
3265
+ decideSuppression(info) {
3266
+ return this.walkSuppression(info, true);
3267
+ }
3268
+ walkSuppression(info, commit) {
3269
+ this.reloadIfChanged();
3270
+ this.syncSuppressScripts();
3271
+ if (info.serverId === 'heartbeat')
3272
+ return null;
3273
+ const addressed = Array.isArray(info.tags) && info.tags.includes('chat:addressed');
3274
+ let event;
3275
+ const observers = [];
3276
+ for (const compiled of this.compiledPolicies) {
3277
+ if (!this.compiledMatches(compiled, info))
3278
+ continue;
3279
+ const suppress = compiled.policy.suppress;
3280
+ if (suppress) {
3281
+ if (addressed)
3282
+ continue;
3283
+ if (suppress.script !== undefined) {
3284
+ event ??= this.suppressScriptEvent(info);
3285
+ const ran = this.suppressScripts.run(compiled.policy.name, event, commit);
3286
+ // A script that failed shows the event: it goes on to the rules after.
3287
+ if (!ran || !('hide' in ran) || !ran.hide)
3288
+ continue;
3289
+ }
3290
+ // A suppressed event never reaches evaluate, so the passthrough rules
3291
+ // ahead of this one count it here, as evaluate would have: none of
3292
+ // them fires for it, and each fires on its schedule. A dry run counts
3293
+ // nothing.
3294
+ if (commit)
3295
+ for (const observer of observers)
3296
+ this.computeDecision(observer, info);
3297
+ return compiled.policy.name;
3298
+ }
3299
+ if (compiled.policy.passthrough && !this.wouldTrigger(compiled.policy, info)) {
3300
+ observers.push(compiled.policy);
3301
+ continue;
3302
+ }
3303
+ return null;
3304
+ }
3305
+ return null;
3306
+ }
3307
+ /** 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. */
3308
+ passesOverSuppress(info) {
3309
+ return info.suppressionAsked === true || info.serverId === 'heartbeat'
3310
+ || (Array.isArray(info.tags) && info.tags.includes('chat:addressed'));
3311
+ }
3312
+ /** The scripts in step with the rules in force (SuppressScripts.sync: only changed ones are compiled). */
3313
+ syncSuppressScripts() {
3314
+ this.suppressScripts.sync(this.config.policies.map((p) => ({ name: p.name, ...(p.suppress?.script !== undefined ? { script: p.suppress.script } : {}) })));
3315
+ }
3316
+ /** What a suppress script is given for an event: plain JSON, its metadata included where it can be. */
3317
+ suppressScriptEvent(info) {
3318
+ let metadata = {};
3319
+ try {
3320
+ metadata = JSON.parse(JSON.stringify(info.metadata ?? {}));
3321
+ }
3322
+ catch {
3323
+ metadata = {};
3324
+ }
3325
+ const channel = this.routeChannelFor(info) ?? (info.channelId || undefined);
3326
+ const author = this.extractAuthorId(info.metadata);
3327
+ return {
3328
+ eventType: info.eventType,
3329
+ source: info.serverId,
3330
+ ...(channel ? { channel } : {}),
3331
+ ...(author ? { author } : {}),
3332
+ tags: [...(info.tags ?? [])],
3333
+ text: info.content,
3334
+ at: this.now(),
3335
+ metadata,
3336
+ };
3337
+ }
3338
+ /**
3339
+ * Whether a suppress script compiles to a function, on the scripts' worker:
3340
+ * an error's text, or null. wake_add_rule asks before it saves a rule.
3341
+ */
3342
+ checkSuppressScript(script) {
3343
+ return this.suppressScripts.check(script);
3344
+ }
3345
+ /** A suppress rule's script state as it stands (a copy), for a host or a test. */
3346
+ suppressScriptState(name) {
3347
+ this.syncSuppressScripts();
3348
+ return this.suppressScripts.stateOf(name);
3349
+ }
3350
+ /**
3351
+ * Count a row the framework suppressed by `name` (suppressionFor's
3352
+ * answer) once it's written. The rule's stats count it as a match, so
3353
+ * gate_status shows how many events each suppress rule took since start.
3354
+ */
3355
+ noteSuppressed(name, info) {
3356
+ const policyStats = this.stats.get(name) ?? { matchCount: 0, lastMatchTimestamp: null };
3357
+ policyStats.matchCount++;
3358
+ policyStats.lastMatchTimestamp = this.now();
3359
+ this.stats.set(name, policyStats);
3360
+ this.emitTrace({
3361
+ type: 'gate:suppressed',
3362
+ policyName: name,
3363
+ eventType: info.eventType,
3364
+ ...(info.serverId ? { serverId: info.serverId } : {}),
3365
+ ...(info.channelId ? { channelId: info.channelId } : {}),
3366
+ });
3367
+ }
3045
3368
  /** The canonical synthetic events the probe table runs: chat shapes as MCPL delivers them, and a heartbeat. */
3046
3369
  probeEvents() {
3047
3370
  const chat = (label, eventType, metadata, tags) => ({
@@ -3100,7 +3423,12 @@ export class EventGate {
3100
3423
  */
3101
3424
  probeTable() {
3102
3425
  return this.probeEvents().map(({ label, info }) => {
3103
- const d = this.probe(info);
3426
+ const suppressedBy = this.suppressionFor(info);
3427
+ if (suppressedBy !== null) {
3428
+ return { probe: label, eventType: info.eventType, policy: suppressedBy, behavior: 'suppress', wouldWake: false };
3429
+ }
3430
+ // As admission asks: suppression first, so a script that says show lets the probe on.
3431
+ const d = this.probe({ ...info, suppressionAsked: true });
3104
3432
  return {
3105
3433
  probe: label,
3106
3434
  eventType: info.eventType,
@@ -3147,7 +3475,6 @@ export class EventGate {
3147
3475
  type: 'gate:shadow-warnings',
3148
3476
  reason,
3149
3477
  warnings: warnings.map(formatShadowWarning),
3150
- timestamp: this.now(),
3151
3478
  });
3152
3479
  }
3153
3480
  }
@@ -3204,6 +3531,7 @@ export class EventGate {
3204
3531
  routeChannelId: this.routeChannelFor(info),
3205
3532
  ...(typeof info.metadata?.messageId === 'string' ? { messageId: info.metadata.messageId } : {}),
3206
3533
  ...(typeof info.metadata?.eventId === 'string' ? { eventId: info.metadata.eventId } : {}),
3534
+ ...(info.recipients !== undefined ? { recipients: [...info.recipients] } : {}),
3207
3535
  };
3208
3536
  const existing = this.debounceTimers.get(policy.name);
3209
3537
  // A judge's maxWaitMs caps how far new events push the batch back: it's
@@ -3258,15 +3586,14 @@ export class EventGate {
3258
3586
  }
3259
3587
  catch (err) {
3260
3588
  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`);
3589
+ console.error(`[gate] Rule ${quotedValue(policyName)}: its pending batch of ${events.length} ${one ? 'event' : 'events'} `
3590
+ + (deliveredInPart(err) ?? `couldn't be delivered (${errorText(err)}), so the batch is dropped`));
3263
3591
  return;
3264
3592
  }
3265
3593
  this.emitTrace({
3266
3594
  type: 'gate:debounce-delivered',
3267
3595
  policyName,
3268
3596
  eventCount: events.length,
3269
- timestamp: Date.now(),
3270
3597
  });
3271
3598
  }
3272
3599
  // =========================================================================
@@ -3286,11 +3613,37 @@ export class EventGate {
3286
3613
  type: 'gate:held-dropped',
3287
3614
  dropped: batched.length - events.length,
3288
3615
  channels: [...new Set(batched.filter((e) => !events.includes(e)).map(heldAt))],
3289
- timestamp: this.now(),
3290
3616
  });
3291
3617
  }
3292
3618
  if (events.length === 0)
3293
3619
  return;
3620
+ // Who each delivery woke, so that a throw after a wake is told as a
3621
+ // delivery in part (PartialDelivery), the batch dropped only for those
3622
+ // it didn't wake.
3623
+ const woke = [];
3624
+ if (events.every((e) => e.recipients === undefined)) {
3625
+ const names = this.getAgentNamesFn();
3626
+ deliveredTracking(woke, names, () => this.deliverBatch(events, names, undefined, woke));
3627
+ return;
3628
+ }
3629
+ const woken = new Set(events.flatMap((e) => e.recipients ?? []));
3630
+ if (events.some((e) => e.recipients === undefined))
3631
+ for (const name of this.getAgentNamesFn())
3632
+ woken.add(name);
3633
+ const groups = new Map();
3634
+ for (const agentName of woken) {
3635
+ const seen = events.filter((e) => e.recipients === undefined || e.recipients.includes(agentName));
3636
+ const key = seen.map((e) => events.indexOf(e)).join(',');
3637
+ const group = groups.get(key) ?? { agents: [], events: seen };
3638
+ group.agents.push(agentName);
3639
+ groups.set(key, group);
3640
+ }
3641
+ deliveredTracking(woke, [...woken], () => {
3642
+ for (const group of groups.values())
3643
+ this.deliverBatch(group.events, group.agents, group.agents, woke);
3644
+ });
3645
+ }
3646
+ deliverBatch(events, woken, audience, woke) {
3294
3647
  const policyNames = [...new Set(events.map(e => e.policyName))];
3295
3648
  // Events whose content the framework already stored at arrival (MCPL
3296
3649
  // channel-incoming / push-event) get a compact REFERENCE — re-quoting
@@ -3346,7 +3699,12 @@ export class EventGate {
3346
3699
  const text = `[${NOTICE_TAG.gate}: ${events.length} event${events.length > 1 ? 's' : ''} matched]\n\n${lines.join('\n')}`;
3347
3700
  const only = events.length === 1 && events[0].inContext ? events[0] : undefined;
3348
3701
  let line;
3349
- if (only && this.newestEntry(only)) {
3702
+ // Where a line stored now landed, for the delivery agent's wake, as the
3703
+ // self-wake's notice places its own (readsRowFor): a live turn that
3704
+ // carried and answered the line drops the wake. Other agents' wakes stay
3705
+ // plain by readsRowFor's rule.
3706
+ const placement = {};
3707
+ if (only && this.newestEntry(only, audience)) {
3350
3708
  const shownChannel = only.routeChannelId ?? only.channelId;
3351
3709
  line = {
3352
3710
  entry: entryRef(only),
@@ -3360,7 +3718,7 @@ export class EventGate {
3360
3718
  this.addMessageFn('user', [{ type: 'text', text }], {
3361
3719
  source: 'gate:debounce',
3362
3720
  policies: policyNames,
3363
- });
3721
+ }, undefined, placement, audience);
3364
3722
  }
3365
3723
  // Provenance of the batched wake (who/where woke the agent): the newest
3366
3724
  // ADDRESSED event first, else the newest event naming a channel or an
@@ -3370,19 +3728,20 @@ export class EventGate {
3370
3728
  // back to whoever addressed the agent, not to whichever channel last saw
3371
3729
  // any traffic. Ambient-only batches set no locus. A batch whose line the
3372
3730
  // 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.
3731
+ // shared by every agent woken with these messages, so it is written once.
3374
3732
  const chosen = wakeProvenance(events);
3375
3733
  const provenance = line ? { ...chosen, line } : chosen;
3376
- for (const agentName of this.getAgentNamesFn()) {
3377
- this.requestInferenceFn(agentName, 'gate:debounce', 'gate', provenance);
3734
+ for (const agentName of woken) {
3735
+ this.requestInferenceFn(agentName, 'gate:debounce', 'gate', provenance, line ? undefined : placement);
3736
+ woke?.push(agentName);
3378
3737
  }
3379
3738
  }
3380
3739
  /** isNewestEntry for one event; a callback fault keeps the line. */
3381
- newestEntry(event) {
3740
+ newestEntry(event, recipients) {
3382
3741
  if (!this.isNewestEntryFn)
3383
3742
  return false;
3384
3743
  try {
3385
- return this.isNewestEntryFn(entryRef(event)) === true;
3744
+ return this.isNewestEntryFn(entryRef(event), recipients) === true;
3386
3745
  }
3387
3746
  catch {
3388
3747
  return false;
@@ -3496,7 +3855,8 @@ export class EventGate {
3496
3855
  * the judge to weigh what waiting costs: each agent's pending self-wake
3497
3856
  * (skip_reply's wake_in_seconds), the end of a sleep, and the pending
3498
3857
  * 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).
3858
+ * when it fires; a judge rule's goes to the judge, so it only may). A batch,
3859
+ * and a sleep with no agent named, reach the gate's recipients alone.
3500
3860
  */
3501
3861
  upcomingWakes(agentNames, exceptPolicy) {
3502
3862
  const wakes = [];
@@ -3504,20 +3864,22 @@ export class EventGate {
3504
3864
  for (const name of agentNames) {
3505
3865
  const due = this.selfWakeDue.get(name);
3506
3866
  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` });
3867
+ wakes.push({ at: due.at, certain: true, kind: 'self-wake', what: `${agentNames.length > 1 ? `${name}'s` : 'its'} own ${due.source} timer` });
3508
3868
  }
3509
3869
  }
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' });
3870
+ const recipients = this.getAgentNamesFn();
3871
+ const recipient = agentNames.some((name) => recipients.includes(name));
3872
+ if (this.sleepUntil > now && (this.sleepAgent === undefined ? recipient : agentNames.includes(this.sleepAgent))) {
3873
+ wakes.push({ at: this.sleepUntil, certain: true, kind: 'sleep-end', what: 'the end of its sleep' });
3512
3874
  }
3513
3875
  for (const [name, state] of this.debounceTimers) {
3514
- if (name === exceptPolicy || state.events.length === 0)
3876
+ if (!recipient || name === exceptPolicy || state.events.length === 0)
3515
3877
  continue;
3516
3878
  const n = state.events.length;
3517
3879
  const batch = `${n} ${n === 1 ? 'event' : 'events'}`;
3518
3880
  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}` });
3881
+ ? { at: state.dueAt, certain: false, kind: 'judge-batch', what: `rule ${quotedValue(name)}'s batch of ${batch} goes to the judge` }
3882
+ : { at: state.dueAt, certain: true, kind: 'debounce', what: `rule ${quotedValue(name)}'s debounced batch of ${batch}` });
3521
3883
  }
3522
3884
  return wakes.sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
3523
3885
  }
@@ -3616,7 +3978,6 @@ export class EventGate {
3616
3978
  ...(!failed && typeof verdict.inputTokens === 'number' ? { inputTokens: verdict.inputTokens } : {}),
3617
3979
  ...(!failed && typeof verdict.cacheReadTokens === 'number' ? { cacheReadTokens: verdict.cacheReadTokens } : {}),
3618
3980
  ...(!failed && typeof verdict.outputTokens === 'number' ? { outputTokens: verdict.outputTokens } : {}),
3619
- timestamp: Date.now(),
3620
3981
  });
3621
3982
  if (this.disposed)
3622
3983
  return;
@@ -3643,8 +4004,8 @@ export class EventGate {
3643
4004
  }
3644
4005
  catch (err) {
3645
4006
  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`);
4007
+ console.error(`[gate] a judged batch of ${events.length} ${one ? 'event' : 'events'} `
4008
+ + (deliveredInPart(err) ?? `couldn't be delivered (${errorText(err)}), so the batch is dropped`));
3648
4009
  }
3649
4010
  }
3650
4011
  changesJudgedContext(agentName) {
@@ -3713,8 +4074,8 @@ export class EventGate {
3713
4074
  * buffer through the same path a turn's end uses. Self-wake and sleep-expiry
3714
4075
  * timers deliberately do NOT route through this — their inference requests
3715
4076
  * 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.
4077
+ * and releases at resume, whereas the buffer's deliverEvents wakes every
4078
+ * agent a batch is for.
3718
4079
  */
3719
4080
  setQuiesced(quiesced) {
3720
4081
  if (this.quiesced === quiesced)
@@ -3745,8 +4106,12 @@ export class EventGate {
3745
4106
  }
3746
4107
  catch (err) {
3747
4108
  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`);
4109
+ const held = `${events.length} ${one ? 'event' : 'events'}`;
4110
+ const partly = deliveredInPart(err);
4111
+ console.error(partly
4112
+ ? `[gate] a batch of ${held}, held for an agent's turn or the host's quiesce, ${partly}`
4113
+ : `[gate] ${held} held for an agent's turn or the host's quiesce couldn't be delivered (${errorText(err)}), `
4114
+ + `so ${one ? "it's" : "they're"} dropped`);
3750
4115
  }
3751
4116
  });
3752
4117
  }
@@ -3815,7 +4180,6 @@ export class EventGate {
3815
4180
  matchedPolicy: 'asleep',
3816
4181
  trigger: false,
3817
4182
  behavior: 'skip',
3818
- timestamp: this.now(),
3819
4183
  });
3820
4184
  return { until: this.sleepUntil };
3821
4185
  }
@@ -3876,13 +4240,13 @@ export class EventGate {
3876
4240
  matchedPolicy: 'self-wake',
3877
4241
  trigger: true,
3878
4242
  behavior: 'always',
3879
- timestamp: this.now(),
3880
4243
  });
3881
4244
  // Compact in-window notice: without it the wake turn contains nothing
3882
4245
  // new, so the agent can't tell WHAT woke it (its own timer vs. a
3883
4246
  // 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).
4247
+ // before the inference request so it rides the wake turn: it is stored
4248
+ // at once, and while a turn of the agent's is alive it is kept out of
4249
+ // the window until the agent's next presentation point.
3886
4250
  const nowIso = new Date(this.now()).toISOString().slice(0, 19) + 'Z';
3887
4251
  // Deliver the notice into the WAKING agent's window — a self-wake
3888
4252
  // armed by a non-primary agent (e.g. a subconscious cadence) must not
@@ -3905,7 +4269,6 @@ export class EventGate {
3905
4269
  matchedPolicy: 'self-wake',
3906
4270
  trigger: false,
3907
4271
  behavior: 'skip',
3908
- timestamp: this.now(),
3909
4272
  });
3910
4273
  return { inMs: ms, armed: true };
3911
4274
  }
@@ -3953,7 +4316,6 @@ export class EventGate {
3953
4316
  matchedPolicy: 'asleep',
3954
4317
  trigger: true,
3955
4318
  behavior: 'always',
3956
- timestamp: this.now(),
3957
4319
  });
3958
4320
  const detail = note ? `${reason}: ${note}` : reason;
3959
4321
  const targets = agent ? [agent] : this.getAgentNamesFn();
@@ -4088,6 +4450,7 @@ export class EventGate {
4088
4450
  return { success: true, data: status };
4089
4451
  }
4090
4452
  getStatus() {
4453
+ this.syncSuppressScripts();
4091
4454
  const policies = this.config.policies.map(p => {
4092
4455
  const stats = this.stats.get(p.name);
4093
4456
  const debounce = this.debounceTimers.get(p.name);
@@ -4096,6 +4459,9 @@ export class EventGate {
4096
4459
  behavior: p.behavior,
4097
4460
  matchCount: stats?.matchCount ?? 0,
4098
4461
  lastMatchTimestamp: stats?.lastMatchTimestamp ?? null,
4462
+ ...(p.suppress
4463
+ ? { suppress: { ...p.suppress, ...(p.suppress.script !== undefined && this.suppressScripts.status(p.name) ? { scriptStatus: this.suppressScripts.status(p.name) } : {}) } }
4464
+ : {}),
4099
4465
  };
4100
4466
  if (typeof p.behavior === 'object' && 'debounce' in p.behavior) {
4101
4467
  result.debounceState = {
@@ -4157,6 +4523,13 @@ export class EventGate {
4157
4523
  // Fresh, not the cache: the sources the host knows (eventTypesFrom)
4158
4524
  // change as servers connect, with no change to the rules.
4159
4525
  shadowWarnings: this.lintShadows().map(formatShadowWarning),
4526
+ ...(() => {
4527
+ const rule = firstCatchAll(this.config.policies, { lettingBy: true });
4528
+ return rule ? { catchAll: {
4529
+ rule,
4530
+ 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.`,
4531
+ } } : {};
4532
+ })(),
4160
4533
  totalEvaluations: this.totalEvaluations,
4161
4534
  defaultDecisions: {
4162
4535
  triggered: this.defaultTriggered,
@@ -4192,6 +4565,7 @@ export class EventGate {
4192
4565
  // =========================================================================
4193
4566
  dispose() {
4194
4567
  this.gateScript.dispose();
4568
+ this.suppressScripts.dispose();
4195
4569
  for (const state of this.debounceTimers.values()) {
4196
4570
  clearTimeout(state.timer);
4197
4571
  }