@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
@@ -17,6 +17,7 @@ import { ensureStateRegistered } from '../module-registry.js';
17
17
  import { boundedErrorMessage as thrownText } from '../error-message.js';
18
18
 
19
19
  import type { StateCheckpoint, JsonPatchOperation } from './types.js';
20
+ import type { TraceEventInput } from '../types/trace.js';
20
21
 
21
22
  // ============================================================================
22
23
  // Constants
@@ -198,12 +199,12 @@ export function applyJsonPatch(doc: unknown, ops: JsonPatchOperation[]): unknown
198
199
 
199
200
  export class CheckpointManager {
200
201
  private store: JsStore;
201
- private emitTraceFn: (event: { type: string; [key: string]: unknown }) => void;
202
+ private emitTraceFn: (event: TraceEventInput) => void;
202
203
  private trees = new Map<string, FeatureSetState>();
203
204
 
204
205
  constructor(
205
206
  store: JsStore,
206
- emitTraceFn: (event: { type: string; [key: string]: unknown }) => void,
207
+ emitTraceFn: (event: TraceEventInput) => void,
207
208
  ) {
208
209
  this.store = store;
209
210
  this.emitTraceFn = emitTraceFn;
@@ -19,6 +19,7 @@ import {
19
19
  UNKNOWN_FEATURE_SET,
20
20
  } from './errors.js';
21
21
  import { isKnownCapabilityPath } from './capability-grant.js';
22
+ import type { TraceEventInput } from '../types/trace.js';
22
23
  import { logValue } from './log-value.js';
23
24
 
24
25
  // ============================================================================
@@ -136,7 +137,7 @@ export class FeatureSetManager {
136
137
  * turned off. Omit for scratch derivations (nothing is installed).
137
138
  */
138
139
  constructor(
139
- private readonly emitTrace?: (event: { type: string; [key: string]: unknown }) => void,
140
+ private readonly emitTrace?: (event: TraceEventInput) => void,
140
141
  ) {}
141
142
 
142
143
  /**
@@ -175,7 +175,8 @@ export function conversationKey(source: InboundSource): string | undefined {
175
175
  * It expects JSON-shaped values, as the MCPL lanes deliver them (parsed from
176
176
  * JSON). Any other object is written by its own enumerable entries, unlike
177
177
  * JSON.stringify: a Date becomes `{}`, as the store's own write keeps it, but
178
- * a deferred write recovered from its JSON file brings it back as a string.
178
+ * a write an earlier release's deferral queue left in its JSON file, which
179
+ * the importer stores at boot, brings it back as a string.
179
180
  *
180
181
  * sourceBodyDigest's serializer, kept out of the package API: serialization
181
182
  * and framing both stay inside sourceBodyDigest, so consumers don't
@@ -29,6 +29,7 @@ import { hostStoppedWhileHandling } from './errors.js';
29
29
  import type { HookOrchestrator } from './hook-orchestrator.js';
30
30
  import { logValue } from './log-value.js';
31
31
  import { streamBounds, wholeCallMs, withHostSetting, type InferenceBounds } from '../inference-bounds.js';
32
+ import type { TraceEventInput } from '../types/trace.js';
32
33
 
33
34
  // ============================================================================
34
35
  // Responder interface
@@ -38,8 +39,10 @@ import { streamBounds, wholeCallMs, withHostSetting, type InferenceBounds } from
38
39
  interface Responder {
39
40
  respond(result: McplInferenceRequestResult): void;
40
41
  respondError(code: number, message: string, data?: unknown): void;
41
- /** The JSON-RPC request ID, used for streaming chunk correlation. */
42
- requestId?: string | number;
42
+ /** The JSON-RPC request ID, used for streaming chunk correlation: a
43
+ * request sent with id null is answered whole, since its chunks couldn't
44
+ * name it. */
45
+ requestId?: string | number | null;
43
46
  /** Sends a streamed chunk on the transport the request came on, as its
44
47
  * answer is sent. */
45
48
  sendChunk?(params: InferenceChunkParams): void;
@@ -184,7 +187,7 @@ export class InferenceRouter {
184
187
  private hookOrchestrator: HookOrchestrator;
185
188
  private featureSetManager: FeatureSetManager;
186
189
  private routingPolicy: InferenceRoutingPolicy | null;
187
- private emitTraceFn: (event: { type: string; [key: string]: unknown }) => void;
190
+ private emitTraceFn: (event: TraceEventInput) => void;
188
191
  /** The refusals for want of a model that have been logged: one line per
189
192
  * server with no policy, and per server and feature set with one, so a
190
193
  * server that keeps asking doesn't flood the operator's log. */
@@ -196,7 +199,7 @@ export class InferenceRouter {
196
199
  hookOrchestrator: HookOrchestrator,
197
200
  featureSetManager: FeatureSetManager,
198
201
  routingPolicy: InferenceRoutingPolicy | null,
199
- emitTraceFn: (event: { type: string; [key: string]: unknown }) => void,
202
+ emitTraceFn: (event: TraceEventInput) => void,
200
203
  private readonly inferenceBounds: () => InferenceBounds | null = () => null,
201
204
  ) {
202
205
  this.membrane = membrane;
@@ -6,7 +6,9 @@
6
6
  * the ordinary channel/push path the moment it is admitted, exactly as today;
7
7
  * the coalescer only remembers WHERE it landed. A later occurrence of the same
8
8
  * subject edits or removes that message in place while it is still unread
9
- * (above the agent's consumed watermark, not folded by compression), and
9
+ * (above the agent's consumed watermark, a position in the agent's window,
10
+ * #291: a row not in the window yet is unread wherever it is in the store;
11
+ * not folded by compression), and
10
12
  * appends once it has been read. Nothing is staged, journaled or capped: on
11
13
  * any uncertainty (restart, branch switch, eviction) the host behaves as an
12
14
  * append-only host would, which is what the RFC's principle 4 asks for.
@@ -67,10 +69,16 @@ export class EmptyContentError extends CoalesceError {
67
69
  /** Where an unread occurrence currently lives. */
68
70
  export interface CoalescingPlacement {
69
71
  agent: string;
70
- /** Stored message id in that agent's context manager. */
72
+ /** Stored message id in that agent's context manager. Absent for a write
73
+ * kept in memory (a surgery's hold, or the wait for its agent's
74
+ * recovery), which counts as read and can't be removed. */
71
75
  messageId?: string;
72
- /** Entry id in the framework's deferred-write queue (turn-alive deferral). */
73
- deferredId?: string;
76
+ /** Set on a placement that wasn't presented to `agent` at its write: a
77
+ * write kept in memory (no `messageId` yet), or one stored while a turn
78
+ * of the agent's was alive, or with a presentation at rest that failed
79
+ * (#291's presentation index). No request of the agent's reads it until
80
+ * it is presented. */
81
+ presented?: false;
74
82
  }
75
83
 
76
84
  export interface CoalescingScope { kind: 'featureSet' | 'channel'; id: string }
@@ -98,6 +106,20 @@ export interface CoalescedOccurrence<E = unknown> {
98
106
  identity?: { messageId?: string; author?: { id: string; name: string }; threadId?: string };
99
107
  /** The event the ordinary path would have queued — the host delivers it. */
100
108
  event: E;
109
+ /**
110
+ * The suppress rule that took this occurrence, or null when none did, once
111
+ * the host has decided (its wake, or its row, whichever came first). Decided
112
+ * once for the occurrence, since a rule's script answers once per event:
113
+ * its row and any later wake read this. Snapshots carry it.
114
+ */
115
+ suppressionDecision?: string | null;
116
+ /**
117
+ * Set on a batch's latest occurrence when the batch holds an occurrence
118
+ * addressed to the agent (`chat:addressed`), this one or an earlier one. An
119
+ * addressed event is never suppressed, so such a batch is shown, whatever its
120
+ * latest occurrence would be decided. Snapshots carry it.
121
+ */
122
+ batchAddressed?: true;
101
123
  /** Set by the coalescer: deliver into this agent only (§3.2 audience may
102
124
  * narrow, never widen — a replacement or notice follows the prior delivery). */
103
125
  deliverTo?: string;
@@ -135,8 +157,19 @@ interface DeferredBatch<E> {
135
157
  * acceptance receipt, which doesn't carry it, reads as not all ambient:
136
158
  * the side that keeps a wake. */
137
159
  allAmbient?: true;
138
- /** Set on a batch restored from a snapshot whose render may have started. */
160
+ /** Set on a batch restored from a snapshot whose render may have started,
161
+ * and on one whose render's delivery threw: its recovery delivers the
162
+ * fallback, without asking the server again. */
139
163
  noRender?: boolean;
164
+ /** Set, to the event id the delivery carried, on a batch whose render's
165
+ * delivery threw where the host can't tell whether it stored the
166
+ * occurrence (CoalescerHost.mayHaveStored). Its recovery's delivery of
167
+ * that event says so (DeliveryOptions.mayBeStored), so the host stores the
168
+ * fallback only where it doesn't find the occurrence stored. A later
169
+ * notice that joins the batch moves its latest event on, and the recovery
170
+ * then delivers that one as any fallback. In memory only: a restored
171
+ * batch that may have rendered is settled by wasPublished. */
172
+ mayBeStored?: string;
140
173
  /** The batch opened on the agent's first view of its subject: a fresh
141
174
  * `initial` birth, or a first-view occupant it displaced that nothing had
142
175
  * shown. Its first delivery is a first view too, while nothing of the
@@ -166,8 +199,8 @@ export interface AssemblyResult {
166
199
  * read: empty or blank render (§5.2), cancelled, revoked before the render
167
200
  * or at its response, a render-start that could not be made durable (the
168
201
  * batch then waits for its recovery wake), a batch whose audience doesn't
169
- * include the agent, a render placed in a deferred queue (another
170
- * agent's, or this agent's own behind a write held there), a render
202
+ * include the agent, a render placed where the turn doesn't read it
203
+ * (stored for another agent whose turn is alive, not presented to it), a render
171
204
  * another assembly froze (its compile may show it, as that request's), or
172
205
  * a cause whose batch was withdrawn, displaced or consumed by another
173
206
  * request. */
@@ -293,12 +326,31 @@ export interface CoalescingReceiptRecord {
293
326
  batch: { latest: CoalescedOccurrence<unknown>; notices: DeferredNotice[]; dropped: number } | null;
294
327
  }
295
328
 
329
+ export type CoalescingAudit = { subject: string; eventId: string } & (
330
+ | { kind: 'received'; retract: boolean; deferred: boolean }
331
+ | { kind: 'removed' }
332
+ | { kind: 'remove-failed' }
333
+ | { kind: 'revoked' }
334
+ | { kind: 'displaced'; batch?: true }
335
+ | { kind: 'render-cancelled'; reason?: 'suspend' }
336
+ | { kind: 'recovery-wake'; attempt: number; delayMs: number }
337
+ | { kind: 'rendered'; source: 'render' | 'fallback'; empty: boolean }
338
+ | { kind: 'render-failed'; serverId: string; field?: string; error: string }
339
+ /** The render's delivery threw, and its batch went to the recovery wake.
340
+ * `mayBeStored`: the host couldn't tell whether a row landed
341
+ * (CoalescerHost.mayHaveStored), so the recovery's delivery reads the
342
+ * store before it stores the fallback. */
343
+ | { kind: 'deliver-failed'; error: string; mayBeStored?: true }
344
+ | { kind: 'late-render'; discarded: true }
345
+ | { kind: 'persist-failed'; error: string }
346
+ | { kind: 'accepted-observer-failed'; error: string }
347
+ );
348
+
296
349
  export interface CoalescerHost<E> {
297
350
  /** True iff the placement still exists and, as far as the host can tell,
298
351
  * no model request has included it. */
299
352
  isUnread(placement: CoalescingPlacement): boolean;
300
- /** True iff nothing may have shown the placement to the agent: it is still
301
- * in the deferred queue and known not to be stored already, or it is
353
+ /** True iff nothing may have shown the placement to the agent: it is
302
354
  * stored where no committing compile has begun to assemble it and no tool
303
355
  * boundary has injected past it. Unlike
304
356
  * isUnread, a compile counts from its start: a doubt counts as shown. */
@@ -307,18 +359,36 @@ export interface CoalescerHost<E> {
307
359
  remove(placement: CoalescingPlacement): boolean;
308
360
  /** Ordinary delivery of the event; undefined when routing delivered nowhere.
309
361
  * A replacement is delivered as a fresh message after the unread prior was
310
- * removed: it lands where a fresh event lands (§4.1 allows either place). */
311
- deliver(occurrence: CoalescedOccurrence<E>, materialized?: McplContentBlock[], assemblingFor?: string): Promise<CoalescingPlacement | undefined>;
362
+ * removed: it lands where a fresh event lands (§4.1 allows either place).
363
+ * A render's delivery that throws keeps its batch for its recovery wake,
364
+ * which delivers the fallback. A throw is taken as a delivery that stored
365
+ * none of the occurrence, unless mayHaveStored says the host can't tell:
366
+ * so a delivery whose store threw after rows of the occurrence landed
367
+ * answers as stored by them, as the framework's does (context-manager's
368
+ * WriteLandedError). Where the host can't tell (the framework: context-
369
+ * manager's WriteOutcomeUnknownError), the recovery's delivery carries
370
+ * `options.mayBeStored`: the host looks for the occurrence's rows first,
371
+ * by the delivery identity they carry (subject and event id, as
372
+ * wasPublished), and answers the placement of those it finds, storing
373
+ * nothing. It stores the fallback where it finds none, and also where
374
+ * its store can't be read to tell, saying it may then be stored twice. */
375
+ deliver(occurrence: CoalescedOccurrence<E>, materialized?: McplContentBlock[], assemblingFor?: string, options?: DeliveryOptions): Promise<CoalescingPlacement | undefined>;
376
+ /** Whether a delivery's throw leaves the host unable to tell whether it
377
+ * stored the occurrence (the framework's: context-manager's
378
+ * WriteOutcomeUnknownError). Absent, or false, a throw stored none of it. */
379
+ mayHaveStored?(error: unknown): boolean;
312
380
  /** Queue a wake for a batch that has no model-visible content yet. */
313
381
  wakeForBatch(occurrence: CoalescedOccurrence<E>): Promise<void>;
314
- /** Queue an ordinary push wake for `agent`, under the batch's own wake
315
- * policy, to read a render of the batch placed in its deferred queue that
316
- * no request consumed, at `deferredId`. It carries no subject, so the
317
- * subject's other wakes, and later withdrawals of them, leave it.
318
- * `allAmbient`: every notice the render stands for was tagged
319
- * `chat:ambient` (DeferredBatch.allAmbient), so the wake is marked
382
+ /** Queue an ordinary push wake for the placement's agent, under the
383
+ * batch's own wake policy, to read a render of the batch that no request
384
+ * consumed, placed where the turn being assembled doesn't read it: stored
385
+ * and not presented to the agent (`placement`, which the host reads the
386
+ * wake's mark from). It carries
387
+ * no subject, so the subject's other wakes, and later withdrawals of
388
+ * them, leave it. `allAmbient`: every notice the render stands for was
389
+ * tagged `chat:ambient` (DeferredBatch.allAmbient), so the wake is marked
320
390
  * ambient. */
321
- wakeToRead(occurrence: CoalescedOccurrence<E>, agent: string, deferredId?: string, allAmbient?: boolean): Promise<void>;
391
+ wakeToRead(occurrence: CoalescedOccurrence<E>, placement: CoalescingPlacement, allAmbient?: boolean): Promise<void>;
322
392
  /** Withdraw unstarted wakes whose sole cause is this subject. */
323
393
  cancelWake(subject: string): void;
324
394
  /** The batch's audience still holds the authority it was admitted under. */
@@ -326,7 +396,7 @@ export interface CoalescerHost<E> {
326
396
  /** Agents that read this occurrence's delivery target (may spawn a fork). */
327
397
  audience(occurrence: CoalescedOccurrence<E>): Promise<string[]>;
328
398
  render(occurrence: CoalescedOccurrence<E>, params: PushRenderParams): Promise<PushRenderResult>;
329
- audit(record: Record<string, unknown>): void;
399
+ audit(record: CoalescingAudit): void;
330
400
  /** Persist (throttled by the host); the thunk builds the snapshot lazily. */
331
401
  save(snapshot: () => CoalescingSnapshot): void;
332
402
  /** Persist the snapshot NOW (a render is about to start: that boundary must
@@ -355,6 +425,15 @@ export interface CoalescerHost<E> {
355
425
  now?(): number;
356
426
  }
357
427
 
428
+ /** What the coalescer tells the host of a delivery (CoalescerHost.deliver). */
429
+ export interface DeliveryOptions {
430
+ /** A render's recovery, after a delivery of this event whose throw left the
431
+ * host unable to tell whether it stored the occurrence (mayHaveStored):
432
+ * the host looks for its rows first, and stores it where it finds none,
433
+ * or can't read its store to tell. */
434
+ mayBeStored?: true;
435
+ }
436
+
358
437
  export interface CoalescerOptions {
359
438
  retryWindowMs?: number;
360
439
  /** Base delay before re-waking a batch whose render-start commit failed
@@ -738,8 +817,9 @@ export class PushCoalescer<E = unknown> {
738
817
  /**
739
818
  * Remove the occupant and the run it builds on, newest first, auditing each
740
819
  * as `kind`. Every member is unread: refreshOccupant has just run, under the
741
- * lock. The host's removal may still refuse one, such as a held write the
742
- * host can't tell isn't already stored. A member the host
820
+ * lock. The host's removal may still refuse one, such as a row a reader
821
+ * fork was handed as framing, or any while a surgery holds the store. A
822
+ * member the host
743
823
  * can't remove may be read, so the walk stops there, leaving it and the
744
824
  * renders it builds on, and the history becomes `unknown`.
745
825
  * Returns false then. It doesn't touch `state.occupant`, which still names
@@ -937,6 +1017,7 @@ export class PushCoalescer<E = unknown> {
937
1017
  // Later notices stay in the batch's audience.
938
1018
  occurrence = { ...occurrence, deliverTo: state.batch.latest.deliverTo };
939
1019
  }
1020
+ if (occurrence.tags?.includes('chat:addressed') || state.batch?.latest.batchAddressed) occurrence = { ...occurrence, batchAddressed: true };
940
1021
  const notice: DeferredNotice = { eventId: occurrence.eventId, timestamp: occurrence.timestamp, ...(occurrence.data !== undefined ? { data: structuredClone(occurrence.data) } : {}) };
941
1022
  if (state.batch) {
942
1023
  state.batch.notices.push(notice);
@@ -1003,10 +1084,15 @@ export class PushCoalescer<E = unknown> {
1003
1084
  * A batch that opened while another render ran is rendered once that
1004
1085
  * render settles. Resolves whether it materialized.
1005
1086
  */
1006
- async materializeNow(agentName: string, subject: string): Promise<boolean> {
1087
+ async materializeNow(agentName: string, subject: string, onlyIfLatest?: string): Promise<boolean> {
1007
1088
  for (;;) {
1008
1089
  if (this.suspended) return false;
1009
- const started = await this.locked(() => this.freeze(subject, agentName));
1090
+ // `onlyIfLatest`: a newer occurrence took the batch over while this
1091
+ // waited for the lock, and its own decision and wake are the batch's.
1092
+ const started = await this.locked(() => {
1093
+ if (onlyIfLatest !== undefined && this.subjects.get(subject)?.batch?.latest.eventId !== onlyIfLatest) return Promise.resolve(undefined);
1094
+ return this.freeze(subject, agentName);
1095
+ });
1010
1096
  if (!started || started === 'declined') return false;
1011
1097
  await started.done;
1012
1098
  this.persist();
@@ -1086,7 +1172,7 @@ export class PushCoalescer<E = unknown> {
1086
1172
  this.scheduleRecovery(subject, state);
1087
1173
  return 'declined';
1088
1174
  }
1089
- this.clearRecovery(subject);
1175
+ this.disarmRecovery(subject);
1090
1176
  if (this.suspended) { state.rendering = undefined; state.batch = batch; return undefined; }
1091
1177
  // Authority (grant, registration, binding) is re-checked immediately
1092
1178
  // before dispatch: a server id reassigned to another endpoint during the
@@ -1142,9 +1228,13 @@ export class PushCoalescer<E = unknown> {
1142
1228
  }
1143
1229
  // Settlement runs under the lock: between the RPC's return and the state
1144
1230
  // update no admission can withdraw or replace what is being published.
1231
+ let failed = false;
1232
+ let current = false;
1233
+ let recover = false;
1145
1234
  await this.locked(async () => {
1146
1235
  try {
1147
1236
  if (rendering.cancelled || state.rendering !== rendering || this.suspended) return;
1237
+ current = true;
1148
1238
  // Rule 4: authority is re-checked at response.
1149
1239
  if (!this.host.authorized(occurrence)) { this.host.audit({ kind: 'revoked', subject, eventId: occurrence.eventId }); return; }
1150
1240
  const empty = isVisiblyEmptyContent(content);
@@ -1159,15 +1249,82 @@ export class PushCoalescer<E = unknown> {
1159
1249
  // a run's oldest render can be one.
1160
1250
  const run = state.occupant?.rendered ? state.occupant : undefined;
1161
1251
  const firstView = batch.firstView === true && state.history === 'none' && !state.occupant;
1162
- const placement = await this.deliver({ ...occurrence, timestamp, content, ...(firstView ? { firstView: true } : {}) }, content, assemblingFor);
1163
- if (placement?.deferredId) {
1164
- // Landed in a deferred queue: another agent's, whose turn is alive,
1165
- // or the assembling agent's own, behind a write held there (the
1166
- // host's bypassDeferralFor), or after its assembly ended, as any
1167
- // write during its turn. Still unread
1168
- // there, so it stays replaceable and withdrawable. The turn being
1169
- // assembled doesn't read it, so it isn't materialized for that turn,
1170
- // and no request has consumed it. The freeze withdrew the queue's
1252
+ let placement: CoalescingPlacement | undefined;
1253
+ try {
1254
+ placement = await this.deliver(
1255
+ { ...occurrence, timestamp, content, ...(firstView ? { firstView: true } : {}) }, content, assemblingFor,
1256
+ batch.mayBeStored === occurrence.eventId ? { mayBeStored: true } : undefined,
1257
+ );
1258
+ } catch (error) {
1259
+ // A delivery that throws goes to its batch's recovery, which
1260
+ // delivers the fallback (the host's deliver). A store that threw
1261
+ // after the occurrence's rows landed (context-manager's own
1262
+ // listener failing after its append, or a sharded append whose
1263
+ // clean-up failed too) doesn't come here: context-manager names
1264
+ // the rows (WriteLandedError), and the framework's delivery takes
1265
+ // the occurrence as stored by them, placed, presented and woken
1266
+ // for, so no fallback is stored beside them. Any other throw stored
1267
+ // nothing, unless the host can't tell (mayHaveStored: the store
1268
+ // couldn't be read to tell whether a row landed). Then nothing is
1269
+ // stored at once: the recovery's delivery says the occurrence may
1270
+ // be stored (mayBeStored), and the host, its store read again,
1271
+ // places the rows it finds and stores no fallback, stores the
1272
+ // fallback where it finds none, and where its store still can't
1273
+ // be read, stores the fallback and logs that the agent may see the
1274
+ // occurrence twice. A batch opened while this render ran (§5.4
1275
+ // rule 1) has its own fallback delivered at the recovery, which
1276
+ // isn't this occurrence and isn't looked for: this render, if it
1277
+ // landed, stays where it is, unplaced. The mark stays on a batch
1278
+ // whose recovery's delivery throws again, so the next one looks
1279
+ // again.
1280
+ failed = true;
1281
+ const mayHaveStored = this.host.mayHaveStored?.(error) === true;
1282
+ this.host.audit({ kind: 'deliver-failed', subject, eventId: occurrence.eventId, error: String(error), ...(mayHaveStored ? { mayBeStored: true } : {}) });
1283
+ if (state.batch) state.batch.noRender = true;
1284
+ else {
1285
+ batch.noRender = true;
1286
+ if (mayHaveStored) batch.mayBeStored = occurrence.eventId;
1287
+ state.batch = batch;
1288
+ }
1289
+ // Saved now, not only once the assembly's other renders settle: a
1290
+ // newer batch's last snapshot, its acceptance's, has it rendering
1291
+ // as usual, and a restart before the next save would render it on
1292
+ // top of the render the agent never got. Written at once and made
1293
+ // durable, as the render-start boundary is (saveNow, then commit):
1294
+ // the host's throttled save would leave that window open for its
1295
+ // delay. Where either throws, the throttled save is asked for.
1296
+ let written = false;
1297
+ try {
1298
+ if (this.host.saveNow) { this.host.saveNow(this.snapshot()); written = true; }
1299
+ } catch (saveError) {
1300
+ this.host.audit({ kind: 'persist-failed', subject, eventId: occurrence.eventId, error: String(saveError) });
1301
+ }
1302
+ if (!written) this.persist();
1303
+ // Its recovery is armed once the render has settled (the finally
1304
+ // below): a recovery that fires while a render is under way does
1305
+ // nothing and isn't armed again, and the commit may outlast the
1306
+ // backoff.
1307
+ recover = true;
1308
+ if (written) {
1309
+ try {
1310
+ await this.host.commit?.();
1311
+ } catch (commitError) {
1312
+ this.host.audit({ kind: 'persist-failed', subject, eventId: occurrence.eventId, error: String(commitError) });
1313
+ this.persist();
1314
+ }
1315
+ }
1316
+ return;
1317
+ }
1318
+ if (placement?.presented === false) {
1319
+ // Not presented to its agent: stored for another agent, whose
1320
+ // turn is alive, or for the assembling agent after its assembly
1321
+ // ended, as any write during its turn; or kept in memory (no id)
1322
+ // until its hold's release stores it, which the host counts as
1323
+ // read and can't remove, so of what follows only the wake applies
1324
+ // to it. A stored one is still unread there, so it stays
1325
+ // replaceable and withdrawable. The turn being assembled
1326
+ // doesn't read it, so it isn't materialized for that turn, and no
1327
+ // request has consumed it. The freeze withdrew the placement's
1171
1328
  // agent's wake for this batch; give it an ordinary wake, as any
1172
1329
  // delivery during its turn has, beside any wakes a newer batch has,
1173
1330
  // so that its next turn reads the content (the wake starts no turn
@@ -1187,7 +1344,7 @@ export class PushCoalescer<E = unknown> {
1187
1344
  await this.host.wakeForBatch(state.batch.latest);
1188
1345
  }
1189
1346
  state.audienceAgent = placement.agent;
1190
- await this.host.wakeToRead(occurrence, placement.agent, placement.deferredId, batch.allAmbient === true);
1347
+ await this.host.wakeToRead(occurrence, placement, batch.allAmbient === true);
1191
1348
  return;
1192
1349
  }
1193
1350
  // The materialized occurrence is consumed by the request being
@@ -1199,6 +1356,8 @@ export class PushCoalescer<E = unknown> {
1199
1356
  if (placement) state.audienceAgent = placement.agent;
1200
1357
  } finally {
1201
1358
  if (state.rendering === rendering) state.rendering = undefined;
1359
+ if (current && !failed) this.clearRecovery(subject);
1360
+ if (recover) this.scheduleRecovery(subject, state);
1202
1361
  }
1203
1362
  });
1204
1363
  }
@@ -22,6 +22,7 @@ import type { InboundSource } from './inbound-source.js';
22
22
  import { isSilentHeartbeatMarker, isVisiblyEmptyConversion } from './visible-content.js';
23
23
  import { logDetail, logValue } from './log-value.js';
24
24
  import { RefusalLines } from './refusal-lines.js';
25
+ import type { TraceEventInput } from '../types/trace.js';
25
26
 
26
27
  // ============================================================================
27
28
  // McplPushEvent (the ProcessEvent shape pushed to the queue)
@@ -35,6 +36,8 @@ import { RefusalLines } from './refusal-lines.js';
35
36
  * events.ts modification is deferred.
36
37
  */
37
38
  export interface McplPushEvent {
39
+ /** The occurrence's suppression, when a coalesced push's wake decided it (CoalescedOccurrence.suppressionDecision). */
40
+ suppressionDecision?: string | null;
38
41
  type: 'mcpl:push-event';
39
42
  serverId: string;
40
43
  featureSet: string;
@@ -48,13 +51,15 @@ export interface McplPushEvent {
48
51
  targetAgents?: string[];
49
52
  /** RFC-006: set when the coalescer delivers; rides the wake it queues. */
50
53
  coalescingSubject?: string;
51
- /** RFC-006 assembly: materialized for this agent's turn, stored directly
52
- * while its assembly runs, after what is queued for it (the framework's
53
- * addMessage, bypassDeferralFor). */
54
+ /** RFC-006 assembly: materialized for this agent's turn while its
55
+ * assembly runs, which presents it before the turn's compile. */
54
56
  assemblingFor?: string;
55
57
  /** The coalescer's mark: the agent's first view of the subject (see
56
58
  * CoalescedOccurrence.firstView). */
57
59
  firstView?: boolean;
60
+ /** A batch's recovery after a delivery that may have stored the push (the
61
+ * framework's FrameworkWriteOptions.mayBeStored). */
62
+ mayBeStored?: true;
58
63
  /** Host acceptance time (epoch ms), stamped where the push is admitted. */
59
64
  acceptedAt?: number;
60
65
  /** Framework-owned source envelope, frozen at a coalesced occurrence's
@@ -111,7 +116,7 @@ interface Responder {
111
116
  export class PushHandler {
112
117
  private featureSetManager: FeatureSetManager;
113
118
  private pushEventFn: (event: McplPushEvent) => void;
114
- private emitTraceFn: (event: { type: string; [key: string]: unknown }) => void;
119
+ private emitTraceFn: (event: TraceEventInput) => void;
115
120
  private shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean;
116
121
  private dedup = new DedupSet(1000);
117
122
  /** Holds back repeats of the line for a channel-scoped push refused because
@@ -121,7 +126,7 @@ export class PushHandler {
121
126
  constructor(
122
127
  featureSetManager: FeatureSetManager,
123
128
  pushEventFn: (event: McplPushEvent) => void,
124
- emitTraceFn: (event: { type: string; [key: string]: unknown }) => void,
129
+ emitTraceFn: (event: TraceEventInput) => void,
125
130
  shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean,
126
131
  private readonly handleCoalesced?: (serverId: string, params: PushEventParams, event: McplPushEvent) => Promise<PushEventResult>,
127
132
  /** An ordinary (uncoalesced) push was just admitted, before it is queued