@animalabs/agent-framework 0.22.2 → 0.23.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 (997) hide show
  1. package/README.md +108 -4
  2. package/dist/src/agent.d.ts +108 -17
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +207 -67
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/mcp-server-core.js +1 -1
  7. package/dist/src/api/mcp-server-core.js.map +1 -1
  8. package/dist/src/api/server.d.ts.map +1 -1
  9. package/dist/src/api/server.js +5 -1
  10. package/dist/src/api/server.js.map +1 -1
  11. package/dist/src/api/types.d.ts +1 -1
  12. package/dist/src/api/types.d.ts.map +1 -1
  13. package/dist/src/branch-names.d.ts +14 -0
  14. package/dist/src/branch-names.d.ts.map +1 -0
  15. package/dist/src/branch-names.js +22 -0
  16. package/dist/src/branch-names.js.map +1 -0
  17. package/dist/src/closest-names.d.ts +28 -0
  18. package/dist/src/closest-names.d.ts.map +1 -0
  19. package/dist/src/closest-names.js +98 -0
  20. package/dist/src/closest-names.js.map +1 -0
  21. package/dist/src/code-execution/index.d.ts +1 -1
  22. package/dist/src/code-execution/index.d.ts.map +1 -1
  23. package/dist/src/code-execution/index.js.map +1 -1
  24. package/dist/src/code-execution/py-runner.d.ts +50 -7
  25. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  26. package/dist/src/code-execution/py-runner.js +258 -90
  27. package/dist/src/code-execution/py-runner.js.map +1 -1
  28. package/dist/src/code-execution/runtime-py.d.ts +3 -2
  29. package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
  30. package/dist/src/code-execution/runtime-py.js +94 -8
  31. package/dist/src/code-execution/runtime-py.js.map +1 -1
  32. package/dist/src/code-execution/tool-definition.d.ts +17 -1
  33. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  34. package/dist/src/code-execution/tool-definition.js +72 -6
  35. package/dist/src/code-execution/tool-definition.js.map +1 -1
  36. package/dist/src/config-checks.d.ts +28 -0
  37. package/dist/src/config-checks.d.ts.map +1 -0
  38. package/dist/src/config-checks.js +56 -0
  39. package/dist/src/config-checks.js.map +1 -0
  40. package/dist/src/deadline-timer.d.ts +21 -0
  41. package/dist/src/deadline-timer.d.ts.map +1 -0
  42. package/dist/src/deadline-timer.js +43 -0
  43. package/dist/src/deadline-timer.js.map +1 -0
  44. package/dist/src/error-message.d.ts +30 -0
  45. package/dist/src/error-message.d.ts.map +1 -0
  46. package/dist/src/error-message.js +50 -0
  47. package/dist/src/error-message.js.map +1 -0
  48. package/dist/src/framework.d.ts +1529 -165
  49. package/dist/src/framework.d.ts.map +1 -1
  50. package/dist/src/framework.js +8264 -1939
  51. package/dist/src/framework.js.map +1 -1
  52. package/dist/src/gate/event-gate.d.ts +8 -0
  53. package/dist/src/gate/event-gate.d.ts.map +1 -1
  54. package/dist/src/gate/event-gate.js +36 -5
  55. package/dist/src/gate/event-gate.js.map +1 -1
  56. package/dist/src/held-writes.d.ts +16 -0
  57. package/dist/src/held-writes.d.ts.map +1 -0
  58. package/dist/src/held-writes.js +56 -0
  59. package/dist/src/held-writes.js.map +1 -0
  60. package/dist/src/index.d.ts +10 -4
  61. package/dist/src/index.d.ts.map +1 -1
  62. package/dist/src/index.js +11 -5
  63. package/dist/src/index.js.map +1 -1
  64. package/dist/src/mcpl/capability-grant.d.ts +4 -0
  65. package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
  66. package/dist/src/mcpl/capability-grant.js +1 -1
  67. package/dist/src/mcpl/capability-grant.js.map +1 -1
  68. package/dist/src/mcpl/channel-registry.d.ts +233 -18
  69. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  70. package/dist/src/mcpl/channel-registry.js +975 -386
  71. package/dist/src/mcpl/channel-registry.js.map +1 -1
  72. package/dist/src/mcpl/checkpoint-manager.d.ts +21 -1
  73. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  74. package/dist/src/mcpl/checkpoint-manager.js +91 -30
  75. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  76. package/dist/src/mcpl/conversation-router.d.ts +19 -3
  77. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  78. package/dist/src/mcpl/conversation-router.js +23 -8
  79. package/dist/src/mcpl/conversation-router.js.map +1 -1
  80. package/dist/src/mcpl/convert-block.d.ts +20 -0
  81. package/dist/src/mcpl/convert-block.d.ts.map +1 -0
  82. package/dist/src/mcpl/convert-block.js +88 -0
  83. package/dist/src/mcpl/convert-block.js.map +1 -0
  84. package/dist/src/mcpl/errors.d.ts +13 -0
  85. package/dist/src/mcpl/errors.d.ts.map +1 -1
  86. package/dist/src/mcpl/errors.js +28 -0
  87. package/dist/src/mcpl/errors.js.map +1 -1
  88. package/dist/src/mcpl/hook-orchestrator.d.ts +23 -0
  89. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  90. package/dist/src/mcpl/hook-orchestrator.js +83 -72
  91. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  92. package/dist/src/mcpl/inbound-source.d.ts +114 -0
  93. package/dist/src/mcpl/inbound-source.d.ts.map +1 -0
  94. package/dist/src/mcpl/inbound-source.js +83 -0
  95. package/dist/src/mcpl/inbound-source.js.map +1 -0
  96. package/dist/src/mcpl/index.d.ts +7 -1
  97. package/dist/src/mcpl/index.d.ts.map +1 -1
  98. package/dist/src/mcpl/index.js +5 -1
  99. package/dist/src/mcpl/index.js.map +1 -1
  100. package/dist/src/mcpl/inference-router.d.ts +19 -13
  101. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  102. package/dist/src/mcpl/inference-router.js +155 -87
  103. package/dist/src/mcpl/inference-router.js.map +1 -1
  104. package/dist/src/mcpl/log-value.d.ts +7 -0
  105. package/dist/src/mcpl/log-value.d.ts.map +1 -1
  106. package/dist/src/mcpl/log-value.js +9 -0
  107. package/dist/src/mcpl/log-value.js.map +1 -1
  108. package/dist/src/mcpl/modern-connection.d.ts +227 -0
  109. package/dist/src/mcpl/modern-connection.d.ts.map +1 -0
  110. package/dist/src/mcpl/modern-connection.js +1088 -0
  111. package/dist/src/mcpl/modern-connection.js.map +1 -0
  112. package/dist/src/mcpl/prefix-rule.d.ts +18 -0
  113. package/dist/src/mcpl/prefix-rule.d.ts.map +1 -0
  114. package/dist/src/mcpl/prefix-rule.js +25 -0
  115. package/dist/src/mcpl/prefix-rule.js.map +1 -0
  116. package/dist/src/mcpl/protocol-family.d.ts +179 -0
  117. package/dist/src/mcpl/protocol-family.d.ts.map +1 -0
  118. package/dist/src/mcpl/protocol-family.js +697 -0
  119. package/dist/src/mcpl/protocol-family.js.map +1 -0
  120. package/dist/src/mcpl/push-coalescer.d.ts +58 -17
  121. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  122. package/dist/src/mcpl/push-coalescer.js +93 -32
  123. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  124. package/dist/src/mcpl/push-handler.d.ts +16 -9
  125. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  126. package/dist/src/mcpl/push-handler.js +18 -83
  127. package/dist/src/mcpl/push-handler.js.map +1 -1
  128. package/dist/src/mcpl/reference-fetcher.d.ts +76 -13
  129. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  130. package/dist/src/mcpl/reference-fetcher.js +275 -66
  131. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  132. package/dist/src/mcpl/references.d.ts +98 -18
  133. package/dist/src/mcpl/references.d.ts.map +1 -1
  134. package/dist/src/mcpl/references.js +170 -32
  135. package/dist/src/mcpl/references.js.map +1 -1
  136. package/dist/src/mcpl/server-connection.d.ts +406 -31
  137. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  138. package/dist/src/mcpl/server-connection.js +1331 -272
  139. package/dist/src/mcpl/server-connection.js.map +1 -1
  140. package/dist/src/mcpl/server-registry.d.ts +35 -6
  141. package/dist/src/mcpl/server-registry.d.ts.map +1 -1
  142. package/dist/src/mcpl/server-registry.js +63 -13
  143. package/dist/src/mcpl/server-registry.js.map +1 -1
  144. package/dist/src/mcpl/skip-reply-wake.d.ts +36 -0
  145. package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -0
  146. package/dist/src/mcpl/skip-reply-wake.js +55 -0
  147. package/dist/src/mcpl/skip-reply-wake.js.map +1 -0
  148. package/dist/src/mcpl/tool-result-normalize.d.ts +54 -0
  149. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -0
  150. package/dist/src/mcpl/tool-result-normalize.js +232 -0
  151. package/dist/src/mcpl/tool-result-normalize.js.map +1 -0
  152. package/dist/src/mcpl/transport.d.ts +104 -12
  153. package/dist/src/mcpl/transport.d.ts.map +1 -1
  154. package/dist/src/mcpl/transport.js +315 -85
  155. package/dist/src/mcpl/transport.js.map +1 -1
  156. package/dist/src/mcpl/types.d.ts +76 -19
  157. package/dist/src/mcpl/types.d.ts.map +1 -1
  158. package/dist/src/mcpl/types.js.map +1 -1
  159. package/dist/src/mcpl/visible-content.d.ts +16 -0
  160. package/dist/src/mcpl/visible-content.d.ts.map +1 -1
  161. package/dist/src/mcpl/visible-content.js +26 -0
  162. package/dist/src/mcpl/visible-content.js.map +1 -1
  163. package/dist/src/module-registry.d.ts +34 -6
  164. package/dist/src/module-registry.d.ts.map +1 -1
  165. package/dist/src/module-registry.js +119 -31
  166. package/dist/src/module-registry.js.map +1 -1
  167. package/dist/src/modules/api/index.d.ts +1 -1
  168. package/dist/src/modules/api/index.d.ts.map +1 -1
  169. package/dist/src/modules/api/index.js +3 -2
  170. package/dist/src/modules/api/index.js.map +1 -1
  171. package/dist/src/modules/discord/discord-js-client.d.ts +43 -2
  172. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
  173. package/dist/src/modules/discord/discord-js-client.js +286 -67
  174. package/dist/src/modules/discord/discord-js-client.js.map +1 -1
  175. package/dist/src/modules/discord/index.d.ts +53 -5
  176. package/dist/src/modules/discord/index.d.ts.map +1 -1
  177. package/dist/src/modules/discord/index.js +712 -186
  178. package/dist/src/modules/discord/index.js.map +1 -1
  179. package/dist/src/modules/discord/refusal.d.ts +14 -0
  180. package/dist/src/modules/discord/refusal.d.ts.map +1 -0
  181. package/dist/src/modules/discord/refusal.js +14 -0
  182. package/dist/src/modules/discord/refusal.js.map +1 -0
  183. package/dist/src/modules/discord/types.d.ts +27 -4
  184. package/dist/src/modules/discord/types.d.ts.map +1 -1
  185. package/dist/src/modules/health/index.d.ts.map +1 -1
  186. package/dist/src/modules/health/index.js +8 -3
  187. package/dist/src/modules/health/index.js.map +1 -1
  188. package/dist/src/modules/history/index.d.ts +3 -1
  189. package/dist/src/modules/history/index.d.ts.map +1 -1
  190. package/dist/src/modules/history/index.js +359 -127
  191. package/dist/src/modules/history/index.js.map +1 -1
  192. package/dist/src/modules/history/semantic.d.ts +28 -1
  193. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  194. package/dist/src/modules/history/semantic.js +177 -32
  195. package/dist/src/modules/history/semantic.js.map +1 -1
  196. package/dist/src/modules/index.d.ts +1 -1
  197. package/dist/src/modules/index.d.ts.map +1 -1
  198. package/dist/src/modules/index.js +1 -1
  199. package/dist/src/modules/index.js.map +1 -1
  200. package/dist/src/modules/unknown-tool.d.ts +10 -0
  201. package/dist/src/modules/unknown-tool.d.ts.map +1 -0
  202. package/dist/src/modules/unknown-tool.js +17 -0
  203. package/dist/src/modules/unknown-tool.js.map +1 -0
  204. package/dist/src/modules/workspace/disk-agreement.d.ts +57 -4
  205. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  206. package/dist/src/modules/workspace/disk-agreement.js +75 -7
  207. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  208. package/dist/src/modules/workspace/effects.d.ts +66 -7
  209. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  210. package/dist/src/modules/workspace/effects.js +303 -46
  211. package/dist/src/modules/workspace/effects.js.map +1 -1
  212. package/dist/src/modules/workspace/index.d.ts +313 -45
  213. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  214. package/dist/src/modules/workspace/index.js +1404 -304
  215. package/dist/src/modules/workspace/index.js.map +1 -1
  216. package/dist/src/modules/workspace/observe.d.ts +315 -16
  217. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  218. package/dist/src/modules/workspace/observe.js +727 -37
  219. package/dist/src/modules/workspace/observe.js.map +1 -1
  220. package/dist/src/modules/workspace/reconcile.d.ts +58 -7
  221. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  222. package/dist/src/modules/workspace/reconcile.js +521 -56
  223. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  224. package/dist/src/modules/workspace/types.d.ts +28 -5
  225. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  226. package/dist/src/modules/workspace/types.js.map +1 -1
  227. package/dist/src/modules/workspace/watcher.d.ts +49 -2
  228. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  229. package/dist/src/modules/workspace/watcher.js +158 -29
  230. package/dist/src/modules/workspace/watcher.js.map +1 -1
  231. package/dist/src/prose-drafts.d.ts +240 -0
  232. package/dist/src/prose-drafts.d.ts.map +1 -0
  233. package/dist/src/prose-drafts.js +362 -0
  234. package/dist/src/prose-drafts.js.map +1 -0
  235. package/dist/src/prose-segments.d.ts +21 -2
  236. package/dist/src/prose-segments.d.ts.map +1 -1
  237. package/dist/src/prose-segments.js +46 -9
  238. package/dist/src/prose-segments.js.map +1 -1
  239. package/dist/src/recovery/offline-branch.d.ts +1 -0
  240. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  241. package/dist/src/recovery/offline-branch.js +3 -1
  242. package/dist/src/recovery/offline-branch.js.map +1 -1
  243. package/dist/src/safe-slice.d.ts +5 -1
  244. package/dist/src/safe-slice.d.ts.map +1 -1
  245. package/dist/src/safe-slice.js +9 -1
  246. package/dist/src/safe-slice.js.map +1 -1
  247. package/dist/src/silent-heartbeat.d.ts +112 -39
  248. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  249. package/dist/src/silent-heartbeat.js +100 -29
  250. package/dist/src/silent-heartbeat.js.map +1 -1
  251. package/dist/src/tool-args.d.ts +40 -0
  252. package/dist/src/tool-args.d.ts.map +1 -0
  253. package/dist/src/tool-args.js +207 -0
  254. package/dist/src/tool-args.js.map +1 -0
  255. package/dist/src/tool-image-ledger.d.ts +87 -19
  256. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  257. package/dist/src/tool-image-ledger.js +135 -32
  258. package/dist/src/tool-image-ledger.js.map +1 -1
  259. package/dist/src/tool-presentation.d.ts +9 -0
  260. package/dist/src/tool-presentation.d.ts.map +1 -1
  261. package/dist/src/tool-presentation.js +266 -62
  262. package/dist/src/tool-presentation.js.map +1 -1
  263. package/dist/src/tool-result-guard.d.ts.map +1 -1
  264. package/dist/src/tool-result-guard.js +7 -10
  265. package/dist/src/tool-result-guard.js.map +1 -1
  266. package/dist/src/tool-result-history.d.ts +32 -3
  267. package/dist/src/tool-result-history.d.ts.map +1 -1
  268. package/dist/src/tool-result-history.js +106 -19
  269. package/dist/src/tool-result-history.js.map +1 -1
  270. package/dist/src/tune-out/coordinator.d.ts +25 -1
  271. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  272. package/dist/src/tune-out/coordinator.js +260 -65
  273. package/dist/src/tune-out/coordinator.js.map +1 -1
  274. package/dist/src/types/agent.d.ts +44 -22
  275. package/dist/src/types/agent.d.ts.map +1 -1
  276. package/dist/src/types/events.d.ts +25 -0
  277. package/dist/src/types/events.d.ts.map +1 -1
  278. package/dist/src/types/framework.d.ts +38 -7
  279. package/dist/src/types/framework.d.ts.map +1 -1
  280. package/dist/src/types/inference-log.d.ts +1 -1
  281. package/dist/src/types/inference-log.d.ts.map +1 -1
  282. package/dist/src/types/module.d.ts +77 -14
  283. package/dist/src/types/module.d.ts.map +1 -1
  284. package/dist/src/types/trace.d.ts +97 -8
  285. package/dist/src/types/trace.d.ts.map +1 -1
  286. package/dist/src/usage/usage-tracker.d.ts +5 -0
  287. package/dist/src/usage/usage-tracker.d.ts.map +1 -1
  288. package/dist/src/usage/usage-tracker.js +35 -8
  289. package/dist/src/usage/usage-tracker.js.map +1 -1
  290. package/package.json +5 -4
  291. package/src/agent.ts +257 -72
  292. package/src/api/mcp-server-core.ts +1 -1
  293. package/src/api/server.ts +4 -1
  294. package/src/api/types.ts +1 -1
  295. package/src/branch-names.ts +19 -0
  296. package/src/closest-names.ts +94 -0
  297. package/src/code-execution/index.ts +1 -0
  298. package/src/code-execution/py-runner.ts +304 -93
  299. package/src/code-execution/runtime-py.ts +94 -8
  300. package/src/code-execution/tool-definition.ts +85 -7
  301. package/src/config-checks.ts +50 -0
  302. package/src/deadline-timer.ts +44 -0
  303. package/src/error-message.ts +48 -0
  304. package/src/framework.ts +8625 -1984
  305. package/src/gate/event-gate.ts +38 -5
  306. package/src/held-writes.ts +59 -0
  307. package/src/index.ts +29 -5
  308. package/src/mcpl/capability-grant.ts +1 -1
  309. package/src/mcpl/channel-registry.ts +1095 -411
  310. package/src/mcpl/checkpoint-manager.ts +104 -29
  311. package/src/mcpl/conversation-router.ts +28 -8
  312. package/src/mcpl/convert-block.ts +105 -0
  313. package/src/mcpl/errors.ts +28 -0
  314. package/src/mcpl/hook-orchestrator.ts +84 -77
  315. package/src/mcpl/inbound-source.ts +156 -0
  316. package/src/mcpl/index.ts +23 -1
  317. package/src/mcpl/inference-router.ts +179 -103
  318. package/src/mcpl/log-value.ts +10 -0
  319. package/src/mcpl/modern-connection.ts +1182 -0
  320. package/src/mcpl/prefix-rule.ts +32 -0
  321. package/src/mcpl/protocol-family.ts +691 -0
  322. package/src/mcpl/push-coalescer.ts +110 -36
  323. package/src/mcpl/push-handler.ts +20 -90
  324. package/src/mcpl/reference-fetcher.ts +318 -67
  325. package/src/mcpl/references.ts +183 -34
  326. package/src/mcpl/server-connection.ts +1404 -280
  327. package/src/mcpl/server-registry.ts +63 -14
  328. package/src/mcpl/skip-reply-wake.ts +73 -0
  329. package/src/mcpl/tool-result-normalize.ts +294 -0
  330. package/src/mcpl/transport.ts +320 -76
  331. package/src/mcpl/types.ts +77 -19
  332. package/src/mcpl/visible-content.ts +28 -0
  333. package/src/module-registry.ts +126 -36
  334. package/src/modules/api/index.ts +3 -2
  335. package/src/modules/discord/discord-js-client.ts +306 -74
  336. package/src/modules/discord/index.ts +737 -201
  337. package/src/modules/discord/refusal.ts +13 -0
  338. package/src/modules/discord/types.ts +27 -6
  339. package/src/modules/health/index.ts +8 -3
  340. package/src/modules/history/index.ts +410 -120
  341. package/src/modules/history/semantic.ts +189 -32
  342. package/src/modules/index.ts +1 -1
  343. package/src/modules/unknown-tool.ts +16 -0
  344. package/src/modules/workspace/disk-agreement.ts +97 -8
  345. package/src/modules/workspace/effects.ts +291 -36
  346. package/src/modules/workspace/index.ts +1469 -321
  347. package/src/modules/workspace/observe.ts +784 -41
  348. package/src/modules/workspace/reconcile.ts +619 -64
  349. package/src/modules/workspace/types.ts +28 -5
  350. package/src/modules/workspace/watcher.ts +154 -30
  351. package/src/prose-drafts.ts +481 -0
  352. package/src/prose-segments.ts +51 -8
  353. package/src/recovery/offline-branch.ts +4 -1
  354. package/src/safe-slice.ts +9 -1
  355. package/src/silent-heartbeat.ts +184 -59
  356. package/src/tool-args.ts +213 -0
  357. package/src/tool-image-ledger.ts +153 -34
  358. package/src/tool-presentation.ts +233 -42
  359. package/src/tool-result-guard.ts +7 -8
  360. package/src/tool-result-history.ts +124 -20
  361. package/src/tune-out/coordinator.ts +259 -65
  362. package/src/types/agent.ts +44 -24
  363. package/src/types/events.ts +25 -0
  364. package/src/types/framework.ts +38 -7
  365. package/src/types/inference-log.ts +1 -1
  366. package/src/types/module.ts +79 -14
  367. package/src/types/trace.ts +103 -8
  368. package/src/usage/usage-tracker.ts +32 -8
  369. package/dist/test/activation-request-sanitize.test.d.ts +0 -2
  370. package/dist/test/activation-request-sanitize.test.d.ts.map +0 -1
  371. package/dist/test/activation-request-sanitize.test.js +0 -89
  372. package/dist/test/activation-request-sanitize.test.js.map +0 -1
  373. package/dist/test/agent-cache-ttl.test.d.ts +0 -2
  374. package/dist/test/agent-cache-ttl.test.d.ts.map +0 -1
  375. package/dist/test/agent-cache-ttl.test.js +0 -41
  376. package/dist/test/agent-cache-ttl.test.js.map +0 -1
  377. package/dist/test/agent-runtime-settings.test.d.ts +0 -2
  378. package/dist/test/agent-runtime-settings.test.d.ts.map +0 -1
  379. package/dist/test/agent-runtime-settings.test.js +0 -1598
  380. package/dist/test/agent-runtime-settings.test.js.map +0 -1
  381. package/dist/test/api-admin-gate.test.d.ts +0 -2
  382. package/dist/test/api-admin-gate.test.d.ts.map +0 -1
  383. package/dist/test/api-admin-gate.test.js +0 -116
  384. package/dist/test/api-admin-gate.test.js.map +0 -1
  385. package/dist/test/assistant-persist-refusal.test.d.ts +0 -2
  386. package/dist/test/assistant-persist-refusal.test.d.ts.map +0 -1
  387. package/dist/test/assistant-persist-refusal.test.js +0 -63
  388. package/dist/test/assistant-persist-refusal.test.js.map +0 -1
  389. package/dist/test/batched-wake-locus.test.d.ts +0 -2
  390. package/dist/test/batched-wake-locus.test.d.ts.map +0 -1
  391. package/dist/test/batched-wake-locus.test.js +0 -166
  392. package/dist/test/batched-wake-locus.test.js.map +0 -1
  393. package/dist/test/busy-agent-scheduler.test.d.ts +0 -2
  394. package/dist/test/busy-agent-scheduler.test.d.ts.map +0 -1
  395. package/dist/test/busy-agent-scheduler.test.js +0 -92
  396. package/dist/test/busy-agent-scheduler.test.js.map +0 -1
  397. package/dist/test/channel-incoming-targeting.test.d.ts +0 -2
  398. package/dist/test/channel-incoming-targeting.test.d.ts.map +0 -1
  399. package/dist/test/channel-incoming-targeting.test.js +0 -81
  400. package/dist/test/channel-incoming-targeting.test.js.map +0 -1
  401. package/dist/test/channel-label-history.test.d.ts +0 -2
  402. package/dist/test/channel-label-history.test.d.ts.map +0 -1
  403. package/dist/test/channel-label-history.test.js +0 -424
  404. package/dist/test/channel-label-history.test.js.map +0 -1
  405. package/dist/test/channel-lifecycle-provenance.test.d.ts +0 -2
  406. package/dist/test/channel-lifecycle-provenance.test.d.ts.map +0 -1
  407. package/dist/test/channel-lifecycle-provenance.test.js +0 -47
  408. package/dist/test/channel-lifecycle-provenance.test.js.map +0 -1
  409. package/dist/test/channel-registry-routing.test.d.ts +0 -2
  410. package/dist/test/channel-registry-routing.test.d.ts.map +0 -1
  411. package/dist/test/channel-registry-routing.test.js +0 -446
  412. package/dist/test/channel-registry-routing.test.js.map +0 -1
  413. package/dist/test/channel-registry-typing.test.d.ts +0 -2
  414. package/dist/test/channel-registry-typing.test.d.ts.map +0 -1
  415. package/dist/test/channel-registry-typing.test.js +0 -91
  416. package/dist/test/channel-registry-typing.test.js.map +0 -1
  417. package/dist/test/channels-changed-respond-first.test.d.ts +0 -2
  418. package/dist/test/channels-changed-respond-first.test.d.ts.map +0 -1
  419. package/dist/test/channels-changed-respond-first.test.js +0 -74
  420. package/dist/test/channels-changed-respond-first.test.js.map +0 -1
  421. package/dist/test/checkpoint-attribution.test.d.ts +0 -2
  422. package/dist/test/checkpoint-attribution.test.d.ts.map +0 -1
  423. package/dist/test/checkpoint-attribution.test.js +0 -200
  424. package/dist/test/checkpoint-attribution.test.js.map +0 -1
  425. package/dist/test/code-execution.test.d.ts +0 -11
  426. package/dist/test/code-execution.test.d.ts.map +0 -1
  427. package/dist/test/code-execution.test.js +0 -1272
  428. package/dist/test/code-execution.test.js.map +0 -1
  429. package/dist/test/component-defaults.test.d.ts +0 -2
  430. package/dist/test/component-defaults.test.d.ts.map +0 -1
  431. package/dist/test/component-defaults.test.js +0 -54
  432. package/dist/test/component-defaults.test.js.map +0 -1
  433. package/dist/test/conversation-fork-injection-scoping.test.d.ts +0 -2
  434. package/dist/test/conversation-fork-injection-scoping.test.d.ts.map +0 -1
  435. package/dist/test/conversation-fork-injection-scoping.test.js +0 -200
  436. package/dist/test/conversation-fork-injection-scoping.test.js.map +0 -1
  437. package/dist/test/conversation-router.test.d.ts +0 -2
  438. package/dist/test/conversation-router.test.d.ts.map +0 -1
  439. package/dist/test/conversation-router.test.js +0 -174
  440. package/dist/test/conversation-router.test.js.map +0 -1
  441. package/dist/test/conversation-routing.test.d.ts +0 -7
  442. package/dist/test/conversation-routing.test.d.ts.map +0 -1
  443. package/dist/test/conversation-routing.test.js +0 -621
  444. package/dist/test/conversation-routing.test.js.map +0 -1
  445. package/dist/test/deferred-wake-integrity.test.d.ts +0 -29
  446. package/dist/test/deferred-wake-integrity.test.d.ts.map +0 -1
  447. package/dist/test/deferred-wake-integrity.test.js +0 -320
  448. package/dist/test/deferred-wake-integrity.test.js.map +0 -1
  449. package/dist/test/discord-awareness-outbox.test.d.ts +0 -2
  450. package/dist/test/discord-awareness-outbox.test.d.ts.map +0 -1
  451. package/dist/test/discord-awareness-outbox.test.js +0 -371
  452. package/dist/test/discord-awareness-outbox.test.js.map +0 -1
  453. package/dist/test/discord-history-delete-window.test.d.ts +0 -2
  454. package/dist/test/discord-history-delete-window.test.d.ts.map +0 -1
  455. package/dist/test/discord-history-delete-window.test.js +0 -81
  456. package/dist/test/discord-history-delete-window.test.js.map +0 -1
  457. package/dist/test/discord-history-sync.test.d.ts +0 -2
  458. package/dist/test/discord-history-sync.test.d.ts.map +0 -1
  459. package/dist/test/discord-history-sync.test.js +0 -56
  460. package/dist/test/discord-history-sync.test.js.map +0 -1
  461. package/dist/test/discord-typing-keepalive.test.d.ts +0 -2
  462. package/dist/test/discord-typing-keepalive.test.d.ts.map +0 -1
  463. package/dist/test/discord-typing-keepalive.test.js +0 -77
  464. package/dist/test/discord-typing-keepalive.test.js.map +0 -1
  465. package/dist/test/ephemeral-settle.test.d.ts +0 -2
  466. package/dist/test/ephemeral-settle.test.d.ts.map +0 -1
  467. package/dist/test/ephemeral-settle.test.js +0 -354
  468. package/dist/test/ephemeral-settle.test.js.map +0 -1
  469. package/dist/test/event-gate-passthrough.test.d.ts +0 -15
  470. package/dist/test/event-gate-passthrough.test.d.ts.map +0 -1
  471. package/dist/test/event-gate-passthrough.test.js +0 -289
  472. package/dist/test/event-gate-passthrough.test.js.map +0 -1
  473. package/dist/test/event-gate-policy-mutation.test.d.ts +0 -2
  474. package/dist/test/event-gate-policy-mutation.test.d.ts.map +0 -1
  475. package/dist/test/event-gate-policy-mutation.test.js +0 -137
  476. package/dist/test/event-gate-policy-mutation.test.js.map +0 -1
  477. package/dist/test/event-gate-rate-limit.test.d.ts +0 -2
  478. package/dist/test/event-gate-rate-limit.test.d.ts.map +0 -1
  479. package/dist/test/event-gate-rate-limit.test.js +0 -627
  480. package/dist/test/event-gate-rate-limit.test.js.map +0 -1
  481. package/dist/test/event-gate-script.test.d.ts +0 -2
  482. package/dist/test/event-gate-script.test.d.ts.map +0 -1
  483. package/dist/test/event-gate-script.test.js +0 -95
  484. package/dist/test/event-gate-script.test.js.map +0 -1
  485. package/dist/test/event-gate-selfwake.test.d.ts +0 -15
  486. package/dist/test/event-gate-selfwake.test.d.ts.map +0 -1
  487. package/dist/test/event-gate-selfwake.test.js +0 -125
  488. package/dist/test/event-gate-selfwake.test.js.map +0 -1
  489. package/dist/test/event-gate-tags.test.d.ts +0 -2
  490. package/dist/test/event-gate-tags.test.d.ts.map +0 -1
  491. package/dist/test/event-gate-tags.test.js +0 -76
  492. package/dist/test/event-gate-tags.test.js.map +0 -1
  493. package/dist/test/event-gate.test.d.ts +0 -2
  494. package/dist/test/event-gate.test.d.ts.map +0 -1
  495. package/dist/test/event-gate.test.js +0 -919
  496. package/dist/test/event-gate.test.js.map +0 -1
  497. package/dist/test/explicit-prose-routing.test.d.ts +0 -11
  498. package/dist/test/explicit-prose-routing.test.d.ts.map +0 -1
  499. package/dist/test/explicit-prose-routing.test.js +0 -1715
  500. package/dist/test/explicit-prose-routing.test.js.map +0 -1
  501. package/dist/test/framework.test.d.ts +0 -5
  502. package/dist/test/framework.test.d.ts.map +0 -1
  503. package/dist/test/framework.test.js +0 -1048
  504. package/dist/test/framework.test.js.map +0 -1
  505. package/dist/test/gather-context-timeout.test.d.ts +0 -2
  506. package/dist/test/gather-context-timeout.test.d.ts.map +0 -1
  507. package/dist/test/gather-context-timeout.test.js +0 -120
  508. package/dist/test/gather-context-timeout.test.js.map +0 -1
  509. package/dist/test/health-snapshot.test.d.ts +0 -2
  510. package/dist/test/health-snapshot.test.d.ts.map +0 -1
  511. package/dist/test/health-snapshot.test.js +0 -74
  512. package/dist/test/health-snapshot.test.js.map +0 -1
  513. package/dist/test/helpers/coalescing-fixture.d.ts +0 -74
  514. package/dist/test/helpers/coalescing-fixture.d.ts.map +0 -1
  515. package/dist/test/helpers/coalescing-fixture.js +0 -152
  516. package/dist/test/helpers/coalescing-fixture.js.map +0 -1
  517. package/dist/test/helpers/coalescing-kill-child.d.ts +0 -2
  518. package/dist/test/helpers/coalescing-kill-child.d.ts.map +0 -1
  519. package/dist/test/helpers/coalescing-kill-child.js +0 -24
  520. package/dist/test/helpers/coalescing-kill-child.js.map +0 -1
  521. package/dist/test/helpers/mock-membrane.d.ts +0 -53
  522. package/dist/test/helpers/mock-membrane.d.ts.map +0 -1
  523. package/dist/test/helpers/mock-membrane.js +0 -168
  524. package/dist/test/helpers/mock-membrane.js.map +0 -1
  525. package/dist/test/helpers/pytc-stop-rows.d.ts +0 -11
  526. package/dist/test/helpers/pytc-stop-rows.d.ts.map +0 -1
  527. package/dist/test/helpers/pytc-stop-rows.js +0 -143
  528. package/dist/test/helpers/pytc-stop-rows.js.map +0 -1
  529. package/dist/test/helpers/quiesce-crash-child.d.ts +0 -2
  530. package/dist/test/helpers/quiesce-crash-child.d.ts.map +0 -1
  531. package/dist/test/helpers/quiesce-crash-child.js +0 -156
  532. package/dist/test/helpers/quiesce-crash-child.js.map +0 -1
  533. package/dist/test/hide-then-turn.test.d.ts +0 -2
  534. package/dist/test/hide-then-turn.test.d.ts.map +0 -1
  535. package/dist/test/hide-then-turn.test.js +0 -101
  536. package/dist/test/hide-then-turn.test.js.map +0 -1
  537. package/dist/test/history-module-overview.test.d.ts +0 -2
  538. package/dist/test/history-module-overview.test.d.ts.map +0 -1
  539. package/dist/test/history-module-overview.test.js +0 -711
  540. package/dist/test/history-module-overview.test.js.map +0 -1
  541. package/dist/test/history-module-realstore.test.d.ts +0 -2
  542. package/dist/test/history-module-realstore.test.d.ts.map +0 -1
  543. package/dist/test/history-module-realstore.test.js +0 -363
  544. package/dist/test/history-module-realstore.test.js.map +0 -1
  545. package/dist/test/history-module-semantic.test.d.ts +0 -2
  546. package/dist/test/history-module-semantic.test.d.ts.map +0 -1
  547. package/dist/test/history-module-semantic.test.js +0 -213
  548. package/dist/test/history-module-semantic.test.js.map +0 -1
  549. package/dist/test/history-module-ux.test.d.ts +0 -2
  550. package/dist/test/history-module-ux.test.d.ts.map +0 -1
  551. package/dist/test/history-module-ux.test.js +0 -304
  552. package/dist/test/history-module-ux.test.js.map +0 -1
  553. package/dist/test/history-module.test.d.ts +0 -2
  554. package/dist/test/history-module.test.d.ts.map +0 -1
  555. package/dist/test/history-module.test.js +0 -619
  556. package/dist/test/history-module.test.js.map +0 -1
  557. package/dist/test/history-semantic-review-findings.test.d.ts +0 -2
  558. package/dist/test/history-semantic-review-findings.test.d.ts.map +0 -1
  559. package/dist/test/history-semantic-review-findings.test.js +0 -338
  560. package/dist/test/history-semantic-review-findings.test.js.map +0 -1
  561. package/dist/test/history-semantic-sync-converges.test.d.ts +0 -2
  562. package/dist/test/history-semantic-sync-converges.test.d.ts.map +0 -1
  563. package/dist/test/history-semantic-sync-converges.test.js +0 -109
  564. package/dist/test/history-semantic-sync-converges.test.js.map +0 -1
  565. package/dist/test/host-quiesce.test.d.ts +0 -2
  566. package/dist/test/host-quiesce.test.d.ts.map +0 -1
  567. package/dist/test/host-quiesce.test.js +0 -988
  568. package/dist/test/host-quiesce.test.js.map +0 -1
  569. package/dist/test/inference-exhaustion-rewind.test.d.ts +0 -2
  570. package/dist/test/inference-exhaustion-rewind.test.d.ts.map +0 -1
  571. package/dist/test/inference-exhaustion-rewind.test.js +0 -387
  572. package/dist/test/inference-exhaustion-rewind.test.js.map +0 -1
  573. package/dist/test/inference-failure-bounds.test.d.ts +0 -2
  574. package/dist/test/inference-failure-bounds.test.d.ts.map +0 -1
  575. package/dist/test/inference-failure-bounds.test.js +0 -1202
  576. package/dist/test/inference-failure-bounds.test.js.map +0 -1
  577. package/dist/test/inference-failure-observability.test.d.ts +0 -2
  578. package/dist/test/inference-failure-observability.test.d.ts.map +0 -1
  579. package/dist/test/inference-failure-observability.test.js +0 -84
  580. package/dist/test/inference-failure-observability.test.js.map +0 -1
  581. package/dist/test/journal-tool.test.d.ts +0 -2
  582. package/dist/test/journal-tool.test.d.ts.map +0 -1
  583. package/dist/test/journal-tool.test.js +0 -33
  584. package/dist/test/journal-tool.test.js.map +0 -1
  585. package/dist/test/kv-unified-open-flight-retry.test.d.ts +0 -2
  586. package/dist/test/kv-unified-open-flight-retry.test.d.ts.map +0 -1
  587. package/dist/test/kv-unified-open-flight-retry.test.js +0 -189
  588. package/dist/test/kv-unified-open-flight-retry.test.js.map +0 -1
  589. package/dist/test/kv-unified-wiring.test.d.ts +0 -2
  590. package/dist/test/kv-unified-wiring.test.d.ts.map +0 -1
  591. package/dist/test/kv-unified-wiring.test.js +0 -95
  592. package/dist/test/kv-unified-wiring.test.js.map +0 -1
  593. package/dist/test/late-events-after-stop.test.d.ts +0 -2
  594. package/dist/test/late-events-after-stop.test.d.ts.map +0 -1
  595. package/dist/test/late-events-after-stop.test.js +0 -122
  596. package/dist/test/late-events-after-stop.test.js.map +0 -1
  597. package/dist/test/liveness-watchdog.test.d.ts +0 -2
  598. package/dist/test/liveness-watchdog.test.d.ts.map +0 -1
  599. package/dist/test/liveness-watchdog.test.js +0 -94
  600. package/dist/test/liveness-watchdog.test.js.map +0 -1
  601. package/dist/test/machine-close-provenance.test.d.ts +0 -2
  602. package/dist/test/machine-close-provenance.test.d.ts.map +0 -1
  603. package/dist/test/machine-close-provenance.test.js +0 -116
  604. package/dist/test/machine-close-provenance.test.js.map +0 -1
  605. package/dist/test/maintenance-timer.test.d.ts +0 -2
  606. package/dist/test/maintenance-timer.test.d.ts.map +0 -1
  607. package/dist/test/maintenance-timer.test.js +0 -1827
  608. package/dist/test/maintenance-timer.test.js.map +0 -1
  609. package/dist/test/mcp-server-bin.test.d.ts +0 -2
  610. package/dist/test/mcp-server-bin.test.d.ts.map +0 -1
  611. package/dist/test/mcp-server-bin.test.js +0 -142
  612. package/dist/test/mcp-server-bin.test.js.map +0 -1
  613. package/dist/test/mcpl-access-provider.test.d.ts +0 -2
  614. package/dist/test/mcpl-access-provider.test.d.ts.map +0 -1
  615. package/dist/test/mcpl-access-provider.test.js +0 -20
  616. package/dist/test/mcpl-access-provider.test.js.map +0 -1
  617. package/dist/test/mcpl-awareness-barrier.test.d.ts +0 -2
  618. package/dist/test/mcpl-awareness-barrier.test.d.ts.map +0 -1
  619. package/dist/test/mcpl-awareness-barrier.test.js +0 -769
  620. package/dist/test/mcpl-awareness-barrier.test.js.map +0 -1
  621. package/dist/test/mcpl-capability-grant.test.d.ts +0 -10
  622. package/dist/test/mcpl-capability-grant.test.d.ts.map +0 -1
  623. package/dist/test/mcpl-capability-grant.test.js +0 -111
  624. package/dist/test/mcpl-capability-grant.test.js.map +0 -1
  625. package/dist/test/mcpl-capability-scoping.test.d.ts +0 -2
  626. package/dist/test/mcpl-capability-scoping.test.d.ts.map +0 -1
  627. package/dist/test/mcpl-capability-scoping.test.js +0 -266
  628. package/dist/test/mcpl-capability-scoping.test.js.map +0 -1
  629. package/dist/test/mcpl-child-env.test.d.ts +0 -2
  630. package/dist/test/mcpl-child-env.test.d.ts.map +0 -1
  631. package/dist/test/mcpl-child-env.test.js +0 -158
  632. package/dist/test/mcpl-child-env.test.js.map +0 -1
  633. package/dist/test/mcpl-coalesced-content-parity.test.d.ts +0 -2
  634. package/dist/test/mcpl-coalesced-content-parity.test.d.ts.map +0 -1
  635. package/dist/test/mcpl-coalesced-content-parity.test.js +0 -532
  636. package/dist/test/mcpl-coalesced-content-parity.test.js.map +0 -1
  637. package/dist/test/mcpl-coalescing.test.d.ts +0 -2
  638. package/dist/test/mcpl-coalescing.test.d.ts.map +0 -1
  639. package/dist/test/mcpl-coalescing.test.js +0 -1125
  640. package/dist/test/mcpl-coalescing.test.js.map +0 -1
  641. package/dist/test/mcpl-config-diagnostics.test.d.ts +0 -2
  642. package/dist/test/mcpl-config-diagnostics.test.d.ts.map +0 -1
  643. package/dist/test/mcpl-config-diagnostics.test.js +0 -452
  644. package/dist/test/mcpl-config-diagnostics.test.js.map +0 -1
  645. package/dist/test/mcpl-empty-content.test.d.ts +0 -2
  646. package/dist/test/mcpl-empty-content.test.d.ts.map +0 -1
  647. package/dist/test/mcpl-empty-content.test.js +0 -368
  648. package/dist/test/mcpl-empty-content.test.js.map +0 -1
  649. package/dist/test/mcpl-gate-roundtrip.test.d.ts +0 -2
  650. package/dist/test/mcpl-gate-roundtrip.test.d.ts.map +0 -1
  651. package/dist/test/mcpl-gate-roundtrip.test.js +0 -285
  652. package/dist/test/mcpl-gate-roundtrip.test.js.map +0 -1
  653. package/dist/test/mcpl-list-status.test.d.ts +0 -2
  654. package/dist/test/mcpl-list-status.test.d.ts.map +0 -1
  655. package/dist/test/mcpl-list-status.test.js +0 -82
  656. package/dist/test/mcpl-list-status.test.js.map +0 -1
  657. package/dist/test/mcpl-manifest-changed.test.d.ts +0 -2
  658. package/dist/test/mcpl-manifest-changed.test.d.ts.map +0 -1
  659. package/dist/test/mcpl-manifest-changed.test.js +0 -178
  660. package/dist/test/mcpl-manifest-changed.test.js.map +0 -1
  661. package/dist/test/mcpl-push-convert.test.d.ts +0 -2
  662. package/dist/test/mcpl-push-convert.test.d.ts.map +0 -1
  663. package/dist/test/mcpl-push-convert.test.js +0 -42
  664. package/dist/test/mcpl-push-convert.test.js.map +0 -1
  665. package/dist/test/mcpl-reconnect-events.test.d.ts +0 -2
  666. package/dist/test/mcpl-reconnect-events.test.d.ts.map +0 -1
  667. package/dist/test/mcpl-reconnect-events.test.js +0 -144
  668. package/dist/test/mcpl-reconnect-events.test.js.map +0 -1
  669. package/dist/test/mcpl-reconnect-reregistration.test.d.ts +0 -2
  670. package/dist/test/mcpl-reconnect-reregistration.test.d.ts.map +0 -1
  671. package/dist/test/mcpl-reconnect-reregistration.test.js +0 -238
  672. package/dist/test/mcpl-reconnect-reregistration.test.js.map +0 -1
  673. package/dist/test/mcpl-reference-fetcher.test.d.ts +0 -2
  674. package/dist/test/mcpl-reference-fetcher.test.d.ts.map +0 -1
  675. package/dist/test/mcpl-reference-fetcher.test.js +0 -427
  676. package/dist/test/mcpl-reference-fetcher.test.js.map +0 -1
  677. package/dist/test/mcpl-reference-fields.test.d.ts +0 -2
  678. package/dist/test/mcpl-reference-fields.test.d.ts.map +0 -1
  679. package/dist/test/mcpl-reference-fields.test.js +0 -149
  680. package/dist/test/mcpl-reference-fields.test.js.map +0 -1
  681. package/dist/test/mcpl-rejection-log-lines.test.d.ts +0 -2
  682. package/dist/test/mcpl-rejection-log-lines.test.d.ts.map +0 -1
  683. package/dist/test/mcpl-rejection-log-lines.test.js +0 -349
  684. package/dist/test/mcpl-rejection-log-lines.test.js.map +0 -1
  685. package/dist/test/mcpl-request-timeout.test.d.ts +0 -2
  686. package/dist/test/mcpl-request-timeout.test.d.ts.map +0 -1
  687. package/dist/test/mcpl-request-timeout.test.js +0 -138
  688. package/dist/test/mcpl-request-timeout.test.js.map +0 -1
  689. package/dist/test/mcpl-runtime-lifecycle.test.d.ts +0 -7
  690. package/dist/test/mcpl-runtime-lifecycle.test.d.ts.map +0 -1
  691. package/dist/test/mcpl-runtime-lifecycle.test.js +0 -142
  692. package/dist/test/mcpl-runtime-lifecycle.test.js.map +0 -1
  693. package/dist/test/mcpl-stderr-tail.test.d.ts +0 -2
  694. package/dist/test/mcpl-stderr-tail.test.d.ts.map +0 -1
  695. package/dist/test/mcpl-stderr-tail.test.js +0 -210
  696. package/dist/test/mcpl-stderr-tail.test.js.map +0 -1
  697. package/dist/test/mcpl-tool-class-listing.test.d.ts +0 -2
  698. package/dist/test/mcpl-tool-class-listing.test.d.ts.map +0 -1
  699. package/dist/test/mcpl-tool-class-listing.test.js +0 -147
  700. package/dist/test/mcpl-tool-class-listing.test.js.map +0 -1
  701. package/dist/test/mcpl-tool-classes.test.d.ts +0 -7
  702. package/dist/test/mcpl-tool-classes.test.d.ts.map +0 -1
  703. package/dist/test/mcpl-tool-classes.test.js +0 -80
  704. package/dist/test/mcpl-tool-classes.test.js.map +0 -1
  705. package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts +0 -2
  706. package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts.map +0 -1
  707. package/dist/test/mcpl-tool-lifecycle-e2e.test.js +0 -237
  708. package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +0 -1
  709. package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts +0 -11
  710. package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts.map +0 -1
  711. package/dist/test/mcpl-tool-lifecycle-emitter.test.js +0 -412
  712. package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +0 -1
  713. package/dist/test/mcpl-tool-lifecycle-script.test.d.ts +0 -2
  714. package/dist/test/mcpl-tool-lifecycle-script.test.d.ts.map +0 -1
  715. package/dist/test/mcpl-tool-lifecycle-script.test.js +0 -288
  716. package/dist/test/mcpl-tool-lifecycle-script.test.js.map +0 -1
  717. package/dist/test/mcpl-tool-lifecycle.test.d.ts +0 -11
  718. package/dist/test/mcpl-tool-lifecycle.test.d.ts.map +0 -1
  719. package/dist/test/mcpl-tool-lifecycle.test.js +0 -270
  720. package/dist/test/mcpl-tool-lifecycle.test.js.map +0 -1
  721. package/dist/test/mcpl-transport-close.test.d.ts +0 -2
  722. package/dist/test/mcpl-transport-close.test.d.ts.map +0 -1
  723. package/dist/test/mcpl-transport-close.test.js +0 -373
  724. package/dist/test/mcpl-transport-close.test.js.map +0 -1
  725. package/dist/test/mcpl-transport-spawn.test.d.ts +0 -2
  726. package/dist/test/mcpl-transport-spawn.test.d.ts.map +0 -1
  727. package/dist/test/mcpl-transport-spawn.test.js +0 -58
  728. package/dist/test/mcpl-transport-spawn.test.js.map +0 -1
  729. package/dist/test/mcpl-ws-transport.test.d.ts +0 -2
  730. package/dist/test/mcpl-ws-transport.test.d.ts.map +0 -1
  731. package/dist/test/mcpl-ws-transport.test.js +0 -259
  732. package/dist/test/mcpl-ws-transport.test.js.map +0 -1
  733. package/dist/test/module-context-integration.test.d.ts +0 -18
  734. package/dist/test/module-context-integration.test.d.ts.map +0 -1
  735. package/dist/test/module-context-integration.test.js +0 -181
  736. package/dist/test/module-context-integration.test.js.map +0 -1
  737. package/dist/test/offline-recovery-branch.test.d.ts +0 -2
  738. package/dist/test/offline-recovery-branch.test.d.ts.map +0 -1
  739. package/dist/test/offline-recovery-branch.test.js +0 -570
  740. package/dist/test/offline-recovery-branch.test.js.map +0 -1
  741. package/dist/test/operator-surgery.test.d.ts +0 -2
  742. package/dist/test/operator-surgery.test.d.ts.map +0 -1
  743. package/dist/test/operator-surgery.test.js +0 -595
  744. package/dist/test/operator-surgery.test.js.map +0 -1
  745. package/dist/test/ops-alert.test.d.ts +0 -2
  746. package/dist/test/ops-alert.test.d.ts.map +0 -1
  747. package/dist/test/ops-alert.test.js +0 -153
  748. package/dist/test/ops-alert.test.js.map +0 -1
  749. package/dist/test/overbudget-drain-breaker.test.d.ts +0 -2
  750. package/dist/test/overbudget-drain-breaker.test.d.ts.map +0 -1
  751. package/dist/test/overbudget-drain-breaker.test.js +0 -198
  752. package/dist/test/overbudget-drain-breaker.test.js.map +0 -1
  753. package/dist/test/pack-installs-alone.test.d.ts +0 -2
  754. package/dist/test/pack-installs-alone.test.d.ts.map +0 -1
  755. package/dist/test/pack-installs-alone.test.js +0 -284
  756. package/dist/test/pack-installs-alone.test.js.map +0 -1
  757. package/dist/test/per-agent-delivery.test.d.ts +0 -2
  758. package/dist/test/per-agent-delivery.test.d.ts.map +0 -1
  759. package/dist/test/per-agent-delivery.test.js +0 -108
  760. package/dist/test/per-agent-delivery.test.js.map +0 -1
  761. package/dist/test/physical-window-restart.test.d.ts +0 -16
  762. package/dist/test/physical-window-restart.test.d.ts.map +0 -1
  763. package/dist/test/physical-window-restart.test.js +0 -487
  764. package/dist/test/physical-window-restart.test.js.map +0 -1
  765. package/dist/test/placed-reaction-baseline.test.d.ts +0 -2
  766. package/dist/test/placed-reaction-baseline.test.d.ts.map +0 -1
  767. package/dist/test/placed-reaction-baseline.test.js +0 -312
  768. package/dist/test/placed-reaction-baseline.test.js.map +0 -1
  769. package/dist/test/present-while-acting.test.d.ts +0 -19
  770. package/dist/test/present-while-acting.test.d.ts.map +0 -1
  771. package/dist/test/present-while-acting.test.js +0 -1649
  772. package/dist/test/present-while-acting.test.js.map +0 -1
  773. package/dist/test/prose-segments.test.d.ts +0 -2
  774. package/dist/test/prose-segments.test.d.ts.map +0 -1
  775. package/dist/test/prose-segments.test.js +0 -52
  776. package/dist/test/prose-segments.test.js.map +0 -1
  777. package/dist/test/prose-stream-router.test.d.ts +0 -2
  778. package/dist/test/prose-stream-router.test.d.ts.map +0 -1
  779. package/dist/test/prose-stream-router.test.js +0 -171
  780. package/dist/test/prose-stream-router.test.js.map +0 -1
  781. package/dist/test/provider-acceleration-cooldown.test.d.ts +0 -2
  782. package/dist/test/provider-acceleration-cooldown.test.d.ts.map +0 -1
  783. package/dist/test/provider-acceleration-cooldown.test.js +0 -435
  784. package/dist/test/provider-acceleration-cooldown.test.js.map +0 -1
  785. package/dist/test/provider-waits.test.d.ts +0 -2
  786. package/dist/test/provider-waits.test.d.ts.map +0 -1
  787. package/dist/test/provider-waits.test.js +0 -2259
  788. package/dist/test/provider-waits.test.js.map +0 -1
  789. package/dist/test/puppet-tool-call.test.d.ts +0 -2
  790. package/dist/test/puppet-tool-call.test.d.ts.map +0 -1
  791. package/dist/test/puppet-tool-call.test.js +0 -226
  792. package/dist/test/puppet-tool-call.test.js.map +0 -1
  793. package/dist/test/puppet-turn-reservation.test.d.ts +0 -2
  794. package/dist/test/puppet-turn-reservation.test.d.ts.map +0 -1
  795. package/dist/test/puppet-turn-reservation.test.js +0 -139
  796. package/dist/test/puppet-turn-reservation.test.js.map +0 -1
  797. package/dist/test/reaction-baseline-platform.test.d.ts +0 -2
  798. package/dist/test/reaction-baseline-platform.test.d.ts.map +0 -1
  799. package/dist/test/reaction-baseline-platform.test.js +0 -124
  800. package/dist/test/reaction-baseline-platform.test.js.map +0 -1
  801. package/dist/test/record-journal.test.d.ts +0 -6
  802. package/dist/test/record-journal.test.d.ts.map +0 -1
  803. package/dist/test/record-journal.test.js +0 -269
  804. package/dist/test/record-journal.test.js.map +0 -1
  805. package/dist/test/refusal-reaction-baseline.test.d.ts +0 -2
  806. package/dist/test/refusal-reaction-baseline.test.d.ts.map +0 -1
  807. package/dist/test/refusal-reaction-baseline.test.js +0 -81
  808. package/dist/test/refusal-reaction-baseline.test.js.map +0 -1
  809. package/dist/test/refusal-rewind.test.d.ts +0 -2
  810. package/dist/test/refusal-rewind.test.d.ts.map +0 -1
  811. package/dist/test/refusal-rewind.test.js +0 -117
  812. package/dist/test/refusal-rewind.test.js.map +0 -1
  813. package/dist/test/refusal-single-call.test.d.ts +0 -2
  814. package/dist/test/refusal-single-call.test.d.ts.map +0 -1
  815. package/dist/test/refusal-single-call.test.js +0 -177
  816. package/dist/test/refusal-single-call.test.js.map +0 -1
  817. package/dist/test/release-bump.test.d.ts +0 -2
  818. package/dist/test/release-bump.test.d.ts.map +0 -1
  819. package/dist/test/release-bump.test.js +0 -248
  820. package/dist/test/release-bump.test.js.map +0 -1
  821. package/dist/test/release-changelog.test.d.ts +0 -2
  822. package/dist/test/release-changelog.test.d.ts.map +0 -1
  823. package/dist/test/release-changelog.test.js +0 -159
  824. package/dist/test/release-changelog.test.js.map +0 -1
  825. package/dist/test/release-holds.test.d.ts +0 -2
  826. package/dist/test/release-holds.test.d.ts.map +0 -1
  827. package/dist/test/release-holds.test.js +0 -456
  828. package/dist/test/release-holds.test.js.map +0 -1
  829. package/dist/test/same-round-think-policy.test.d.ts +0 -2
  830. package/dist/test/same-round-think-policy.test.d.ts.map +0 -1
  831. package/dist/test/same-round-think-policy.test.js +0 -853
  832. package/dist/test/same-round-think-policy.test.js.map +0 -1
  833. package/dist/test/save-recent-image.test.d.ts +0 -26
  834. package/dist/test/save-recent-image.test.d.ts.map +0 -1
  835. package/dist/test/save-recent-image.test.js +0 -974
  836. package/dist/test/save-recent-image.test.js.map +0 -1
  837. package/dist/test/shutdown-errors.test.d.ts +0 -2
  838. package/dist/test/shutdown-errors.test.d.ts.map +0 -1
  839. package/dist/test/shutdown-errors.test.js +0 -662
  840. package/dist/test/shutdown-errors.test.js.map +0 -1
  841. package/dist/test/silent-heartbeat-rows.test.d.ts +0 -2
  842. package/dist/test/silent-heartbeat-rows.test.d.ts.map +0 -1
  843. package/dist/test/silent-heartbeat-rows.test.js +0 -451
  844. package/dist/test/silent-heartbeat-rows.test.js.map +0 -1
  845. package/dist/test/silent-heartbeat.test.d.ts +0 -2
  846. package/dist/test/silent-heartbeat.test.d.ts.map +0 -1
  847. package/dist/test/silent-heartbeat.test.js +0 -165
  848. package/dist/test/silent-heartbeat.test.js.map +0 -1
  849. package/dist/test/state-scaling-e2e.test.d.ts +0 -22
  850. package/dist/test/state-scaling-e2e.test.d.ts.map +0 -1
  851. package/dist/test/state-scaling-e2e.test.js +0 -206
  852. package/dist/test/state-scaling-e2e.test.js.map +0 -1
  853. package/dist/test/state-scaling.test.d.ts +0 -12
  854. package/dist/test/state-scaling.test.d.ts.map +0 -1
  855. package/dist/test/state-scaling.test.js +0 -251
  856. package/dist/test/state-scaling.test.js.map +0 -1
  857. package/dist/test/stop-with-host-operations.test.d.ts +0 -20
  858. package/dist/test/stop-with-host-operations.test.d.ts.map +0 -1
  859. package/dist/test/stop-with-host-operations.test.js +0 -1174
  860. package/dist/test/stop-with-host-operations.test.js.map +0 -1
  861. package/dist/test/stop-with-work-in-flight.test.d.ts +0 -23
  862. package/dist/test/stop-with-work-in-flight.test.d.ts.map +0 -1
  863. package/dist/test/stop-with-work-in-flight.test.js +0 -784
  864. package/dist/test/stop-with-work-in-flight.test.js.map +0 -1
  865. package/dist/test/surgery-marks-receipt.test.d.ts +0 -2
  866. package/dist/test/surgery-marks-receipt.test.d.ts.map +0 -1
  867. package/dist/test/surgery-marks-receipt.test.js +0 -540
  868. package/dist/test/surgery-marks-receipt.test.js.map +0 -1
  869. package/dist/test/thinking-token-stamp.test.d.ts +0 -12
  870. package/dist/test/thinking-token-stamp.test.d.ts.map +0 -1
  871. package/dist/test/thinking-token-stamp.test.js +0 -129
  872. package/dist/test/thinking-token-stamp.test.js.map +0 -1
  873. package/dist/test/timezone.test.d.ts +0 -2
  874. package/dist/test/timezone.test.d.ts.map +0 -1
  875. package/dist/test/timezone.test.js +0 -18
  876. package/dist/test/timezone.test.js.map +0 -1
  877. package/dist/test/tool-batch-hook.test.d.ts +0 -8
  878. package/dist/test/tool-batch-hook.test.d.ts.map +0 -1
  879. package/dist/test/tool-batch-hook.test.js +0 -275
  880. package/dist/test/tool-batch-hook.test.js.map +0 -1
  881. package/dist/test/tool-policy.test.d.ts +0 -2
  882. package/dist/test/tool-policy.test.d.ts.map +0 -1
  883. package/dist/test/tool-policy.test.js +0 -51
  884. package/dist/test/tool-policy.test.js.map +0 -1
  885. package/dist/test/tool-presentation.test.d.ts +0 -2
  886. package/dist/test/tool-presentation.test.d.ts.map +0 -1
  887. package/dist/test/tool-presentation.test.js +0 -268
  888. package/dist/test/tool-presentation.test.js.map +0 -1
  889. package/dist/test/tool-result-content.test.d.ts +0 -2
  890. package/dist/test/tool-result-content.test.d.ts.map +0 -1
  891. package/dist/test/tool-result-content.test.js +0 -37
  892. package/dist/test/tool-result-content.test.js.map +0 -1
  893. package/dist/test/tool-result-guard.test.d.ts +0 -2
  894. package/dist/test/tool-result-guard.test.d.ts.map +0 -1
  895. package/dist/test/tool-result-guard.test.js +0 -3729
  896. package/dist/test/tool-result-guard.test.js.map +0 -1
  897. package/dist/test/tool-result-history.test.d.ts +0 -7
  898. package/dist/test/tool-result-history.test.d.ts.map +0 -1
  899. package/dist/test/tool-result-history.test.js +0 -156
  900. package/dist/test/tool-result-history.test.js.map +0 -1
  901. package/dist/test/tool-result-spill.test.d.ts +0 -13
  902. package/dist/test/tool-result-spill.test.d.ts.map +0 -1
  903. package/dist/test/tool-result-spill.test.js +0 -586
  904. package/dist/test/tool-result-spill.test.js.map +0 -1
  905. package/dist/test/tool-wrapper-prose-guard-integration.test.d.ts +0 -2
  906. package/dist/test/tool-wrapper-prose-guard-integration.test.d.ts.map +0 -1
  907. package/dist/test/tool-wrapper-prose-guard-integration.test.js +0 -485
  908. package/dist/test/tool-wrapper-prose-guard-integration.test.js.map +0 -1
  909. package/dist/test/tool-wrapper-prose-guard.test.d.ts +0 -2
  910. package/dist/test/tool-wrapper-prose-guard.test.d.ts.map +0 -1
  911. package/dist/test/tool-wrapper-prose-guard.test.js +0 -34
  912. package/dist/test/tool-wrapper-prose-guard.test.js.map +0 -1
  913. package/dist/test/tools-observe-parser-conformance.test.d.ts +0 -2
  914. package/dist/test/tools-observe-parser-conformance.test.d.ts.map +0 -1
  915. package/dist/test/tools-observe-parser-conformance.test.js +0 -129
  916. package/dist/test/tools-observe-parser-conformance.test.js.map +0 -1
  917. package/dist/test/trunk-channel-routing.test.d.ts +0 -19
  918. package/dist/test/trunk-channel-routing.test.d.ts.map +0 -1
  919. package/dist/test/trunk-channel-routing.test.js +0 -652
  920. package/dist/test/trunk-channel-routing.test.js.map +0 -1
  921. package/dist/test/tune-out-e2e.test.d.ts +0 -2
  922. package/dist/test/tune-out-e2e.test.d.ts.map +0 -1
  923. package/dist/test/tune-out-e2e.test.js +0 -142
  924. package/dist/test/tune-out-e2e.test.js.map +0 -1
  925. package/dist/test/tune-out-gate-composition.test.d.ts +0 -2
  926. package/dist/test/tune-out-gate-composition.test.d.ts.map +0 -1
  927. package/dist/test/tune-out-gate-composition.test.js +0 -221
  928. package/dist/test/tune-out-gate-composition.test.js.map +0 -1
  929. package/dist/test/tune-out-lifecycle.test.d.ts +0 -2
  930. package/dist/test/tune-out-lifecycle.test.d.ts.map +0 -1
  931. package/dist/test/tune-out-lifecycle.test.js +0 -121
  932. package/dist/test/tune-out-lifecycle.test.js.map +0 -1
  933. package/dist/test/turn-trigger-provenance.test.d.ts +0 -2
  934. package/dist/test/turn-trigger-provenance.test.d.ts.map +0 -1
  935. package/dist/test/turn-trigger-provenance.test.js +0 -137
  936. package/dist/test/turn-trigger-provenance.test.js.map +0 -1
  937. package/dist/test/unconvertible-tool-result.test.d.ts +0 -2
  938. package/dist/test/unconvertible-tool-result.test.d.ts.map +0 -1
  939. package/dist/test/unconvertible-tool-result.test.js +0 -358
  940. package/dist/test/unconvertible-tool-result.test.js.map +0 -1
  941. package/dist/test/unsent-compiles.test.d.ts +0 -2
  942. package/dist/test/unsent-compiles.test.d.ts.map +0 -1
  943. package/dist/test/unsent-compiles.test.js +0 -139
  944. package/dist/test/unsent-compiles.test.js.map +0 -1
  945. package/dist/test/usage-accounting.test.d.ts +0 -2
  946. package/dist/test/usage-accounting.test.d.ts.map +0 -1
  947. package/dist/test/usage-accounting.test.js +0 -13
  948. package/dist/test/usage-accounting.test.js.map +0 -1
  949. package/dist/test/usage-tracker.test.d.ts +0 -2
  950. package/dist/test/usage-tracker.test.d.ts.map +0 -1
  951. package/dist/test/usage-tracker.test.js +0 -129
  952. package/dist/test/usage-tracker.test.js.map +0 -1
  953. package/dist/test/utils-surface.test.d.ts +0 -2
  954. package/dist/test/utils-surface.test.d.ts.map +0 -1
  955. package/dist/test/utils-surface.test.js +0 -145
  956. package/dist/test/utils-surface.test.js.map +0 -1
  957. package/dist/test/workspace-branch-continuation.test.d.ts +0 -15
  958. package/dist/test/workspace-branch-continuation.test.d.ts.map +0 -1
  959. package/dist/test/workspace-branch-continuation.test.js +0 -183
  960. package/dist/test/workspace-branch-continuation.test.js.map +0 -1
  961. package/dist/test/workspace-materialize-freshness.test.d.ts +0 -16
  962. package/dist/test/workspace-materialize-freshness.test.d.ts.map +0 -1
  963. package/dist/test/workspace-materialize-freshness.test.js +0 -252
  964. package/dist/test/workspace-materialize-freshness.test.js.map +0 -1
  965. package/dist/test/workspace-path-containment.test.d.ts +0 -19
  966. package/dist/test/workspace-path-containment.test.d.ts.map +0 -1
  967. package/dist/test/workspace-path-containment.test.js +0 -91
  968. package/dist/test/workspace-path-containment.test.js.map +0 -1
  969. package/dist/test/workspace-read-file-from-disk.test.d.ts +0 -17
  970. package/dist/test/workspace-read-file-from-disk.test.d.ts.map +0 -1
  971. package/dist/test/workspace-read-file-from-disk.test.js +0 -174
  972. package/dist/test/workspace-read-file-from-disk.test.js.map +0 -1
  973. package/dist/test/workspace-read-image.test.d.ts +0 -2
  974. package/dist/test/workspace-read-image.test.d.ts.map +0 -1
  975. package/dist/test/workspace-read-image.test.js +0 -597
  976. package/dist/test/workspace-read-image.test.js.map +0 -1
  977. package/dist/test/workspace-read-text.test.d.ts +0 -2
  978. package/dist/test/workspace-read-text.test.d.ts.map +0 -1
  979. package/dist/test/workspace-read-text.test.js +0 -101
  980. package/dist/test/workspace-read-text.test.js.map +0 -1
  981. package/dist/test/workspace-restore-order.test.d.ts +0 -16
  982. package/dist/test/workspace-restore-order.test.d.ts.map +0 -1
  983. package/dist/test/workspace-restore-order.test.js +0 -140
  984. package/dist/test/workspace-restore-order.test.js.map +0 -1
  985. package/dist/test/workspace-three-way.test.d.ts +0 -12
  986. package/dist/test/workspace-three-way.test.d.ts.map +0 -1
  987. package/dist/test/workspace-three-way.test.js +0 -4094
  988. package/dist/test/workspace-three-way.test.js.map +0 -1
  989. package/dist/test/workspace-watcher.test.d.ts +0 -2
  990. package/dist/test/workspace-watcher.test.d.ts.map +0 -1
  991. package/dist/test/workspace-watcher.test.js +0 -281
  992. package/dist/test/workspace-watcher.test.js.map +0 -1
  993. package/dist/test/xml-round-persist.test.d.ts +0 -19
  994. package/dist/test/xml-round-persist.test.d.ts.map +0 -1
  995. package/dist/test/xml-round-persist.test.js +0 -235
  996. package/dist/test/xml-round-persist.test.js.map +0 -1
  997. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -12,18 +12,57 @@
