@animalabs/agent-framework 0.23.3 → 0.24.1

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 (355) hide show
  1. package/README.md +212 -42
  2. package/dist/src/agent.d.ts +65 -9
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +98 -12
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/server.d.ts.map +1 -1
  7. package/dist/src/api/server.js +14 -12
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/clock-rows.d.ts +115 -0
  10. package/dist/src/clock-rows.d.ts.map +1 -0
  11. package/dist/src/clock-rows.js +222 -0
  12. package/dist/src/clock-rows.js.map +1 -0
  13. package/dist/src/code-execution/engine.d.ts +177 -0
  14. package/dist/src/code-execution/engine.d.ts.map +1 -0
  15. package/dist/src/code-execution/engine.js +494 -0
  16. package/dist/src/code-execution/engine.js.map +1 -0
  17. package/dist/src/code-execution/index.d.ts +5 -10
  18. package/dist/src/code-execution/index.d.ts.map +1 -1
  19. package/dist/src/code-execution/index.js +5 -9
  20. package/dist/src/code-execution/index.js.map +1 -1
  21. package/dist/src/code-execution/isolate/calls.d.ts +114 -0
  22. package/dist/src/code-execution/isolate/calls.d.ts.map +1 -0
  23. package/dist/src/code-execution/isolate/calls.js +145 -0
  24. package/dist/src/code-execution/isolate/calls.js.map +1 -0
  25. package/dist/src/code-execution/isolate/child-entry.d.ts +2 -0
  26. package/dist/src/code-execution/isolate/child-entry.d.ts.map +1 -0
  27. package/dist/src/code-execution/isolate/child-entry.js +5 -0
  28. package/dist/src/code-execution/isolate/child-entry.js.map +1 -0
  29. package/dist/src/code-execution/isolate/child-main.d.ts +26 -0
  30. package/dist/src/code-execution/isolate/child-main.d.ts.map +1 -0
  31. package/dist/src/code-execution/isolate/child-main.js +132 -0
  32. package/dist/src/code-execution/isolate/child-main.js.map +1 -0
  33. package/dist/src/code-execution/isolate/child.d.ts +44 -0
  34. package/dist/src/code-execution/isolate/child.d.ts.map +1 -0
  35. package/dist/src/code-execution/isolate/child.js +315 -0
  36. package/dist/src/code-execution/isolate/child.js.map +1 -0
  37. package/dist/src/code-execution/isolate/frames.d.ts +80 -0
  38. package/dist/src/code-execution/isolate/frames.d.ts.map +1 -0
  39. package/dist/src/code-execution/isolate/frames.js +210 -0
  40. package/dist/src/code-execution/isolate/frames.js.map +1 -0
  41. package/dist/src/code-execution/isolate/output.d.ts +68 -0
  42. package/dist/src/code-execution/isolate/output.d.ts.map +1 -0
  43. package/dist/src/code-execution/isolate/output.js +143 -0
  44. package/dist/src/code-execution/isolate/output.js.map +1 -0
  45. package/dist/src/code-execution/isolate/prelude.d.ts +109 -0
  46. package/dist/src/code-execution/isolate/prelude.d.ts.map +1 -0
  47. package/dist/src/code-execution/isolate/prelude.js +401 -0
  48. package/dist/src/code-execution/isolate/prelude.js.map +1 -0
  49. package/dist/src/code-execution/isolate/protocol.d.ts +151 -0
  50. package/dist/src/code-execution/isolate/protocol.d.ts.map +1 -0
  51. package/dist/src/code-execution/isolate/protocol.js +150 -0
  52. package/dist/src/code-execution/isolate/protocol.js.map +1 -0
  53. package/dist/src/code-execution/isolate/render.d.ts +57 -0
  54. package/dist/src/code-execution/isolate/render.d.ts.map +1 -0
  55. package/dist/src/code-execution/isolate/render.js +214 -0
  56. package/dist/src/code-execution/isolate/render.js.map +1 -0
  57. package/dist/src/code-execution/isolate/runner.d.ts +250 -0
  58. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -0
  59. package/dist/src/code-execution/isolate/runner.js +854 -0
  60. package/dist/src/code-execution/isolate/runner.js.map +1 -0
  61. package/dist/src/code-execution/isolate/scripts.d.ts +175 -0
  62. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -0
  63. package/dist/src/code-execution/isolate/scripts.js +357 -0
  64. package/dist/src/code-execution/isolate/scripts.js.map +1 -0
  65. package/dist/src/code-execution/quickjs.wasm +0 -0
  66. package/dist/src/code-execution/tool-definition.d.ts +93 -43
  67. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  68. package/dist/src/code-execution/tool-definition.js +176 -142
  69. package/dist/src/code-execution/tool-definition.js.map +1 -1
  70. package/dist/src/deadline-timer.d.ts +3 -0
  71. package/dist/src/deadline-timer.d.ts.map +1 -1
  72. package/dist/src/deadline-timer.js +1 -1
  73. package/dist/src/deadline-timer.js.map +1 -1
  74. package/dist/src/framework.d.ts +1120 -457
  75. package/dist/src/framework.d.ts.map +1 -1
  76. package/dist/src/framework.js +4635 -2505
  77. package/dist/src/framework.js.map +1 -1
  78. package/dist/src/gate/event-gate.d.ts +28 -4
  79. package/dist/src/gate/event-gate.d.ts.map +1 -1
  80. package/dist/src/gate/event-gate.js +89 -13
  81. package/dist/src/gate/event-gate.js.map +1 -1
  82. package/dist/src/held-writes.d.ts +16 -3
  83. package/dist/src/held-writes.d.ts.map +1 -1
  84. package/dist/src/held-writes.js +16 -3
  85. package/dist/src/held-writes.js.map +1 -1
  86. package/dist/src/index.d.ts +8 -6
  87. package/dist/src/index.d.ts.map +1 -1
  88. package/dist/src/index.js +10 -8
  89. package/dist/src/index.js.map +1 -1
  90. package/dist/src/mcpl/channel-registry.d.ts +483 -115
  91. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  92. package/dist/src/mcpl/channel-registry.js +1735 -425
  93. package/dist/src/mcpl/channel-registry.js.map +1 -1
  94. package/dist/src/mcpl/conversation-router.d.ts +19 -39
  95. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  96. package/dist/src/mcpl/conversation-router.js +27 -59
  97. package/dist/src/mcpl/conversation-router.js.map +1 -1
  98. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  99. package/dist/src/mcpl/convert-block.js +9 -5
  100. package/dist/src/mcpl/convert-block.js.map +1 -1
  101. package/dist/src/mcpl/inbound-source.d.ts +50 -0
  102. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  103. package/dist/src/mcpl/inbound-source.js +130 -0
  104. package/dist/src/mcpl/inbound-source.js.map +1 -1
  105. package/dist/src/mcpl/index.d.ts +2 -1
  106. package/dist/src/mcpl/index.d.ts.map +1 -1
  107. package/dist/src/mcpl/index.js +3 -2
  108. package/dist/src/mcpl/index.js.map +1 -1
  109. package/dist/src/mcpl/log-value.d.ts +6 -0
  110. package/dist/src/mcpl/log-value.d.ts.map +1 -1
  111. package/dist/src/mcpl/log-value.js +8 -0
  112. package/dist/src/mcpl/log-value.js.map +1 -1
  113. package/dist/src/mcpl/modern-connection.d.ts +15 -6
  114. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  115. package/dist/src/mcpl/modern-connection.js +168 -53
  116. package/dist/src/mcpl/modern-connection.js.map +1 -1
  117. package/dist/src/mcpl/prose-grammar.d.ts +4 -5
  118. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  119. package/dist/src/mcpl/prose-grammar.js +4 -5
  120. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  121. package/dist/src/mcpl/protocol-family.d.ts +97 -10
  122. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  123. package/dist/src/mcpl/protocol-family.js +174 -17
  124. package/dist/src/mcpl/protocol-family.js.map +1 -1
  125. package/dist/src/mcpl/push-coalescer.d.ts +132 -14
  126. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  127. package/dist/src/mcpl/push-coalescer.js +227 -50
  128. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  129. package/dist/src/mcpl/push-handler.d.ts +9 -20
  130. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  131. package/dist/src/mcpl/push-handler.js +7 -49
  132. package/dist/src/mcpl/push-handler.js.map +1 -1
  133. package/dist/src/mcpl/reference-fetcher.d.ts +2 -2
  134. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  135. package/dist/src/mcpl/reference-fetcher.js +4 -4
  136. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  137. package/dist/src/mcpl/server-connection.d.ts +50 -21
  138. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  139. package/dist/src/mcpl/server-connection.js +158 -74
  140. package/dist/src/mcpl/server-connection.js.map +1 -1
  141. package/dist/src/mcpl/tags.d.ts +24 -0
  142. package/dist/src/mcpl/tags.d.ts.map +1 -1
  143. package/dist/src/mcpl/tags.js +33 -0
  144. package/dist/src/mcpl/tags.js.map +1 -1
  145. package/dist/src/mcpl/tool-result-normalize.d.ts +2 -1
  146. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  147. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  148. package/dist/src/mcpl/transport.d.ts +55 -7
  149. package/dist/src/mcpl/transport.d.ts.map +1 -1
  150. package/dist/src/mcpl/transport.js +163 -34
  151. package/dist/src/mcpl/transport.js.map +1 -1
  152. package/dist/src/mcpl/types.d.ts +22 -6
  153. package/dist/src/mcpl/types.d.ts.map +1 -1
  154. package/dist/src/mcpl/types.js.map +1 -1
  155. package/dist/src/module-registry.d.ts +4 -2
  156. package/dist/src/module-registry.d.ts.map +1 -1
  157. package/dist/src/module-registry.js +100 -13
  158. package/dist/src/module-registry.js.map +1 -1
  159. package/dist/src/modules/discord/index.d.ts.map +1 -1
  160. package/dist/src/modules/discord/index.js +8 -13
  161. package/dist/src/modules/discord/index.js.map +1 -1
  162. package/dist/src/modules/history/index.d.ts.map +1 -1
  163. package/dist/src/modules/history/index.js +27 -4
  164. package/dist/src/modules/history/index.js.map +1 -1
  165. package/dist/src/modules/history/semantic.d.ts +15 -3
  166. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  167. package/dist/src/modules/history/semantic.js +27 -5
  168. package/dist/src/modules/history/semantic.js.map +1 -1
  169. package/dist/src/modules/index.d.ts +1 -1
  170. package/dist/src/modules/index.d.ts.map +1 -1
  171. package/dist/src/modules/index.js.map +1 -1
  172. package/dist/src/modules/unknown-tool.d.ts +4 -1
  173. package/dist/src/modules/unknown-tool.d.ts.map +1 -1
  174. package/dist/src/modules/unknown-tool.js +4 -1
  175. package/dist/src/modules/unknown-tool.js.map +1 -1
  176. package/dist/src/modules/workspace/branch-intent.d.ts +3 -1
  177. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  178. package/dist/src/modules/workspace/branch-intent.js +3 -1
  179. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  180. package/dist/src/modules/workspace/edit-plan.d.ts +265 -0
  181. package/dist/src/modules/workspace/edit-plan.d.ts.map +1 -0
  182. package/dist/src/modules/workspace/edit-plan.js +794 -0
  183. package/dist/src/modules/workspace/edit-plan.js.map +1 -0
  184. package/dist/src/modules/workspace/edit-text.d.ts +117 -0
  185. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -0
  186. package/dist/src/modules/workspace/edit-text.js +584 -0
  187. package/dist/src/modules/workspace/edit-text.js.map +1 -0
  188. package/dist/src/modules/workspace/effects.d.ts +5 -4
  189. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  190. package/dist/src/modules/workspace/effects.js +78 -25
  191. package/dist/src/modules/workspace/effects.js.map +1 -1
  192. package/dist/src/modules/workspace/index.d.ts +493 -62
  193. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  194. package/dist/src/modules/workspace/index.js +2216 -652
  195. package/dist/src/modules/workspace/index.js.map +1 -1
  196. package/dist/src/modules/workspace/observe.d.ts +213 -38
  197. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  198. package/dist/src/modules/workspace/observe.js +416 -36
  199. package/dist/src/modules/workspace/observe.js.map +1 -1
  200. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  201. package/dist/src/modules/workspace/reconcile.js +15 -8
  202. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  203. package/dist/src/modules/workspace/text.d.ts +32 -0
  204. package/dist/src/modules/workspace/text.d.ts.map +1 -0
  205. package/dist/src/modules/workspace/text.js +67 -0
  206. package/dist/src/modules/workspace/text.js.map +1 -0
  207. package/dist/src/modules/workspace/types.d.ts +210 -6
  208. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  209. package/dist/src/modules/workspace/types.js.map +1 -1
  210. package/dist/src/modules/workspace/watcher.d.ts +3 -2
  211. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  212. package/dist/src/modules/workspace/watcher.js.map +1 -1
  213. package/dist/src/operator-log.d.ts +52 -24
  214. package/dist/src/operator-log.d.ts.map +1 -1
  215. package/dist/src/operator-log.js.map +1 -1
  216. package/dist/src/recovery/discord-awareness-outbox.d.ts +496 -72
  217. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  218. package/dist/src/recovery/discord-awareness-outbox.js +1196 -289
  219. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  220. package/dist/src/recovery/offline-branch.d.ts +26 -1
  221. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  222. package/dist/src/recovery/offline-branch.js +164 -39
  223. package/dist/src/recovery/offline-branch.js.map +1 -1
  224. package/dist/src/recovery/recover-cli.js +23 -4
  225. package/dist/src/recovery/recover-cli.js.map +1 -1
  226. package/dist/src/send-tools.d.ts +11 -0
  227. package/dist/src/send-tools.d.ts.map +1 -0
  228. package/dist/src/send-tools.js +11 -0
  229. package/dist/src/send-tools.js.map +1 -0
  230. package/dist/src/timezone.d.ts +4 -2
  231. package/dist/src/timezone.d.ts.map +1 -1
  232. package/dist/src/timezone.js +4 -2
  233. package/dist/src/timezone.js.map +1 -1
  234. package/dist/src/tool-image-ledger.d.ts +46 -7
  235. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  236. package/dist/src/tool-image-ledger.js +60 -7
  237. package/dist/src/tool-image-ledger.js.map +1 -1
  238. package/dist/src/tool-result-guard.d.ts +14 -11
  239. package/dist/src/tool-result-guard.d.ts.map +1 -1
  240. package/dist/src/tool-result-guard.js +17 -14
  241. package/dist/src/tool-result-guard.js.map +1 -1
  242. package/dist/src/tool-result-history.d.ts +3 -2
  243. package/dist/src/tool-result-history.d.ts.map +1 -1
  244. package/dist/src/tool-result-history.js +3 -2
  245. package/dist/src/tool-result-history.js.map +1 -1
  246. package/dist/src/tune-out/coordinator.d.ts +21 -0
  247. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  248. package/dist/src/tune-out/coordinator.js +142 -40
  249. package/dist/src/tune-out/coordinator.js.map +1 -1
  250. package/dist/src/types/agent.d.ts +31 -6
  251. package/dist/src/types/agent.d.ts.map +1 -1
  252. package/dist/src/types/events.d.ts +66 -9
  253. package/dist/src/types/events.d.ts.map +1 -1
  254. package/dist/src/types/framework.d.ts +69 -62
  255. package/dist/src/types/framework.d.ts.map +1 -1
  256. package/dist/src/types/module.d.ts +40 -8
  257. package/dist/src/types/module.d.ts.map +1 -1
  258. package/dist/src/types/trace.d.ts +111 -10
  259. package/dist/src/types/trace.d.ts.map +1 -1
  260. package/dist/src/usage/types.d.ts +16 -0
  261. package/dist/src/usage/types.d.ts.map +1 -1
  262. package/dist/src/usage/usage-tracker.d.ts +14 -1
  263. package/dist/src/usage/usage-tracker.d.ts.map +1 -1
  264. package/dist/src/usage/usage-tracker.js +81 -41
  265. package/dist/src/usage/usage-tracker.js.map +1 -1
  266. package/package.json +6 -8
  267. package/src/agent.ts +99 -12
  268. package/src/api/server.ts +14 -12
  269. package/src/clock-rows.ts +302 -0
  270. package/src/code-execution/engine.ts +595 -0
  271. package/src/code-execution/index.ts +5 -17
  272. package/src/code-execution/isolate/calls.ts +199 -0
  273. package/src/code-execution/isolate/child-entry.ts +5 -0
  274. package/src/code-execution/isolate/child-main.ts +132 -0
  275. package/src/code-execution/isolate/child.ts +327 -0
  276. package/src/code-execution/isolate/frames.ts +209 -0
  277. package/src/code-execution/isolate/output.ts +161 -0
  278. package/src/code-execution/isolate/prelude.ts +409 -0
  279. package/src/code-execution/isolate/protocol.ts +240 -0
  280. package/src/code-execution/isolate/render.ts +230 -0
  281. package/src/code-execution/isolate/runner.ts +911 -0
  282. package/src/code-execution/isolate/scripts.ts +479 -0
  283. package/src/code-execution/quickjs.wasm +0 -0
  284. package/src/code-execution/quickjs.wasm.LICENSES +149 -0
  285. package/src/code-execution/tool-definition.ts +236 -167
  286. package/src/deadline-timer.ts +1 -1
  287. package/src/framework.ts +4978 -2671
  288. package/src/gate/event-gate.ts +79 -17
  289. package/src/held-writes.ts +17 -3
  290. package/src/index.ts +32 -22
  291. package/src/mcpl/channel-registry.ts +1892 -471
  292. package/src/mcpl/conversation-router.ts +26 -72
  293. package/src/mcpl/convert-block.ts +9 -5
  294. package/src/mcpl/inbound-source.ts +131 -0
  295. package/src/mcpl/index.ts +4 -2
  296. package/src/mcpl/log-value.ts +9 -0
  297. package/src/mcpl/modern-connection.ts +162 -53
  298. package/src/mcpl/prose-grammar.ts +4 -5
  299. package/src/mcpl/protocol-family.ts +200 -22
  300. package/src/mcpl/push-coalescer.ts +306 -62
  301. package/src/mcpl/push-handler.ts +12 -54
  302. package/src/mcpl/reference-fetcher.ts +4 -4
  303. package/src/mcpl/server-connection.ts +180 -79
  304. package/src/mcpl/tags.ts +36 -0
  305. package/src/mcpl/tool-result-normalize.ts +2 -1
  306. package/src/mcpl/transport.ts +162 -33
  307. package/src/mcpl/types.ts +23 -6
  308. package/src/module-registry.ts +92 -13
  309. package/src/modules/discord/index.ts +8 -13
  310. package/src/modules/history/index.ts +25 -3
  311. package/src/modules/history/semantic.ts +29 -6
  312. package/src/modules/index.ts +1 -0
  313. package/src/modules/unknown-tool.ts +4 -1
  314. package/src/modules/workspace/branch-intent.ts +3 -1
  315. package/src/modules/workspace/edit-plan.ts +954 -0
  316. package/src/modules/workspace/edit-text.ts +618 -0
  317. package/src/modules/workspace/effects.ts +70 -19
  318. package/src/modules/workspace/index.ts +2293 -650
  319. package/src/modules/workspace/observe.ts +465 -56
  320. package/src/modules/workspace/reconcile.ts +14 -7
  321. package/src/modules/workspace/text.ts +68 -0
  322. package/src/modules/workspace/types.ts +211 -6
  323. package/src/modules/workspace/watcher.ts +3 -2
  324. package/src/operator-log.ts +47 -19
  325. package/src/recovery/discord-awareness-outbox.ts +1500 -356
  326. package/src/recovery/offline-branch.ts +189 -55
  327. package/src/recovery/recover-cli.ts +25 -4
  328. package/src/send-tools.ts +11 -0
  329. package/src/timezone.ts +4 -2
  330. package/src/tool-image-ledger.ts +62 -8
  331. package/src/tool-result-guard.ts +18 -15
  332. package/src/tool-result-history.ts +3 -2
  333. package/src/tune-out/coordinator.ts +166 -45
  334. package/src/types/agent.ts +32 -6
  335. package/src/types/events.ts +66 -9
  336. package/src/types/framework.ts +69 -62
  337. package/src/types/module.ts +40 -8
  338. package/src/types/trace.ts +121 -15
  339. package/src/usage/types.ts +16 -0
  340. package/src/usage/usage-tracker.ts +86 -43
  341. package/dist/src/code-execution/py-runner.d.ts +0 -191
  342. package/dist/src/code-execution/py-runner.d.ts.map +0 -1
  343. package/dist/src/code-execution/py-runner.js +0 -655
  344. package/dist/src/code-execution/py-runner.js.map +0 -1
  345. package/dist/src/code-execution/runtime-py.d.ts +0 -50
  346. package/dist/src/code-execution/runtime-py.d.ts.map +0 -1
  347. package/dist/src/code-execution/runtime-py.js +0 -677
  348. package/dist/src/code-execution/runtime-py.js.map +0 -1
  349. package/dist/src/mcpl/prose-stream-router.d.ts +0 -88
  350. package/dist/src/mcpl/prose-stream-router.d.ts.map +0 -1
  351. package/dist/src/mcpl/prose-stream-router.js +0 -291
  352. package/dist/src/mcpl/prose-stream-router.js.map +0 -1
  353. package/src/code-execution/py-runner.ts +0 -791
  354. package/src/code-execution/runtime-py.ts +0 -677
  355. package/src/mcpl/prose-stream-router.ts +0 -268
