@animalabs/agent-framework 0.26.1 → 0.27.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 (276) hide show
  1. package/README.md +57 -7
  2. package/dist/src/agent.d.ts +263 -22
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +531 -54
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/server.d.ts.map +1 -1
  7. package/dist/src/api/server.js +1 -2
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/auto-continue.d.ts +33 -0
  10. package/dist/src/auto-continue.d.ts.map +1 -0
  11. package/dist/src/auto-continue.js +35 -0
  12. package/dist/src/auto-continue.js.map +1 -0
  13. package/dist/src/code-execution/isolate/scripts.d.ts +11 -0
  14. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  15. package/dist/src/code-execution/isolate/scripts.js +5 -1
  16. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  17. package/dist/src/framework.d.ts +829 -156
  18. package/dist/src/framework.d.ts.map +1 -1
  19. package/dist/src/framework.js +3906 -1008
  20. package/dist/src/framework.js.map +1 -1
  21. package/dist/src/gate/event-gate.d.ts +309 -46
  22. package/dist/src/gate/event-gate.d.ts.map +1 -1
  23. package/dist/src/gate/event-gate.js +2117 -395
  24. package/dist/src/gate/event-gate.js.map +1 -1
  25. package/dist/src/gate/index.d.ts +2 -2
  26. package/dist/src/gate/index.d.ts.map +1 -1
  27. package/dist/src/gate/index.js +1 -1
  28. package/dist/src/gate/index.js.map +1 -1
  29. package/dist/src/gate/recipe-reconcile.d.ts +103 -0
  30. package/dist/src/gate/recipe-reconcile.d.ts.map +1 -0
  31. package/dist/src/gate/recipe-reconcile.js +325 -0
  32. package/dist/src/gate/recipe-reconcile.js.map +1 -0
  33. package/dist/src/gate/types.d.ts +68 -8
  34. package/dist/src/gate/types.d.ts.map +1 -1
  35. package/dist/src/index.d.ts +6 -4
  36. package/dist/src/index.d.ts.map +1 -1
  37. package/dist/src/index.js +5 -2
  38. package/dist/src/index.js.map +1 -1
  39. package/dist/src/mcpl/channel-registry.d.ts +433 -113
  40. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  41. package/dist/src/mcpl/channel-registry.js +1152 -434
  42. package/dist/src/mcpl/channel-registry.js.map +1 -1
  43. package/dist/src/mcpl/conversation-router.d.ts +16 -8
  44. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  45. package/dist/src/mcpl/conversation-router.js +29 -15
  46. package/dist/src/mcpl/conversation-router.js.map +1 -1
  47. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  48. package/dist/src/mcpl/convert-block.js +14 -2
  49. package/dist/src/mcpl/convert-block.js.map +1 -1
  50. package/dist/src/mcpl/hook-orchestrator.d.ts +7 -1
  51. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  52. package/dist/src/mcpl/hook-orchestrator.js +32 -7
  53. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  54. package/dist/src/mcpl/index.d.ts +5 -1
  55. package/dist/src/mcpl/index.d.ts.map +1 -1
  56. package/dist/src/mcpl/index.js +4 -0
  57. package/dist/src/mcpl/index.js.map +1 -1
  58. package/dist/src/mcpl/inference-router.d.ts +14 -2
  59. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  60. package/dist/src/mcpl/inference-router.js +73 -28
  61. package/dist/src/mcpl/inference-router.js.map +1 -1
  62. package/dist/src/mcpl/prose-grammar.d.ts +4 -0
  63. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  64. package/dist/src/mcpl/prose-grammar.js +6 -0
  65. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  66. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  67. package/dist/src/mcpl/protocol-family.js +3 -0
  68. package/dist/src/mcpl/protocol-family.js.map +1 -1
  69. package/dist/src/mcpl/push-coalescer.d.ts +20 -2
  70. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  71. package/dist/src/mcpl/push-coalescer.js +33 -11
  72. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  73. package/dist/src/mcpl/reference-fetcher.d.ts +63 -42
  74. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  75. package/dist/src/mcpl/reference-fetcher.js +177 -145
  76. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  77. package/dist/src/mcpl/references.d.ts +380 -46
  78. package/dist/src/mcpl/references.d.ts.map +1 -1
  79. package/dist/src/mcpl/references.js +737 -78
  80. package/dist/src/mcpl/references.js.map +1 -1
  81. package/dist/src/mcpl/server-connection.d.ts +82 -3
  82. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  83. package/dist/src/mcpl/server-connection.js +132 -7
  84. package/dist/src/mcpl/server-connection.js.map +1 -1
  85. package/dist/src/mcpl/tool-lifecycle.d.ts +11 -0
  86. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  87. package/dist/src/mcpl/tool-lifecycle.js +21 -0
  88. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  89. package/dist/src/mcpl/tool-presence.d.ts +259 -0
  90. package/dist/src/mcpl/tool-presence.d.ts.map +1 -0
  91. package/dist/src/mcpl/tool-presence.js +628 -0
  92. package/dist/src/mcpl/tool-presence.js.map +1 -0
  93. package/dist/src/mcpl/tool-result-normalize.d.ts +9 -3
  94. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  95. package/dist/src/mcpl/tool-result-normalize.js +14 -6
  96. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  97. package/dist/src/mcpl/tool-signature.d.ts +39 -0
  98. package/dist/src/mcpl/tool-signature.d.ts.map +1 -0
  99. package/dist/src/mcpl/tool-signature.js +292 -0
  100. package/dist/src/mcpl/tool-signature.js.map +1 -0
  101. package/dist/src/mcpl/toolset-host.d.ts +200 -0
  102. package/dist/src/mcpl/toolset-host.d.ts.map +1 -0
  103. package/dist/src/mcpl/toolset-host.js +621 -0
  104. package/dist/src/mcpl/toolset-host.js.map +1 -0
  105. package/dist/src/mcpl/toolsets-module.d.ts +80 -0
  106. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -0
  107. package/dist/src/mcpl/toolsets-module.js +431 -0
  108. package/dist/src/mcpl/toolsets-module.js.map +1 -0
  109. package/dist/src/mcpl/transport.d.ts +2 -2
  110. package/dist/src/mcpl/transport.d.ts.map +1 -1
  111. package/dist/src/mcpl/transport.js +3 -2
  112. package/dist/src/mcpl/transport.js.map +1 -1
  113. package/dist/src/mcpl/types.d.ts +21 -1
  114. package/dist/src/mcpl/types.d.ts.map +1 -1
  115. package/dist/src/mcpl/types.js.map +1 -1
  116. package/dist/src/mcpl/visible-content.d.ts +7 -9
  117. package/dist/src/mcpl/visible-content.d.ts.map +1 -1
  118. package/dist/src/mcpl/visible-content.js +21 -9
  119. package/dist/src/mcpl/visible-content.js.map +1 -1
  120. package/dist/src/module-registry.d.ts +23 -8
  121. package/dist/src/module-registry.d.ts.map +1 -1
  122. package/dist/src/module-registry.js +68 -15
  123. package/dist/src/module-registry.js.map +1 -1
  124. package/dist/src/modules/discord/index.js +1 -1
  125. package/dist/src/modules/discord/index.js.map +1 -1
  126. package/dist/src/modules/history/index.d.ts +2 -1
  127. package/dist/src/modules/history/index.d.ts.map +1 -1
  128. package/dist/src/modules/history/index.js +51 -17
  129. package/dist/src/modules/history/index.js.map +1 -1
  130. package/dist/src/modules/history/semantic.d.ts +108 -6
  131. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  132. package/dist/src/modules/history/semantic.js +273 -57
  133. package/dist/src/modules/history/semantic.js.map +1 -1
  134. package/dist/src/modules/workspace/direct-write.d.ts +181 -0
  135. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -0
  136. package/dist/src/modules/workspace/direct-write.js +384 -0
  137. package/dist/src/modules/workspace/direct-write.js.map +1 -0
  138. package/dist/src/modules/workspace/edit-text.d.ts +26 -12
  139. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
  140. package/dist/src/modules/workspace/edit-text.js +58 -28
  141. package/dist/src/modules/workspace/edit-text.js.map +1 -1
  142. package/dist/src/modules/workspace/effects.d.ts +10 -0
  143. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  144. package/dist/src/modules/workspace/effects.js +1 -1
  145. package/dist/src/modules/workspace/effects.js.map +1 -1
  146. package/dist/src/modules/workspace/index.d.ts +246 -27
  147. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  148. package/dist/src/modules/workspace/index.js +1216 -236
  149. package/dist/src/modules/workspace/index.js.map +1 -1
  150. package/dist/src/modules/workspace/read-page.d.ts +41 -0
  151. package/dist/src/modules/workspace/read-page.d.ts.map +1 -0
  152. package/dist/src/modules/workspace/read-page.js +213 -0
  153. package/dist/src/modules/workspace/read-page.js.map +1 -0
  154. package/dist/src/modules/workspace/result-text.d.ts +67 -0
  155. package/dist/src/modules/workspace/result-text.d.ts.map +1 -0
  156. package/dist/src/modules/workspace/result-text.js +94 -0
  157. package/dist/src/modules/workspace/result-text.js.map +1 -0
  158. package/dist/src/modules/workspace/text.d.ts +29 -12
  159. package/dist/src/modules/workspace/text.d.ts.map +1 -1
  160. package/dist/src/modules/workspace/text.js +35 -33
  161. package/dist/src/modules/workspace/text.js.map +1 -1
  162. package/dist/src/modules/workspace/types.d.ts +41 -17
  163. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  164. package/dist/src/modules/workspace/types.js.map +1 -1
  165. package/dist/src/sent-rows.d.ts +59 -0
  166. package/dist/src/sent-rows.d.ts.map +1 -0
  167. package/dist/src/sent-rows.js +145 -0
  168. package/dist/src/sent-rows.js.map +1 -0
  169. package/dist/src/silent-heartbeat.d.ts +17 -10
  170. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  171. package/dist/src/silent-heartbeat.js.map +1 -1
  172. package/dist/src/stable-hash.d.ts +3 -0
  173. package/dist/src/stable-hash.d.ts.map +1 -0
  174. package/dist/src/stable-hash.js +15 -0
  175. package/dist/src/stable-hash.js.map +1 -0
  176. package/dist/src/stored-injections.d.ts +99 -0
  177. package/dist/src/stored-injections.d.ts.map +1 -0
  178. package/dist/src/stored-injections.js +106 -0
  179. package/dist/src/stored-injections.js.map +1 -0
  180. package/dist/src/text-as-stored.d.ts +4 -3
  181. package/dist/src/text-as-stored.d.ts.map +1 -1
  182. package/dist/src/text-as-stored.js +4 -3
  183. package/dist/src/text-as-stored.js.map +1 -1
  184. package/dist/src/thinking-drops.d.ts +81 -0
  185. package/dist/src/thinking-drops.d.ts.map +1 -0
  186. package/dist/src/thinking-drops.js +103 -0
  187. package/dist/src/thinking-drops.js.map +1 -0
  188. package/dist/src/tool-image-ledger.d.ts +9 -8
  189. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  190. package/dist/src/tool-image-ledger.js +9 -8
  191. package/dist/src/tool-image-ledger.js.map +1 -1
  192. package/dist/src/tool-presentation.d.ts.map +1 -1
  193. package/dist/src/tool-presentation.js +3 -1
  194. package/dist/src/tool-presentation.js.map +1 -1
  195. package/dist/src/tool-result-guard.d.ts +4 -2
  196. package/dist/src/tool-result-guard.d.ts.map +1 -1
  197. package/dist/src/tool-result-guard.js +21 -6
  198. package/dist/src/tool-result-guard.js.map +1 -1
  199. package/dist/src/tool-result-history.d.ts +186 -21
  200. package/dist/src/tool-result-history.d.ts.map +1 -1
  201. package/dist/src/tool-result-history.js +428 -89
  202. package/dist/src/tool-result-history.js.map +1 -1
  203. package/dist/src/tune-out/coordinator.d.ts +17 -9
  204. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  205. package/dist/src/tune-out/coordinator.js +84 -37
  206. package/dist/src/tune-out/coordinator.js.map +1 -1
  207. package/dist/src/types/agent.d.ts +186 -7
  208. package/dist/src/types/agent.d.ts.map +1 -1
  209. package/dist/src/types/events.d.ts +19 -2
  210. package/dist/src/types/events.d.ts.map +1 -1
  211. package/dist/src/types/framework.d.ts +24 -3
  212. package/dist/src/types/framework.d.ts.map +1 -1
  213. package/dist/src/types/module.d.ts +90 -7
  214. package/dist/src/types/module.d.ts.map +1 -1
  215. package/dist/src/types/trace.d.ts +57 -2
  216. package/dist/src/types/trace.d.ts.map +1 -1
  217. package/package.json +4 -3
  218. package/src/agent.ts +593 -60
  219. package/src/api/server.ts +1 -2
  220. package/src/auto-continue.ts +42 -0
  221. package/src/code-execution/isolate/scripts.ts +11 -3
  222. package/src/framework.ts +4004 -1026
  223. package/src/gate/event-gate.ts +2315 -389
  224. package/src/gate/index.ts +2 -1
  225. package/src/gate/recipe-reconcile.ts +355 -0
  226. package/src/gate/types.ts +70 -8
  227. package/src/index.ts +7 -4
  228. package/src/mcpl/channel-registry.ts +1220 -467
  229. package/src/mcpl/conversation-router.ts +34 -16
  230. package/src/mcpl/convert-block.ts +14 -1
  231. package/src/mcpl/hook-orchestrator.ts +33 -6
  232. package/src/mcpl/index.ts +6 -1
  233. package/src/mcpl/inference-router.ts +79 -34
  234. package/src/mcpl/prose-grammar.ts +7 -0
  235. package/src/mcpl/protocol-family.ts +2 -0
  236. package/src/mcpl/push-coalescer.ts +51 -13
  237. package/src/mcpl/reference-fetcher.ts +201 -162
  238. package/src/mcpl/references.ts +852 -78
  239. package/src/mcpl/server-connection.ts +145 -7
  240. package/src/mcpl/tool-lifecycle.ts +21 -0
  241. package/src/mcpl/tool-presence.ts +650 -0
  242. package/src/mcpl/tool-result-normalize.ts +18 -6
  243. package/src/mcpl/tool-signature.ts +285 -0
  244. package/src/mcpl/toolset-host.ts +637 -0
  245. package/src/mcpl/toolsets-module.ts +451 -0
  246. package/src/mcpl/transport.ts +3 -2
  247. package/src/mcpl/types.ts +22 -1
  248. package/src/mcpl/visible-content.ts +22 -9
  249. package/src/module-registry.ts +77 -24
  250. package/src/modules/discord/index.ts +1 -1
  251. package/src/modules/history/index.ts +48 -18
  252. package/src/modules/history/semantic.ts +308 -54
  253. package/src/modules/workspace/direct-write.ts +425 -0
  254. package/src/modules/workspace/edit-text.ts +82 -35
  255. package/src/modules/workspace/effects.ts +1 -1
  256. package/src/modules/workspace/index.ts +1274 -241
  257. package/src/modules/workspace/read-page.ts +222 -0
  258. package/src/modules/workspace/result-text.ts +119 -0
  259. package/src/modules/workspace/text.ts +58 -32
  260. package/src/modules/workspace/types.ts +41 -17
  261. package/src/sent-rows.ts +162 -0
  262. package/src/silent-heartbeat.ts +17 -10
  263. package/src/stable-hash.ts +14 -0
  264. package/src/stored-injections.ts +131 -0
  265. package/src/text-as-stored.ts +4 -3
  266. package/src/thinking-drops.ts +141 -0
  267. package/src/tool-image-ledger.ts +9 -8
  268. package/src/tool-presentation.ts +3 -1
  269. package/src/tool-result-guard.ts +19 -6
  270. package/src/tool-result-history.ts +488 -88
  271. package/src/tune-out/coordinator.ts +80 -37
  272. package/src/types/agent.ts +189 -7
  273. package/src/types/events.ts +19 -2
  274. package/src/types/framework.ts +21 -3
  275. package/src/types/module.ts +86 -7
  276. package/src/types/trace.ts +58 -2