12
12
  * - Expose synthesized tools: channel_list, channel_open, channel_close, channel_publish
13
13
  * - Build channel context for beforeInference params
14
14
  */
15
- import { INLINE_WITHHELD_TEXT, isInlineContradiction, referenceStubOrNull } from './references.js';
15
+ import { convertBlock } from './convert-block.js';
16
+ import { isDeepStrictEqual } from 'node:util';
17
+ import { ensureStateRegistered } from '../module-registry.js';
16
18
  import { expandCoreTags } from './tags.js';
17
19
  import { coalesceRejectionReason, EmptyContentError, validateCoalescedContent } from './push-coalescer.js';
18
- import { isVisiblyEmptyContent } from './visible-content.js';
20
+ import { isVisiblyEmptyConversion } from './visible-content.js';
19
21
  import { logDetail, logValue } from './log-value.js';
20
22
  import { RefusalLines } from './refusal-lines.js';
21
23
  import { CapabilityGrant } from './capability-grant.js';
24
+ import { McplRequestError } from './server-connection.js';
25
+ import { readSkipReplyWake } from './skip-reply-wake.js';
22
26
  // ============================================================================
23
27
  // Typing indicator interval (Discord typing lasts ~10s, so 7s keeps it alive)
24
28
  // ============================================================================