@@ -22,7 +22,6 @@ import type { InboundSource } from './inbound-source.js';
22
22
  import { isSilentHeartbeatMarker, isVisiblyEmptyConversion } from './visible-content.js';
23
23
  import { logDetail, logValue } from './log-value.js';
24
24
  import { RefusalLines } from './refusal-lines.js';
25
- import { channelNotPermitted } from './errors.js';
26
25
 
27
26
  // ============================================================================
28
27
  // McplPushEvent (the ProcessEvent shape pushed to the queue)
@@ -49,8 +48,13 @@ export interface McplPushEvent {
49
48
  targetAgents?: string[];
50
49
  /** RFC-006: set when the coalescer delivers; rides the wake it queues. */
51
50
  coalescingSubject?: string;
52
- /** RFC-006 assembly: materialized for this agent's turn (store directly). */
51
+ /** RFC-006 assembly: materialized for this agent's turn, stored directly
52
+ * while its assembly runs, after what is queued for it (the framework's
53
+ * addMessage, bypassDeferralFor). */
53
54
  assemblingFor?: string;
55
+ /** The coalescer's mark: the agent's first view of the subject (see
56
+ * CoalescedOccurrence.firstView). */
57
+ firstView?: boolean;
54
58
  /** Host acceptance time (epoch ms), stamped where the push is admitted. */
55
59
  acceptedAt?: number;
56
60
  /** Framework-owned source envelope, frozen at a coalesced occurrence's
@@ -110,9 +114,8 @@ export class PushHandler {
110
114
  private emitTraceFn: (event: { type: string; [key: string]: unknown }) => void;
111
115
  private shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean;
112
116
  private dedup = new DedupSet(1000);
113
- /** Holds back repeats of the line for a push refused for its channel: a
114
- * channel-scoped one whose channel isn't declared (-32023) or may not be
115
- * used (-32017), and one naming a channel another server registers. */
117
+ /** Holds back repeats of the line for a channel-scoped push refused because
118
+ * its channel isn't declared (-32023) or may not be used (-32017). */
116
119
  private channelRefusalLines = new RefusalLines();
117
120
 
118
121
  constructor(
@@ -125,9 +128,6 @@ export class PushHandler {
125
128
  * or acknowledged: return its source envelope, frozen now
126
129
  * (mcpl/inbound-source.ts). Coalesced work is stamped by its own path. */
127
130
  private readonly acceptInbound?: (event: McplPushEvent) => InboundSource | undefined,
128
- /** The channel a push names, and the server that registers it, when
129
- * another server registers that channel id and this server doesn't. */
130
- private readonly channelOfAnotherServer?: (serverId: string, params: PushEventParams) => { channelId: string; holder: string } | undefined,
131
131
  ) {
132
132
  this.featureSetManager = featureSetManager;
133
133
  this.pushEventFn = pushEventFn;
@@ -140,7 +140,6 @@ export class PushHandler {
140
140
  *
141
141
  * 1. Validate feature set
142
142
  * 2. Convert content blocks; reject visibly-empty content
143
- * Refuse a push naming a channel another server registers
144
143
  * 3. Deduplicate by eventId
145
144
  * 4. Optionally check shouldTriggerInference callback
146
145
  * 5. Push event to queue
@@ -214,18 +213,6 @@ export class PushHandler {
214
213
  return;
215
214
  }
216
215
 
217
- // A push is its own server's event, and the agent's reply to it goes to
218
- // the channel the push names. A channel's id is the agent's locus, so a
219
- // push naming a channel another server registers, which this server
220
- // doesn't, would have the reply sent through that other server. It's
221
- // refused, before dedup, so a retry once this server registers the
222
- // channel isn't refused as a duplicate.
223
- const elsewhere = this.channelOfAnotherServer?.(serverId, params);
224
- if (elsewhere) {
225
- this.refuseChannelOfAnotherServer(serverId, params, elsewhere, responder);
226
- return;
227
- }
228
-
229
216
  // 3. Deduplicate by eventId. A coalesced occurrence is deduplicated by the
230
217
  // coalescer's receipts instead (RFC-006 §3.1: a retry within the window
231
218
  // gets its original result, which this set could not return).
@@ -242,12 +229,15 @@ export class PushHandler {
242
229
  .filter((b): b is ContentBlock & { type: 'text' } => b.type === 'text')
243
230
  .map((b) => b.text)
244
231
  .join('\n');
232
+ // The host's own fields come after the server's origin, as a channel
233
+ // message's do, so an origin can't give the gate another server's id,
234
+ // feature set, event id or event type.
245
235
  const metadata: Record<string, unknown> = {
236
+ ...(params.origin ?? {}),
246
237
  serverId,
247
238
  featureSet: params.featureSet,
248
239
  eventId: params.eventId,
249
240
  eventType: 'mcpl:push-event',
250
- ...(params.origin ?? {}),
251
241
  ...(params.tags ? { tags: params.tags } : {}),
252
242
  };
253
243
  triggerInference = this.shouldTriggerInference(textContent, metadata);
@@ -353,38 +343,6 @@ export class PushHandler {
353
343
  });
354
344
  }
355
345
 
356
- /** A push naming a channel another server registers: refused with -32017.
357
- * The operator's line names the server that registers it; the answer to
358
- * the pushing server doesn't. The line prints at most once a minute per
359
- * channel, as the other channel refusals' do, and every refusal is traced. */
360
- private refuseChannelOfAnotherServer(
361
- serverId: string,
362
- params: PushEventParams,
363
- elsewhere: { channelId: string; holder: string },
364
- responder: Responder | undefined,
365
- ): void {
366
- const held = this.channelRefusalLines.admit(serverId, elsewhere.channelId);
367
- if (held !== null) {
368
- console.error(
369
- `[push-event-rejected] server=${serverId} featureSet=${logValue(params.featureSet ?? '')} eventId=${logValue(params.eventId ?? '')} ` +
370
- `channel=${logValue(elsewhere.channelId)} reason=channel-of-another-server: registered by server ${logValue(elsewhere.holder)}, not this one` +
371
- (held ? ` (${held} more since the last line)` : ''),
372
- );
373
- }
374
- this.emitTraceFn({
375
- type: 'mcpl:push-event-rejected',
376
- serverId,
377
- eventId: typeof params.eventId === 'string' ? params.eventId : '',
378
- featureSet: params.featureSet,
379
- reason: 'channel-of-another-server',
380
- channelId: elsewhere.channelId,
381
- detail: `registered by server ${elsewhere.holder}`,
382
- });
383
- const error = channelNotPermitted(elsewhere.channelId);
384
- if (responder?.respondError) responder.respondError(error.code, error.message, error.data);
385
- else responder?.respond({ accepted: false, reason: error.message });
386
- }
387
-
388
346
  /** Loud, like every other push rejection: the producer's wake went nowhere.
389
347
  * A missing eventId prints as `""` and traces as `''`, as on every push line. */
390
348
  private traceEmptyRejection(serverId: string, params: PushEventParams): void {
@@ -224,7 +224,7 @@ export class ReferenceFetcher {
224
224
  constructor(
225
225
  private readonly resolveServer: (serverId: string) => ServerResolution | undefined,
226
226
  /** Persist the payload; returns a mount-prefixed path or null when no
227
- * writable workspace exists. File name is host-generated (§7.3). */
227
+ * writable mount without an include exists. File name is host-generated (§7.3). */
228
228
  private readonly saveFile: (fileName: string, data: Buffer, mimeType: string) => Promise<string | null>,
229
229
  ) {}
230
230
 
@@ -463,9 +463,9 @@ export class ReferenceFetcher {
463
463
  const observedMime = sniffMime(data) ?? headerMime ?? claimedMime ?? 'application/octet-stream';
464
464
  const ext = EXT_BY_MIME.get(observedMime) ?? EXT_BY_MIME.get(headerMime ?? '') ?? EXT_BY_MIME.get(claimedMime ?? '') ?? 'bin';
465
465
  // §7.3: host-generated storage name; the server's `name` is display-only.
466
- // A null from the save hook covers no workspace, no writable mount and
467
- // a failed write, and the hook logs which. A throw is the save's failure
468
- // too, not the fetch's, so it's logged here.
466
+ // A null from the save hook covers no workspace, no writable mount
467
+ // without an include and a failed write, and the hook logs which. A
468
+ // throw is the save's failure too, not the fetch's, so it's logged here.
469
469
  const fileName = `${record.refId}.${ext}`;
470
470
  let path: string | null;
471
471
  try {
@@ -15,12 +15,15 @@ import { openTransport, STDERR_END_WAIT_MS, type McplTransport, type TransportCl
15
15
  import { maskNegotiatedCapabilities } from './capability-mask.js';
16
16
  import { CapabilityGrant, expandAdvertisementShorthand } from './capability-grant.js';
17
17
  import { CAPABILITY_DISABLED, boundedForLog, describeCause } from './errors.js';
18
- import { logDetail, logValue, logValueRedacted } from './log-value.js';
18
+ import { logDetail, logValue, logValueRedacted, loggedCode } from './log-value.js';
19
19
  import {
20
20
  LEGACY_MCP_PROTOCOL_VERSION,
21
21
  MODERN_MCP_PROTOCOL_VERSION,
22
22
  checkServerConfig,
23
23
  redactServerText,
24
+ redactedData,
25
+ serverTextRedactor,
26
+ type ServerTextRedactor,
24
27
  } from './protocol-family.js';
25
28
 
26
29
  import type {
@@ -84,18 +87,30 @@ export interface McplConnectionCloseInfo {
84
87
  * Spring Boot + JDA servers can take 5-10s to boot, so 30s is safe. */
85
88
  const INITIALIZE_TIMEOUT_MS = 30_000;
86
89
 
90
+ /** How many errors down a `cause` chain stderrTailOf looks. */
91
+ const STDERR_TAIL_CAUSE_LINKS = 8;
92
+
87
93
  /** A failed connect's `stderrTail`, the last of the server's stderr that its
88
- * handshake's error keeps, when there is one. */
94
+ * handshake's error keeps, when there is one: the error's own, or the
95
+ * nearest one in its `cause` chain, since a launch that couldn't be reaped
96
+ * (McplUnreapedLaunchError) and the framework's refusals wrap the
97
+ * handshake's error. It looks at most STDERR_TAIL_CAUSE_LINKS causes down,
98
+ * so a chain that loops ends. */
89
99
  export function stderrTailOf(error: unknown): string | undefined {
90
- const tail = (error as { stderrTail?: unknown } | null | undefined)?.stderrTail;
91
- return typeof tail === 'string' && tail !== '' ? tail : undefined;
100
+ let at = error;
101
+ for (let link = 0; link <= STDERR_TAIL_CAUSE_LINKS && typeof at === 'object' && at !== null; link++) {
102
+ const { stderrTail, cause } = at as { stderrTail?: unknown; cause?: unknown };
103
+ if (typeof stderrTail === 'string' && stderrTail !== '') return stderrTail;
104
+ at = cause;
105
+ }
106
+ return undefined;
92
107
  }
93
108
 
94
109
  /** For the host's own log line about a failed connect: the server's stderr
95
- * that the error keeps as `stderrTail`, with the server's credentials
96
- * taken out by `redact` (its redactServerText), one marked line per line of
97
- * it, each through log-value, since it's the server's text. '' when there's
98
- * none. */
110
+ * that the error, or the nearest error it wraps, keeps as `stderrTail`
111
+ * (stderrTailOf), with the server's credentials taken out by `redact` (its
112
+ * redactServerText), one marked line per line of it, each through
113
+ * log-value, since it's the server's text. '' when there's none. */
99
114
  export function withStderr(error: unknown, redact: (text: string) => string): string {
100
115
  const tail = stderrTailOf(error);
101
116
  if (tail === undefined) return '';
@@ -156,11 +171,13 @@ export class McplProtocolVersionError extends Error {
156
171
  constructor(
157
172
  message: string,
158
173
  readonly kind: 'rejected' | 'mismatch',
159
- /** Revision the server answered with (`mismatch`; null when it gave none). */
174
+ /** Revision the server answered with (`mismatch`; null when it gave
175
+ * none), redacted as the server's words are. */
160
176
  readonly returned: string | null,
161
177
  /** Revisions the server says it supports (`rejected`, when stated). */
162
178
  readonly supported: readonly string[] | null,
163
- /** The JSON-RPC error's code and data, verbatim (`rejected`). */
179
+ /** The JSON-RPC error's code, and its data with each string in it
180
+ * redacted (`rejected`). */
164
181
  readonly code?: number,
165
182
  readonly data?: unknown,
166
183
  /** The revision this client offered: the legacy engine's, unless a
@@ -172,8 +189,13 @@ export class McplProtocolVersionError extends Error {
172
189
  }
173
190
  }
174
191
 
175
- function protocolVersionRejection(id: string, error: { code: number; message: string; data?: unknown }): McplProtocolVersionError {
176
- const data = error.data as { supported?: unknown } | undefined;
192
+ function protocolVersionRejection(config: McplServerConfig, error: { code: number; message: string; data?: unknown }): McplProtocolVersionError {
193
+ const id = config.id;
194
+ // The server's words and data, redacted before they're bounded or kept:
195
+ // the error goes to the host. One build for all of them, so a
196
+ // configuration that can't be read is read once.
197
+ const redact = serverTextRedactor(config).now();
198
+ const data = redactedData(error.data, redact) as { supported?: unknown } | undefined;
177
199
  const supported = Array.isArray(data?.supported) && data.supported.every((v) => typeof v === 'string')
178
200
  ? (data.supported as string[])
179
201
  : null;
@@ -183,14 +205,15 @@ function protocolVersionRejection(id: string, error: { code: number; message: st
183
205
  return new McplProtocolVersionError(
184
206
  `MCPL server "${id}" does not support MCP ${MCP_PROTOCOL_VERSION}` +
185
207
  // The server's message can be any JSON value, so it's read without
186
- // throwing (describeCause) and bounded, as the initialize error is.
187
- (supported ? ` (it supports: ${supported.join(', ') || 'none listed'}).` : ` (${boundedForLog(describeCause(error.message))}).`) +
208
+ // throwing (describeCause), and redacted, then bounded, as the
209
+ // initialize error is.
210
+ (supported ? ` (it supports: ${boundedForLog(supported.join(', ')) || 'none listed'}).` : ` (${boundedForLog(redact(describeCause(error.message)))}).`) +
188
211
  modernHint,
189
212
  'rejected',
190
213
  null,
191
214
  supported,
192
215
  error.code,
193
- error.data,
216
+ data,
194
217
  );
195
218
  }
196
219
 
@@ -218,16 +241,21 @@ class HandshakeCleanupError extends Error {
218
241
  * is confirmed, so the caller can still clean up, and should start nothing
219
242
  * else for this server until then. connect() and connectWithReconnect()
220
243
  * throw this, and the modern engine's ModernMcpConnection.connect() too.
221
- * `cause` is the connect's own failure.
244
+ * `cause` is the connect's own failure. `connection` isn't enumerable, so
245
+ * printing the error doesn't print the connection's configuration, its
246
+ * credentials included.
222
247
  */
223
248
  export class McplUnreapedLaunchError<Connection extends { close(): Promise<void> } = McplServerConnection> extends Error {
249
+ declare readonly connection: Connection;
250
+
224
251
  constructor(
225
- readonly connection: Connection,
252
+ connection: Connection,
226
253
  failure: Error,
227
254
  /** The cleanup's own failure: the child it couldn't confirm exited. */
228
255
  readonly cleanup: Error,
229
256
  ) {
230
257
  super(`${failure.message}; its cleanup also failed: ${cleanup.message}`, { cause: failure });
258
+ Object.defineProperty(this, 'connection', { value: connection, enumerable: false });
231
259
  this.name = 'McplUnreapedLaunchError';
232
260
  }
233
261
  }
@@ -250,8 +278,9 @@ export class McplRequestError extends Error {
250
278
  constructor(
251
279
  message: string,
252
280
  readonly outcome: 'not-sent' | 'error-response' | 'no-response',
253
- /** The JSON-RPC error's `data`, verbatim, when the server supplied one
254
- * (e.g. which parts of a multi-part send were posted). */
281
+ /** The JSON-RPC error's `data`, when the server supplied one (e.g. which
282
+ * parts of a multi-part send were posted), with each string in it
283
+ * redacted (redactedData): a host hands it on. */
255
284
  readonly data?: unknown,
256
285
  ) {
257
286
  super(message);
@@ -529,8 +558,8 @@ export class McplServerConnection extends EventEmitter {
529
558
 
530
559
  // Event buffering: control-plane traffic can be released independently from
531
560
  // inference-bearing data-plane traffic. This lets a server finish its own
532
- // registration (and answer host tools/call requests) while startup/reconnect
533
- // reconciliation keeps agent wakes behind a durable barrier.
561
+ // registration (and answer host tools/call requests) while its data plane
562
+ // stays held, as through a quiesce.
534
563
  private controlPlaneReady = false;
535
564
  private dataPlaneReady = false;
536
565
  private bufferedEvents: HeldItem[] = [];
@@ -568,6 +597,15 @@ export class McplServerConnection extends EventEmitter {
568
597
 
569
598
  // Reconnect state (adapted from Anarchid/agent-framework@mcpl-module-proto)
570
599
  private config: McplServerConfig | null = null;
600
+ /** The redactor of the server's text for `config`, made at its first error
601
+ * answer, by when `config` is set, and kept: a framework-launched child's
602
+ * `env` is a getter that reads the awareness ledger, so a redactor built
603
+ * per answer would read it per answer (#540's review, R1-2). An answer
604
+ * takes one build of it for its words, data and log line (`now()`), so
605
+ * it reads the ledger at most once. It builds again only after a build
606
+ * that failed, which withholds that answer's text, or once a dial notes
607
+ * something new (serverTextRedactor). */
608
+ private redactor: ServerTextRedactor | null = null;
571
609
  private hostCapabilities: McplHostCapabilities | null = null;
572
610
  private reconnectEnabled = false;
573
611
  private reconnectIntervalMs = 5000;
@@ -666,8 +704,8 @@ export class McplServerConnection extends EventEmitter {
666
704
  `(long pause backlog${dropped > 0 ? `; ${dropped} dropped at cap` : ''})`,
667
705
  );
668
706
  }
669
- // Re-enter the gate for every buffered item. A control handler (notably
670
- // tools/list_changed) may install a newer data-plane barrier while this
707
+ // Re-enter the gate for every buffered item. A control handler (a
708
+ // host/command quiesce, say) may pause the data plane while this
671
709
  // snapshot is being flushed; later data items must observe that pause.
672
710
  this.flushHeld(pending);
673
711
  }
@@ -831,9 +869,8 @@ export class McplServerConnection extends EventEmitter {
831
869
 
832
870
  /**
833
871
  * Close the data plane synchronously while leaving an already-released
834
- * control plane live. Used before list-change reconciliation so no new
835
- * inference event can beat barrier installation. A reconnect closes both
836
- * planes instead.
872
+ * control plane live. Used by quiesce, and by reconnectAfterFailure before
873
+ * its teardown. A reconnect closes both planes instead.
837
874
  */
838
875
  pauseDataPlane(): void {
839
876
  this.dataPlaneReady = false;
@@ -841,8 +878,8 @@ export class McplServerConnection extends EventEmitter {
841
878
 
842
879
  /**
843
880
  * Override emit to buffer server→host events until the corresponding plane
844
- * is ready. Lifecycle events always pass through so reconnect can install a
845
- * new data-plane gate before the fresh transport is exposed.
881
+ * is ready. Lifecycle events always pass through so the reconnect listener
882
+ * runs before the fresh transport's buffered traffic is released.
846
883
  */
847
884
  override emit(event: string | symbol, ...args: unknown[]): boolean {
848
885
  const name = typeof event === 'string' ? event : '';
@@ -1169,18 +1206,22 @@ export class McplServerConnection extends EventEmitter {
1169
1206
  },
1170
1207
  };
1171
1208
 
1209
+ // When the transport fails, the end of the server's stderr often says
1210
+ // why, such as a missing token or a crash at startup. It goes on the
1211
+ // error as `stderrTail`, never into the message: an error's message
1212
+ // travels to traces and tool results, and a server's stderr is its own
1213
+ // text, which can hold a credential.
1214
+ const failed = (failure: string): Error & { stderrTail?: string } => {
1215
+ // It goes to the host too, so it's redacted, from a window of the
1216
+ // stream longer than the tail, so a credential the tail's cut
1217
+ // would split is whole when it's matched (stderrTailRedacted).
1218
+ const tail = transport.stderrTailRedacted(serverTextRedactor(config)).trimEnd();
1219
+ return Object.assign(new Error(failure), tail ? { stderrTail: tail } : {});
1220
+ };
1221
+
1172
1222
  // Await the initialize response, racing an early transport close/error,
1173
1223
  // a timeout and the owner's close. Every path cleans up its listeners.
1174
1224
  const { mcpl: rawCapabilities, mcpToolsAdvertised, protocolVersion } = await new Promise<{ mcpl: McplCapabilities | null; mcpToolsAdvertised: boolean; protocolVersion: string }>((resolve, reject) => {
1175
- // When the transport fails, the end of the server's stderr often says
1176
- // why, such as a missing token or a crash at startup. It goes on the
1177
- // error as `stderrTail`, never into the message: an error's message
1178
- // travels to traces and tool results, and a server's stderr is its
1179
- // own text, which can hold a credential.
1180
- const failed = (failure: string): Error & { stderrTail?: string } => {
1181
- const tail = transport.stderrTail.trimEnd();
1182
- return Object.assign(new Error(failure), tail ? { stderrTail: tail } : {});
1183
- };
1184
1225
  const cleanup = () => {
1185
1226
  transport.off('line', onLine);
1186
1227
  transport.off('close', onClose);
@@ -1222,21 +1263,25 @@ export class McplServerConnection extends EventEmitter {
1222
1263
  // listener after cleanup() has cleared the timeout: an uncaught
1223
1264
  // exception, which ends a host that doesn't catch them, and a
1224
1265
  // handshake left to wait forever. It's the server's text in an
1225
- // error's message, so it's bounded as errors.ts says.
1266
+ // error the host gets, so it's redacted, then bounded as
1267
+ // errors.ts says: a cut or a replaced character first would
1268
+ // leave part of a credential the redactor can't match.
1226
1269
  reject(msg.error.code === UNSUPPORTED_PROTOCOL_VERSION_CODE
1227
- ? protocolVersionRejection(config.id, msg.error)
1228
- : new Error(`MCPL server "${config.id}" initialize error: ${boundedForLog(describeCause(msg.error.message))}`));
1270
+ ? protocolVersionRejection(config, msg.error)
1271
+ : new Error(`MCPL server "${config.id}" initialize error: ${boundedForLog(redactServerText(describeCause(msg.error.message), config))}`));
1229
1272
  return;
1230
1273
  }
1231
1274
  const result = msg.result as Record<string, unknown> | undefined;
1232
1275
  // The server's answer is the established revision. This engine
1233
1276
  // implements exactly one, so any other answer (or none) means the
1234
1277
  // two sides never agreed on what they're speaking.
1235
- const returned = typeof result?.protocolVersion === 'string' ? result.protocolVersion : null;
1236
- if (returned !== MCP_PROTOCOL_VERSION) {
1278
+ const answered = typeof result?.protocolVersion === 'string' ? result.protocolVersion : null;
1279
+ if (answered !== MCP_PROTOCOL_VERSION) {
1280
+ // The server's words go to the host, so they're redacted.
1281
+ const returned = answered === null ? null : redactServerText(answered, config);
1237
1282
  reject(new McplProtocolVersionError(
1238
1283
  `MCPL server "${config.id}" answered initialize with protocolVersion ` +
1239
- `${returned === null ? '(none)' : JSON.stringify(returned)}; this client's legacy engine speaks only ${MCP_PROTOCOL_VERSION}`,
1284
+ `${returned === null ? '(none)' : boundedForLog(JSON.stringify(returned))}; this client's legacy engine speaks only ${MCP_PROTOCOL_VERSION}`,
1240
1285
  'mismatch',
1241
1286
  returned,
1242
1287
  null,
@@ -1251,7 +1296,7 @@ export class McplServerConnection extends EventEmitter {
1251
1296
  resolve({
1252
1297
  mcpl: (experimental?.mcpl as McplCapabilities) ?? null,
1253
1298
  mcpToolsAdvertised: caps?.tools !== undefined,
1254
- protocolVersion: returned,
1299
+ protocolVersion: answered,
1255
1300
  });
1256
1301
  };
1257
1302
  const onClose = (info: TransportCloseInfo) => {
@@ -1281,6 +1326,22 @@ export class McplServerConnection extends EventEmitter {
1281
1326
  transport.writeLine(JSON.stringify(initRequest));
1282
1327
  });
1283
1328
 
1329
+ // The transport has found the child's stdin or stdout failed before the
1330
+ // handshake could hand it on, such as when this host's answer to a ping
1331
+ // the server sent met EPIPE. Nothing more is written to the child then,
1332
+ // so the connection couldn't serve. It's read here, once the answer has
1333
+ // passed its own checks, rather than as the answer arrives: a write's
1334
+ // EPIPE is reported only once the callback that wrote it has returned,
1335
+ // so a ping and the answer read in one callback would pass a check made
1336
+ // there. A child that outlived SIGKILL may already have said so with
1337
+ // nothing listening (the transport's 'unreaped' is wired only once the
1338
+ // handshake hands it on). So the handshake fails, and its cleanup holds
1339
+ // the launch if it can't reap the child. A failure of the write below
1340
+ // is reported only once the caller has wired the transport, so it
1341
+ // reaches the connection as a loss, like any later one.
1342
+ const pipeFailure = transport.pipeFailure;
1343
+ if (pipeFailure) throw failed(`MCPL server "${config.id}" stdio pipe failed with ${pipeFailure.message} during the handshake`);
1344
+
1284
1345
  // Send `initialized` notification (no id)
1285
1346
  transport.writeLine(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }));
1286
1347
 
@@ -1485,9 +1546,13 @@ export class McplServerConnection extends EventEmitter {
1485
1546
  return this.sendRequest(McplMethod.ChannelsOpen, params as unknown as Record<string, unknown>, { timeoutMs }) as Promise<ChannelsOpenResult>;
1486
1547
  }
1487
1548
 
1488
- /** Send `channels/close` request and await result. */
1549
+ /** Send `channels/close` request and await result. It keeps a deadline as
1550
+ * channels/open does: a delivery waits for a channel's queued lifecycle
1551
+ * work, closes included, so a close that is never answered would hold the
1552
+ * delivery, and the turn that waits for it, for good. */
1489
1553
  sendChannelsClose(params: ChannelsCloseParams): Promise<ChannelsCloseResult> {
1490
- return this.sendRequest(McplMethod.ChannelsClose, params as unknown as Record<string, unknown>) as Promise<ChannelsCloseResult>;
1554
+ const timeoutMs = this.requestTimeoutMs > 0 ? this.requestTimeoutMs : this.publishDeadlineMs;
1555
+ return this.sendRequest(McplMethod.ChannelsClose, params as unknown as Record<string, unknown>, { timeoutMs }) as Promise<ChannelsCloseResult>;
1491
1556
  }
1492
1557
 
1493
1558
  /** Send `channels/acknowledge` request and await result. */
@@ -1585,10 +1650,11 @@ export class McplServerConnection extends EventEmitter {
1585
1650
  return { ...rest, tools };
1586
1651
  }
1587
1652
  if (typeof result.nextCursor !== 'string') {
1588
- throw malformed(`nextCursor must be a string, got ${JSON.stringify(result.nextCursor)}`);
1653
+ // The cursor is the server's, so it's redacted, then bounded.
1654
+ throw malformed(`nextCursor must be a string, got ${boundedForLog(redactServerText(JSON.stringify(result.nextCursor), this.config ?? {}))}`);
1589
1655
  }
1590
1656
  if (seen.has(result.nextCursor)) {
1591
- throw new Error(`MCPL server "${this.id}" repeated tools/list cursor ${JSON.stringify(result.nextCursor)}`);
1657
+ throw new Error(`MCPL server "${this.id}" repeated tools/list cursor ${boundedForLog(redactServerText(JSON.stringify(result.nextCursor), this.config ?? {}))}`);
1592
1658
  }
1593
1659
  seen.add(result.nextCursor);
1594
1660
  cursor = result.nextCursor;
@@ -1895,9 +1961,8 @@ export class McplServerConnection extends EventEmitter {
1895
1961
  // boundary resets, so control traffic too (a server's channels/register
1896
1962
  // right after initialize) must wait for the new grant rather than be
1897
1963
  // refused against the empty one. The reconnect lifecycle event bypasses
1898
- // buffering; its framework listener installs the awareness barrier,
1899
- // re-establishes the grant (§5.3 policy responses are never buffered),
1900
- // and then opens the planes.
1964
+ // buffering; its framework listener re-establishes the grant (§5.3
1965
+ // policy responses are never buffered), and then opens the planes.
1901
1966
  this.controlPlaneReady = false;
1902
1967
  this.dataPlaneReady = false;
1903
1968
  this.resetPolicyForTransportBoundary();
@@ -1921,6 +1986,18 @@ export class McplServerConnection extends EventEmitter {
1921
1986
  // Private: message sending
1922
1987
  // ==========================================================================
1923
1988
 
1989
+ /**
1990
+ * A request refused before anything is kept for it, as `error`, with its
1991
+ * operator's line logged after the refusal: a host logger that throws then
1992
+ * loses that line, and can't turn the refusal into a throw from the call.
1993
+ */
1994
+ private static refused(error: McplRequestError, line: string): Promise<never> {
1995
+ return new Promise((_, reject) => {
1996
+ reject(error);
1997
+ console.error(line);
1998
+ });
1999
+ }
2000
+
1924
2001
  /**
1925
2002
  * Send a JSON-RPC request and return a promise that resolves with the result
1926
2003
  * or rejects with a JSON-RPC error.
@@ -1958,14 +2035,16 @@ export class McplServerConnection extends EventEmitter {
1958
2035
  // pipe's error are the operator's, so they go to the log.
1959
2036
  const pipeFailure = this.transport?.pipeFailure;
1960
2037
  if (pipeFailure) {
1961
- console.error(`MCPL server "${this.id}": ${method} not sent, since its stdio pipe failed: ${logDetail(pipeFailure.message)}`);
1962
- return Promise.reject(new McplRequestError(
1963
- `The request wasn't sent: this host can no longer write to the "${this.id}" server, so nothing ran. ` +
1964
- (this.willReconnect
1965
- ? 'This host is reconnecting to it; try again later.'
1966
- : "This host won't reconnect to it on its own. If you need it, ask whoever runs this host."),
1967
- 'not-sent',
1968
- ));
2038
+ return McplServerConnection.refused(
2039
+ new McplRequestError(
2040
+ `The request wasn't sent: this host can no longer write to the "${this.id}" server, so nothing ran. ` +
2041
+ (this.willReconnect
2042
+ ? 'This host is reconnecting to it; try again later.'
2043
+ : "This host won't reconnect to it on its own. If you need it, ask whoever runs this host."),
2044
+ 'not-sent',
2045
+ ),
2046
+ `MCPL server "${this.id}": ${method} not sent, since its stdio pipe failed: ${logDetail(pipeFailure.message)}`,
2047
+ );
1969
2048
  }
1970
2049
 
1971
2050
  const id = this.nextRequestId++;
@@ -1975,6 +2054,23 @@ export class McplServerConnection extends EventEmitter {
1975
2054
  id,
1976
2055
  params,
1977
2056
  };
2057
+ // A request JSON can't write (a BigInt, a cycle) never leaves this host,
2058
+ // and calling again fails the same way, so it's refused before anything
2059
+ // is kept for it, in the modern engine's words (#556). The method, id and
2060
+ // the serializer's words are the operator's.
2061
+ let line: string;
2062
+ try {
2063
+ line = JSON.stringify(request);
2064
+ } catch (err) {
2065
+ return McplServerConnection.refused(
2066
+ new McplRequestError(
2067
+ `The request wasn't sent: this host couldn't send it to the "${this.id}" server, so nothing ran. ` +
2068
+ 'If calling it again fails, ask whoever runs this host.',
2069
+ 'not-sent',
2070
+ ),
2071
+ `MCPL server "${this.id}": ${method} (id=${id}) not sent, since it can't be written as JSON: ${logDetail(err instanceof Error ? err.message : err)}`,
2072
+ );
2073
+ }
1978
2074
 
1979
2075
  return new Promise((resolve, reject) => {
1980
2076
  const pending: PendingRequest = { resolve, reject, method };
@@ -2017,20 +2113,21 @@ export class McplServerConnection extends EventEmitter {
2017
2113
  }
2018
2114
  this.pendingRequests.set(id, pending);
2019
2115
  try {
2020
- this.transport!.writeLine(JSON.stringify(request));
2116
+ this.transport!.writeLine(line);
2021
2117
  } catch (err) {
2022
2118
  // A synchronous write rejection sent no request. Remove its pending
2023
2119
  // state and watchdog before reporting that it is safe to retry. The
2024
- // method, id and the transport's words are the operator's.
2120
+ // method, id and the transport's words are the operator's, logged
2121
+ // after the refusal, as refused() logs.
2025
2122
  this.pendingRequests.delete(id);
2026
2123
  if (pending.timer) clearTimeout(pending.timer);
2027
- console.error(
2028
- `MCPL server "${this.id}": writing ${method} (id=${id}) failed: ${logDetail(err instanceof Error ? err.message : err)}`,
2029
- );
2030
2124
  reject(new McplRequestError(
2031
2125
  `The request wasn't sent: this host couldn't write it to the "${this.id}" server, so nothing ran. Try again later.`,
2032
2126
  'not-sent',
2033
2127
  ));
2128
+ console.error(
2129
+ `MCPL server "${this.id}": writing ${method} (id=${id}) failed: ${logDetail(err instanceof Error ? err.message : err)}`,
2130
+ );
2034
2131
  }
2035
2132
  });
2036
2133
  }
@@ -2389,16 +2486,18 @@ export class McplServerConnection extends EventEmitter {
2389
2486
  if (pending.timer) clearTimeout(pending.timer);
2390
2487
 
2391
2488
  if (response.error) {
2392
- // The server's own words, after a word on whose they are, bounded as
2393
- // errors.ts says, since they're the server's text. Only a string is
2394
- // words: a message of any other JSON value reads as none. The method,
2395
- // id and JSON-RPC code are the operator's, so they go to the log, which
2396
- // prints any value a server can send without throwing (logValue). The
2397
- // caller's text keeps the server's words unredacted, as a JSON-RPC
2398
- // answer passes (redactServerText's scope); the log is a record of the
2399
- // server's text, so the message is redacted there, whatever JSON value
2400
- // it is, as every other line this connection writes is.
2401
- const said = typeof response.error.message === 'string' ? boundedForLog(response.error.message.trim()) : '';
2489
+ // The server's own words, after a word on whose they are, redacted and
2490
+ // then bounded as errors.ts says, since they're the server's text in
2491
+ // an error the caller gets (redactServerText's scope), and its data's
2492
+ // strings redacted too. It's trimmed after it's redacted, so a
2493
+ // credential that ends in a space is whole when it's matched. Only a
2494
+ // string is words: a message of any other JSON value reads as none.
2495
+ // The method and id are the operator's, so they go to the log, and the
2496
+ // code (loggedCode) and the message, whatever JSON values they are, go
2497
+ // there redacted (logValueRedacted), as every other line this
2498
+ // connection writes is.
2499
+ const redact = (this.redactor ??= serverTextRedactor(this.config ?? {})).now();
2500
+ const said = typeof response.error.message === 'string' ? boundedForLog(redact(response.error.message).trim()) : '';
2402
2501
  // The call is settled before its log line: its entry is gone and its
2403
2502
  // timer cleared, so nothing else would settle it, and a host's logger
2404
2503
  // can throw. The dispatch's containment reports such a throw. (A
@@ -2410,11 +2509,11 @@ export class McplServerConnection extends EventEmitter {
2410
2509
  : `The "${this.id}" server answered with an error without saying what went wrong, so whether the request took effect is unknown. ` +
2411
2510
  'Check its effect before trying again.',
2412
2511
  'error-response',
2413
- (response.error as { data?: unknown }).data,
2512
+ redactedData((response.error as { data?: unknown }).data, redact),
2414
2513
  ));
2415
2514
  console.error(
2416
- `MCPL server "${this.id}" answered ${pending.method} (id=${response.id}) with error ${logValue(response.error.code)}: ` +
2417
- logValueRedacted(response.error.message, (text) => redactServerText(text, this.config ?? {})),
2515
+ `MCPL server "${this.id}" answered ${pending.method} (id=${response.id}) with error ${loggedCode(response.error.code, redact)}: ` +
2516
+ logValueRedacted(response.error.message, redact),
2418
2517
  );
2419
2518
  } else {
2420
2519
  pending.resolve(response.result);
@@ -2540,8 +2639,10 @@ export class McplServerConnection extends EventEmitter {
2540
2639
  // reconnecting, and is lost as on a close, with the permanent failure
2541
2640
  // that raises the alert and takes its tools off the list. Its id stays
2542
2641
  // held until close() confirms the exit. A close the host has begun
2543
- // reports the failure itself. During a handshake nothing listens yet:
2544
- // the handshake's own cleanup holds the launch, once the handshake fails.
2642
+ // reports the failure itself. During a handshake nothing listens yet: a
2643
+ // handshake whose pipe has failed fails, when its answer comes (it's
2644
+ // refused then), when the child's exit closes the transport, or at its
2645
+ // timeout, and its own cleanup holds the launch.
2545
2646
  transport.on('unreaped', (failure: Error) => {
2546
2647
  if (this.closed || this.transport !== transport) return;
2547
2648
  this.holdUnreaped(transport, failure);