@@ -37,6 +37,7 @@
37
37
  import { createHash } from 'node:crypto';
38
38
  import type { ContextStrategy } from '@animalabs/context-manager';
39
39
  import type { ChannelDescriptor } from './types.js';
40
+ import { destinationKey, type ChannelRef } from './channel-registry.js';
40
41
 
41
42
  // ============================================================================
42
43
  // Config & types
@@ -108,6 +109,9 @@ export interface ConversationRouterConfig {
108
109
 
109
110
  export interface ConversationBinding {
110
111
  channelId: string;
112
+ /** The channel's server: channel ids are unique only within one server's
113
+ * connection (MCPL §14.2), so each server's channel has its own binding. */
114
+ serverId: string;
111
115
  agentName: string;
112
116
  /** Engagement counter per channel — fresh forks get a new generation. */
113
117
  generation: number;
@@ -157,11 +161,15 @@ export const DEFAULT_CLOSURE_PROMPT =
157
161
  export class ConversationRouter {
158
162
  private config: ConversationRouterConfig;
159
163
 
160
- /** Active bindings, keyed by channelId. */
164
+ /** Active bindings, keyed by the channel's server and id (destinationKey):
165
+ * two servers' channels with one id are two conversations. */
161
166
  private bindings = new Map<string, ConversationBinding>();
162
167
 
163
- /** Engagement counter per channel — survives unbind, so a re-bound channel
164
- * gets a distinct fork agent name. */
168
+ /** Engagement counter per channel id — survives unbind, so a re-bound
169
+ * channel gets a distinct fork agent name. Kept by bare id, as it's
170
+ * persisted: two servers sharing an id share a counter, which still never
171
+ * repeats a name, and their forks' names differ anyway, since each hashes
172
+ * its server (forkAgentName). */
165
173
  private generations = new Map<string, number>();
166
174
 
167
175
  constructor(config: ConversationRouterConfig) {
@@ -191,7 +199,7 @@ export class ConversationRouter {
191
199
  * unchanged; a failed spawn doesn't leave a dangling route.
192
200
  */
193
201
  route(facts: IncomingMessageFacts): RoutingDecision {
194
- const existing = this.bindings.get(facts.channelId);
202
+ const existing = this.bindings.get(bindingKey({ serverId: facts.serverId ?? '', channelId: facts.channelId }));
195
203
 
196
204
  if (existing) {
197
205
  return {
@@ -223,39 +231,44 @@ export class ConversationRouter {
223
231
  * Record a binding after the framework successfully spawned the fork.
224
232
  * Returns the new binding.
225
233
  */
226
- bind(channelId: string, agentName: string, generation: number, now = Date.now()): ConversationBinding {
227
- this.generations.set(channelId, generation);
234
+ bind(channel: ChannelRef, agentName: string, generation: number, now = Date.now()): ConversationBinding {
235
+ // The counter is kept by bare id, and two servers' spawns on one id run
236
+ // beside each other, so a bind can finish after a later spawn's: the
237
+ // counter keeps the higher, or the next spawn would propose a fork name
238
+ // already used.
239
+ this.generations.set(channel.channelId, Math.max(this.generations.get(channel.channelId) ?? 0, generation));
228
240
  const binding: ConversationBinding = {
229
- channelId,
241
+ channelId: channel.channelId,
242
+ serverId: channel.serverId,
230
243
  agentName,
231
244
  generation,
232
245
  boundAt: now,
233
246
  lastActivity: now,
234
247
  };
235
- this.bindings.set(channelId, binding);
248
+ this.bindings.set(bindingKey(channel), binding);
236
249
  return binding;
237
250
  }
238
251
 
239
252
  /** Record delivered activity on an existing binding. Does not bind a
240
253
  * channel or advance its generation. Call after successful delivery, not
241
254
  * when previewing a route. The optional clock supports deterministic use. */
242
- touch(channelId: string, now = Date.now()): void {
243
- const binding = this.bindings.get(channelId);
255
+ touch(channel: ChannelRef, now = Date.now()): void {
256
+ const binding = this.bindings.get(bindingKey(channel));
244
257
  if (binding) binding.lastActivity = now;
245
258
  }
246
259
 
247
- unbind(channelId: string): void {
248
- this.bindings.delete(channelId);
260
+ unbind(channel: ChannelRef): void {
261
+ this.bindings.delete(bindingKey(channel));
249
262
  }
250
263
 
251
- getBinding(channelId: string): ConversationBinding | undefined {
252
- return this.bindings.get(channelId);
264
+ getBinding(channel: ChannelRef): ConversationBinding | undefined {
265
+ return this.bindings.get(bindingKey(channel));
253
266
  }
254
267
 
255
268
  /** Reverse lookup: the channel an agent is bound to, if any. */
256
- channelForAgent(agentName: string): string | undefined {
269
+ channelForAgent(agentName: string): ChannelRef | undefined {
257
270
  for (const binding of this.bindings.values()) {
258
- if (binding.agentName === agentName) return binding.channelId;
271
+ if (binding.agentName === agentName) return { serverId: binding.serverId, channelId: binding.channelId };
259
272
  }
260
273
  return undefined;
261
274
  }
@@ -325,6 +338,11 @@ export class ConversationRouter {
325
338
  /** The readable part of a fork's name stops here: uniqueness rides on the hash. */
326
339
  const FORK_NAME_READABLE_MAX = 64;
327
340
 
341
+ /** A binding's key: its channel's server and id (destinationKey). */
342
+ function bindingKey(channel: ChannelRef): string {
343
+ return destinationKey(channel);
344
+ }
345
+
328
346
  /**
329
347
  * A fork's agent name: `<prefix>-<readable>-g<generation>-<hash>`, such as
330
348
  * `conversation-C123-g2-e08e564271867d16` for Slack's channel `C123` on the
@@ -11,6 +11,7 @@ import {
11
11
  IMAGE_NO_DATA_TEXT,
12
12
  IMAGE_NO_MEDIA_TYPE_TEXT,
13
13
  INLINE_WITHHELD_TEXT,
14
+ classifyBlock,
14
15
  isInlineContradiction,
15
16
  referenceStubOrNull,
16
17
  unknownBlockText,
@@ -85,12 +86,24 @@ export function convertBlock(block: McplContentBlock, provenance: string, server
85
86
  }
86
87
  return { type: 'text', text: isFilledString(block.data) ? AUDIO_NO_MEDIA_TYPE_TEXT : AUDIO_NO_DATA_TEXT };
87
88
 
88
- case 'resource':
89
+ case 'resource': {
90
+ // MCP's embedded resource shows as the block its contents make would
91
+ // show here (RFC-005 §8.1), its uri never.
92
+ const c = classifyBlock(block);
93
+ if (c.kind === 'embedded' && c.contents.kind === 'block') {
94
+ return convertBlock(c.contents.block as McplContentBlock, provenance, serverId);
95
+ }
89
96
  // RFC-005: reference blocks become bounded stubs — never raw URIs
90
97
  // (a signed URL is a bearer credential that looks like a location).
91
98
  return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? '[reference]' };
99
+ }
92
100
 
93
101
  default: {
102
+ // MCP's link to a resource isn't one of MCPL's own blocks, but a server
103
+ // can send one, and RFC-005 §8.1 makes it a reference, shown as one.
104
+ if ((block as { type?: unknown }).type === 'resource_link') {
105
+ return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? '[reference]' };
106
+ }
94
107
  // Unknown wire block types previously fell off the exhaustive switch
95
108
  // and propagated `undefined` into ContentBlock[]. Fail visibly. A type
96
109
  // that isn't a string is no type, and is never converted to one: an
@@ -71,7 +71,9 @@ function convertContent(content: string | McplContentBlock[], serverId: string):
71
71
 
72
72
  /**
73
73
  * Convert an MCPL wire-format context injection, from the server `serverId`,
74
- * to a context-manager ContextInjection.
74
+ * to a context-manager ContextInjection. It builds only these four fields,
75
+ * so a server can't opt an injection out of being stored
76
+ * (ModuleContextInjection.placedEachCompile, which only a module may set).
75
77
  */
76
78
  function convertMcplInjection(mcplInj: McplContextInjection, serverId: string): ContextInjection {
77
79
  return {
@@ -114,8 +116,17 @@ export class HookOrchestrator {
114
116
  *
115
117
  * Returns aggregated ContextInjection[] ready for context-manager's compile().
116
118
  * Fail-open: servers that time out or error are silently skipped.
119
+ *
120
+ * `channelsFor` gives each server its own channel context (§14.4), in
121
+ * place of `params.channels`: channel ids are unique only within one
122
+ * server's connection (§14.2), so each server is told only of its own
123
+ * channels (ChannelRegistry.buildChannelContext). Undefined from it leaves
124
+ * the field out for that server.
117
125
  */
118
- async beforeInference(params: BeforeInferenceParams): Promise<ContextInjection[]> {
126
+ async beforeInference(
127
+ params: BeforeInferenceParams,
128
+ channelsFor?: (serverId: string) => BeforeInferenceParams['channels'],
129
+ ): Promise<ContextInjection[]> {
119
130
  // §5.4: the GRANT decides fan-out, not the raw advertisement — which
120
131
  // also fixes selection for the recursive §5.1 shape, where
121
132
  // `beforeInference: {observe: true, …}` is an object, not `true`. A
@@ -132,7 +143,7 @@ export class HookOrchestrator {
132
143
 
133
144
  this._isInHook = true;
134
145
  try {
135
- return await this.fanOutBeforeInference(servers, params);
146
+ return await this.fanOutBeforeInference(servers, params, channelsFor);
136
147
  } finally {
137
148
  this._isInHook = false;
138
149
  }
@@ -163,17 +174,33 @@ export class HookOrchestrator {
163
174
  private async fanOutBeforeInference(
164
175
  servers: McplServerConnection[],
165
176
  params: BeforeInferenceParams,
177
+ channelsFor?: (serverId: string) => BeforeInferenceParams['channels'],
166
178
  ): Promise<ContextInjection[]> {
167
179
  const results = await Promise.allSettled(
168
- servers.map((server) => {
180
+ // Async, so a throw building one server's channel context fails that
181
+ // server alone, as its timeout or transport error would.
182
+ servers.map(async (server) => {
183
+ let base = params;
184
+ if (channelsFor) {
185
+ const { channels: _shared, ...rest } = params;
186
+ let channels: BeforeInferenceParams['channels'];
187
+ try {
188
+ channels = channelsFor(server.id);
189
+ } catch (err) {
190
+ // A bug, not a server's failure: said, then this server is skipped.
191
+ console.error(`[beforeInference] the channel context for "${server.id}" couldn't be built, so it isn't called:`, err);
192
+ throw err;
193
+ }
194
+ base = channels ? { ...rest, channels } : rest;
195
+ }
169
196
  // §10.1: a server not granted observe MUST receive
170
197
  // `userMessage: null` — the field is ABSENT authority, not merely
171
198
  // discouraged — while the hook is still invoked so granted
172
199
  // injection positions keep working (write-without-read).
173
200
  const perServer: BeforeInferenceParams =
174
201
  CapabilityGrant.of(server).has('contextHooks.beforeInference.observe')
175
- ? params
176
- : { ...params, userMessage: null };
202
+ ? base
203
+ : { ...base, userMessage: null };
177
204
  return withTimeout(
178
205
  server.sendBeforeInference(perServer),
179
206
  BEFORE_INFERENCE_TIMEOUT_MS,
package/src/mcpl/index.ts CHANGED
@@ -88,6 +88,11 @@ export type {
88
88
  } from './types.js';
89
89
 
90
90
  export { McplMethod } from './types.js';
91
+ // RFC-014 tool presence and toolsets (host side).
92
+ export { ToolsetHub, ToolsetController, TOOLSETS_MODULE, META_TOOL_NAMES } from './toolset-host.js';
93
+ export { ToolsetsModule, TOOLSETS_REMINDER_NAMESPACE } from './toolsets-module.js';
94
+ export { ToolPresence } from './tool-presence.js';
95
+ export type { ChangeSet, MetaToolNames, ToolsetDecl } from './tool-presence.js';
91
96
 
92
97
  // Error codes and factories
93
98
  export {
@@ -171,7 +176,7 @@ export { InferenceRouter } from './inference-router.js';
171
176
 
172
177
  // Channel registry (channel lifecycle, incoming messages, synthesized tools)
173
178
  export { ChannelRegistry } from './channel-registry.js';
174
- export type { PublishRefusalReason } from './channel-registry.js';
179
+ export type { ChannelRef, PublishRefusalReason } from './channel-registry.js';
175
180
 
176
181
  // Inbound source envelope: where each accepted item came from, stamped once;
177
182
  // and the digest of a delivered or stored body
@@ -26,6 +26,7 @@ import type {
26
26
  import type { FeatureSetManager } from './feature-set-manager.js';
27
27
  import { McplFeatureSetError } from './feature-set-manager.js';
28
28
  import type { HookOrchestrator } from './hook-orchestrator.js';
29
+ import { logValue } from './log-value.js';
29
30
 
30
31
  // ============================================================================
31
32
  // Responder interface
@@ -181,6 +182,10 @@ export class InferenceRouter {
181
182
  private featureSetManager: FeatureSetManager;
182
183
  private routingPolicy: InferenceRoutingPolicy | null;
183
184
  private emitTraceFn: (event: { type: string; [key: string]: unknown }) => void;
185
+ /** The refusals for want of a model that have been logged: one line per
186
+ * server with no policy, and per server and feature set with one, so a
187
+ * server that keeps asking doesn't flood the operator's log. */
188
+ private loggedRefusals = new Set<string>();
184
189
 
185
190
  constructor(
186
191
  membrane: Membrane,
@@ -201,7 +206,7 @@ export class InferenceRouter {
201
206
  *
202
207
  * 1. Validate feature set
203
208
  * 2. Check loop prevention (reject if inside a hook)
204
- * 3. Resolve model from routing policy
209
+ * 3. Resolve model from routing policy, refusing when it names none
205
210
  * 4. Build membrane request
206
211
  * 5. Run inference, streaming its visible text as chunks when asked
207
212
  * 6. Answer with the result, or with the error for any ending that isn't
@@ -233,11 +238,62 @@ export class InferenceRouter {
233
238
  return;
234
239
  }
235
240
 
236
- // 3. Resolve model
241
+ // Streaming needs a way to send chunks and an id to tag them with;
242
+ // without either, the request is answered whole.
243
+ const sendChunk = params.stream ? responder.sendChunk?.bind(responder) : undefined;
244
+ const chunkRequestId = sendChunk ? responder.requestId : undefined;
245
+ const streamed = chunkRequestId != null;
246
+
247
+ // 3. Resolve model. With no routing policy, or one that names no model
248
+ // for this feature set, the operator has chosen no model for this
249
+ // server's inference: refused rather than guessed, since a model picked
250
+ // here would run, on whatever credentials and at whatever price it
251
+ // carries, for a server the operator never chose it for. The answer is
252
+ // -32603, the code the router answers a failed inference with, as it
253
+ // answered this request when it reached the provider with no model; the
254
+ // server's grant can still list inferenceRequest, so not -32002.
237
255
  const model = this.resolveModel(
238
256
  params.featureSet,
239
257
  params.conversationId,
240
258
  );
259
+ if (model === undefined) {
260
+ const policy = Boolean(this.routingPolicy);
261
+ // One reason, quoted for where it goes: JSON for the server, logValue
262
+ // for the host log.
263
+ const reason = (quote: (value: string) => string) => policy
264
+ ? `the inference routing policy (inferenceRouting) names no model for feature set ${quote(params.featureSet)}`
265
+ : 'the host sets no inference routing policy (inferenceRouting)';
266
+ const message = `inference/request refused: ${reason(JSON.stringify)}, so the host has no model to run it on; ` +
267
+ "it stays refused until the host's operator configures one";
268
+ // Logged once per server with no policy, and once per server and
269
+ // feature set with one, since each feature set the policy misses is
270
+ // its own entry to add; validateInbound (step 1) bounds feature sets
271
+ // to those the server declares.
272
+ const key = JSON.stringify(policy ? [serverId, params.featureSet] : [serverId]);
273
+ if (!this.loggedRefusals.has(key)) {
274
+ this.loggedRefusals.add(key);
275
+ console.error(
276
+ `[mcpl] ${logValue(serverId)}: inference/request refused: ${reason(logValue)}; ` +
277
+ (policy
278
+ ? 'give it a default, or a byFeature or wildcards entry for that feature set. ' +
279
+ 'This line prints once per server and feature set'
280
+ : 'set inferenceRouting in the framework config to choose the model server-initiated inference runs on. ' +
281
+ 'This line prints once per server') +
282
+ '; later refusals are answered and traced but not logged.',
283
+ );
284
+ }
285
+ // The failure a trace watcher saw when this request failed at the
286
+ // provider, still seen; no inference_start, since none started.
287
+ this.emitTraceFn({
288
+ type: 'mcpl:inference_fail',
289
+ serverId,
290
+ model: null,
291
+ error: message,
292
+ streamed,
293
+ });
294
+ responder.respondError(-32603, message);
295
+ return;
296
+ }
241
297
 
242
298
  // Emit trace: inference start
243
299
  this.emitTraceFn({
@@ -263,12 +319,6 @@ export class InferenceRouter {
263
319
  },
264
320
  };
265
321
 
266
- // Streaming needs a way to send chunks and an id to tag them with;
267
- // without either, the request is answered whole.
268
- const sendChunk = params.stream ? responder.sendChunk?.bind(responder) : undefined;
269
- const chunkRequestId = sendChunk ? responder.requestId : undefined;
270
- const streamed = chunkRequestId != null;
271
-
272
322
  try {
273
323
  // 5. Run inference. Both modes end in membrane's response, and one
274
324
  // builder makes the result from it.
@@ -325,37 +375,32 @@ export class InferenceRouter {
325
375
  * 2. Exact feature set match
326
376
  * 3. Wildcard pattern match (first match wins)
327
377
  * 4. Default from policy
328
- * 5. Fallback string 'default'
378
+ *
379
+ * Only an entry naming a model counts: one that is empty or not a string
380
+ * names none and is passed over, as a missing one is. That includes what a
381
+ * server-sent conversation id or feature set such as `constructor` or
382
+ * `__proto__` finds on Object.prototype, which is never a string. A
383
+ * conversation id that is falsy reads no override.
384
+ *
385
+ * Undefined with no policy, or when none of these names a model: the
386
+ * request is then refused.
329
387
  */
330
388
  private resolveModel(
331
389
  featureSet: string,
332
390
  conversationId?: string,
333
- ): string {
334
- if (!this.routingPolicy) {
335
- return 'default';
391
+ ): string | undefined {
392
+ const policy = this.routingPolicy;
393
+ if (!policy) {
394
+ return undefined;
336
395
  }
337
-
338
- // Check per-conversation override
339
- if (conversationId && this.routingPolicy.overrides?.[conversationId]) {
340
- return this.routingPolicy.overrides[conversationId];
341
- }
342
-
343
- // Check exact feature set match
344
- if (this.routingPolicy.byFeature?.[featureSet]) {
345
- return this.routingPolicy.byFeature[featureSet];
346
- }
347
-
348
- // Check wildcard patterns
349
- if (this.routingPolicy.wildcards) {
350
- for (const [pattern, matchedModel] of Object.entries(this.routingPolicy.wildcards)) {
351
- if (wildcardMatch(pattern, featureSet)) {
352
- return matchedModel;
353
- }
354
- }
355
- }
356
-
357
- // Default
358
- return this.routingPolicy.default;
396
+ const named = (value: unknown): string | undefined =>
397
+ typeof value === 'string' && value !== '' ? value : undefined;
398
+
399
+ return (conversationId ? named(policy.overrides?.[conversationId]) : undefined)
400
+ ?? named(policy.byFeature?.[featureSet])
401
+ // First wildcard that matches and names a model.
402
+ ?? Object.entries(policy.wildcards ?? {}).find(([pattern, m]) => named(m) && wildcardMatch(pattern, featureSet))?.[1]
403
+ ?? named(policy.default);
359
404
  }
360
405
 
361
406
  // ==========================================================================
@@ -19,6 +19,13 @@ export interface ProsePrefix {
19
19
  body: string;
20
20
  }
21
21
 
22
+ /** Whether a `>>` (or `>>>`) prefix line reads `name` as a target: one token,
23
+ * and not `skip_reply`, which the line reads as private speech. For a name
24
+ * offered to an agent to write after `>>`. */
25
+ export function isProseTargetToken(name: string): boolean {
26
+ return /^\S+$/.test(name) && name !== 'skip_reply';
27
+ }
28
+
22
29
  /** A REAL prefix line: `>>` immediately followed by a target token. */
23
30
  export const PROSE_PREFIX_LINE = /^>>\S/;
24
31
 
@@ -641,6 +641,8 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
641
641
  export function mcplPolicyGrants(config: McplServerConfig): string[] {
642
642
  const grants: string[] = [];
643
643
  if (isEffective(config.allowHostCommands)) grants.push('allowHostCommands');
644
+ // RFC-014 tool hints are an MCPL advertisement.
645
+ if (config.toolHints === true) grants.push('toolHints');
644
646
  // An allow-list, except that a pattern matching a path denied by default
645
647
  // grants it (computeGrant's explicitlyEnabled), named by the path.
646
648
  const enabled = config.enabledCapabilities as unknown;
@@ -127,6 +127,14 @@ interface DeferredBatch<E> {
127
127
  latest: CoalescedOccurrence<E>;
128
128
  notices: DeferredNotice[];
129
129
  dropped: number;
130
+ /** Every notice folded into the batch, a dropped one included, and a
131
+ * plain occurrence a notice displaced unread (Occupant.ambient), was
132
+ * tagged `chat:ambient` (RFC-001), so its render asks nothing of the agent
133
+ * (CoalescerHost.wakeToRead). Snapshots carry it. A batch restored
134
+ * without it, from a snapshot written before it existed or from an
135
+ * acceptance receipt, which doesn't carry it, reads as not all ambient:
136
+ * the side that keeps a wake. */
137
+ allAmbient?: true;
130
138
  /** Set on a batch restored from a snapshot whose render may have started. */
131
139
  noRender?: boolean;
132
140
  /** The batch opened on the agent's first view of its subject: a fresh
@@ -184,10 +192,22 @@ interface Occupant {
184
192
  * render that joins a run finds a version placed. In memory only: a
185
193
  * snapshot's occupant comes back as read. */
186
194
  firstView?: true;
195
+ /** A plain occurrence its server tagged `chat:ambient`. A notice that
196
+ * displaces it unread stands its batch's render in its place, so that
197
+ * render is all ambient only if this was (DeferredBatch.allAmbient). In
198
+ * memory only, as `firstView`: a snapshot's occupant comes back as read,
199
+ * and only an unread one is displaced. */
200
+ ambient?: true;
187
201
  /** The run's earlier renders, oldest first, each placed ahead of this one. */
188
202
  before?: Array<Omit<Occupant, 'before'>>;
189
203
  }
190
204
 
205
+ /** An occupant's `ambient` mark, for a plain occurrence its server tagged
206
+ * `chat:ambient`. */
207
+ function ambientOccupant(occurrence: { tags?: string[] }): { ambient?: true } {
208
+ return occurrence.tags?.includes('chat:ambient') === true ? { ambient: true } : {};
209
+ }
210
+
191
211
  /** The occupant holding a run's renders, given oldest first: the newest,
192
212
  * carrying the rest as `before`. */
193
213
  function runOf(members: Array<Omit<Occupant, 'before'>>): Occupant | undefined {
@@ -246,7 +266,7 @@ export interface CoalescingSnapshot {
246
266
  firstViewEnded?: true;
247
267
  /** `firstView` only on a batch that wasn't rendering: one that was may
248
268
  * have been shown. */
249
- batch?: { latest: CoalescedOccurrence<unknown>; notices: DeferredNotice[]; dropped: number; rendering?: boolean; firstView?: true };
269
+ batch?: { latest: CoalescedOccurrence<unknown>; notices: DeferredNotice[]; dropped: number; rendering?: boolean; firstView?: true; allAmbient?: true };
250
270
  }>;
251
271
  }
252
272
  /** One receipt, written synchronously at acceptance (bridges the snapshot throttle). */
@@ -299,8 +319,11 @@ export interface CoalescerHost<E> {
299
319
  /** Queue an ordinary push wake for `agent`, under the batch's own wake
300
320
  * policy, to read a render of the batch placed in its deferred queue that
301
321
  * no request consumed, at `deferredId`. It carries no subject, so the
302
- * subject's other wakes, and later withdrawals of them, leave it. */
303
- wakeToRead(occurrence: CoalescedOccurrence<E>, agent: string, deferredId?: string): Promise<void>;
322
+ * subject's other wakes, and later withdrawals of them, leave it.
323
+ * `allAmbient`: every notice the render stands for was tagged
324
+ * `chat:ambient` (DeferredBatch.allAmbient), so the wake is marked
325
+ * ambient. */
326
+ wakeToRead(occurrence: CoalescedOccurrence<E>, agent: string, deferredId?: string, allAmbient?: boolean): Promise<void>;
304
327
  /** Withdraw unstarted wakes whose sole cause is this subject. */
305
328
  cancelWake(subject: string): void;
306
329
  /** The batch's audience still holds the authority it was admitted under. */
@@ -416,14 +439,20 @@ export function validateCoalescedContent(
416
439
  /**
417
440
  * Whether the converter (`convertBlock` in convert-block.ts, which every lane
418
441
  * uses) shows a block as itself, its text or its image or
419
- * audio source, or as a reference stub, and not as a placeholder: an
442
+ * audio source (for MCP's embedded resource, its contents'), or as a
443
+ * reference stub, and not as a placeholder: an
420
444
  * unrecognized block, a no-data or no-media-type one, withheld inline data,
421
445
  * or a text block whose text isn't a string. This follows its branches in
422
446
  * their order.
423
447
  */
424
448
  function showsAsItself(b: Record<string, unknown>): boolean {
425
449
  if (b.type === 'text') return typeof b.text === 'string';
426
- if (b.type === 'resource') return classifyBlock(b).kind === 'reference';
450
+ if (b.type === 'resource' || b.type === 'resource_link') {
451
+ const c = classifyBlock(b);
452
+ // MCP's embedded resource shows as the block its contents make.
453
+ if (c.kind === 'embedded') return c.contents.kind === 'block' && showsAsItself(c.contents.block);
454
+ return c.kind === 'reference';
455
+ }
427
456
  if (b.type !== 'image' && b.type !== 'audio') return false;
428
457
  if (isInlineContradiction(b)) return false;
429
458
  if (isFilledString(b.uri) && b.disposition !== undefined) return classifyBlock(b).kind === 'reference';
@@ -833,7 +862,7 @@ export class PushCoalescer<E = unknown> {
833
862
  // context that read a version, never a new one (§3.2).
834
863
  const placement = await this.deliver({ ...occurrence, deliverTo: state.audienceAgent });
835
864
  if (placement) {
836
- state.occupant = { eventId: occurrence.eventId, placement };
865
+ state.occupant = { eventId: occurrence.eventId, placement, ...ambientOccupant(occurrence) };
837
866
  state.audienceAgent = placement.agent;
838
867
  }
839
868
  return 'noted';
@@ -858,7 +887,7 @@ export class PushCoalescer<E = unknown> {
858
887
  ...occurrence, deliverTo: prior.placement.agent,
859
888
  ...(firstView ? { firstView: true } : {}),
860
889
  });
861
- state.occupant = placement ? { eventId: occurrence.eventId, placement, ...(firstView ? { firstView: true as const } : {}) } : undefined;
890
+ state.occupant = placement ? { eventId: occurrence.eventId, placement, ...(firstView ? { firstView: true as const } : {}), ...ambientOccupant(occurrence) } : undefined;
862
891
  if (placement) state.audienceAgent = placement.agent;
863
892
  return 'replaced';
864
893
  }
@@ -872,7 +901,7 @@ export class PushCoalescer<E = unknown> {
872
901
  // batch's place (a batch's own first view ends with the subject's).
873
902
  const firstView = hadBatch ? batchFirstView && state.history === 'none' : outcome === 'first' && !state.firstViewEnded;
874
903
  const placement = await this.deliver(hadBatch && firstView ? { ...occurrence, firstView: true } : occurrence);
875
- state.occupant = placement ? { eventId: occurrence.eventId, placement, ...(firstView ? { firstView: true as const } : {}) } : undefined;
904
+ state.occupant = placement ? { eventId: occurrence.eventId, placement, ...(firstView ? { firstView: true as const } : {}), ...ambientOccupant(occurrence) } : undefined;
876
905
  if (placement) state.audienceAgent = placement.agent;
877
906
  return outcome;
878
907
  }
@@ -887,6 +916,10 @@ export class PushCoalescer<E = unknown> {
887
916
  // batches already open too). A render still under way is weighed when a
888
917
  // batch's first delivery is.
889
918
  let firstView = state.history === 'none' && !state.firstViewEnded;
919
+ // Whether every notice the batch's render stands for was tagged
920
+ // `chat:ambient` (DeferredBatch.allAmbient): this one, and a plain
921
+ // occurrence it displaces unread, whose place the render takes.
922
+ let ambient = occurrence.tags?.includes('chat:ambient') === true;
890
923
  if (occupantUnread && state.occupant?.rendered) {
891
924
  // An unread render is a delta the next render builds on (§5.2), not a
892
925
  // plain occurrence: it stays, and the batch follows its agent (§3.2), so
@@ -897,6 +930,7 @@ export class PushCoalescer<E = unknown> {
897
930
  // §5.1: a notice displaces an unread plain occurrence and opens a batch
898
931
  // — in the audience that held the plain occurrence (§3.2).
899
932
  if (!this.occupantLeaves(state, state.occupant)) firstView = false;
933
+ if (state.occupant.ambient !== true) ambient = false;
900
934
  this.host.audit({ kind: 'displaced', subject, eventId: state.occupant.eventId });
901
935
  if (this.host.remove(state.occupant.placement)) {
902
936
  outcome = 'replaced';
@@ -913,10 +947,11 @@ export class PushCoalescer<E = unknown> {
913
947
  state.batch.notices.push(notice);
914
948
  if (state.batch.notices.length > this.maxNotices) { state.batch.notices.shift(); state.batch.dropped++; }
915
949
  state.batch.latest = occurrence;
950
+ if (!ambient) delete state.batch.allAmbient;
916
951
  outcome = 'replaced';
917
952
  } else {
918
953
  // Rule 1: a notice during a render opens a NEW batch ("first").
919
- state.batch = { latest: occurrence, notices: [notice], dropped: 0, ...(firstView ? { firstView: true as const } : {}) };
954
+ state.batch = { latest: occurrence, notices: [notice], dropped: 0, ...(firstView ? { firstView: true as const } : {}), ...(ambient ? { allAmbient: true as const } : {}) };
920
955
  }
921
956
  await this.host.wakeForBatch(occurrence);
922
957
  return outcome;
@@ -1122,7 +1157,9 @@ export class PushCoalescer<E = unknown> {
1122
1157
  // agent's wake for this batch; give it an ordinary wake, as any
1123
1158
  // delivery during its turn has, beside any wakes a newer batch has,
1124
1159
  // so that its next turn reads the content (the wake starts no turn
1125
- // once the agent has read it, or it's removed).
1160
+ // once a request that carried it is answered and, if the wake
1161
+ // carries a channel and the batch wasn't all ambient, the agent has
1162
+ // published there since, or once it's removed).
1126
1163
  // The run's renders keep their own records, whatever else an
1127
1164
  // occupant carries, a first view's mark included.
1128
1165
  let before: Occupant['before'];
@@ -1136,7 +1173,7 @@ export class PushCoalescer<E = unknown> {
1136
1173
  await this.host.wakeForBatch(state.batch.latest);
1137
1174
  }
1138
1175
  state.audienceAgent = placement.agent;
1139
- await this.host.wakeToRead(occurrence, placement.agent, placement.deferredId);
1176
+ await this.host.wakeToRead(occurrence, placement.agent, placement.deferredId, batch.allAmbient === true);
1140
1177
  return;
1141
1178
  }
1142
1179
  // The materialized occurrence is consumed by the request being
@@ -1180,6 +1217,7 @@ export class PushCoalescer<E = unknown> {
1180
1217
  state.batch = {
1181
1218
  latest: s.batch.latest as CoalescedOccurrence<E>, notices: s.batch.notices, dropped: s.batch.dropped,
1182
1219
  ...(s.batch.rendering ? { noRender: true } : s.batch.firstView ? { firstView: true as const } : {}),
1220
+ ...(s.batch.allAmbient ? { allAmbient: true as const } : {}),
1183
1221
  };
1184
1222
  }
1185
1223
  this.subjects.set(s.subject, state);
@@ -1265,8 +1303,8 @@ export class PushCoalescer<E = unknown> {
1265
1303
  // restore() reads only these. After a restart nothing of the subject
1266
1304
  // is tracked as unread, a run included: its history counts as read.
1267
1305
  ...(s.occupant ? { occupant: { eventId: s.occupant.eventId, placement: s.occupant.placement } } : {}),
1268
- ...(s.batch ? { batch: { latest: s.batch.latest as CoalescedOccurrence<unknown>, notices: s.batch.notices, dropped: s.batch.dropped, ...(s.batch.noRender ? { rendering: true } : s.batch.firstView ? { firstView: true as const } : {}) } }
1269
- : s.rendering ? { batch: { latest: s.rendering.batch.latest as CoalescedOccurrence<unknown>, notices: s.rendering.batch.notices, dropped: s.rendering.batch.dropped, rendering: true } }
1306
+ ...(s.batch ? { batch: { latest: s.batch.latest as CoalescedOccurrence<unknown>, notices: s.batch.notices, dropped: s.batch.dropped, ...(s.batch.noRender ? { rendering: true } : s.batch.firstView ? { firstView: true as const } : {}), ...(s.batch.allAmbient ? { allAmbient: true as const } : {}) } }
1307
+ : s.rendering ? { batch: { latest: s.rendering.batch.latest as CoalescedOccurrence<unknown>, notices: s.rendering.batch.notices, dropped: s.rendering.batch.dropped, rendering: true, ...(s.rendering.batch.allAmbient ? { allAmbient: true as const } : {}) } }
1270
1308
  : {}),
1271
1309
  })),
1272
1310
  };