25
29
  const TYPING_INTERVAL_MS = 7_000;
30
+ const MAX_CHANNEL_LIFECYCLE_ATTEMPTS = 5;
31
+ /** A successful RPC can still fail to establish the latest channel target. */
32
+ class ChannelLifecycleConvergenceError extends Error {
33
+ }
34
+ /** A channels/open answered for a channel other than the one it named. */
35
+ class ChannelOpenSubstitutionError extends Error {
36
+ }
37
+ /** An operation queued for a server's registrations that were removed. */
38
+ class ChannelLifecycleCancelledError extends Error {
39
+ }
26
40
  const CHANNEL_LIFECYCLE_LOG_ID = 'mcpl/channel-lifecycle';
41
+ /**
42
+ * What a channels/open answer says about which channel the server opened:
43
+ * its `channel.id`, or else the `channelId` of a history item. Each is
44
+ * evidence. A string other than the requested id names another channel (ids
45
+ * compare exactly, as the server's traffic is matched). Null or absence names
46
+ * no channel, and gives undefined, as does an answer that names none at all.
47
+ * Any other value is malformed: it names nothing the host could register.
48
+ */
49
+ function substitutedChannel(result, requested) {
50
+ const named = (value) => {
51
+ if (value === undefined || value === null || value === requested)
52
+ return undefined;
53
+ return typeof value === 'string' ? { kind: 'other', id: value } : { kind: 'malformed', value };
54
+ };
55
+ const answered = named(result?.channel?.id);
56
+ if (answered)
57
+ return answered;
58
+ const history = Array.isArray(result?.history) ? result.history : [];
59
+ for (const item of history) {
60
+ const from = named(item?.channelId);
61
+ if (from)
62
+ return from;
63
+ }
64
+ return undefined;
65
+ }
27
66
  /**
28
67
  * Durable "which label did this channelId have, each time we saw it" log.
29
68
  * `resolveProseTarget()` resolves a label to an id only for channels
@@ -82,84 +121,25 @@ function shallowEqualRecord(a, b) {
82
121
  }
83
122
  return true;
84
123
  }
124
+ function sameChannelTarget(a, b) {
125
+ return a.type === b.type && isDeepStrictEqual(a.address, b.address);
126
+ }
85
127
  // ============================================================================
86
- // Content Conversion: McplContentBlock → membrane ContentBlock
128
+ // Channel Tool Definitions
87
129
  // ============================================================================
88
130
  /**
89
- * Convert a single MCPL wire-format content block to a membrane ContentBlock.
131
+ * `journal`'s description. It tells the model to call `skip_reply` to end the
132
+ * turn only where the agent may call it: the framework builds each agent's
133
+ * surface with that agent's permission, and a refused `skip_reply` leaves the
134
+ * turn going on.
90
135
  */
91
- function convertBlock(block) {
92
- switch (block.type) {
93
- case 'text':
94
- // Text that isn't a string is no text, as the request sanitizer and
95
- // the visible-content check already read it.
96
- return { type: 'text', text: typeof block.text === 'string' ? block.text : '' };
97
- case 'image':
98
- if (isInlineContradiction(block)) {
99
- // RFC-005 vector 2: inline data claiming bulk disposition — fail
100
- // closed, withhold the data (checked BEFORE the data branch).
101
- return { type: 'text', text: INLINE_WITHHELD_TEXT };
102
- }
103
- if (isFilledString(block.uri) && block.disposition) {
104
- // RFC-005 uri-form media with a disposition claim: stub, do not
105
- // hand the URI to the provider or inline it.
106
- return { type: 'text', text: referenceStubOrNull(block, 'attachment on channel message') ?? '[reference]' };
107
- }
108
- if (isFilledString(block.data) && isFilledString(block.mimeType)) {
109
- return {
110
- type: 'image',
111
- source: { type: 'base64', data: block.data, mediaType: block.mimeType },
112
- };
113
- }
114
- if (isFilledString(block.uri)) {
115
- return {
116
- type: 'image',
117
- source: { type: 'url', url: block.uri },
118
- };
119
- }
120
- return { type: 'text', text: isFilledString(block.data) ? '[Image: no media type]' : '[Image: no data]' };
121
- case 'audio':
122
- if (isInlineContradiction(block)) {
123
- // RFC-005 vector 2: inline data claiming bulk disposition — fail
124
- // closed, withhold the data (checked BEFORE the data branch).
125
- return { type: 'text', text: INLINE_WITHHELD_TEXT };
126
- }
127
- if (isFilledString(block.uri) && block.disposition) {
128
- // RFC-005 uri-form media with a disposition claim: stub, do not
129
- // hand the URI to the provider or inline it.
130
- return { type: 'text', text: referenceStubOrNull(block, 'attachment on channel message') ?? '[reference]' };
131
- }
132
- if (isFilledString(block.data) && isFilledString(block.mimeType)) {
133
- return {
134
- type: 'audio',
135
- source: { type: 'base64', data: block.data, mediaType: block.mimeType },
136
- };
137
- }
138
- return { type: 'text', text: isFilledString(block.data) ? '[Audio: no media type]' : '[Audio: no data]' };
139
- case 'resource':
140
- // RFC-005: reference blocks become bounded stubs — never raw URIs
141
- // (a signed URL is a bearer credential that looks like a location).
142
- return { type: 'text', text: referenceStubOrNull(block, 'attachment on channel message') ?? '[reference]' };
143
- default: {
144
- // Unknown wire block types previously fell off the exhaustive switch
145
- // and propagated `undefined` into ContentBlock[]. Fail visibly. A type
146
- // that isn't a string is no type, and is never converted to one: an
147
- // object can throw when it is.
148
- const type = block.type;
149
- return { type: 'text', text: `[unrecognized content block: ${typeof type === 'string' ? type : 'untyped'}]` };
150
- }
151
- }
152
- }
153
- /** A string with something in it: the only data, mimeType or uri a block's
154
- * source is built from, and the only uri that makes it a reference. Anything
155
- * else a sender puts there leaves the block without it, so conversion never
156
- * throws and never copies a non-string. */
157
- function isFilledString(value) {
158
- return typeof value === 'string' && value !== '';
136
+ export function journalToolDescription(canSkipReply) {
137
+ return 'Write an entry in your private journal. The entry stays in your own context and ' +
138
+ 'memory and is NOT sent to any channel, surface or person. Use it for anything longer ' +
139
+ 'than a line that you want to keep for yourself — reflections, what you decided and ' +
140
+ 'why, notes for later. It does not end your turn and does not affect where ordinary ' +
141
+ 'text is routed' + (canSkipReply ? '; to end the turn without replying, call skip_reply afterwards.' : '.');
159
142
  }
160
- // ============================================================================
161
- // Channel Tool Definitions
162
- // ============================================================================
163
143
  const CHANNEL_TOOL_DEFINITIONS = [
164
144
  {
165
145
  name: 'channel_list',
@@ -168,9 +148,8 @@ const CHANNEL_TOOL_DEFINITIONS = [
168
148
  },
169
149
  {
170
150
  name: 'channel_open',
171
- description: 'Open a channel to start receiving its ordinary ongoing traffic. The MCPL ' +
172
- 'integration performs its own subscribe/join/attach operation. Optionally request ' +
173
- 'history preceding the message that invited you into the channel.',
151
+ description: 'Open a channel. What an open channel delivers to you depends on its MCPL server. ' +
152
+ 'Optionally request history preceding the message that invited you into the channel.',
174
153
  inputSchema: {
175
154
  type: 'object',
176
155
  properties: {
@@ -209,7 +188,8 @@ const CHANNEL_TOOL_DEFINITIONS = [
209
188
  },
210
189
  {
211
190
  name: 'channel_close',
212
- description: 'Close a channel to stop receiving messages',
191
+ description: 'Close a channel. Messages that address you still reach you; what else stops reaching you ' +
192
+ 'depends on its server.',
213
193
  inputSchema: {
214
194
  type: 'object',
215
195
  properties: {
@@ -221,11 +201,11 @@ const CHANNEL_TOOL_DEFINITIONS = [
221
201
  },
222
202
  {
223
203
  name: 'channel_publish',
224
- description: 'Publish a message to a channel. If channelId is omitted, publishes to the most recent incoming channel.',
204
+ description: 'Publish a message to a channel. If channelId is omitted, publishes to the channel this conversation is bound to, if any, else to the most recent incoming channel. A subagent has neither, and must name one.',
225
205
  inputSchema: {
226
206
  type: 'object',
227
207
  properties: {
228
- channelId: { type: 'string', description: 'ID of the channel to publish to (defaults to the most recent incoming channel)' },
208
+ channelId: { type: 'string', description: 'ID of the channel to publish to (defaults to the conversation\'s bound channel, else the most recent incoming channel; required of a subagent)' },
229
209
  content: { type: 'string', description: 'Text content to publish' },
230
210
  text: { type: 'string', description: 'Alias for content' },
231
211
  },
@@ -234,6 +214,9 @@ const CHANNEL_TOOL_DEFINITIONS = [
234
214
  },
235
215
  {
236
216
  name: 'think',
217
+ // No agent's surface shows this description: getToolsForAgent gives every
218
+ // agent its own think (buildThinkTool), which names skip_reply only where
219
+ // that agent may call it.
237
220
  description: 'Reason privately. The content stays in your own context and is NOT sent to any ' +
238
221
  'channel or surface. Same-round routing of ordinary text beside think() depends on ' +
239
222
  'your current same_round_think_text_policy; inspect or change it with agent_settings. ' +
@@ -252,11 +235,7 @@ const CHANNEL_TOOL_DEFINITIONS = [
252
235
  },
253
236
  {
254
237
  name: 'journal',
255
- description: 'Write an entry in your private journal. The entry stays in your own context and ' +
256
- 'memory and is NOT sent to any channel, surface or person. Use it for anything longer ' +
257
- 'than a line that you want to keep for yourself — reflections, what you decided and ' +
258
- 'why, notes for later. It does not end your turn and does not affect where ordinary ' +
259
- 'text is routed; to end the turn without replying, call skip_reply afterwards.',
238
+ description: journalToolDescription(true),
260
239
  inputSchema: {
261
240
  type: 'object',
262
241
  properties: {
@@ -283,14 +262,95 @@ const CHANNEL_TOOL_DEFINITIONS = [
283
262
  wake_in_seconds: {
284
263
  type: 'number',
285
264
  description: 'Optional self-wake: if nothing else wakes you first, you wake again after this ' +
286
- 'many seconds (1 = go again almost immediately; clamped to 1–3600). Any other ' +
287
- 'wake before then cancels it. Omit to stay idle until the next external wake.',
265
+ 'many seconds, from 1 to 3600; a larger number waits 3600 (1 = go again almost ' +
266
+ 'immediately). Any other wake before then cancels it. Omit to stay idle until the ' +
267
+ 'next external wake.',
288
268
  },
289
269
  },
290
270
  required: [],
291
271
  },
292
272
  },
293
273
  ];
274
+ // ============================================================================
275
+ // Constructor Options
276
+ // ============================================================================
277
+ /** Bound connector values for notices, logs and an outcome's reason. A
278
+ * PublishOutcome keeps the connector's structured data (`detail`) whole. */
279
+ export function connectorText(value) {
280
+ let text;
281
+ try {
282
+ text = typeof value === 'string' ? value : JSON.stringify(value) ?? String(value);
283
+ }
284
+ catch {
285
+ return '[unserializable connector value]';
286
+ }
287
+ const limit = 2000;
288
+ const suffix = '… [truncated]';
289
+ return text.length > limit ? text.slice(0, limit - suffix.length) + suffix : text;
290
+ }
291
+ /**
292
+ * What a channels/publish request that threw establishes. Only a request
293
+ * that never left the host proves nothing was posted. An error response may
294
+ * follow a partial post (a multi-part send whose later part failed), so it
295
+ * is unknown, with the connector's own words, bounded, and its data kept for
296
+ * the resident to judge. The publish executor and channel_publish both read
297
+ * a failed request this way.
298
+ */
299
+ function publishErrorOutcome(err, destination, at) {
300
+ // This host's own words for a request that wasn't sent or wasn't answered
301
+ // are sentences, and every composition of the reason adds its own ending,
302
+ // so one final period comes off them. A server's error answer keeps its
303
+ // words as they came.
304
+ const text = connectorText(err instanceof Error ? err.message : String(err));
305
+ const message = err instanceof McplRequestError && err.outcome !== 'error-response' ? text.replace(/(?<!\.)\.$/, '') : text;
306
+ const notSent = err instanceof McplRequestError && err.outcome === 'not-sent';
307
+ const detail = err instanceof McplRequestError ? err.data : undefined;
308
+ return {
309
+ status: notSent ? 'failed' : 'unknown',
310
+ destination,
311
+ reason: notSent ? message : `${message} (delivery uncertain)`,
312
+ ...(detail !== undefined ? { detail } : {}),
313
+ at,
314
+ };
315
+ }
316
+ /**
317
+ * What a channels/publish answer establishes. A final publish is a request,
318
+ * so success requires an explicit receipt; a missing or malformed one leaves
319
+ * delivery uncertain (anima-research/agent-framework#163's rule).
320
+ * `delivered: false` proves nothing went out only when the receipt names no
321
+ * message at all: a message id beside it, valid or malformed, contradicts it,
322
+ * since something may have been posted. The publish executor and
323
+ * channel_publish both read an answer this way.
324
+ */
325
+ function publishReceiptOutcome(result, destination, at) {
326
+ const receipt = (result ?? {});
327
+ const messageId = typeof receipt.messageId === 'string' && receipt.messageId ? receipt.messageId : undefined;
328
+ if (receipt.delivered === true) {
329
+ return { status: 'delivered', destination, ...(messageId ? { messageId } : {}), at };
330
+ }
331
+ if (receipt.delivered === false) {
332
+ if (receipt.messageId === undefined || receipt.messageId === null) {
333
+ return { status: 'failed', destination, reason: `server "${destination.serverId}" reported delivered:false`, at };
334
+ }
335
+ return {
336
+ status: 'unknown',
337
+ destination,
338
+ ...(messageId ? { messageId } : {}),
339
+ reason: messageId
340
+ ? `server "${destination.serverId}" reported delivered:false but named posted message ${connectorText(messageId)} (delivery uncertain)`
341
+ : `server "${destination.serverId}" reported delivered:false beside a malformed message id ` +
342
+ `${connectorText(receipt.messageId)} (contradictory receipt; delivery uncertain)`,
343
+ at,
344
+ };
345
+ }
346
+ return {
347
+ status: 'unknown',
348
+ destination,
349
+ ...(messageId ? { messageId } : {}),
350
+ reason: `server "${destination.serverId}" returned no valid delivery receipt (delivery uncertain)`,
351
+ at,
352
+ };
353
+ }
294
354
  /** The machine decision sources a module-origin channel_close may record.
295
355
  * Closed on purpose: honest provenance means a small, auditable vocabulary,
296
356
  * not arbitrary self-description. */
@@ -306,6 +366,7 @@ export class ChannelRegistry {
306
366
  onChannelAutoOpened;
307
367
  homeChannelResolver;
308
368
  activeChannelResolver;
369
+ takesGlobalFallback;
309
370
  store;
310
371
  /** Registered channels, keyed by `{serverId}:{channelId}`. */
311
372
  channels = new Map();
@@ -374,9 +435,58 @@ export class ChannelRegistry {
374
435
  /** One-time migration inputs from the retired recipe auto-open policy. */
375
436
  legacyPolicies = new Map();
376
437
  migratedLegacyPolicies = new Set();
438
+ // Keep one transport operation in flight per logical channel, including
439
+ // across descriptor replacement. Settled tails release themselves; a
440
+ // failed operation must not poison later decisions.
441
+ channelLifecycleTails = new Map();
442
+ // Bumped when a server's registrations are removed: an operation queued
443
+ // before that is cancelled when it would run, rather than acting on a
444
+ // closing connection or on a later re-registration's entries.
445
+ serverRemovals = new Map();
446
+ // Operations queued or running per channel that will still read its
447
+ // target before confirming anything. Unlike the tail, which settles a few
448
+ // microtasks after its operation's last check, this drops at that check.
449
+ lifecycleOutstanding = new Map();
450
+ // Losing an open receipt does not confirm closure. Keep that distinction
451
+ // private while preserving the public open:boolean availability projection.
452
+ unconfirmedChannelTargets = new WeakSet();
453
+ invalidateTransportConfirmation(entry) {
454
+ entry.open = false;
455
+ this.unconfirmedChannelTargets.add(entry);
456
+ }
457
+ /** Whether the host directs this server's channel lifecycle (§14.1: its
458
+ * `channels.lifecycle` grant). Where it doesn't, the server opens and
459
+ * closes its own channels, and sends no receipt the host could wait for. */
460
+ directsLifecycle(serverId) {
461
+ return CapabilityGrant.of(this.serverRegistry.getServer(serverId)).has('channels.lifecycle');
462
+ }
463
+ /** A registration's entry: nothing about its transport is known until a
464
+ * receipt for its target confirms it open or closed. */
465
+ registerEntry(key, serverId, descriptor) {
466
+ const entry = { serverId, descriptor, open: false };
467
+ this.channels.set(key, entry);
468
+ this.unconfirmedChannelTargets.add(entry);
469
+ }
470
+ hasPendingLifecycle(entry) {
471
+ return this.channelLifecycleTails.has(this.lifecycleKey(entry.serverId, entry.descriptor.id));
472
+ }
377
473
  handleCoalescedIncoming;
474
+ acceptInbound;
475
+ channelTools;
378
476
  constructor(serverRegistry, featureSetManager, pushEventFn, emitTraceFn, options) {
379
477
  this.handleCoalescedIncoming = options?.handleCoalescedIncoming;
478
+ this.acceptInbound = options?.acceptInbound;
479
+ this.channelTools = CHANNEL_TOOL_DEFINITIONS.map((tool) => {
480
+ if (tool.name !== 'skip_reply' || options?.eventGatePresent === true)
481
+ return tool;
482
+ const schema = tool.inputSchema;
483
+ const { wake_in_seconds: _wake, ...properties } = schema.properties;
484
+ return {
485
+ ...tool,
486
+ description: tool.description.replace(' To end this turn but come back on your own shortly, set wake_in_seconds.', ''),
487
+ inputSchema: { ...schema, properties },
488
+ };
489
+ });
380
490
  this.serverRegistry = serverRegistry;
381
491
  this.featureSetManager = featureSetManager;
382
492
  this.pushEventFn = pushEventFn;
@@ -387,6 +497,7 @@ export class ChannelRegistry {
387
497
  this.onChannelAutoOpened = options?.onChannelAutoOpened;
388
498
  this.homeChannelResolver = options?.homeChannelResolver;
389
499
  this.activeChannelResolver = options?.activeChannelResolver;
500
+ this.takesGlobalFallback = options?.takesGlobalFallback;
390
501
  this.store = options?.store;
391
502
  this.initializeLifecycleStore();
392
503
  this.initializeLabelHistoryStore();
@@ -422,11 +533,7 @@ export class ChannelRegistry {
422
533
  continue;
423
534
  }
424
535
  const key = `${serverId}:${channel.id}`;
425
- this.channels.set(key, {
426
- serverId,
427
- descriptor: channel,
428
- open: false,
429
- });
536
+ this.registerEntry(key, serverId, channel);
430
537
  this.appendLabelSighting(channel.id, channel.label, extractDmMeta(channel.metadata));
431
538
  registeredIds.push(channel.id);
432
539
  results.push({ id: channel.id, accepted: true });
@@ -459,6 +566,7 @@ export class ChannelRegistry {
459
566
  // channels/register — a changed-set cannot smuggle in what register
460
567
  // would have rejected (PR #79 review blocker 5).
461
568
  const addedResults = [];
569
+ const retargeted = [];
462
570
  // Process removed channels — itemized truthfully: removal of a channel
463
571
  // we never had is reported, not silently accepted.
464
572
  if (params.removed) {
@@ -487,6 +595,21 @@ export class ChannelRegistry {
487
595
  this.emitTraceFn({ type: 'mcpl:channel-descriptor-rejected', serverId, channelId: channel.id, reason: 'update for unregistered channel' });
488
596
  continue;
489
597
  }
598
+ // A live subscription to the old target does not confirm the new
599
+ // type/address. Label-only updates preserve that confirmation. A
600
+ // channel decided open (or tuned out) is opened at its new target.
601
+ if (!sameChannelTarget(existing.descriptor, channel)) {
602
+ this.invalidateTransportConfirmation(existing);
603
+ // An operation queued or running for this channel reads its target
604
+ // again before it confirms anything, and after a failed RPC, so it
605
+ // opens the new one. With none, a channel decided open (or tuned
606
+ // out) is reconciled here.
607
+ const desired = this.getDesiredState(serverId, channel.id);
608
+ if ((desired === 'open' || desired === 'tuned-out') &&
609
+ !this.lifecycleOutstanding.has(this.lifecycleKey(serverId, channel.id))) {
610
+ retargeted.push(channel);
611
+ }
612
+ }
490
613
  existing.descriptor = channel;
491
614
  this.appendLabelSighting(channel.id, channel.label, extractDmMeta(channel.metadata));
492
615
  addedResults.push({ id: channel.id, accepted: true });
@@ -503,11 +626,7 @@ export class ChannelRegistry {
503
626
  continue;
504
627
  }
505
628
  const key = `${serverId}:${channel.id}`;
506
- this.channels.set(key, {
507
- serverId,
508
- descriptor: channel,
509
- open: false,
510
- });
629
+ this.registerEntry(key, serverId, channel);
511
630
  this.appendLabelSighting(channel.id, channel.label, extractDmMeta(channel.metadata));
512
631
  addedResults.push({ id: channel.id, accepted: true });
513
632
  accepted.push(channel);
@@ -520,8 +639,9 @@ export class ChannelRegistry {
520
639
  // deadlocks both sides until one times out (#160). The verdicts are
521
640
  // settled above, so nothing in the response depends on reconciling.
522
641
  responder?.respond({ results: addedResults });
523
- if (accepted.length > 0)
524
- await this.reconcileChannels(serverId, accepted);
642
+ const reconcile = [...accepted, ...retargeted];
643
+ if (reconcile.length > 0)
644
+ await this.reconcileChannels(serverId, reconcile);
525
645
  this.emitTraceFn({
526
646
  type: 'mcpl:channels-changed',
527
647
  serverId,
@@ -677,8 +797,8 @@ export class ChannelRegistry {
677
797
  }
678
798
  if (message.tags)
679
799
  message.tags = expandCoreTags(message.tags);
680
- const convertedContent = message.content.map(convertBlock);
681
- if (!emptyAllowed && isVisiblyEmptyContent(convertedContent)) {
800
+ const convertedContent = message.content.map((block) => convertBlock(block, 'attachment on channel message', serverId));
801
+ if (!emptyAllowed && isVisiblyEmptyConversion(message.content, convertedContent)) {
682
802
  rejectEmpty();
683
803
  continue;
684
804
  }
@@ -688,15 +808,34 @@ export class ChannelRegistry {
688
808
  // exactly the authority a self-attested channel would be stealing).
689
809
  // A coalesced item is "accepted" only once the coalescer admits it, so
690
810
  // for those this runs after the hook (below).
811
+ const incomingEntry = this.channels.get(incomingKey);
691
812
  const markAccepted = () => {
692
813
  this.defaultPublishChannel = message.channelId;
693
814
  this.defaultPublishMessageId = message.messageId;
694
815
  this.defaultPublishThreadId = message.threadId;
695
- // A server sending channels/incoming is authoritative evidence that
696
- // the transport is actually open. This repairs transient status only;
697
- // durable desired state still changes exclusively through lifecycle
698
- // operations.
699
- this.channels.get(incomingKey).open = true;
816
+ // Where the host directs this server's lifecycle, only a receipt for
817
+ // the current target confirms it: traffic shows the transport open
818
+ // only for a confirmed target, and only for a channel still decided
819
+ // open or tuned out. After a retarget, traffic for this id may still
820
+ // come from the old subscription, and a close receipt that landed
821
+ // first (or during the coalescer's await) stands. Where the host
822
+ // doesn't direct it, the server opens its own channels and its
823
+ // traffic is the only evidence the host gets, so it marks the
824
+ // channel open, as it always has. The entry is the one the message
825
+ // was accepted for: one a re-registration put in its place meanwhile
826
+ // waits for its own evidence. Durable desired state still changes
827
+ // exclusively through lifecycle operations.
828
+ if (this.directsLifecycle(serverId)) {
829
+ if (this.unconfirmedChannelTargets.has(incomingEntry))
830
+ return;
831
+ const desired = this.getDesiredState(serverId, message.channelId);
832
+ if (desired !== 'open' && desired !== 'tuned-out')
833
+ return;
834
+ }
835
+ else {
836
+ this.unconfirmedChannelTargets.delete(incomingEntry);
837
+ }
838
+ incomingEntry.open = true;
700
839
  };
701
840
  if (!coalesced)
702
841
  markAccepted();
@@ -705,7 +844,7 @@ export class ChannelRegistry {
705
844
  if (this.shouldTriggerInference) {
706
845
  // The text the agent's context gets, as the push lane's filter reads
707
846
  // it: the converted text blocks, a placeholder or reference stub
708
- // among them, and a text block's non-string text as empty text.
847
+ // among them, NON_STRING_TEXT_PLACEHOLDER included.
709
848
  const textContent = convertedContent
710
849
  .filter((b) => b.type === 'text')
711
850
  .map((b) => b.text)
@@ -734,6 +873,7 @@ export class ChannelRegistry {
734
873
  metadata: message.metadata,
735
874
  ...(message.tags ? { tags: message.tags } : {}),
736
875
  triggerInference,
876
+ acceptedAt: Date.now(),
737
877
  };
738
878
  if (coalesced) {
739
879
  // RFC-006 §14.3: a coalesced message is admitted like any other and
@@ -759,6 +899,12 @@ export class ChannelRegistry {
759
899
  }
760
900
  continue;
761
901
  }
902
+ // The source envelope is frozen here, at admission, before the message
903
+ // is queued or acknowledged: a rename or rebind while it waits in the
904
+ // queue cannot change where it says it came from.
905
+ const inboundSource = this.acceptInbound?.(event);
906
+ if (inboundSource)
907
+ event.inboundSource = inboundSource;
762
908
  // Push to the processing queue
763
909
  // Cast through unknown because McplChannelIncomingEvent matches the
764
910
  // CustomEvent `${string}:${string}` type pattern but lacks an index signature.
@@ -801,27 +947,19 @@ export class ChannelRegistry {
801
947
  // time a message reveals it (labels must show people, not ids).
802
948
  if (existing.descriptor.metadata?.lazyRegistered &&
803
949
  label && existing.descriptor.label === channelId) {
804
- existing.descriptor.label = label;
805
- if (extraMetadata) {
806
- existing.descriptor.metadata = { ...existing.descriptor.metadata, ...extraMetadata };
807
- }
950
+ const metadata = extraMetadata
951
+ ? { ...existing.descriptor.metadata, ...extraMetadata }
952
+ : existing.descriptor.metadata;
808
953
  // The channel now has a real label, not just the bare id — durable.
809
- this.appendLabelSighting(existing.descriptor.id, existing.descriptor.label, extractDmMeta(existing.descriptor.metadata));
954
+ // Recorded before the live descriptor takes it, since this branch's
955
+ // guard reads the descriptor: an append that throws leaves the
956
+ // placeholder, so the next push tries again.
957
+ this.appendLabelSighting(existing.descriptor.id, label, extractDmMeta(metadata));
958
+ existing.descriptor.label = label;
959
+ existing.descriptor.metadata = metadata;
810
960
  }
811
961
  return;
812
962
  }
813
- const key = `${serverId}:${channelId}`;
814
- this.channels.set(key, {
815
- serverId,
816
- descriptor: {
817
- id: channelId,
818
- type: serverId,
819
- label: label ?? channelId,
820
- direction: 'bidirectional',
821
- metadata: { lazyRegistered: true, ...(extraMetadata ?? {}) },
822
- },
823
- open: false,
824
- });
825
963
  // Only persist a REAL label durably. Falling back to the bare
826
964
  // channelId as a placeholder (no server-supplied label at all — e.g. a
827
965
  // Discord DM push missing both channelName and an author name,
@@ -831,11 +969,24 @@ export class ChannelRegistry {
831
969
  // label-less post-restart event takes — recording the placeholder here
832
970
  // would clobber the good label initializeLabelHistoryStore() just
833
971
  // replayed, defeating the entire point of the durable log. The live
834
- // descriptor above still carries the placeholder for in-process
972
+ // descriptor below still carries the placeholder for in-process
835
973
  // routing; only the durable write is skipped when there's no real
836
- // label to record.
974
+ // label to record. A real label is recorded before the entry exists:
975
+ // an append that throws registers nothing, so the next push tries again.
837
976
  if (label)
838
977
  this.appendLabelSighting(channelId, label, extractDmMeta(extraMetadata));
978
+ const key = `${serverId}:${channelId}`;
979
+ this.channels.set(key, {
980
+ serverId,
981
+ descriptor: {
982
+ id: channelId,
983
+ type: serverId,
984
+ label: label ?? channelId,
985
+ direction: 'bidirectional',
986
+ metadata: { lazyRegistered: true, ...(extraMetadata ?? {}) },
987
+ },
988
+ open: false,
989
+ });
839
990
  this.emitTraceFn({
840
991
  type: 'mcpl:channel-lazy-registered',
841
992
  serverId,
@@ -958,6 +1109,12 @@ export class ChannelRegistry {
958
1109
  getDescriptor(channelId) {
959
1110
  return this.findChannelEntry(channelId)?.descriptor;
960
1111
  }
1112
+ /** The registered label of `channelId` on exactly `serverId` (no
1113
+ * cross-server first match), or undefined when it has none. */
1114
+ getChannelLabel(serverId, channelId) {
1115
+ const label = this.channels.get(`${serverId}:${channelId}`)?.descriptor.label;
1116
+ return typeof label === 'string' && label.length > 0 ? label : undefined;
1117
+ }
961
1118
  isChannelOpen(channelId) {
962
1119
  return this.findChannelEntry(channelId)?.open === true;
963
1120
  }
@@ -983,7 +1140,7 @@ export class ChannelRegistry {
983
1140
  * Get synthesized tool definitions for channel operations.
984
1141
  */
985
1142
  getChannelTools() {
986
- return CHANNEL_TOOL_DEFINITIONS;
1143
+ return this.channelTools;
987
1144
  }
988
1145
  /**
989
1146
  * Handle a call to one of the synthesized channel tools.
@@ -1008,7 +1165,7 @@ export class ChannelRegistry {
1008
1165
  case 'channel_decline':
1009
1166
  return this.handleToolDecline(input);
1010
1167
  case 'channel_publish':
1011
- return this.handleToolPublish(input);
1168
+ return this.handleToolPublish(input, origin);
1012
1169
  case 'think':
1013
1170
  return this.handleToolThink(input);
1014
1171
  case 'journal':
@@ -1036,9 +1193,15 @@ export class ChannelRegistry {
1036
1193
  // (heartbeats). Without this, a fork
1037
1194
  // was advertised the global locus but published somewhere else; and a trunk
1038
1195
  // agent was told the wrong channel under concurrency.
1039
- const home = agentName ? this.homeChannelResolver?.(agentName) : undefined;
1040
- const active = agentName ? this.activeChannelResolver?.(agentName) : undefined;
1041
- const outgoing = home ?? active ?? this.defaultPublishChannel;
1196
+ // Each lookup is keyed on whether a name was given, not its truthiness:
1197
+ // '' is a name an ephemeral run can have, and resolveLocus reads it too.
1198
+ const home = agentName !== undefined ? this.homeChannelResolver?.(agentName) : undefined;
1199
+ const active = agentName !== undefined ? this.activeChannelResolver?.(agentName) : undefined;
1200
+ // An ephemeral run takes no global fallback (takesGlobalFallback): neither
1201
+ // the latest inbound message's channel as its outgoing one, nor that
1202
+ // message as the one it is replying to.
1203
+ const fallback = agentName !== undefined ? this.globalFallbackFor(agentName) : this.defaultPublishChannel;
1204
+ const outgoing = home ?? active ?? fallback;
1042
1205
  if (openChannels.length === 0 && !outgoing) {
1043
1206
  return undefined;
1044
1207
  }
@@ -1046,9 +1209,9 @@ export class ChannelRegistry {
1046
1209
  // Incoming: the most-recent inbound message (what the agent is replying to).
1047
1210
  // Left process-global — per-channel inbound tracking (the right messageId for
1048
1211
  // a fork's own channel) is a separate concern from the outbound routing fix.
1049
- if (this.defaultPublishChannel && this.defaultPublishMessageId) {
1212
+ if (fallback && this.defaultPublishMessageId) {
1050
1213
  context.incoming = {
1051
- channelId: this.defaultPublishChannel,
1214
+ channelId: fallback,
1052
1215
  messageId: this.defaultPublishMessageId,
1053
1216
  threadId: this.defaultPublishThreadId,
1054
1217
  };
@@ -1074,6 +1237,10 @@ export class ChannelRegistry {
1074
1237
  * server's channels don't linger and route speech into the void.
1075
1238
  */
1076
1239
  removeServer(serverId) {
1240
+ // Every lifecycle operation queued for this server is cancelled when it
1241
+ // would run (ChannelLifecycleCancelledError), so none acts on a closing
1242
+ // connection or on a later re-registration's entries.
1243
+ this.serverRemovals.set(serverId, (this.serverRemovals.get(serverId) ?? 0) + 1);
1077
1244
  for (const [key, entry] of this.channels) {
1078
1245
  if (entry.serverId !== serverId)
1079
1246
  continue;
@@ -1112,14 +1279,7 @@ export class ChannelRegistry {
1112
1279
  initializeLifecycleStore() {
1113
1280
  if (!this.store)
1114
1281
  return;
1115
- try {
1116
- this.store.registerState({ id: CHANNEL_LIFECYCLE_LOG_ID, strategy: 'append_log' });
1117
- }
1118
- catch (error) {
1119
- if (!(error instanceof Error) || !error.message.includes('State already exists')) {
1120
- throw error;
1121
- }
1122
- }
1282
+ ensureStateRegistered(this.store, { id: CHANNEL_LIFECYCLE_LOG_ID, strategy: 'append_log' });
1123
1283
  const raw = this.store.getStateJson(CHANNEL_LIFECYCLE_LOG_ID);
1124
1284
  if (!Array.isArray(raw))
1125
1285
  return;
@@ -1157,6 +1317,15 @@ export class ChannelRegistry {
1157
1317
  }
1158
1318
  }
1159
1319
  }
1320
+ /**
1321
+ * Each writer appends its record here before it changes memory for it, so
1322
+ * an append that throws, as on a full disk, fails the operation with that
1323
+ * change unmade, and memory and the log still agree. A retry then records
1324
+ * the change rather than finding it in memory already, where
1325
+ * setDesiredState's same-state return would record nothing, and a restart
1326
+ * replays what memory held. appendLabelSighting keeps the same order for
1327
+ * the label history.
1328
+ */
1160
1329
  appendLifecycleEvent(event) {
1161
1330
  this.store?.appendToStateJson(CHANNEL_LIFECYCLE_LOG_ID, event);
1162
1331
  }
@@ -1173,14 +1342,7 @@ export class ChannelRegistry {
1173
1342
  initializeLabelHistoryStore() {
1174
1343
  if (!this.store)
1175
1344
  return;
1176
- try {
1177
- this.store.registerState({ id: CHANNEL_LABEL_HISTORY_LOG_ID, strategy: 'append_log' });
1178
- }
1179
- catch (error) {
1180
- if (!(error instanceof Error) || !error.message.includes('State already exists')) {
1181
- throw error;
1182
- }
1183
- }
1345
+ ensureStateRegistered(this.store, { id: CHANNEL_LABEL_HISTORY_LOG_ID, strategy: 'append_log' });
1184
1346
  const raw = this.store.getStateJson(CHANNEL_LABEL_HISTORY_LOG_ID);
1185
1347
  if (!Array.isArray(raw))
1186
1348
  return;
@@ -1260,6 +1422,16 @@ export class ChannelRegistry {
1260
1422
  const isDmUnchanged = isDm === undefined || this.dmClassified.get(channelId) === true;
1261
1423
  if (labelUnchanged && recipientIdUnchanged && recipientNameUnchanged && isDmUnchanged)
1262
1424
  return;
1425
+ // The log first, as for lifecycle events: an append that throws leaves
1426
+ // the history in memory as it was, so the next sighting records it.
1427
+ this.store?.appendToStateJson(CHANNEL_LABEL_HISTORY_LOG_ID, {
1428
+ channelId,
1429
+ label,
1430
+ ts: Date.now(),
1431
+ ...(recipientId ? { recipientId } : {}),
1432
+ ...(recipientName ? { recipientName } : {}),
1433
+ ...(isDm ? { isDm } : {}),
1434
+ });
1263
1435
  this.labelHistory.set(channelId, label);
1264
1436
  let seen = this.allLabelsSeen.get(channelId);
1265
1437
  if (!seen) {
@@ -1273,14 +1445,6 @@ export class ChannelRegistry {
1273
1445
  this.dmRecipientNames.set(channelId, recipientName);
1274
1446
  if (isDm)
1275
1447
  this.dmClassified.set(channelId, true);
1276
- this.store?.appendToStateJson(CHANNEL_LABEL_HISTORY_LOG_ID, {
1277
- channelId,
1278
- label,
1279
- ts: Date.now(),
1280
- ...(recipientId ? { recipientId } : {}),
1281
- ...(recipientName ? { recipientName } : {}),
1282
- ...(isDm ? { isDm } : {}),
1283
- });
1284
1448
  }
1285
1449
  /**
1286
1450
  * Look up a label spec against label *history* rather than the live
@@ -1498,7 +1662,6 @@ export class ChannelRegistry {
1498
1662
  const key = this.lifecycleKey(serverId, channelId);
1499
1663
  if (this.desiredStates.get(key)?.state === desired)
1500
1664
  return;
1501
- this.desiredStates.set(key, { state: desired, source });
1502
1665
  this.appendLifecycleEvent({
1503
1666
  kind: 'desired-state',
1504
1667
  serverId,
@@ -1507,6 +1670,7 @@ export class ChannelRegistry {
1507
1670
  source,
1508
1671
  timestamp: new Date().toISOString(),
1509
1672
  });
1673
+ this.desiredStates.set(key, { state: desired, source });
1510
1674
  }
1511
1675
  // ==========================================================================
1512
1676
  // Tune-out state (issue #77) — durable in the lifecycle log
@@ -1519,12 +1683,6 @@ export class ChannelRegistry {
1519
1683
  */
1520
1684
  enterTuneOut(serverId, channelId, params, source) {
1521
1685
  const key = this.lifecycleKey(serverId, channelId);
1522
- this.desiredStates.set(key, {
1523
- state: 'tuned-out',
1524
- source,
1525
- tuneOut: params,
1526
- wakeCount: 0,
1527
- });
1528
1686
  this.appendLifecycleEvent({
1529
1687
  kind: 'desired-state',
1530
1688
  serverId,
@@ -1534,6 +1692,12 @@ export class ChannelRegistry {
1534
1692
  source,
1535
1693
  timestamp: new Date().toISOString(),
1536
1694
  });
1695
+ this.desiredStates.set(key, {
1696
+ state: 'tuned-out',
1697
+ source,
1698
+ tuneOut: params,
1699
+ wakeCount: 0,
1700
+ });
1537
1701
  }
1538
1702
  /**
1539
1703
  * End the active tune-out, returning the channel to `nextState`.
@@ -1547,7 +1711,6 @@ export class ChannelRegistry {
1547
1711
  if (current?.state !== 'tuned-out' || !current.tuneOut)
1548
1712
  return null;
1549
1713
  const ended = { params: current.tuneOut, wakeCount: current.wakeCount ?? 0 };
1550
- this.desiredStates.set(key, { state: nextState, source });
1551
1714
  this.appendLifecycleEvent({
1552
1715
  kind: 'desired-state',
1553
1716
  serverId,
@@ -1556,6 +1719,7 @@ export class ChannelRegistry {
1556
1719
  source,
1557
1720
  timestamp: new Date().toISOString(),
1558
1721
  });
1722
+ this.desiredStates.set(key, { state: nextState, source });
1559
1723
  return ended;
1560
1724
  }
1561
1725
  /**
@@ -1570,16 +1734,17 @@ export class ChannelRegistry {
1570
1734
  const current = this.desiredStates.get(key);
1571
1735
  if (current?.state !== 'tuned-out' || !current.tuneOut)
1572
1736
  return null;
1573
- current.wakeCount = (current.wakeCount ?? 0) + 1;
1737
+ const wakeCount = (current.wakeCount ?? 0) + 1;
1574
1738
  this.appendLifecycleEvent({
1575
1739
  kind: 'tune-out-wake',
1576
1740
  serverId,
1577
1741
  channelId,
1578
1742
  epochId: current.tuneOut.epochId,
1579
- wakeCount: current.wakeCount,
1743
+ wakeCount,
1580
1744
  timestamp: new Date().toISOString(),
1581
1745
  });
1582
- return { params: current.tuneOut, wakeCount: current.wakeCount };
1746
+ current.wakeCount = wakeCount;
1747
+ return { params: current.tuneOut, wakeCount };
1583
1748
  }
1584
1749
  /** Active tune-out params + wake count for a channel, or null. */
1585
1750
  getTuneOutState(serverId, channelId) {
@@ -1603,19 +1768,13 @@ export class ChannelRegistry {
1603
1768
  readCoordinatorState(stateId) {
1604
1769
  if (!this.store)
1605
1770
  return null;
1606
- try {
1607
- this.store.registerState({ id: stateId, strategy: 'snapshot' });
1608
- }
1609
- catch { /* already registered */ }
1771
+ ensureStateRegistered(this.store, { id: stateId, strategy: 'snapshot' });
1610
1772
  return this.store.getStateJson(stateId);
1611
1773
  }
1612
1774
  writeCoordinatorState(stateId, value) {
1613
1775
  if (!this.store)
1614
1776
  return;
1615
- try {
1616
- this.store.registerState({ id: stateId, strategy: 'snapshot' });
1617
- }
1618
- catch { /* already registered */ }
1777
+ ensureStateRegistered(this.store, { id: stateId, strategy: 'snapshot' });
1619
1778
  this.store.setStateJson(stateId, value);
1620
1779
  }
1621
1780
  /**
@@ -1646,13 +1805,13 @@ export class ChannelRegistry {
1646
1805
  : 'closed';
1647
1806
  this.setDesiredState(serverId, channel.id, desired, 'legacy-recipe-migration');
1648
1807
  }
1649
- this.migratedLegacyPolicies.add(serverId);
1650
1808
  this.appendLifecycleEvent({
1651
1809
  kind: 'legacy-policy-migrated',
1652
1810
  serverId,
1653
1811
  source: Array.isArray(policy) ? 'allow-list' : policy,
1654
1812
  timestamp: new Date().toISOString(),
1655
1813
  });
1814
+ this.migratedLegacyPolicies.add(serverId);
1656
1815
  }
1657
1816
  /**
1658
1817
  * True when the server's recipe subscription policy wants this channel
@@ -1700,6 +1859,223 @@ export class ChannelRegistry {
1700
1859
  : 'default-closed');
1701
1860
  return channel.initiallyOpen !== true && byPolicy;
1702
1861
  }
1862
+ /**
1863
+ * Apply current durable intent, serializing the transport rather than the
1864
+ * server's registration ACK. Re-read after every await: registration,
1865
+ * descriptor updates, or a newer decision may have replaced our snapshot.
1866
+ */
1867
+ applyDesiredChannelState(serverId, channelId, options) {
1868
+ const lifecycleKey = this.lifecycleKey(serverId, channelId);
1869
+ const removals = this.serverRemovals.get(serverId) ?? 0;
1870
+ const previous = this.channelLifecycleTails.get(lifecycleKey) ?? Promise.resolve();
1871
+ this.lifecycleOutstanding.set(lifecycleKey, (this.lifecycleOutstanding.get(lifecycleKey) ?? 0) + 1);
1872
+ const operation = previous.catch(() => { }).then(() => this.runLifecycleOperation(serverId, channelId, removals, options));
1873
+ // Keep the rejection visible to existing joiners, while observing it
1874
+ // immediately and letting each queued successor start after either result.
1875
+ const tail = operation.then(() => { });
1876
+ this.channelLifecycleTails.set(lifecycleKey, tail);
1877
+ const cleanup = () => {
1878
+ if (this.channelLifecycleTails.get(lifecycleKey) === tail) {
1879
+ this.channelLifecycleTails.delete(lifecycleKey);
1880
+ }
1881
+ };
1882
+ void tail.then(cleanup, cleanup);
1883
+ return operation;
1884
+ }
1885
+ /** One queued lifecycle operation, run when its turn in the queue comes. */
1886
+ async runLifecycleOperation(serverId, channelId, removals, options) {
1887
+ const channelKey = `${serverId}:${channelId}`;
1888
+ const lifecycleKey = this.lifecycleKey(serverId, channelId);
1889
+ try {
1890
+ let attempts = 0;
1891
+ for (;;) {
1892
+ if ((this.serverRemovals.get(serverId) ?? 0) !== removals) {
1893
+ throw new ChannelLifecycleCancelledError(`Channel ${channelId}: its server's registrations were removed while this operation was queued`);
1894
+ }
1895
+ const entry = this.channels.get(channelKey);
1896
+ if (!entry)
1897
+ throw new Error(`Channel is no longer registered: ${channelId}`);
1898
+ const server = this.serverRegistry.getServer(serverId);
1899
+ if (!server)
1900
+ throw new Error(`Server not found: ${serverId}`);
1901
+ if (!CapabilityGrant.of(server).has('channels.lifecycle')) {
1902
+ throw new Error(`channels.lifecycle not in "${serverId}"'s effective grant (§14.1)`);
1903
+ }
1904
+ const descriptor = entry.descriptor;
1905
+ const desired = this.getDesiredState(serverId, channelId);
1906
+ const open = desired === 'open' || desired === 'tuned-out';
1907
+ // Per-RPC timeouts do not bound a stream of promptly successful but
1908
+ // continually superseded receipts. Bound this whole operation, then
1909
+ // release its queue slot and surface the failure to delivery joiners.
1910
+ // Its open flag needs no change here: every retry follows a replaced
1911
+ // registration, a retarget (already invalidated, whether the old
1912
+ // target's RPC was answered or failed), or a receipt for this very
1913
+ // target.
1914
+ if (attempts >= MAX_CHANNEL_LIFECYCLE_ATTEMPTS) {
1915
+ const error = new ChannelLifecycleConvergenceError(`Channel ${channelId} did not converge after ${attempts} lifecycle attempts; its target or desired state kept changing`);
1916
+ this.emitTraceFn({
1917
+ type: 'mcpl:channel-reconcile-failed',
1918
+ serverId, channelId, desired, attempts, error: error.message,
1919
+ });
1920
+ throw error;
1921
+ }
1922
+ attempts++;
1923
+ const sameTarget = () => this.serverRegistry.getServer(serverId) === server &&
1924
+ sameChannelTarget(entry.descriptor, descriptor);
1925
+ let result;
1926
+ try {
1927
+ if (open) {
1928
+ result = await server.sendChannelsOpen({
1929
+ channelId, type: descriptor.type, address: descriptor.address,
1930
+ ...(options?.history ? { history: options.history } : {}),
1931
+ });
1932
+ }
1933
+ else {
1934
+ await server.sendChannelsClose({ channelId });
1935
+ }
1936
+ }
1937
+ catch (error) {
1938
+ // An RPC can fail after the server acted, so a failure for the
1939
+ // current target leaves its outcome unknown: it is marked
1940
+ // unconfirmed with open unchanged, which keeps a confirmed-open
1941
+ // channel usable after a failed backscroll (the mark is read only
1942
+ // while not open). A failure from an older connection or target
1943
+ // also withdraws the open flag.
1944
+ if (this.channels.get(channelKey) === entry) {
1945
+ if (sameTarget())
1946
+ this.unconfirmedChannelTargets.add(entry);
1947
+ else
1948
+ this.invalidateTransportConfirmation(entry);
1949
+ }
1950
+ // A retarget while this RPC was out queued nothing for the new
1951
+ // target, since handleChanged leaves that to this operation. So a
1952
+ // failure for the old target says nothing about the new one, and a
1953
+ // channel still decided open or tuned out tries its new target,
1954
+ // within the attempt limit.
1955
+ if (this.channels.get(channelKey) === entry && !sameChannelTarget(entry.descriptor, descriptor)) {
1956
+ const now = this.getDesiredState(serverId, channelId);
1957
+ if (now === 'open' || now === 'tuned-out')
1958
+ continue;
1959
+ }
1960
+ throw error;
1961
+ }
1962
+ // A server can answer for a channel other than the one named: one
1963
+ // that falls back to its first channel of the type for an id it no
1964
+ // longer knows opens that channel and returns its history. Whatever
1965
+ // becomes of this receipt, the server did that: say so, and undo the
1966
+ // open it made if the host wants that channel closed.
1967
+ const substitute = open ? substitutedChannel(result, channelId) : undefined;
1968
+ if (substitute !== undefined) {
1969
+ // Each id prints through logValue, as the rejection lines' ids do,
1970
+ // so an odd one can't add a line or a field.
1971
+ const answered = logValue(substitute.kind === 'other' ? substitute.id : substitute.value);
1972
+ console.error(`[channel-open-substituted] server=${serverId} requested=${logValue(channelId)} answered=${answered}`);
1973
+ this.emitTraceFn({ type: 'mcpl:channel-open-substituted', serverId, channelId, answeredFor: answered });
1974
+ // A malformed id names nothing registered here, so there is
1975
+ // nothing to close.
1976
+ if (substitute.kind === 'other')
1977
+ this.closeSubstitutedChannel(serverId, substitute.id);
1978
+ }
1979
+ // Never apply an old receipt to the current target, even transiently:
1980
+ // a corrective open might fail before that stale bit is replaced.
1981
+ if (this.channels.get(channelKey) !== entry)
1982
+ continue;
1983
+ if (!sameTarget()) {
1984
+ this.invalidateTransportConfirmation(entry);
1985
+ continue;
1986
+ }
1987
+ // A current receipt for another channel confirms nothing about this
1988
+ // one, and its history is another channel's. So the open fails,
1989
+ // nothing in the answer is used, and this channel's open state is
1990
+ // unconfirmed: a server that doesn't resolve this id may not resolve
1991
+ // its sends either.
1992
+ if (substitute !== undefined) {
1993
+ this.invalidateTransportConfirmation(entry);
1994
+ throw new ChannelOpenSubstitutionError(`Server "${serverId}" answered channels/open for ${channelId} with ` +
1995
+ (substitute.kind === 'other' ? 'a different channel' : 'a channel id that is not a string') +
1996
+ `; ${channelId} is not marked open, and nothing in the answer is used`);
1997
+ }
1998
+ entry.open = open;
1999
+ this.unconfirmedChannelTargets.delete(entry);
2000
+ const now = this.getDesiredState(serverId, channelId);
2001
+ if (open !== (now === 'open' || now === 'tuned-out'))
2002
+ continue;
2003
+ return { open, result };
2004
+ }
2005
+ }
2006
+ finally {
2007
+ // Synchronous with the operation's last look at its target: from here
2008
+ // on, nothing it does will open a new target.
2009
+ const left = (this.lifecycleOutstanding.get(lifecycleKey) ?? 1) - 1;
2010
+ if (left > 0)
2011
+ this.lifecycleOutstanding.set(lifecycleKey, left);
2012
+ else
2013
+ this.lifecycleOutstanding.delete(lifecycleKey);
2014
+ }
2015
+ }
2016
+ /**
2017
+ * A server that answered an open with another channel really opened that
2018
+ * one. When it's registered here and the host wants it closed, close it
2019
+ * through its own lifecycle queue: otherwise its traffic would reach the
2020
+ * resident, marked open by its first incoming message. A channel the host
2021
+ * wants open is left alone, and a close can't be answered for another
2022
+ * channel, so this never starts another substitution. An id the host
2023
+ * doesn't know needs nothing: §14.5 refuses its traffic.
2024
+ */
2025
+ closeSubstitutedChannel(serverId, channelId) {
2026
+ if (!this.channels.has(`${serverId}:${channelId}`))
2027
+ return;
2028
+ if (this.getDesiredState(serverId, channelId) !== 'closed')
2029
+ return;
2030
+ this.applyDesiredChannelState(serverId, channelId).catch((err) => {
2031
+ // Removal while queued is cancellation, as in reconcileChannels, and
2032
+ // exhausted convergence emitted its own diagnostic.
2033
+ if (!this.channels.has(`${serverId}:${channelId}`) ||
2034
+ err instanceof ChannelLifecycleCancelledError ||
2035
+ err instanceof ChannelLifecycleConvergenceError)
2036
+ return;
2037
+ this.emitTraceFn({
2038
+ type: 'mcpl:channel-reconcile-failed',
2039
+ serverId, channelId, desired: 'closed', error: err.message,
2040
+ });
2041
+ });
2042
+ }
2043
+ /**
2044
+ * Join outstanding lifecycle work without recording new intent. Retargeting
2045
+ * may have invalidated open confirmation while the decision remains pending.
2046
+ * A close that settles during the wait remains authoritative; waiting for
2047
+ * it is not an instruction to open the channel again.
2048
+ */
2049
+ async waitForOpenChannel(entry) {
2050
+ const lifecycleKey = this.lifecycleKey(entry.serverId, entry.descriptor.id);
2051
+ for (;;) {
2052
+ const pending = this.channelLifecycleTails.get(lifecycleKey);
2053
+ if (!pending)
2054
+ break;
2055
+ try {
2056
+ await pending;
2057
+ }
2058
+ catch (error) {
2059
+ // A redundant backscroll failure can leave a known-open channel
2060
+ // usable. Exhausted convergence is different: it never established
2061
+ // the current target, so joined delivery must fail visibly.
2062
+ if (error instanceof ChannelLifecycleConvergenceError)
2063
+ throw error;
2064
+ }
2065
+ // A settled tail releases its own key. One still current after it
2066
+ // settled would be awaited again without ever yielding to the event
2067
+ // loop, so stop waiting rather than spin.
2068
+ if (this.channelLifecycleTails.get(lifecycleKey) === pending)
2069
+ break;
2070
+ }
2071
+ const current = this.channels.get(`${entry.serverId}:${entry.descriptor.id}`);
2072
+ if (!current)
2073
+ throw new Error('Channel registration was removed while delivery waited');
2074
+ if (!current.open || this.getDesiredState(current.serverId, current.descriptor.id) === 'closed') {
2075
+ throw new Error('Channel is not confirmed open for delivery, or was selected for closure while delivery waited');
2076
+ }
2077
+ return current;
2078
+ }
1703
2079
  async reconcileChannels(serverId, channels) {
1704
2080
  const server = this.serverRegistry.getServer(serverId);
1705
2081
  if (!server)
@@ -1720,57 +2096,17 @@ export class ChannelRegistry {
1720
2096
  // announced to the agent as one batched notice after the loop, so a
1721
2097
  // first boot on a new policy doesn't produce one notice per channel.
1722
2098
  const policyOpened = [];
2099
+ // Record all admissions before waiting on any channel. A slow first RPC
2100
+ // must not delay intent for the rest of this registration batch.
1723
2101
  for (const channel of channels) {
1724
- if (this.ensureInitialDesiredState(serverId, channel)) {
1725
- policyOpened.push({ channelId: channel.id, label: channel.label });
1726
- }
1727
- const key = `${serverId}:${channel.id}`;
1728
- const desired = this.getDesiredState(serverId, channel.id);
1729
- const entry = this.channels.get(key);
1730
- // Tuned-out sits on the OPEN side of reconcile: traffic must keep
1731
- // arriving (the subconscious reads it); only main's wake/visibility
1732
- // is diverted, downstream at ingestion.
1733
- if (desired !== 'open' && desired !== 'tuned-out') {
1734
- try {
1735
- await server.sendChannelsClose({ channelId: channel.id });
1736
- if (entry)
1737
- entry.open = false;
1738
- }
1739
- catch (err) {
1740
- this.emitTraceFn({
1741
- type: 'mcpl:channel-reconcile-failed',
1742
- serverId,
1743
- channelId: channel.id,
1744
- desired,
1745
- error: err.message,
1746
- });
1747
- }
1748
- continue;
1749
- }
1750
- try {
1751
- await server.sendChannelsOpen({
1752
- channelId: channel.id,
1753
- type: channel.type,
1754
- address: channel.address,
1755
- });
1756
- if (entry)
1757
- entry.open = true;
1758
- }
1759
- catch (err) {
1760
- if (entry)
1761
- entry.open = false;
1762
- this.emitTraceFn({
1763
- type: 'mcpl:channel-reconcile-failed',
1764
- serverId,
1765
- channelId: channel.id,
1766
- desired,
1767
- error: err.message,
1768
- });
2102
+ const entry = this.channels.get(`${serverId}:${channel.id}`);
2103
+ if (entry && this.ensureInitialDesiredState(serverId, entry.descriptor)) {
2104
+ policyOpened.push({ channelId: channel.id, label: entry.descriptor.label });
1769
2105
  }
1770
2106
  }
1771
- // Announce policy admissions to the agent — nothing may start flowing
1772
- // traffic into their window without them being told, and told how to
1773
- // opt out (channel_close; their decision outranks policy).
2107
+ // Announce the durable admission decision before awaiting transport.
2108
+ // This is intent, not confirmation that an open RPC succeeded. It stays
2109
+ // truthful if the first attempt fails, and a retry needs no new admission.
1774
2110
  if (policyOpened.length > 0) {
1775
2111
  try {
1776
2112
  this.onChannelAutoOpened?.({
@@ -1783,6 +2119,32 @@ export class ChannelRegistry {
1783
2119
  console.error('onChannelAutoOpened (policy) failed:', err);
1784
2120
  }
1785
2121
  }
2122
+ // Each channel has its own lifecycle queue, so a held RPC for one (up to
2123
+ // its timeout, times its attempts) doesn't delay its siblings.
2124
+ await Promise.allSettled(channels.map(async (channel) => {
2125
+ if (!this.channels.has(`${serverId}:${channel.id}`))
2126
+ return;
2127
+ try {
2128
+ await this.applyDesiredChannelState(serverId, channel.id);
2129
+ }
2130
+ catch (err) {
2131
+ // Removal while queued is cancellation, not a failed subscription.
2132
+ // Exhausted convergence and a substituted answer already emitted
2133
+ // their own diagnostics.
2134
+ if (!this.channels.has(`${serverId}:${channel.id}`) ||
2135
+ err instanceof ChannelLifecycleCancelledError ||
2136
+ err instanceof ChannelLifecycleConvergenceError ||
2137
+ err instanceof ChannelOpenSubstitutionError)
2138
+ return;
2139
+ this.emitTraceFn({
2140
+ type: 'mcpl:channel-reconcile-failed',
2141
+ serverId,
2142
+ channelId: channel.id,
2143
+ desired: this.getDesiredState(serverId, channel.id),
2144
+ error: err.message,
2145
+ });
2146
+ }
2147
+ }));
1786
2148
  }
1787
2149
  // ==========================================================================
1788
2150
  // Private: Typing notification
@@ -1879,33 +2241,35 @@ export class ChannelRegistry {
1879
2241
  };
1880
2242
  }
1881
2243
  /**
1882
- * The single open executor: record durable desired-open intent, tell the
1883
- * server to subscribe, mark the live entry open. Every path that opens a
1884
- * channel — the agent's channel_open tool, delivery into a closed locus,
1885
- * an explicit send into a closed channel — funnels here, so lifecycle
1886
- * events, desired state, and live state can never diverge by path.
2244
+ * Record durable desired-open intent, then queue the channel's lifecycle
2245
+ * work (applyDesiredChannelState), which tells the server to subscribe and
2246
+ * marks the live entry open on a receipt for its current target. Every
2247
+ * open the agent or its speech asks for — the channel_open tool, delivery
2248
+ * into a closed locus, an explicit send into a closed channel — comes
2249
+ * here. Reconciliation queues that work directly, as the closes do
2250
+ * (channel_close, a decline, and the close of a substituted channel).
1887
2251
  * Desired state is recorded BEFORE the server round-trip: intent sticks
1888
2252
  * even if the subscribe fails, and reconciliation retries later.
2253
+ * Automatic delivery needs open transport, not a change of attention:
2254
+ * preserve tune-out epochs both before and during the round-trip.
2255
+ *
2256
+ * `opened` says whether this call turned a closed channel open, which is
2257
+ * what the delivery and reply notices tell the resident. Reconfirming the
2258
+ * transport of a channel already decided open or tuned out isn't one.
1889
2259
  */
1890
2260
  async openChannelNow(entry, source, history) {
1891
- this.setDesiredState(entry.serverId, entry.descriptor.id, 'open', source);
1892
- const server = this.serverRegistry.getServer(entry.serverId);
1893
- if (!server) {
1894
- throw new Error(`Server not found: ${entry.serverId}`);
2261
+ const transportOnly = source !== 'agent-tool';
2262
+ const before = this.getDesiredState(entry.serverId, entry.descriptor.id);
2263
+ const opened = before !== 'open' && before !== 'tuned-out';
2264
+ if (!transportOnly || before !== 'tuned-out') {
2265
+ this.setDesiredState(entry.serverId, entry.descriptor.id, 'open', source);
1895
2266
  }
1896
- if (!CapabilityGrant.of(server).has('channels.lifecycle')) {
1897
- // Desired state is already recorded — intent sticks; reconciliation
1898
- // will retry if the grant later widens (§6.7 expansion-on-receipt).
1899
- throw new Error(`channels.lifecycle not in "${entry.serverId}"'s effective grant (§14.1)`);
2267
+ const applied = await this.applyDesiredChannelState(entry.serverId, entry.descriptor.id, { history });
2268
+ const desired = this.getDesiredState(entry.serverId, entry.descriptor.id);
2269
+ if (!applied.open || (desired !== 'open' && !(transportOnly && desired === 'tuned-out'))) {
2270
+ throw new Error('Channel open was superseded by a newer lifecycle decision');
1900
2271
  }
1901
- const result = await server.sendChannelsOpen({
1902
- channelId: entry.descriptor.id,
1903
- type: entry.descriptor.type,
1904
- address: entry.descriptor.address,
1905
- ...(history ? { history } : {}),
1906
- });
1907
- entry.open = true;
1908
- return result;
2272
+ return { result: applied.result, opened };
1909
2273
  }
1910
2274
  /**
1911
2275
  * Resolve a `>>` prose-routing target (explicit prose routing —
@@ -2091,19 +2455,25 @@ export class ChannelRegistry {
2091
2455
  return { status: 'unknown-channel' };
2092
2456
  if (matches.length > 1)
2093
2457
  return { status: 'ambiguous' };
2094
- const entry = matches[0];
2095
- const resolved = { channelId: entry.descriptor.id, label: entry.descriptor.label };
2096
- if (entry.open && this.getDesiredState(entry.serverId, entry.descriptor.id) === 'open') {
2097
- return { status: 'already-open', ...resolved };
2098
- }
2458
+ let entry = matches[0];
2099
2459
  try {
2100
- await this.openChannelNow(entry, 'opened-by-reply');
2101
- this.emitTraceFn({
2102
- type: 'mcpl:channel-opened-by-send',
2103
- serverId: entry.serverId,
2104
- channelId: entry.descriptor.id,
2105
- });
2106
- return { status: 'opened', ...resolved };
2460
+ if (this.hasPendingLifecycle(entry)) {
2461
+ entry = await this.waitForOpenChannel(entry);
2462
+ }
2463
+ const desired = this.getDesiredState(entry.serverId, entry.descriptor.id);
2464
+ if (entry.open && (desired === 'open' || desired === 'tuned-out')) {
2465
+ return { status: 'already-open', channelId: entry.descriptor.id, label: entry.descriptor.label };
2466
+ }
2467
+ const { opened } = await this.openChannelNow(entry, 'opened-by-reply');
2468
+ entry = await this.waitForOpenChannel(entry);
2469
+ if (opened) {
2470
+ this.emitTraceFn({
2471
+ type: 'mcpl:channel-opened-by-send',
2472
+ serverId: entry.serverId,
2473
+ channelId: entry.descriptor.id,
2474
+ });
2475
+ }
2476
+ return { status: opened ? 'opened' : 'already-open', channelId: entry.descriptor.id, label: entry.descriptor.label };
2107
2477
  }
2108
2478
  catch (err) {
2109
2479
  this.emitTraceFn({
@@ -2112,7 +2482,9 @@ export class ChannelRegistry {
2112
2482
  channelId: entry.descriptor.id,
2113
2483
  error: err.message,
2114
2484
  });
2115
- return { status: 'open-failed', ...resolved };
2485
+ const current = this.channels.get(`${entry.serverId}:${entry.descriptor.id}`);
2486
+ return { status: 'open-failed', channelId: entry.descriptor.id,
2487
+ ...(current ? { label: current.descriptor.label } : {}) };
2116
2488
  }
2117
2489
  }
2118
2490
  async handleToolOpen(input) {
@@ -2128,11 +2500,13 @@ export class ChannelRegistry {
2128
2500
  // A tuned-out channel is transport-open but attention-diverted; a plain
2129
2501
  // open would silently discard the epoch (and its pending backlog dump).
2130
2502
  // Cancelling is the tune-out coordinator's flow, reached via its own
2131
- // tool — refuse with the pointer rather than eat the state.
2503
+ // tool — refuse with the pointer rather than eat the state. As an error
2504
+ // result, the agent reads the pointer itself; `data` names the refusal
2505
+ // for an in-process caller.
2132
2506
  if (this.getDesiredState(entry.serverId, input.channelId) === 'tuned-out') {
2133
2507
  return {
2134
2508
  success: false,
2135
- isError: false,
2509
+ isError: true,
2136
2510
  error: `Channel ${input.channelId} is tuned out. Cancel the tune-out ` +
2137
2511
  `(tune_out with mode: 'cancel') to resume normal attention — ` +
2138
2512
  `cancelling delivers the diverted backlog.`,
@@ -2140,7 +2514,7 @@ export class ChannelRegistry {
2140
2514
  };
2141
2515
  }
2142
2516
  const alreadyDesiredOpen = this.getDesiredState(entry.serverId, input.channelId) === 'open';
2143
- if (entry.open && alreadyDesiredOpen && !input.backscroll) {
2517
+ if (entry.open && alreadyDesiredOpen && !input.backscroll && !this.hasPendingLifecycle(entry)) {
2144
2518
  return {
2145
2519
  success: true,
2146
2520
  data: { channelId: input.channelId, status: 'already open' },
@@ -2153,7 +2527,7 @@ export class ChannelRegistry {
2153
2527
  ...(input.beforeMessageId ? { beforeMessageId: input.beforeMessageId } : {}),
2154
2528
  }
2155
2529
  : undefined;
2156
- const result = await this.openChannelNow(entry, 'agent-tool', history);
2530
+ const { result } = await this.openChannelNow(entry, 'agent-tool', history);
2157
2531
  return {
2158
2532
  success: true,
2159
2533
  data: {
@@ -2165,9 +2539,18 @@ export class ChannelRegistry {
2165
2539
  };
2166
2540
  }
2167
2541
  catch (err) {
2542
+ // The decision was recorded before the round-trip and outlives its
2543
+ // failure. Say so: otherwise a later delivery that opens the channel
2544
+ // would be the resident's first sign that it was still wanted open.
2545
+ const current = this.channels.get(`${entry.serverId}:${input.channelId}`);
2546
+ const stillSelected = current !== undefined && !current.open &&
2547
+ this.getDesiredState(entry.serverId, input.channelId) === 'open';
2168
2548
  return {
2169
2549
  success: false,
2170
- error: `Failed to open channel: ${err.message}`,
2550
+ error: `Failed to open channel: ${err.message}` +
2551
+ (stillSelected
2552
+ ? ' (it stays selected for opening: a later delivery or reconnect tries again, and channel_close withdraws that)'
2553
+ : ''),
2171
2554
  isError: true,
2172
2555
  };
2173
2556
  }
@@ -2183,7 +2566,8 @@ export class ChannelRegistry {
2183
2566
  };
2184
2567
  }
2185
2568
  const alreadyDesiredClosed = this.getDesiredState(entry.serverId, input.channelId) === 'closed';
2186
- if (!entry.open && alreadyDesiredClosed) {
2569
+ if (!entry.open && alreadyDesiredClosed && !this.hasPendingLifecycle(entry) &&
2570
+ !this.unconfirmedChannelTargets.has(entry)) {
2187
2571
  return {
2188
2572
  success: true,
2189
2573
  data: { channelId: input.channelId, status: 'already closed' },
@@ -2245,8 +2629,10 @@ export class ChannelRegistry {
2245
2629
  isError: true,
2246
2630
  };
2247
2631
  }
2248
- await server.sendChannelsClose({ channelId: input.channelId });
2249
- entry.open = false;
2632
+ const applied = await this.applyDesiredChannelState(entry.serverId, input.channelId);
2633
+ if (applied.open || this.getDesiredState(entry.serverId, input.channelId) !== 'closed') {
2634
+ throw new Error('Channel close was superseded by a newer lifecycle decision');
2635
+ }
2250
2636
  return {
2251
2637
  success: true,
2252
2638
  data: { channelId: input.channelId, status: 'closed' },
@@ -2269,7 +2655,7 @@ export class ChannelRegistry {
2269
2655
  if (entry.open || this.getDesiredState(entry.serverId, input.channelId) === 'open') {
2270
2656
  return {
2271
2657
  success: false,
2272
- error: `Channel is already open: ${input.channelId}. Use channel_close to leave it.`,
2658
+ error: `Channel is already open: ${input.channelId}. Use channel_close to close it.`,
2273
2659
  isError: true,
2274
2660
  };
2275
2661
  }
@@ -2307,6 +2693,24 @@ export class ChannelRegistry {
2307
2693
  }
2308
2694
  }
2309
2695
  }
2696
+ // Where the host directs the lifecycle, a decline goes through the
2697
+ // lifecycle queue like any closed decision, unless a receipt confirmed
2698
+ // the transport closed with nothing pending: an unconfirmed transport may
2699
+ // be open, and pending work must settle on this decision. A failed close
2700
+ // is reported; the decision stands.
2701
+ let closeError;
2702
+ const current = this.channels.get(`${entry.serverId}:${input.channelId}`);
2703
+ if (current && this.directsLifecycle(entry.serverId) &&
2704
+ (this.unconfirmedChannelTargets.has(current) || this.hasPendingLifecycle(current))) {
2705
+ try {
2706
+ const applied = await this.applyDesiredChannelState(entry.serverId, input.channelId);
2707
+ if (applied.open)
2708
+ closeError = 'the close was superseded by a newer lifecycle decision';
2709
+ }
2710
+ catch (error) {
2711
+ closeError = error.message;
2712
+ }
2713
+ }
2310
2714
  this.appendLifecycleEvent({
2311
2715
  kind: 'invitation-declined',
2312
2716
  serverId: entry.serverId,
@@ -2323,6 +2727,7 @@ export class ChannelRegistry {
2323
2727
  acknowledged,
2324
2728
  ...(representation ? { representation } : {}),
2325
2729
  ...(acknowledgmentError ? { acknowledgmentError } : {}),
2730
+ ...(closeError ? { closeError: `Failed to close the channel's transport: ${closeError}` } : {}),
2326
2731
  },
2327
2732
  };
2328
2733
  }
@@ -2330,7 +2735,8 @@ export class ChannelRegistry {
2330
2735
  * Handle the synthesized `think` tool — a private reasoning scratchpad. It
2331
2736
  * sends nothing, and (unlike before) does NOT silence the turn: trailing
2332
2737
  * plain text is still routed as the reply. The thought stays in the agent's
2333
- * own context/chronicle. To deliberately not reply, the agent uses skip_reply.
2738
+ * own context/chronicle. To deliberately not reply, an agent that may call
2739
+ * skip_reply uses it.
2334
2740
  */
2335
2741
  handleToolThink(input) {
2336
2742
  return {
@@ -2339,9 +2745,11 @@ export class ChannelRegistry {
2339
2745
  // tool_use block; don't duplicate it in the result.
2340
2746
  data: {
2341
2747
  noted: true,
2748
+ // No tool to end the turn with: this handler can't see whether the
2749
+ // caller may call skip_reply, and think's own description says so
2750
+ // where it may.
2342
2751
  note: 'Thought recorded (private — not sent anywhere). Same-round text routing depends on ' +
2343
- 'your current same_round_think_text_policy; use agent_settings get to inspect it, or ' +
2344
- 'call skip_reply to end the turn without replying.',
2752
+ 'your current same_round_think_text_policy; use agent_settings get to inspect it.',
2345
2753
  },
2346
2754
  };
2347
2755
  }
@@ -2375,12 +2783,13 @@ export class ChannelRegistry {
2375
2783
  */
2376
2784
  handleToolSkipReply(input) {
2377
2785
  // The self-wake itself is armed by the framework (which owns the
2378
- // EventGate) BEFORE this dispatch; when the gate is absent the framework
2379
- // strips the field so this confirmation stays truthful.
2380
- const wakeSecs = Number(input.wake_in_seconds);
2381
- const selfWake = Number.isFinite(wakeSecs) && wakeSecs > 0
2382
- ? Math.max(1, Math.min(3600, Math.floor(wakeSecs)))
2383
- : undefined;
2786
+ // EventGate) BEFORE its dispatch reaches this handler, from the same
2787
+ // reading, and the framework refuses a wake the host can't set before
2788
+ // dispatching. A caller of this registry's handleChannelToolCall
2789
+ // (AgentFramework.channels) goes past both, so its note names a wake
2790
+ // nothing armed.
2791
+ const wake = readSkipReplyWake(input);
2792
+ const selfWake = wake.kind === 'wake' ? wake.seconds : undefined;
2384
2793
  return {
2385
2794
  success: true,
2386
2795
  // The note says "ended the turn" — make it TRUE. Without endTurn the
@@ -2464,122 +2873,283 @@ export class ChannelRegistry {
2464
2873
  }
2465
2874
  resolveLocus(conversationId) {
2466
2875
  const home = this.homeChannelResolver?.(conversationId);
2467
- return home ?? this.activeChannelResolver?.(conversationId) ?? this.defaultPublishChannel ?? null;
2876
+ return home ?? this.activeChannelResolver?.(conversationId) ?? this.globalFallbackFor(conversationId);
2468
2877
  }
2469
- async routeSpeech(conversationId, text,
2878
+ /** The process-global fallback channel for this conversation, or null when it takes none (takesGlobalFallback). */
2879
+ globalFallbackFor(conversationId) {
2880
+ return this.takesGlobalFallback?.(conversationId) === false ? null : this.defaultPublishChannel;
2881
+ }
2882
+ /**
2883
+ * Resolve a publish target to exactly one registered channel: `serverId`
2884
+ * plus `channelId` when the caller has the server, otherwise a channel id
2885
+ * registered by exactly one server. A shared id is refused, never resolved
2886
+ * to whichever server registered it first.
2887
+ */
2888
+ resolveDestination(target) {
2889
+ const entry = this.findExactEntry(target);
2890
+ if ('error' in entry)
2891
+ return entry;
2892
+ return {
2893
+ destination: {
2894
+ serverId: entry.entry.serverId,
2895
+ channelId: entry.entry.descriptor.id,
2896
+ ...(entry.entry.descriptor.label ? { label: entry.entry.descriptor.label } : {}),
2897
+ },
2898
+ };
2899
+ }
2900
+ findExactEntry(target) {
2901
+ // A supplied selector is checked, never read as omission: an empty or
2902
+ // non-string serverId would otherwise widen to "any server".
2903
+ if (typeof target.channelId !== 'string' || !target.channelId) {
2904
+ return { error: 'the destination names no channel id' };
2905
+ }
2906
+ if (target.serverId !== undefined && (typeof target.serverId !== 'string' || !target.serverId)) {
2907
+ return { error: 'serverId must name a server (omit it to resolve the channel id alone)' };
2908
+ }
2909
+ const matches = [...this.channels.values()].filter((e) => e.descriptor.id === target.channelId && (!target.serverId || e.serverId === target.serverId));
2910
+ if (matches.length === 0) {
2911
+ return {
2912
+ error: target.serverId
2913
+ ? `no registered channel "${target.channelId}" on server "${target.serverId}"`
2914
+ : `no registered channel "${target.channelId}"`,
2915
+ };
2916
+ }
2917
+ if (matches.length > 1) {
2918
+ return {
2919
+ error: `channel id "${target.channelId}" is registered by more than one MCPL server; the destination must name its server`,
2920
+ };
2921
+ }
2922
+ return { entry: matches[0] };
2923
+ }
2924
+ /**
2925
+ * Publish `text` to one registered channel and report what the attempt
2926
+ * established (PublishOutcome). The delivery executor behind plain speech
2927
+ * (deliverSpeech / routeSpeech) and resident resends of held drafts.
2928
+ * channel_publish (and publishForAgent, which shares its handler) resolves
2929
+ * and sends on its own path, and reads its answer through the same
2930
+ * publishReceiptOutcome and publishErrorOutcome.
2931
+ *
2932
+ * Resolution is exact (resolveDestination): `serverId` plus `channelId`
2933
+ * when the caller has the server, otherwise the channel id must be
2934
+ * registered by exactly one server (a shared id is refused, never routed
2935
+ * through whichever server came first). An omitted `serverId` means "not
2936
+ * in use"; a supplied one must be a non-empty server id, and an empty or
2937
+ * non-string selector is refused rather than read as omission. It is not
2938
+ * possible to send into a closed channel: a closed but registered
2939
+ * destination is opened first (`openSource` names why), once its server
2940
+ * and that server's channels.publish grant are known to take the send, so
2941
+ * a refused send opens nothing; and a failed open sends nothing.
2942
+ *
2943
+ * Only the connector's `delivered: true` confirms a post. `failed` means
2944
+ * nothing was posted: the attempt stopped before the request was written,
2945
+ * or the connector answered `delivered: false` and named no message (no
2946
+ * `messageId`, or null). Everything else is `unknown`: once the request
2947
+ * has been handed to the transport, an error response, a timeout, a lost
2948
+ * connection, or a missing, malformed or contradictory receipt (such as
2949
+ * `delivered: false` beside a message id, valid or not) means the post may
2950
+ * or may not exist, and the caller must not treat it as safe to retry
2951
+ * blindly.
2952
+ */
2953
+ async publish(conversationId, text, target, openSource = 'opened-by-delivery') {
2954
+ const at = () => Date.now();
2955
+ const found = this.findExactEntry(target);
2956
+ if ('error' in found)
2957
+ return { status: 'failed', reason: found.error, at: at() };
2958
+ let entry = found.entry;
2959
+ const destination = {
2960
+ serverId: entry.serverId,
2961
+ channelId: entry.descriptor.id,
2962
+ ...(entry.descriptor.label ? { label: entry.descriptor.label } : {}),
2963
+ };
2964
+ // A send that would be refused is refused before anything opens: a
2965
+ // closed channel opens only for words that can go out.
2966
+ const server = this.serverRegistry.getServer(entry.serverId);
2967
+ if (!server) {
2968
+ return { status: 'failed', destination, reason: `server "${entry.serverId}" not found`, at: at() };
2969
+ }
2970
+ // §14.1: channels/publish requires channels.publish in the grant. The
2971
+ // host not sending is the enforcement.
2972
+ if (!CapabilityGrant.of(server).has('channels.publish')) {
2973
+ return {
2974
+ status: 'failed',
2975
+ destination,
2976
+ reason: `channels.publish not in "${entry.serverId}"'s effective grant (§14.1)`,
2977
+ at: at(),
2978
+ };
2979
+ }
2980
+ // Lifecycle work already queued for the channel settles first: words go
2981
+ // out only into the channel it leaves, and fail visibly if it leaves
2982
+ // none open.
2983
+ if (this.hasPendingLifecycle(entry)) {
2984
+ try {
2985
+ entry = await this.waitForOpenChannel(entry);
2986
+ }
2987
+ catch (err) {
2988
+ return {
2989
+ status: 'failed',
2990
+ destination,
2991
+ reason: `lifecycle work prevented delivery: ${connectorText(err instanceof Error ? err.message : String(err))}`,
2992
+ at: at(),
2993
+ };
2994
+ }
2995
+ }
2996
+ // INVARIANT: it is not possible to send into a closed channel. A send
2997
+ // into a closed-but-registered channel (the canonical case: a DM that
2998
+ // woke the agent, since DMs register closed) OPENS it first, so typing
2999
+ // indicators, reaction machinery and inbound forwarding come alive with
3000
+ // the words. If the open fails, nothing goes out: a half-alive delivery
3001
+ // is worse than a loud marker. A channel decided closed is engaged even
3002
+ // while its transport is still open (a close that failed), as a reply
3003
+ // would engage it, wherever an open can succeed. Without the lifecycle
3004
+ // grant it can't, and a send opens before it publishes, so there a
3005
+ // channel whose transport is open keeps publishing.
3006
+ const engagesDecidedClosed = this.getDesiredState(entry.serverId, entry.descriptor.id) === 'closed' &&
3007
+ CapabilityGrant.of(server).has('channels.lifecycle');
3008
+ if (!entry.open || engagesDecidedClosed) {
3009
+ try {
3010
+ const { opened } = await this.openChannelNow(entry, openSource);
3011
+ entry = await this.waitForOpenChannel(entry);
3012
+ if (opened) {
3013
+ if (target.routed) {
3014
+ console.error(`[routeSpeech] ${conversationId}: locus ${logValue(entry.descriptor.id)} was closed — opened by delivery (speech implies engagement)`);
3015
+ }
3016
+ this.emitTraceFn({
3017
+ type: 'mcpl:channel-opened-by-send',
3018
+ serverId: entry.serverId,
3019
+ channelId: entry.descriptor.id,
3020
+ });
3021
+ if (openSource === 'opened-by-delivery') {
3022
+ try {
3023
+ this.onChannelAutoOpened?.({
3024
+ conversationId,
3025
+ serverId: entry.serverId,
3026
+ source: 'opened-by-delivery',
3027
+ channels: [{ channelId: entry.descriptor.id, label: entry.descriptor.label }],
3028
+ });
3029
+ }
3030
+ catch (err) {
3031
+ console.error('onChannelAutoOpened (delivery) failed:', err);
3032
+ }
3033
+ }
3034
+ }
3035
+ }
3036
+ catch (err) {
3037
+ return {
3038
+ status: 'failed',
3039
+ destination,
3040
+ reason: `channel is closed and open failed: ${connectorText(err instanceof Error ? err.message : String(err))}`,
3041
+ at: at(),
3042
+ };
3043
+ }
3044
+ }
3045
+ let result;
3046
+ try {
3047
+ result = await server.sendChannelsPublish({
3048
+ conversationId,
3049
+ channelId: entry.descriptor.id,
3050
+ content: [{ type: 'text', text }],
3051
+ });
3052
+ }
3053
+ catch (err) {
3054
+ return publishErrorOutcome(err, destination, at());
3055
+ }
3056
+ return publishReceiptOutcome(result, destination, at());
3057
+ }
3058
+ /**
3059
+ * Plain speech for one turn segment: publish `text` to the turn's frozen
3060
+ * locus and report the PublishOutcome. A failure (or an uncertain outcome)
3061
+ * is surfaced as a trace and through onRouteFailure, so the agent learns
3062
+ * its words did not (or may not have) reached anyone. An ephemeral run's
3063
+ * speech with no locus is the exception: it is its caller's result, so it
3064
+ * returns as not delivered and surfaces nothing.
3065
+ */
3066
+ async deliverSpeech(conversationId, text,
2470
3067
  /** The turn's FROZEN locus, snapshotted once by the caller (resolveLocus
2471
3068
  * at turn start) and passed to every segment of the turn. Mandatory:
2472
3069
  * there is deliberately no live-resolution fallback here — a late
2473
3070
  * dispatch resolving live can read the NEXT turn's trigger state or a
2474
3071
  * stale global default and land the reply in the wrong channel (item-3,
2475
3072
  * PR #32, and the 2026-07-22 Sol DM misroute). Explicit `null` means
2476
- * "this turn is pinned to no locus": fail loudly rather than guess. */
2477
- locusChannelId) {
2478
- // Surface a routing failure: emit a trace AND notify the host (which drops
2479
- // a `[discord-send-failed]` marker into chronicle) so the agent learns her
2480
- // reply never reached the human, instead of it vanishing silently.
2481
- const fail = (channelId, reason) => {
2482
- console.error(`[routeSpeech] ${conversationId}: ${reason} — speech NOT routed (${text.length} chars stay in chronicle)`);
3073
+ * "this turn is pinned to no locus": fail loudly rather than guess,
3074
+ * except for an ephemeral run, whose speech is then its caller's
3075
+ * result. */
3076
+ locusChannelId,
3077
+ /** `heldOnFailure`: the caller holds a `failed` delivery's words as a
3078
+ * draft with its own notice, so no failure marker is written for it.
3079
+ * An `unknown` outcome still gets its marker. */
3080
+ opts) {
3081
+ const fail = (channelId, reason, outcome) => {
3082
+ const status = outcome.status === 'unknown' ? 'unknown' : 'failed';
3083
+ const detail = outcome.detail !== undefined ? connectorText(outcome.detail) : undefined;
3084
+ const detailText = detail !== undefined ? `; connector detail: ${detail}` : '';
3085
+ console.error(`[routeSpeech] ${conversationId}: ${reason}${detailText} — speech ${status === 'unknown' ? 'delivery NOT confirmed' : 'NOT routed'} (${text.length} chars stay in chronicle)`);
2483
3086
  this.emitTraceFn({
2484
3087
  type: 'mcpl:speech-route-failed',
2485
3088
  conversationId,
2486
3089
  channelId: channelId ?? '',
2487
3090
  reason,
2488
3091
  textLen: text.length,
3092
+ outcome: status,
3093
+ ...(detail !== undefined ? { detail } : {}),
2489
3094
  });
2490
- this.onRouteFailure?.({ conversationId, channelId, reason, textLen: text.length });
2491
- return null;
3095
+ if (status === 'unknown' || !opts?.heldOnFailure) {
3096
+ this.onRouteFailure?.({ conversationId, channelId, reason, textLen: text.length, outcome: status, ...(detail !== undefined ? { detail } : {}) });
3097
+ }
3098
+ return outcome;
2492
3099
  };
2493
3100
  // Runtime backstop for JS callers the compiler can't see: an omitted
2494
3101
  // locus is a routing bug at the call site, never something to paper over
2495
3102
  // with a live re-resolution.
2496
3103
  if (locusChannelId === undefined) {
2497
- return fail(null, 'caller passed no locus (routing bug: every speech path must snapshot the turn locus)');
2498
- }
2499
- const channelId = locusChannelId;
2500
- if (!channelId) {
3104
+ const reason = 'caller passed no locus (routing bug: every speech path must snapshot the turn locus)';
3105
+ return fail(null, reason, { status: 'failed', reason, at: Date.now() });
3106
+ }
3107
+ if (!locusChannelId) {
3108
+ // An ephemeral run with no channel of its own: its plain speech is its
3109
+ // caller's result (takesGlobalFallback). It returns as not delivered,
3110
+ // with its own reason, and writes no line, trace or notice.
3111
+ if (this.takesGlobalFallback?.(conversationId) === false) {
3112
+ return { status: 'failed', reason: "an ephemeral run's plain speech with no channel of its own is its caller's result", at: Date.now() };
3113
+ }
2501
3114
  // The turn froze with no locus (no home, no triggering channel, no
2502
3115
  // global inbound ever seen) — the agent was told its prose stays in
2503
3116
  // the archive; honor that.
2504
- return fail(null, 'turn has no locus (no home/trigger channel; nothing to deliver into)');
2505
- }
2506
- const entry = this.findChannelEntry(channelId);
2507
- if (!entry) {
2508
- return fail(channelId, `no registered channel for locus "${channelId}"`);
2509
- }
2510
- // INVARIANT: it is not possible to send into a closed channel. Speech
2511
- // routed into a closed-but-registered locus (the canonical case: a DM
2512
- // that woke the agent — DMs register closed) OPENS the channel first,
2513
- // so typing indicators, reaction machinery, and inbound forwarding come
2514
- // alive with the reply. If the open fails, the speech does NOT go out:
2515
- // a half-alive delivery is worse than a loud marker.
2516
- if (!entry.open) {
2517
- try {
2518
- await this.openChannelNow(entry, 'opened-by-delivery');
2519
- console.error(`[routeSpeech] ${conversationId}: locus ${logValue(channelId)} was closed — opened by delivery (speech implies engagement)`);
2520
- this.emitTraceFn({
2521
- type: 'mcpl:channel-opened-by-send',
2522
- serverId: entry.serverId,
2523
- channelId,
2524
- });
2525
- try {
2526
- this.onChannelAutoOpened?.({
2527
- conversationId,
2528
- serverId: entry.serverId,
2529
- source: 'opened-by-delivery',
2530
- channels: [{ channelId, label: entry.descriptor.label }],
2531
- });
2532
- }
2533
- catch (err) {
2534
- console.error('onChannelAutoOpened (delivery) failed:', err);
2535
- }
2536
- }
2537
- catch (err) {
2538
- return fail(channelId, `locus channel is closed and open failed: ${err.message}`);
2539
- }
2540
- }
2541
- const server = this.serverRegistry.getServer(entry.serverId);
2542
- if (!server) {
2543
- return fail(channelId, `server "${entry.serverId}" not found`);
3117
+ const reason = 'turn has no locus (no home/trigger channel; nothing to deliver into)';
3118
+ return fail(null, reason, { status: 'failed', reason, at: Date.now() });
2544
3119
  }
2545
- // §14.1: channels/publish requires channels.publish in the grant. The
2546
- // host not sending is the enforcement — a send to an ungranted server
2547
- // would have it act on authority it was never told it has.
2548
- if (!CapabilityGrant.of(server).has('channels.publish')) {
2549
- return fail(channelId, `channels.publish not in "${entry.serverId}"'s effective grant (§14.1)`);
3120
+ const outcome = await this.publish(conversationId, text, { channelId: locusChannelId, routed: true });
3121
+ const channelId = outcome.destination?.channelId ?? locusChannelId;
3122
+ if (outcome.status !== 'delivered') {
3123
+ return fail(channelId, outcome.reason ?? 'delivery not confirmed', outcome);
2550
3124
  }
2551
- const publishParams = {
2552
- conversationId,
2553
- channelId,
2554
- content: [{ type: 'text', text }],
2555
- };
2556
- const result = await server.sendChannelsPublish(publishParams);
2557
- const delivered = result?.delivered ?? true;
2558
- // Surface the posted message's id (ChannelsPublishResult.messageId) so
2559
- // trace consumers can act on the just-posted message — e.g. a TTS-relay
2560
- // tap editing it down to the words actually voiced on interruption.
2561
- // Previously this was silently dropped here.
2562
- const messageId = result?.messageId;
2563
- // The server accepted the publish RPC but reported the message was not
2564
- // actually delivered (e.g. missing Send Messages permission). Previously
2565
- // this returned `{ delivered: true }`, masking the failure. Surface it.
2566
- if (delivered === false) {
2567
- return fail(channelId, `server "${entry.serverId}" reported delivered:false for "${channelId}"`);
2568
- }
2569
- console.error(`[routeSpeech] ${conversationId}: routed ${text.length} chars -> ${logValue(channelId)} (server=${entry.serverId}, delivered=${delivered})`);
3125
+ console.error(`[routeSpeech] ${conversationId}: routed ${text.length} chars -> ${logValue(channelId)} (server=${outcome.destination.serverId}, delivered=true)`);
2570
3126
  this.emitTraceFn({
2571
3127
  type: 'mcpl:speech-routed',
2572
3128
  conversationId,
2573
- serverId: entry.serverId,
3129
+ serverId: outcome.destination.serverId,
2574
3130
  channelId,
2575
- delivered,
3131
+ delivered: true,
2576
3132
  textLen: text.length,
2577
3133
  text,
2578
- ...(messageId !== undefined ? { messageId } : {}),
3134
+ // Surface the posted message's id (ChannelsPublishResult.messageId) so
3135
+ // trace consumers can act on the just-posted message — e.g. a TTS-relay
3136
+ // tap editing it down to the words actually voiced on interruption.
3137
+ ...(outcome.messageId !== undefined ? { messageId: outcome.messageId } : {}),
2579
3138
  });
2580
- return { delivered, channelId, ...(messageId !== undefined ? { messageId } : {}) };
3139
+ return outcome;
2581
3140
  }
2582
- async handleToolPublish(input) {
3141
+ /** deliverSpeech, reduced to the confirmed delivery (null when there was none). */
3142
+ async routeSpeech(conversationId, text, locusChannelId) {
3143
+ const outcome = await this.deliverSpeech(conversationId, text, locusChannelId);
3144
+ if (outcome.status !== 'delivered')
3145
+ return null;
3146
+ return {
3147
+ delivered: true,
3148
+ channelId: outcome.destination.channelId,
3149
+ ...(outcome.messageId !== undefined ? { messageId: outcome.messageId } : {}),
3150
+ };
3151
+ }
3152
+ async handleToolPublish(input, origin) {
2583
3153
  // Resolve content: accept both `content` and `text` (backward compat)
2584
3154
  const messageText = input.content ?? input.text;
2585
3155
  if (!messageText) {
@@ -2589,12 +3159,17 @@ export class ChannelRegistry {
2589
3159
  isError: true,
2590
3160
  };
2591
3161
  }
2592
- // Resolve channelId: default to most recent incoming channel
2593
- const channelId = input.channelId ?? this.defaultPublishChannel;
3162
+ // Resolve channelId: default to the most recent incoming channel, which an
3163
+ // ephemeral run never takes (takesGlobalFallback), so it names one.
3164
+ const caller = origin?.kind === 'agent' ? origin.agentName : undefined;
3165
+ const channelId = input.channelId ?? (caller !== undefined ? this.globalFallbackFor(caller) : this.defaultPublishChannel);
2594
3166
  if (!channelId) {
2595
3167
  return {
2596
3168
  success: false,
2597
- error: 'No channelId specified and no default channel available',
3169
+ error: caller !== undefined && this.takesGlobalFallback?.(caller) === false
3170
+ ? 'Nothing was published: name the channel with channelId. A subagent has no default channel, since its plain '
3171
+ + 'speech is returned to whoever called it, so channel_publish sends only to a channel it names.'
3172
+ : 'No channelId specified and no default channel available',
2598
3173
  isError: true,
2599
3174
  };
2600
3175
  }
@@ -2614,32 +3189,46 @@ export class ChannelRegistry {
2614
3189
  isError: true,
2615
3190
  };
2616
3191
  }
2617
- try {
2618
- if (!CapabilityGrant.of(server).has('channels.publish')) {
2619
- return {
2620
- success: false,
2621
- error: `channels.publish not in "${server.id}"'s effective grant (§14.1)`,
2622
- isError: true,
2623
- };
2624
- }
2625
- const publishParams = {
2626
- conversationId: '', // Framework will fill this when wired
2627
- channelId,
2628
- content: [{ type: 'text', text: messageText }],
2629
- };
2630
- const result = await server.sendChannelsPublish(publishParams);
2631
- return {
2632
- success: true,
2633
- data: result ?? { delivered: true },
2634
- };
2635
- }
2636
- catch (err) {
3192
+ if (!CapabilityGrant.of(server).has('channels.publish')) {
2637
3193
  return {
2638
3194
  success: false,
2639
- error: `Failed to publish to channel: ${err.message}`,
3195
+ error: `channels.publish not in "${server.id}"'s effective grant (§14.1)`,
2640
3196
  isError: true,
2641
3197
  };
2642
3198
  }
3199
+ const publishParams = {
3200
+ conversationId: '', // Framework will fill this when wired
3201
+ channelId,
3202
+ content: [{ type: 'text', text: messageText }],
3203
+ };
3204
+ const destination = {
3205
+ serverId: entry.serverId,
3206
+ channelId: entry.descriptor.id,
3207
+ ...(entry.descriptor.label ? { label: entry.descriptor.label } : {}),
3208
+ };
3209
+ // The answer counts by the same rule as plain speech's: only the
3210
+ // connector's delivered:true confirms a post.
3211
+ let result;
3212
+ let outcome;
3213
+ try {
3214
+ result = await server.sendChannelsPublish(publishParams);
3215
+ outcome = publishReceiptOutcome(result, destination, Date.now());
3216
+ }
3217
+ catch (err) {
3218
+ outcome = publishErrorOutcome(err, destination, Date.now());
3219
+ }
3220
+ if (outcome.status === 'delivered')
3221
+ return { success: true, data: result };
3222
+ // The connector's data may say what a multi-part send already posted.
3223
+ const detail = outcome.status === 'unknown' && outcome.detail !== undefined ? ` Connector detail: ${connectorText(outcome.detail)}` : '';
3224
+ return {
3225
+ success: false,
3226
+ error: (outcome.status === 'failed'
3227
+ ? `Publish to ${channelId} failed: ${outcome.reason}. Nothing was posted.`
3228
+ : `Publish to ${channelId} wasn't confirmed: ${outcome.reason}. ` +
3229
+ 'It may or may not have been posted, so sending it again could post it twice.') + detail,
3230
+ isError: true,
3231
+ };
2643
3232
  }
2644
3233
  }
2645
3234
  //# sourceMappingURL=channel-registry.js.map