@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
@@ -9,11 +9,12 @@
9
9
  * stdio and WebSocket behave identically above the transport seam.
10
10
  */
11
11
  import { EventEmitter } from 'node:events';
12
- import { openTransport } from './transport.js';
12
+ import { openTransport, STDERR_END_WAIT_MS } from './transport.js';
13
13
  import { maskNegotiatedCapabilities } from './capability-mask.js';
14
14
  import { CapabilityGrant, expandAdvertisementShorthand } from './capability-grant.js';
15
- import { CAPABILITY_DISABLED } from './errors.js';
16
- import { logDetail, logValue } from './log-value.js';
15
+ import { CAPABILITY_DISABLED, boundedForLog, describeCause } from './errors.js';
16
+ import { logDetail, logValue, logValueRedacted } from './log-value.js';
17
+ import { LEGACY_MCP_PROTOCOL_VERSION, MODERN_MCP_PROTOCOL_VERSION, checkServerConfig, redactServerText, } from './protocol-family.js';
17
18
  import { McplMethod } from './types.js';
18
19
  /** Timeout for the initialize handshake in milliseconds.
19
20
  * Spring Boot + JDA servers can take 5-10s to boot, so 30s is safe. */
@@ -25,14 +26,30 @@ export function stderrTailOf(error) {
25
26
  return typeof tail === 'string' && tail !== '' ? tail : undefined;
26
27
  }
27
28
  /** For the host's own log line about a failed connect: the server's stderr
28
- * that the error keeps as `stderrTail`, one marked line per line of it,
29
- * each through log-value, since it's the server's text. '' when there's
29
+ * that the error keeps as `stderrTail`, with the server's credentials
30
+ * taken out by `redact` (its redactServerText), one marked line per line of
31
+ * it, each through log-value, since it's the server's text. '' when there's
30
32
  * none. */
31
- export function withStderr(error) {
33
+ export function withStderr(error, redact) {
32
34
  const tail = stderrTailOf(error);
33
35
  if (tail === undefined)
34
36
  return '';
35
- return '. The last of its stderr:' + tail.split('\n').map((line) => `\n stderr: ${logDetail(line)}`).join('');
37
+ return '. The last of its stderr:' + redact(tail).split('\n').map((line) => `\n stderr: ${logDetail(line)}`).join('');
38
+ }
39
+ /** A transport close's reason, as the host's log and the handshake's error
40
+ * give it: the transport's own words. A WebSocket peer's close reason is the
41
+ * server's text (`reasonFromPeer`), so they give the words the transport uses
42
+ * for a close without one, as `mcpl:server-closed` leaves that text out. A
43
+ * WebSocket close's code, a number, says what the server meant (4001: a
44
+ * refused credential), with or without a reason, so each gives it too: the
45
+ * log line for a close with requests pending has `code=`, and the
46
+ * handshake's error adds it for any WebSocket close, since a close before the
47
+ * handshake completes reaches no 'close' event. A stdio child's exit reads
48
+ * as before. */
49
+ function closeReasonText(info) {
50
+ if (info.reasonFromPeer)
51
+ return 'websocket closed';
52
+ return info.reason ?? 'unknown';
36
53
  }
37
54
  /** Default per-request timeout in milliseconds (see McplServerConfig.requestTimeoutMs).
38
55
  * A live-but-wedged server (accepts requests, never answers, never closes its
@@ -40,8 +57,143 @@ export function withStderr(error) {
40
57
  * endTurn, no further wakes, zero errors anywhere. 60s is generous for real
41
58
  * tool work while still bounding the hang. */
42
59
  const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
43
- /** MCP protocol version used in the initialize handshake. */
44
- const MCP_PROTOCOL_VERSION = '2024-11-05';
60
+ /** MCP protocol version used in the initialize handshake: the only revision
61
+ * this legacy engine offers or accepts. */
62
+ const MCP_PROTOCOL_VERSION = LEGACY_MCP_PROTOCOL_VERSION;
63
+ /** JSON-RPC code for an unsupported protocol version (MCP 2026-07-28 §basic,
64
+ * the code a modern-only server answers our `initialize` with). */
65
+ const UNSUPPORTED_PROTOCOL_VERSION_CODE = -32022;
66
+ /** Upper bound on `tools/list` pages, against a server whose cursors never
67
+ * end. Generous: real inventories fit in a handful of pages. */
68
+ const MAX_TOOL_LIST_PAGES = 100;
69
+ /**
70
+ * The server and this engine have no protocol revision in common, so the
71
+ * connection was never established. This is a protocol-negotiation fact, not
72
+ * a request outcome. Retrying cannot change it: each engine offers one
73
+ * revision, and only configuration can fix the mismatch. A connection that
74
+ * gets this error does not schedule reconnects. The legacy engine and the
75
+ * modern one (ModernMcpConnection, whose pinned revision the server refused)
76
+ * both throw it, so its readers treat them alike.
77
+ *
78
+ * - `rejected`: the server refused the offered revision: the legacy engine's
79
+ * `initialize` answered with error -32022, or a modern server's discovery
80
+ * or error naming no common revision. `supported` is the server's own list,
81
+ * when it gave one.
82
+ * - `mismatch`: the server answered the legacy engine's `initialize` with a
83
+ * revision other than 2024-11-05, or with none.
84
+ */
85
+ export class McplProtocolVersionError extends Error {
86
+ kind;
87
+ returned;
88
+ supported;
89
+ code;
90
+ data;
91
+ offered;
92
+ constructor(message, kind,
93
+ /** Revision the server answered with (`mismatch`; null when it gave none). */
94
+ returned,
95
+ /** Revisions the server says it supports (`rejected`, when stated). */
96
+ supported,
97
+ /** The JSON-RPC error's code and data, verbatim (`rejected`). */
98
+ code, data,
99
+ /** The revision this client offered: the legacy engine's, unless a
100
+ * modern connection's. */
101
+ offered = MCP_PROTOCOL_VERSION) {
102
+ super(message);
103
+ this.kind = kind;
104
+ this.returned = returned;
105
+ this.supported = supported;
106
+ this.code = code;
107
+ this.data = data;
108
+ this.offered = offered;
109
+ this.name = 'McplProtocolVersionError';
110
+ }
111
+ }
112
+ function protocolVersionRejection(id, error) {
113
+ const data = error.data;
114
+ const supported = Array.isArray(data?.supported) && data.supported.every((v) => typeof v === 'string')
115
+ ? data.supported
116
+ : null;
117
+ const modernHint = supported?.includes(MODERN_MCP_PROTOCOL_VERSION)
118
+ ? ` It speaks modern MCP: configure it with protocol: 'modern' (stdio) or reach it over http(s).`
119
+ : '';
120
+ return new McplProtocolVersionError(`MCPL server "${id}" does not support MCP ${MCP_PROTOCOL_VERSION}` +
121
+ // The server's message can be any JSON value, so it's read without
122
+ // throwing (describeCause) and bounded, as the initialize error is.
123
+ (supported ? ` (it supports: ${supported.join(', ') || 'none listed'}).` : ` (${boundedForLog(describeCause(error.message))}).`) +
124
+ modernHint, 'rejected', null, supported, error.code, error.data);
125
+ }
126
+ const errorOf = (value) => (value instanceof Error ? value : new Error(String(value)));
127
+ /**
128
+ * A handshake that failed, and whose cleanup couldn't confirm that the child
129
+ * exited. The transport goes to whoever owns the launch.
130
+ */
131
+ class HandshakeCleanupError extends Error {
132
+ failure;
133
+ cleanup;
134
+ transport;
135
+ constructor(failure, cleanup, transport) {
136
+ super(`${failure.message}; its cleanup also failed: ${cleanup.message}`, { cause: failure });
137
+ this.failure = failure;
138
+ this.cleanup = cleanup;
139
+ this.transport = transport;
140
+ this.name = 'HandshakeCleanupError';
141
+ }
142
+ }
143
+ /**
144
+ * A connect that failed, and whose cleanup couldn't confirm that the child
145
+ * exited: it may still be running. `connection` owns that launch, closed and
146
+ * not reconnecting. Its close() retries the reap and resolves once the exit
147
+ * is confirmed, so the caller can still clean up, and should start nothing
148
+ * else for this server until then. connect() and connectWithReconnect()
149
+ * throw this, and the modern engine's ModernMcpConnection.connect() too.
150
+ * `cause` is the connect's own failure.
151
+ */
152
+ export class McplUnreapedLaunchError extends Error {
153
+ connection;
154
+ cleanup;
155
+ constructor(connection, failure,
156
+ /** The cleanup's own failure: the child it couldn't confirm exited. */
157
+ cleanup) {
158
+ super(`${failure.message}; its cleanup also failed: ${cleanup.message}`, { cause: failure });
159
+ this.connection = connection;
160
+ this.cleanup = cleanup;
161
+ this.name = 'McplUnreapedLaunchError';
162
+ }
163
+ }
164
+ /**
165
+ * Why a request to an MCPL server produced no result, as far as the host can
166
+ * know it. Messages are unchanged from plain errors; the `outcome` makes the
167
+ * difference between "never left the host" and "left, and no answer came
168
+ * back" a fact a caller can act on instead of a string to match:
169
+ * - `not-sent`: the request was refused before anything was written (the
170
+ * connection was already closed);
171
+ * - `error-response`: the server answered with a JSON-RPC error (it may have
172
+ * acted before failing; the error alone does not say);
173
+ * - `no-response`: the request was handed to the transport and no answer
174
+ * came back (timeout, or the connection closed while awaiting). It does
175
+ * not prove the request reached the server — the transport can decline or
176
+ * drop a write — only that the host can no longer say it didn't.
177
+ */
178
+ export class McplRequestError extends Error {
179
+ outcome;
180
+ data;
181
+ constructor(message, outcome,
182
+ /** The JSON-RPC error's `data`, verbatim, when the server supplied one
183
+ * (e.g. which parts of a multi-part send were posted). */
184
+ data) {
185
+ super(message);
186
+ this.outcome = outcome;
187
+ this.data = data;
188
+ this.name = 'McplRequestError';
189
+ }
190
+ }
191
+ /** A request's method for a log line, whatever the server sent there. */
192
+ function describeMethod(method) {
193
+ return typeof method === 'string'
194
+ ? `method ${logValue(method)}`
195
+ : `a method of type ${method === null ? 'null' : typeof method}`;
196
+ }
45
197
  /**
46
198
  * McplServerConnection manages a single JSON-RPC 2.0 connection to an
47
199
  * MCPL server over stdio. Use the static `connect()` factory to create
@@ -63,7 +215,12 @@ const MCP_PROTOCOL_VERSION = '2024-11-05';
63
215
  * server's stderr when its handshake failed by its transport closing, a
64
216
  * transport error or its timeout and it had written any; `error` never
65
217
  * carries it.
66
- * - `'reconnect'` — Background reconnect succeeded
218
+ * - `'reconnect'` — Background reconnect succeeded. The new transport
219
+ * starts with an empty grant and both planes
220
+ * closed, as at initial connect: whoever owns the
221
+ * connection runs the §5.3 policy exchange again,
222
+ * then opens the planes (the framework's listener
223
+ * does).
67
224
  */
68
225
  export class McplServerConnection extends EventEmitter {
69
226
  /** Unique server identifier from config. */
@@ -79,6 +236,9 @@ export class McplServerConnection extends EventEmitter {
79
236
  /** OUTER standard MCP capabilities.tools was present at handshake (§5.1 —
80
237
  * the sole source of the `tools` capability path). */
81
238
  mcpToolsAdvertised = false;
239
+ /** The MCP revision established by the last successful handshake (always
240
+ * 2024-11-05 for this engine), or null before the first one. */
241
+ protocolVersion = null;
82
242
  /** Host-owned per-server authority for host/command (config
83
243
  * allowHostCommands, default false). No capability path exists and the
84
244
  * grant cannot confer it — server params never decide admin authority. */
@@ -133,6 +293,22 @@ export class McplServerConnection extends EventEmitter {
133
293
  this.grant = CapabilityGrant.empty();
134
294
  this.policyEstablished = false;
135
295
  this.transportEpoch++;
296
+ // What the server sent that is still held came on the transport that is
297
+ // now gone. What it said about its own process is dropped: its requests
298
+ // wait on answers that can no longer reach whoever asked (an inference
299
+ // request, a host/command), and its announcements describe a process
300
+ // that is gone (a tools/observe filter, a channel registration), so the
301
+ // next one announces its own. What it reported about the world is kept: a
302
+ // channel's incoming messages and its pushes, which the next process
303
+ // doesn't send again when its bridge records them as forwarded first, as
304
+ // discord-mcpl does with its watermark. They are admitted at the flush
305
+ // against the next transport's grant, like anything held, and an answer
306
+ // to one is never written to the next transport (sameTransport); a
307
+ // connection closed for good, with no next transport, drops them. The
308
+ // host's own reports about the transport stay too: its lifecycle
309
+ // (connect-failed, reconnect-failed), the server's stderr, and
310
+ // orphaned-response audits.
311
+ this.replaceHeld(this.keepAcrossBoundary(this.bufferedEvents));
136
312
  // RFC-007 §6.5: a filter is per connection epoch — after a reconnect the
137
313
  // server sends it again, and until then receives metadata only.
138
314
  this.toolObserveFilter = null;
@@ -142,6 +318,59 @@ export class McplServerConnection extends EventEmitter {
142
318
  // (Sol, PR #84 review — they previously survived into the new epoch).
143
319
  this.manifestState = { lastValidatedRevision: null, lastFetchedAt: null, lastNegotiatedAt: null };
144
320
  }
321
+ /** What of these held items a transport boundary keeps (see
322
+ * resetPolicyForTransportBoundary), in order, each marked `kept`; the
323
+ * rest is logged and dropped. A connection closed for good has no next
324
+ * transport whose grant could admit what the server reported about the
325
+ * world, so that is dropped too. */
326
+ keepAcrossBoundary(held) {
327
+ const forGood = this.closedForGood;
328
+ const drops = (item) => McplServerConnection.PROCESS_EVENTS.has(item.event)
329
+ || (forGood && McplServerConnection.WORLD_EVENTS.has(item.event));
330
+ const kept = held.filter((item) => !drops(item));
331
+ if (kept.length < held.length) {
332
+ // Named by event, with a count, so a dropped request a server was
333
+ // waiting on, which no answer will now reach, can be told apart.
334
+ const counts = new Map();
335
+ for (const item of held)
336
+ if (drops(item))
337
+ counts.set(item.event, (counts.get(item.event) ?? 0) + 1);
338
+ const what = [...counts].map(([event, count]) => `${count} ${logValue(event)}`).join(', ');
339
+ console.error(`[mcpl] ${this.id}: dropped ${held.length - kept.length} held message(s) from a transport that closed` +
340
+ `${forGood ? ', with no transport to come' : ''}: ${what}`);
341
+ }
342
+ return kept.map((item) => ({ ...item, kept: true }));
343
+ }
344
+ /** Hold these items in place of what is held, with heldRequests and
345
+ * keptRequests counting the requests among them: a boundary keeps some of
346
+ * what was held, a flush that crosses one holds the rest of its snapshot
347
+ * again, and a flush starts from nothing held. Notifications need no cap
348
+ * here: hold caps them kept or not, and what this is given never has more
349
+ * than was held. */
350
+ replaceHeld(items) {
351
+ // The kept bound holds here too: boundaries that come with no flush
352
+ // between them each keep what the next transport held, so it's checked
353
+ // where they're joined. The earliest kept are kept, as at hold.
354
+ let kept = 0;
355
+ let past = 0;
356
+ this.bufferedEvents = items.filter((item) => {
357
+ if (item.kept !== true || !McplServerConnection.hasResponder(item))
358
+ return true;
359
+ if (kept < McplServerConnection.MAX_HELD_REQUESTS) {
360
+ kept++;
361
+ return true;
362
+ }
363
+ past++;
364
+ return false;
365
+ });
366
+ this.heldRequests = this.bufferedEvents.filter((item) => !item.kept && McplServerConnection.hasResponder(item)).length;
367
+ this.keptRequests = kept;
368
+ if (past > 0) {
369
+ this.droppedKeptRequests += past;
370
+ console.error(`[mcpl] ${this.id}: kept-request bound (${McplServerConnection.MAX_HELD_REQUESTS}) — ` +
371
+ `dropped ${past} request(s) a closed transport left`);
372
+ }
373
+ }
145
374
  /** The active transport (stdio child or WebSocket). Null for a disconnected
146
375
  * stub awaiting its first background reconnect. */
147
376
  transport;
@@ -159,11 +388,21 @@ export class McplServerConnection extends EventEmitter {
159
388
  /** Set by `reconnectAfterFailure()` before it closes a live transport, so
160
389
  * the transport close handler reports that teardown as `byHost: true`. */
161
390
  hostCloseRequested = false;
391
+ /** Set by `close()`: this host has stopped the connection, as a removal and
392
+ * a restart both do, so a request after it can't say whether the server
393
+ * comes back. */
394
+ closedByThisHost = false;
395
+ /** The host's close attempt: in flight, or done. A failed one is cleared,
396
+ * so the next close() tries again. */
162
397
  closePromise = null;
398
+ /** The host closed this connection while it was live, and its teardown
399
+ * hasn't completed yet: 'close' is emitted once it has. */
163
400
  closeIncomplete = false;
164
- reconnectPromise = null;
165
401
  /** Per-request timeout in ms (0 disables). See McplServerConfig.requestTimeoutMs. */
166
402
  requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
403
+ /** The deadline a channels/publish or channels/open request keeps when
404
+ * requestTimeoutMs is 0 (see sendChannelsPublish). */
405
+ publishDeadlineMs = DEFAULT_REQUEST_TIMEOUT_MS;
167
406
  // Event buffering: control-plane traffic can be released independently from
168
407
  // inference-bearing data-plane traffic. This lets a server finish its own
169
408
  // registration (and answer host tools/call requests) while startup/reconnect
@@ -171,15 +410,37 @@ export class McplServerConnection extends EventEmitter {
171
410
  controlPlaneReady = false;
172
411
  dataPlaneReady = false;
173
412
  bufferedEvents = [];
174
- /** Cap on buffered responder-less events during a plane pause (issue #122).
175
- * Held server→host requests are never dropped and may exceed this. */
176
- static MAX_BUFFERED_EVENTS = 1000;
413
+ /** How many of bufferedEvents are server→host requests, each holding a
414
+ * responder its server waits on. Every change to bufferedEvents keeps it. */
415
+ heldRequests = 0;
416
+ /** How many of bufferedEvents are requests a transport boundary kept:
417
+ * incoming messages and pushes whose transport has closed. No server waits
418
+ * on them, since their answers can't reach it (sameTransport), so they
419
+ * stay out of the held-request budget, which bounds what a live server
420
+ * waits on: a dead process's backlog must not refuse the next process's
421
+ * registration. They have their own bound at the same size, past which a
422
+ * kept request is dropped and logged, there being no one to refuse. */
423
+ keptRequests = 0;
424
+ /** Kept requests dropped at their bound since the planes last opened
425
+ * (ready()), so the first of each run of drops is logged. */
426
+ droppedKeptRequests = 0;
427
+ /** Cap on held notifications (responder-less events) during a plane pause
428
+ * (issue #122), counted apart from requests: past it, the oldest held
429
+ * notification is dropped. */
430
+ static MAX_HELD_NOTIFICATIONS = 1000;
431
+ /** Budget of held requests during a plane pause (#387), counted apart from
432
+ * notifications: past it, a request is refused at once, never dropped. It
433
+ * bounds a server that pipelines requests without waiting for answers. A
434
+ * server that doesn't send a refused request again loses it (discord-mcpl
435
+ * doesn't), so the budget sits far above the notifications' cap, to leave
436
+ * a busy server's long pause room. Requests a transport boundary kept are
437
+ * counted apart from it (keptRequests), with a bound of the same size past
438
+ * which they're dropped, since no server waits on them. */
439
+ static MAX_HELD_REQUESTS = 10_000;
177
440
  droppedBufferedEvents = 0;
178
- /** True while ready()/readyControlPlane() re-emit a buffer snapshot. Items
179
- * re-buffering mid-flush were already admitted once — applying the cap to
180
- * them again would evict events that were legally held (the cap targets
181
- * NEW arrivals during a pause, not churn from control-plane flushes). */
182
- flushDepth = 0;
441
+ /** Requests refused at the budget since the planes last opened (ready()),
442
+ * so each pause's first refusal is logged. */
443
+ refusedHeldRequests = 0;
183
444
  // Reconnect state (adapted from Anarchid/agent-framework@mcpl-module-proto)
184
445
  config = null;
185
446
  hostCapabilities = null;
@@ -190,11 +451,33 @@ export class McplServerConnection extends EventEmitter {
190
451
  /** Consecutive failed connect attempts since the last successful handshake.
191
452
  * Drives the exponential backoff and is reported on reconnect events. */
192
453
  reconnectAttempts = 0;
454
+ /** The reconnect attempt in flight, so close() can abort it and wait for
455
+ * its cleanup. */
456
+ reconnecting = null;
457
+ /**
458
+ * Launches whose cleanup couldn't confirm that the child exited. While any
459
+ * is held, reconnecting is halted, since another launch could run beside
460
+ * one that's still alive. close() retries their reap.
461
+ */
462
+ unreaped = [];
193
463
  /** Whether a background reconnect loop will revive this connection after a
194
- * drop. False once close() has been called (explicit close stops retrying). */
464
+ * drop. False once close() has been called (explicit close stops retrying),
465
+ * after a protocol-version verdict, and while a launch is unreaped. */
195
466
  get willReconnect() {
196
467
  return this.reconnectEnabled;
197
468
  }
469
+ /** Whether a child this connection launched couldn't be confirmed exited.
470
+ * While it's true the connection owns that child, so whatever registered
471
+ * it keeps its id held; close() retries the reap. */
472
+ get holdsUnreapedChild() {
473
+ return this.unreaped.length > 0;
474
+ }
475
+ /** Closed with no reconnect to come: after close(), whether or not its
476
+ * transport close succeeded, or a drop with reconnect off. A connection
477
+ * between retries keeps reconnectEnabled, so it isn't closed for good. */
478
+ get closedForGood() {
479
+ return this.closed && !this.reconnectEnabled;
480
+ }
198
481
  /**
199
482
  * Private constructor. Use `McplServerConnection.connect()` instead.
200
483
  */
@@ -215,58 +498,206 @@ export class McplServerConnection extends EventEmitter {
215
498
  * Mark the connection as ready — flushes any events that arrived between
216
499
  * construction (when setupMessageRouting starts emitting) and now (when
217
500
  * the caller has attached listeners via wireMcplEvents).
501
+ *
502
+ * Once the connection is closed for good it readies nothing. Its policy was
503
+ * reset when it closed, so what the server sent that needs a grant would be
504
+ * refused, and a closed connection sends no response, so nothing it held
505
+ * could be answered. It delivers only its own reports of how it ended (see
506
+ * {@link deliverOwnReports}). A connection between retries isn't closed for
507
+ * good: what it held, such as a reconnect stub's connect-failed, reaches
508
+ * its listeners as it does any connection's, except the incoming messages
509
+ * and pushes a transport boundary kept, which wait for the next
510
+ * transport's grant (see {@link flushHeld}).
218
511
  */
219
512
  ready() {
513
+ if (this.closedForGood) {
514
+ this.deliverOwnReports();
515
+ return;
516
+ }
220
517
  this.controlPlaneReady = true;
221
518
  this.dataPlaneReady = true;
222
519
  const pending = this.bufferedEvents;
223
- this.bufferedEvents = [];
520
+ this.replaceHeld([]);
521
+ // The cap's drops are counted per pause: this flush ends one, so its line
522
+ // reports that pause's drops, and the next pause counts (and logs) its own.
523
+ const dropped = this.droppedBufferedEvents;
524
+ this.droppedBufferedEvents = 0;
525
+ this.refusedHeldRequests = 0;
526
+ this.droppedKeptRequests = 0;
224
527
  if (pending.length > 200) {
225
528
  console.error(`[mcpl] ${this.id}: flushing ${pending.length} buffered event(s) synchronously ` +
226
- `(long pause backlog${this.droppedBufferedEvents > 0 ? `; ${this.droppedBufferedEvents} dropped at cap` : ''})`);
529
+ `(long pause backlog${dropped > 0 ? `; ${dropped} dropped at cap` : ''})`);
227
530
  }
228
- this.flushDepth++;
229
- try {
230
- for (const { event, args } of pending) {
231
- // Re-enter the gate for every buffered item. A control handler (notably
232
- // tools/list_changed) may install a newer data-plane barrier while this
233
- // snapshot is being flushed; later data items must observe that pause.
234
- this.emit(event, ...args);
531
+ // Re-enter the gate for every buffered item. A control handler (notably
532
+ // tools/list_changed) may install a newer data-plane barrier while this
533
+ // snapshot is being flushed; later data items must observe that pause.
534
+ this.flushHeld(pending);
535
+ }
536
+ /**
537
+ * Re-emit a held snapshot through the admission gate (this.emit), inside
538
+ * the containment boundary every inbound event shares
539
+ * (dispatchInboundEvent), in admission order (admissionOrder). A transport
540
+ * boundary crossed during the flush (a listener that closes the connection,
541
+ * say) leaves the rest of the snapshot to the boundary rule, as if it had
542
+ * still been held: what the server said about its own process is dropped,
543
+ * and the rest is held again, after whatever this flush held.
544
+ *
545
+ * Between transports, the incoming messages and pushes a boundary kept
546
+ * stay held for the next transport's grant: admitted now, they would meet
547
+ * the empty grant the boundary left, with no transport to answer on. The
548
+ * host's own reports about the transport (a stub's connect-failed, say)
549
+ * need no grant and go on.
550
+ */
551
+ flushHeld(snapshot) {
552
+ const waits = (item) => item.kept === true && McplServerConnection.WORLD_EVENTS.has(item.event);
553
+ let pending = snapshot;
554
+ if (!this.isConnected && snapshot.some(waits)) {
555
+ this.replaceHeld([...this.bufferedEvents, ...snapshot.filter(waits)]);
556
+ pending = snapshot.filter((item) => !waits(item));
557
+ }
558
+ const ordered = McplServerConnection.admissionOrder(pending);
559
+ const epoch = this.transportEpoch;
560
+ for (let index = 0; index < ordered.length; index++) {
561
+ if (this.transportEpoch !== epoch) {
562
+ this.replaceHeld([...this.bufferedEvents, ...this.keepAcrossBoundary(ordered.slice(index))]);
563
+ return;
235
564
  }
565
+ const { event, args, kept } = ordered[index];
566
+ // What it can't admit yet is held again, as the admission gate would
567
+ // hold it, with what a boundary kept still marked as kept.
568
+ if (kept && !this.planeReadyFor(event))
569
+ this.hold(event, args, kept);
570
+ else
571
+ this.dispatchInboundEvent(event, ...args);
236
572
  }
237
- finally {
238
- this.flushDepth--;
573
+ }
574
+ /** Whether the plane an event belongs to is open (lifecycle events always
575
+ * pass), as the admission gate (emit) decides it. */
576
+ planeReadyFor(event) {
577
+ return McplServerConnection.isLifecycleEvent(event)
578
+ || (McplServerConnection.isControlPlaneEvent(event) ? this.controlPlaneReady : this.dataPlaneReady);
579
+ }
580
+ /**
581
+ * The order a flush admits a held snapshot in: as it was held, except that
582
+ * when it holds items a transport boundary kept, the current transport's
583
+ * channel registrations (`channels-register`, `channels-changed`) go
584
+ * first, each unless it names a channel that something the transport sent
585
+ * before it, and that stays where it was held, also names (below). The
586
+ * registry refuses a message for a channel it doesn't know
587
+ * (§14.5). It keeps a server's channels across a reconnect, but a
588
+ * registration the old process sent that was still held at the boundary
589
+ * was dropped with it (keepAcrossBoundary), so the next process registers
590
+ * that channel anew, behind what was kept: a kept message for it admitted
591
+ * first would be refused, and lost, since a bridge that recorded it as
592
+ * forwarded doesn't send it again.
593
+ * Nothing else moves, so a snapshot with nothing kept is flushed as it was
594
+ * held. A registration also passes whatever its own transport sent before
595
+ * it, which a server that announces its channels first, as a bridge does,
596
+ * never has. A removal stays where it was held, with whatever else it adds
597
+ * or updates, as everything does that isn't a registration. A registration
598
+ * that names a channel which an earlier removal, or an earlier registration
599
+ * that stays, names stays where it was held too, so the writes to one
600
+ * channel keep their order: a channel's removal and its re-registration,
601
+ * and two registrations or updates of it. In a flush that opens both
602
+ * planes, what the transport sent before a removal is admitted before it,
603
+ * its messages for that channel included. A flush that opens only the control
604
+ * plane, as during a quiesce, admits the control traffic, a removal
605
+ * included, and holds the data plane's again until that plane opens, as
606
+ * live traffic is during a quiesce: a message held for a channel a removal
607
+ * took is then refused as the data plane opens.
608
+ */
609
+ static admissionOrder(pending) {
610
+ if (!pending.some((item) => item.kept))
611
+ return pending;
612
+ const registrations = new Set();
613
+ // The channels named so far by a removal or registration that stays
614
+ // where it was held.
615
+ const staying = new Set();
616
+ for (const item of pending) {
617
+ if (item.kept)
618
+ continue;
619
+ if (item.event !== 'channels-register' && item.event !== 'channels-changed')
620
+ continue;
621
+ const removes = McplServerConnection.removedChannelIds(item);
622
+ const registers = McplServerConnection.registeredChannelIds(item);
623
+ if (removes.length === 0 && !registers.some((id) => staying.has(id))) {
624
+ registrations.add(item);
625
+ continue;
626
+ }
627
+ for (const id of [...removes, ...registers])
628
+ staying.add(id);
239
629
  }
630
+ if (registrations.size === 0)
631
+ return pending;
632
+ return [...registrations, ...pending.filter((item) => !registrations.has(item))];
633
+ }
634
+ /** The channels a held `channels-changed` removes. The registry takes any
635
+ * entry of `removed` as a channel id, by its string form, so a number
636
+ * removes the channel of its digits; an object is left out here, since
637
+ * its string form can throw. */
638
+ static removedChannelIds(item) {
639
+ if (item.event !== 'channels-changed')
640
+ return [];
641
+ const params = item.args[0];
642
+ return Array.isArray(params?.removed)
643
+ ? params.removed.filter((id) => typeof id !== 'object' || id === null).map((id) => String(id))
644
+ : [];
645
+ }
646
+ /** The channels a held `channels-register` registers, or a held
647
+ * `channels-changed` adds or updates. */
648
+ static registeredChannelIds(item) {
649
+ const params = item.args[0];
650
+ const lists = item.event === 'channels-register' ? [params?.channels] : [params?.added, params?.updated];
651
+ return lists.flatMap((list) => (Array.isArray(list) ? list : []))
652
+ .map((channel) => channel?.id)
653
+ .filter((id) => typeof id === 'string');
240
654
  }
241
655
  /**
242
656
  * Release only server-establishment and lifecycle/control traffic. Responses
243
657
  * to host requests are never event-buffered, so tools/list and tools/call
244
658
  * responses remain live as well. Inference-bearing requests stay queued in
245
- * their original relative order until {@link ready} is called.
659
+ * their original relative order until {@link ready} is called. Like
660
+ * {@link ready}, once the connection is closed for good it delivers only
661
+ * its own reports of how it ended.
246
662
  */
247
663
  readyControlPlane() {
664
+ if (this.closedForGood) {
665
+ this.deliverOwnReports();
666
+ return;
667
+ }
248
668
  this.controlPlaneReady = true;
249
669
  const pending = this.bufferedEvents;
250
- this.bufferedEvents = [];
251
- this.flushDepth++;
252
- try {
253
- for (const item of pending) {
254
- // Re-enter the ADMISSION gate (this.emit), never super.emit: control
255
- // events include channels-register/changed, which are grant-gated —
256
- // the old direct flush bypassed positive-grant enforcement entirely
257
- // (PR #79 review blocker 3). Data-plane items re-buffer in order via
258
- // the same call, since dataPlaneReady is still false.
259
- this.emit(item.event, ...item.args);
260
- }
261
- }
262
- finally {
263
- this.flushDepth--;
264
- }
670
+ this.replaceHeld([]);
671
+ // Re-enter the ADMISSION gate (this.emit), never super.emit: control
672
+ // events include channels-register/changed, which are grant-gated —
673
+ // the old direct flush bypassed positive-grant enforcement entirely
674
+ // (PR #79 review blocker 3). Data-plane items re-buffer in order via
675
+ // the same call, since dataPlaneReady is still false.
676
+ this.flushHeld(pending);
677
+ }
678
+ /**
679
+ * A connection closed for good still says how it ended. Its own buffered
680
+ * connect-failed and reconnect-failed, such as the permanent failure of a
681
+ * launch held as unreaped before the first gate released it, are the
682
+ * framework's records of it: they need no grant, and its listeners act on
683
+ * them (a trace, and for a permanent failure an alert and its tools leaving
684
+ * the list). What the server sent stays held.
685
+ */
686
+ deliverOwnReports() {
687
+ const own = (item) => item.event === 'connect-failed' || item.event === 'reconnect-failed';
688
+ const reports = this.bufferedEvents.filter(own);
689
+ if (reports.length === 0)
690
+ return;
691
+ // The host's own reports have no responder, so the request counts stand.
692
+ this.bufferedEvents = this.bufferedEvents.filter((item) => !own(item));
693
+ for (const { event, args } of reports)
694
+ super.emit(event, ...args);
265
695
  }
266
696
  /**
267
697
  * Close the data plane synchronously while leaving an already-released
268
- * control plane live. Used before reconnect adoption and list-change
269
- * reconciliation so no new inference event can beat barrier installation.
698
+ * control plane live. Used before list-change reconciliation so no new
699
+ * inference event can beat barrier installation. A reconnect closes both
700
+ * planes instead.
270
701
  */
271
702
  pauseDataPlane() {
272
703
  this.dataPlaneReady = false;
@@ -278,10 +709,7 @@ export class McplServerConnection extends EventEmitter {
278
709
  */
279
710
  emit(event, ...args) {
280
711
  const name = typeof event === 'string' ? event : '';
281
- if (McplServerConnection.isLifecycleEvent(name)
282
- || (McplServerConnection.isControlPlaneEvent(name)
283
- ? this.controlPlaneReady
284
- : this.dataPlaneReady)) {
712
+ if (this.planeReadyFor(name)) {
285
713
  // Positive-grant admission (§5.4/§14.1), at the single choke-point
286
714
  // every path shares: live routing AND buffer flushes re-enter here, so
287
715
  // an event buffered while the connection was staged is authorized
@@ -317,37 +745,104 @@ export class McplServerConnection extends EventEmitter {
317
745
  }
318
746
  return true;
319
747
  }
748
+ // §6.6: a request that will never be answered MUST get an error. A
749
+ // request whose event nothing on this host listens for would never be.
750
+ const request = args[1];
751
+ if (typeof request?.respondError === 'function' && this.listenerCount(event) === 0) {
752
+ request.respondError(-32601, 'Method not found');
753
+ console.error(`[mcpl] ${this.id}: answered -32601 to ${logValue(name)}, which nothing on this host handles`);
754
+ return true;
755
+ }
756
+ // mcpl/manifestChanged (§17.3) and tools/list_changed are notifications.
757
+ // Sent as a request, either is answered with an empty result as it's
758
+ // admitted, and passed on as the notification is, with no responder:
759
+ // the host acts on it the same way, so the answer doesn't depend on what
760
+ // the refresh does, and it's written before any request the refresh
761
+ // sends. This is where live routing and buffer flushes meet, so a request
762
+ // held while its plane is closed is answered only when it's let through,
763
+ // never by a startup that fails first.
764
+ if ((name === 'manifest-changed' || name === 'tools-list-changed') && typeof request?.respond === 'function') {
765
+ request.respond({});
766
+ return super.emit(event, args[0]);
767
+ }
320
768
  return super.emit(event, ...args);
321
769
  }
322
- // Buffer cap (issue #122): a long data-plane pause (quiesce window) on a
323
- // busy server would otherwise grow this array without bound. Only
324
- // responder-LESS events are ever dropped — a buffered server→host REQUEST
325
- // holds a live responder the server is blocking on, and dropping it would
326
- // strand that call forever (the server timing out and reconnecting is the
327
- // accepted bound there, and reconnect re-runs the barrier funnel).
328
- if (this.flushDepth === 0 && this.bufferedEvents.length >= McplServerConnection.MAX_BUFFERED_EVENTS) {
329
- const hasResponder = (item) => {
330
- const responder = item.args[1];
331
- return typeof responder?.respond === 'function';
332
- };
333
- if (!hasResponder({ args })) {
334
- this.droppedBufferedEvents++;
335
- if (this.droppedBufferedEvents % 100 === 1) {
336
- console.error(`[mcpl] ${this.id}: buffered-event cap (${McplServerConnection.MAX_BUFFERED_EVENTS}) — ` +
337
- `dropped ${this.droppedBufferedEvents} responder-less event(s) while the plane is paused`);
338
- }
339
- const dropIndex = this.bufferedEvents.findIndex((item) => !hasResponder(item));
340
- if (dropIndex >= 0) {
341
- this.bufferedEvents.splice(dropIndex, 1); // drop-oldest non-request
342
- this.bufferedEvents.push({ event: name, args });
770
+ return this.hold(name, args);
771
+ }
772
+ /**
773
+ * Hold an event until its plane opens, `kept` when a transport boundary
774
+ * keeps it for the next transport's grant (afterBoundary).
775
+ */
776
+ hold(name, args, kept) {
777
+ // Held until the plane opens. A long pause (a quiesce, say) on a busy or a
778
+ // runaway server would otherwise grow this array without bound, so
779
+ // requests and notifications each have a bound, counted apart. A request
780
+ // holds a responder its server waits on, and dropping it would strand that
781
+ // call, so past MAX_HELD_REQUESTS (#387) it's refused at once with -32000,
782
+ // the code of the host's other transient refusals, and its server can send
783
+ // it again. A notification has no one to tell, so past
784
+ // MAX_HELD_NOTIFICATIONS (issue #122) the oldest held notification is
785
+ // dropped to make room. A request a closed transport left (`kept`) has
786
+ // no server waiting on it, so it's counted apart, as keptRequests, and
787
+ // dropped past its own bound. A flush takes the whole buffer and zeroes
788
+ // its counts, so what it holds again was admitted within these bounds
789
+ // and stays within them.
790
+ const responder = args[1];
791
+ const isRequest = McplServerConnection.hasResponder({ args });
792
+ if (isRequest && kept) {
793
+ // A request a closed transport left (keptRequests): no server waits on
794
+ // it, so past its own bound it's dropped, with nobody to refuse.
795
+ if (this.keptRequests >= McplServerConnection.MAX_HELD_REQUESTS) {
796
+ this.droppedKeptRequests++;
797
+ if (this.droppedKeptRequests % 100 === 1) {
798
+ console.error(`[mcpl] ${this.id}: kept-request bound (${McplServerConnection.MAX_HELD_REQUESTS}) — ` +
799
+ `dropped ${this.droppedKeptRequests} request(s) a closed transport left`);
343
800
  }
344
- // else: buffer is all held requests — drop the incoming event instead.
345
801
  return true;
346
802
  }
803
+ this.bufferedEvents.push({ event: name, args, kept });
804
+ this.keptRequests++;
805
+ return true;
347
806
  }
348
- this.bufferedEvents.push({ event: name, args });
807
+ if (isRequest && this.heldRequests >= McplServerConnection.MAX_HELD_REQUESTS) {
808
+ this.refusedHeldRequests++;
809
+ if (this.refusedHeldRequests % 100 === 1) {
810
+ console.error(`[mcpl] ${this.id}: held-request budget (${McplServerConnection.MAX_HELD_REQUESTS}) — ` +
811
+ `refused ${this.refusedHeldRequests} request(s) with -32000 while the plane is paused`);
812
+ }
813
+ responder?.respondError?.(-32000, `the host is busy: ${McplServerConnection.MAX_HELD_REQUESTS} of this server's requests are already waiting; send this one again later`);
814
+ return true;
815
+ }
816
+ if (!isRequest && this.bufferedEvents.length - this.heldRequests - this.keptRequests >= McplServerConnection.MAX_HELD_NOTIFICATIONS) {
817
+ this.droppedBufferedEvents++;
818
+ if (this.droppedBufferedEvents % 100 === 1) {
819
+ console.error(`[mcpl] ${this.id}: buffered-event cap (${McplServerConnection.MAX_HELD_NOTIFICATIONS}) — ` +
820
+ `dropped ${this.droppedBufferedEvents} responder-less event(s) while the plane is paused`);
821
+ }
822
+ const oldest = this.bufferedEvents.findIndex((item) => !McplServerConnection.hasResponder(item));
823
+ if (oldest >= 0)
824
+ this.bufferedEvents.splice(oldest, 1);
825
+ }
826
+ this.bufferedEvents.push(kept ? { event: name, args, kept } : { event: name, args });
827
+ if (isRequest)
828
+ this.heldRequests++;
349
829
  return true;
350
830
  }
831
+ /** A buffered server→host request: it carries a responder its server waits on. */
832
+ static hasResponder(item) {
833
+ const responder = item.args[1];
834
+ return typeof responder?.respond === 'function';
835
+ }
836
+ /** Events routed from a server's transport that report the world, which
837
+ * the next process doesn't send again when its bridge records them as
838
+ * forwarded first: kept at a transport boundary while held. */
839
+ static WORLD_EVENTS = new Set(['channels-incoming', 'push-event']);
840
+ /** Events routed from a server's transport that speak for its process, not
841
+ * the world: dropped at a transport boundary while held. */
842
+ static get PROCESS_EVENTS() {
843
+ return new Set(Object.values(McplServerConnection.METHOD_TO_EVENT)
844
+ .filter((event) => !McplServerConnection.WORLD_EVENTS.has(event)));
845
+ }
351
846
  /** Event-name form of METHOD_TO_REQUIRED_CAPABILITY, for admission-time
352
847
  * enforcement in emit(). */
353
848
  static EVENT_TO_REQUIRED_CAPABILITY = {
@@ -395,19 +890,46 @@ export class McplServerConnection extends EventEmitter {
395
890
  * capability negotiation, and return a ready-to-use connection.
396
891
  *
397
892
  * Works over stdio (spawned child) or WebSocket, selected by `config` (see
398
- * {@link openTransport}). Throws if the transport can't be opened, the server
399
- * closes/errors before the handshake, or the handshake times out.
893
+ * {@link openTransport}). Throws if the configuration can't be used (before
894
+ * anything is spawned or dialed), the transport can't be opened, the server
895
+ * closes/errors before the handshake, or the handshake times out. If the
896
+ * failed launch's cleanup can't confirm its child exited, it throws
897
+ * {@link McplUnreapedLaunchError}, whose connection owns that launch.
400
898
  */
401
- static async connect(config, hostCapabilities) {
402
- const { transport, capabilities, droppedCapabilities, mcpToolsAdvertised } = await McplServerConnection.handshake(config, hostCapabilities);
899
+ static async connect(config, hostCapabilities, signal) {
900
+ McplServerConnection.checkConfig(config);
901
+ return McplServerConnection.connectChecked(config, hostCapabilities, signal);
902
+ }
903
+ /** {@link connect}, for a configuration its caller has already checked
904
+ * (and warned about) once. `signal` ends the connect at once, its launch
905
+ * reaped as for any failed handshake: its caller's stop() or a
906
+ * disconnect of its id. */
907
+ static async connectChecked(config, hostCapabilities, signal) {
908
+ let established;
909
+ try {
910
+ established = await McplServerConnection.handshake(config, hostCapabilities, signal);
911
+ }
912
+ catch (error) {
913
+ if (!(error instanceof HandshakeCleanupError))
914
+ throw error;
915
+ // The launch may still be running. A connection owns it, closed and
916
+ // halted, so the caller can retry the reap by closing it.
917
+ const owner = McplServerConnection.createDisconnectedStub(config, hostCapabilities);
918
+ owner.holdUnreaped(error.transport, error.cleanup);
919
+ throw new McplUnreapedLaunchError(owner, error.failure, error.cleanup);
920
+ }
921
+ const { transport, capabilities, droppedCapabilities, mcpToolsAdvertised, protocolVersion } = established;
403
922
  const connection = new McplServerConnection(config.id, capabilities, transport);
404
923
  connection.droppedCapabilities = droppedCapabilities;
405
924
  connection.mcpToolsAdvertised = mcpToolsAdvertised;
925
+ connection.protocolVersion = protocolVersion;
406
926
  connection.allowHostCommands = config.allowHostCommands === true;
407
927
  connection.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
408
- // Store config for reconnection
409
- if (config.reconnect) {
410
- connection.config = config;
928
+ // Its configuration, which a reconnect reads and every log line of the
929
+ // server's text redacts with. Only `true` turns reconnect on, as at
930
+ // every other reader of it.
931
+ connection.config = config;
932
+ if (config.reconnect === true) {
411
933
  connection.hostCapabilities = hostCapabilities;
412
934
  connection.reconnectEnabled = true;
413
935
  connection.reconnectIntervalMs = config.reconnectIntervalMs ?? 5000;
@@ -415,15 +937,45 @@ export class McplServerConnection extends EventEmitter {
415
937
  }
416
938
  return connection;
417
939
  }
940
+ /**
941
+ * This engine's configuration check, made before anything is spawned or
942
+ * dialed: every problem {@link checkServerConfig} finds, the same check the
943
+ * framework applies, and the modern family, which belongs to
944
+ * ModernMcpConnection (an http(s) url is never dialed as a WebSocket).
945
+ * Every configuration that worked before still passes but the one
946
+ * serverConfigProblems names.
947
+ */
948
+ static checkConfig(config) {
949
+ const { family } = checkServerConfig(config);
950
+ if (family === 'modern') {
951
+ throw new Error(`MCP server "${config.id}" is configured for modern MCP (${MODERN_MCP_PROTOCOL_VERSION}); ` +
952
+ `McplServerConnection speaks the legacy family (${MCP_PROTOCOL_VERSION} + MCPL) only`);
953
+ }
954
+ }
418
955
  /**
419
956
  * Open a fresh transport and run the MCP/MCPL initialize handshake over it,
420
957
  * returning the connected transport plus the negotiated capabilities. Shared
421
958
  * by {@link connect} and {@link attemptReconnect} so both transports handshake
422
959
  * identically. On any failure the transport is closed so a spawned child /
423
- * open socket never leaks.
960
+ * open socket never leaks. If that close can't confirm the child exited,
961
+ * the failure is a {@link HandshakeCleanupError} carrying the transport, so
962
+ * the caller keeps owning it. `signal` ends a handshake whose connection is
963
+ * closed meanwhile: it cuts a WebSocket dial short, is checked once the
964
+ * transport is open, and races the initialize answer.
424
965
  */
425
- static async handshake(config, hostCapabilities) {
426
- const transport = await openTransport(config);
966
+ static async handshake(config, hostCapabilities, signal) {
967
+ const closedWhileConnecting = () => new Error(`MCPL server "${config.id}" was closed while connecting`);
968
+ if (signal?.aborted)
969
+ throw closedWhileConnecting();
970
+ let transport;
971
+ try {
972
+ transport = await openTransport(config, signal);
973
+ }
974
+ catch (error) {
975
+ if (signal?.aborted)
976
+ throw closedWhileConnecting();
977
+ throw error;
978
+ }
427
979
  // Once the handshake stops listening for 'error', this logs what the
428
980
  // transport reports, until the handshake hands the transport on, or for
429
981
  // good if the handshake fails. EventEmitter throws an 'error' that has no
@@ -431,9 +983,11 @@ export class McplServerConnection extends EventEmitter {
431
983
  // WebSocket frame that arrives with the initialize response, or while a
432
984
  // failed handshake's close() runs, is such an error.
433
985
  const logAfterSettled = (err) => {
434
- console.error(`[mcpl] ${config.id}: transport error after the handshake settled: ${err.message}`);
986
+ console.error(`[mcpl] ${config.id}: transport error after the handshake settled: ${redactServerText(err.message, config)}`);
435
987
  };
436
988
  try {
989
+ if (signal?.aborted)
990
+ throw closedWhileConnecting();
437
991
  const initId = 0; // use id=0 for the handshake
438
992
  const initRequest = {
439
993
  jsonrpc: '2.0',
@@ -445,9 +999,9 @@ export class McplServerConnection extends EventEmitter {
445
999
  clientInfo: { name: 'agent-framework', version: '1.0.0' },
446
1000
  },
447
1001
  };
448
- // Await the initialize response, racing an early transport close/error and
449
- // a timeout. All three paths clean up their listeners.
450
- const { mcpl: rawCapabilities, mcpToolsAdvertised } = await new Promise((resolve, reject) => {
1002
+ // Await the initialize response, racing an early transport close/error,
1003
+ // a timeout and the owner's close. Every path cleans up its listeners.
1004
+ const { mcpl: rawCapabilities, mcpToolsAdvertised, protocolVersion } = await new Promise((resolve, reject) => {
451
1005
  // When the transport fails, the end of the server's stderr often says
452
1006
  // why, such as a missing token or a crash at startup. It goes on the
453
1007
  // error as `stderrTail`, never into the message: an error's message
@@ -461,9 +1015,14 @@ export class McplServerConnection extends EventEmitter {
461
1015
  transport.off('line', onLine);
462
1016
  transport.off('close', onClose);
463
1017
  transport.off('error', onError);
1018
+ signal?.removeEventListener('abort', onAbort);
464
1019
  transport.on('error', logAfterSettled);
465
1020
  clearTimeout(timer);
466
1021
  };
1022
+ const onAbort = () => {
1023
+ cleanup();
1024
+ reject(closedWhileConnecting());
1025
+ };
467
1026
  const onLine = (line) => {
468
1027
  let msg;
469
1028
  try {
@@ -472,14 +1031,46 @@ export class McplServerConnection extends EventEmitter {
472
1031
  catch {
473
1032
  return; // Ignore non-JSON lines (e.g. logback output from Java servers)
474
1033
  }
1034
+ if (msg === null || typeof msg !== 'object' || Array.isArray(msg))
1035
+ return;
1036
+ // A request or a notification from the server is never the
1037
+ // initialize response, whatever its id: a server's own ids can start
1038
+ // at 0, this handshake's. MCP lets a server ping before
1039
+ // initialization completes, and the ping gets its empty result, as
1040
+ // on a connected transport. Anything else it sends now is ignored.
1041
+ if ('method' in msg) {
1042
+ const request = msg;
1043
+ if (request.method === 'ping' && request.id != null) {
1044
+ transport.writeLine(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: {} }));
1045
+ }
1046
+ return;
1047
+ }
475
1048
  if (msg.id !== initId)
476
1049
  return;
477
1050
  cleanup();
478
1051
  if (msg.error) {
479
- reject(new Error(`MCPL server "${config.id}" initialize error: ${msg.error.message}`));
1052
+ // describeCause rather than the template's own conversion: the
1053
+ // server's `message` can be any JSON value, and one that has no
1054
+ // primitive form, such as {"toString":1}, would throw out of this
1055
+ // listener after cleanup() has cleared the timeout: an uncaught
1056
+ // exception, which ends a host that doesn't catch them, and a
1057
+ // handshake left to wait forever. It's the server's text in an
1058
+ // error's message, so it's bounded as errors.ts says.
1059
+ reject(msg.error.code === UNSUPPORTED_PROTOCOL_VERSION_CODE
1060
+ ? protocolVersionRejection(config.id, msg.error)
1061
+ : new Error(`MCPL server "${config.id}" initialize error: ${boundedForLog(describeCause(msg.error.message))}`));
480
1062
  return;
481
1063
  }
482
1064
  const result = msg.result;
1065
+ // The server's answer is the established revision. This engine
1066
+ // implements exactly one, so any other answer (or none) means the
1067
+ // two sides never agreed on what they're speaking.
1068
+ const returned = typeof result?.protocolVersion === 'string' ? result.protocolVersion : null;
1069
+ if (returned !== MCP_PROTOCOL_VERSION) {
1070
+ reject(new McplProtocolVersionError(`MCPL server "${config.id}" answered initialize with protocolVersion ` +
1071
+ `${returned === null ? '(none)' : JSON.stringify(returned)}; this client's legacy engine speaks only ${MCP_PROTOCOL_VERSION}`, 'mismatch', returned, null));
1072
+ return;
1073
+ }
483
1074
  const caps = result?.capabilities;
484
1075
  const experimental = caps?.experimental;
485
1076
  // §5.1: `tools` capability is the OUTER standard MCP member — the
@@ -488,11 +1079,19 @@ export class McplServerConnection extends EventEmitter {
488
1079
  resolve({
489
1080
  mcpl: experimental?.mcpl ?? null,
490
1081
  mcpToolsAdvertised: caps?.tools !== undefined,
1082
+ protocolVersion: returned,
491
1083
  });
492
1084
  };
493
1085
  const onClose = (info) => {
494
1086
  cleanup();
495
- reject(failed(`MCPL server "${config.id}" transport closed before handshake completed (${info.reason ?? 'unknown'})`));
1087
+ const code = transport.kind === 'websocket' && typeof info.code === 'number' ? `, code ${info.code}` : '';
1088
+ const failure = `MCPL server "${config.id}" transport closed before handshake completed (${closeReasonText(info)}${code})`;
1089
+ // The close comes at a stdio child's exit, which Node can emit before
1090
+ // it has read the last of the child's stderr, or when the host closes
1091
+ // the transport, which then ends the child. The tail is taken once
1092
+ // stderr has ended, waiting at most STDERR_END_WAIT_MS. A WebSocket
1093
+ // transport has no stderr to wait for.
1094
+ void transport.stderrEnded(STDERR_END_WAIT_MS).then(() => reject(failed(failure)));
496
1095
  };
497
1096
  const onError = (err) => {
498
1097
  cleanup();
@@ -502,6 +1101,7 @@ export class McplServerConnection extends EventEmitter {
502
1101
  cleanup();
503
1102
  reject(failed(`MCPL server "${config.id}" initialize handshake timed out`));
504
1103
  }, INITIALIZE_TIMEOUT_MS);
1104
+ signal?.addEventListener('abort', onAbort, { once: true });
505
1105
  transport.on('line', onLine);
506
1106
  transport.on('close', onClose);
507
1107
  transport.on('error', onError);
@@ -525,11 +1125,18 @@ export class McplServerConnection extends EventEmitter {
525
1125
  // listener of its own, in the continuation of this return, before any
526
1126
  // I/O callback can emit.
527
1127
  transport.off('error', logAfterSettled);
528
- return { transport, capabilities: scoped, droppedCapabilities: new Set(dropped), mcpToolsAdvertised };
1128
+ return { transport, capabilities: scoped, droppedCapabilities: new Set(dropped), mcpToolsAdvertised, protocolVersion };
529
1129
  }
530
1130
  catch (err) {
531
- // Never leak the child / socket if the handshake fails.
532
- await transport.close().catch(() => { });
1131
+ // Never leak the child / socket if the handshake fails. A cleanup that
1132
+ // can't confirm the child exited is part of the failure, never
1133
+ // swallowed: the launch may still be running, so the caller keeps it.
1134
+ try {
1135
+ await transport.close();
1136
+ }
1137
+ catch (cleanup) {
1138
+ throw new HandshakeCleanupError(errorOf(err), errorOf(cleanup), transport);
1139
+ }
533
1140
  throw err;
534
1141
  }
535
1142
  }
@@ -537,30 +1144,43 @@ export class McplServerConnection extends EventEmitter {
537
1144
  * Connect with reconnect support.
538
1145
  * When `config.reconnect` is true and the initial connection fails,
539
1146
  * resolves immediately with null capabilities and retries in the background.
1147
+ * A configuration this engine can't use, or a protocol-version verdict,
1148
+ * still throws: retrying can't change either. So does a failed launch that
1149
+ * can't be reaped, as in {@link connect}: no retry starts beside a child
1150
+ * that may still be running, and the {@link McplUnreapedLaunchError}
1151
+ * carries the connection that owns it, whose close() retries the reap.
540
1152
  * Adapted from Anarchid/agent-framework@mcpl-module-proto.
541
1153
  */
542
- static async connectWithReconnect(config, hostCapabilities) {
1154
+ static async connectWithReconnect(config, hostCapabilities, signal) {
1155
+ // Outside the retry path below: a configuration error is never retried.
1156
+ McplServerConnection.checkConfig(config);
543
1157
  try {
544
- return await McplServerConnection.connect(config, hostCapabilities);
1158
+ return await McplServerConnection.connectChecked(config, hostCapabilities, signal);
545
1159
  }
546
1160
  catch (error) {
547
- if (!config.reconnect) {
1161
+ // No common protocol revision is a configuration fact, not an outage:
1162
+ // a retry stub would restart the server forever for nothing. A connect
1163
+ // its caller ended (stop(), or a disconnect of its id) isn't retried
1164
+ // either, nor is a launch that couldn't be reaped.
1165
+ if (config.reconnect !== true || error instanceof McplProtocolVersionError || error instanceof McplUnreapedLaunchError || signal?.aborted) {
548
1166
  throw error;
549
1167
  }
550
- // Non-blocking start: create a disconnected stub that will reconnect in background
551
- console.error(`MCPL server "${config.id}" initial connect failed, will retry: ${error.message}${withStderr(error)}`);
552
- const stub = McplServerConnection.createDisconnectedStub(config, hostCapabilities);
553
1168
  // Surface the initial failure. The event is buffered by the emit()
554
1169
  // override until the host wires listeners and calls ready(), so it is
555
1170
  // not lost in the window before wireMcplEvents runs.
1171
+ // Non-blocking start: create a disconnected stub that will reconnect in background
1172
+ console.error(`MCPL server "${config.id}" initial connect failed, will retry: ${redactServerText(error.message, config)}${withStderr(error, (text) => redactServerText(text, config))}`);
1173
+ const stub = McplServerConnection.createDisconnectedStub(config, hostCapabilities);
556
1174
  const stderrTail = stderrTailOf(error);
557
1175
  stub.emit('connect-failed', { error: error.message, ...(stderrTail ? { stderrTail } : {}), attempt: 0 });
1176
+ stub.scheduleReconnect();
558
1177
  return stub;
559
1178
  }
560
1179
  }
561
1180
  /**
562
- * Create a disconnected stub connection that will reconnect in the background.
563
- * Used when initial connect fails and reconnect is enabled.
1181
+ * Create a disconnected stub connection, for a failed initial connect: one
1182
+ * that will reconnect in the background (its caller schedules that), or one
1183
+ * that owns a launch that couldn't be reaped.
564
1184
  * @internal
565
1185
  */
566
1186
  static createDisconnectedStub(config, hostCapabilities) {
@@ -570,14 +1190,31 @@ export class McplServerConnection extends EventEmitter {
570
1190
  stub.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
571
1191
  stub.config = config;
572
1192
  stub.hostCapabilities = hostCapabilities;
573
- stub.reconnectEnabled = true;
1193
+ stub.reconnectEnabled = config.reconnect === true;
574
1194
  stub.reconnectIntervalMs = config.reconnectIntervalMs ?? 5000;
575
1195
  stub.reconnectMaxIntervalMs = config.reconnectMaxIntervalMs ?? 300_000;
576
1196
  stub.reconnectAttempts = 1; // the initial connect already failed
577
- // Schedule background reconnect
578
- stub.scheduleReconnect();
579
1197
  return stub;
580
1198
  }
1199
+ /**
1200
+ * Keep a launch whose cleanup couldn't confirm its child exited, and halt
1201
+ * reconnecting: another launch could run beside one that's still alive.
1202
+ * close() retries the reap.
1203
+ */
1204
+ holdUnreaped(transport, failure) {
1205
+ this.reconnectEnabled = false;
1206
+ if (this.reconnectTimer) {
1207
+ clearTimeout(this.reconnectTimer);
1208
+ this.reconnectTimer = null;
1209
+ }
1210
+ if (!this.unreaped.some((held) => held.transport === transport))
1211
+ this.unreaped.push({ transport, failure });
1212
+ }
1213
+ /** The halt, as a reconnect event's failure text. */
1214
+ unreapedFailure() {
1215
+ const held = this.unreaped[0];
1216
+ return `the previous launch could not be reaped (${held?.failure.message ?? 'unknown'}); reconnecting is halted`;
1217
+ }
581
1218
  // ==========================================================================
582
1219
  // Outbound requests (return Promises)
583
1220
  // ==========================================================================
@@ -606,8 +1243,12 @@ export class McplServerConnection extends EventEmitter {
606
1243
  sendFeatureSetsUpdateRequest(params, timeoutMs = 15_000) {
607
1244
  return this.sendRequest(McplMethod.FeatureSetsUpdate, params, { timeoutMs });
608
1245
  }
609
- /** Send `inference/chunk` notification. */
610
- sendInferenceChunk(params) {
1246
+ /** Send `inference/chunk` notification, on the transport `epoch` names: a
1247
+ * chunk of a request from a transport that has closed goes nowhere, as
1248
+ * that request's answer does. */
1249
+ sendInferenceChunk(params, epoch = this.transportEpoch) {
1250
+ if (epoch !== this.transportEpoch)
1251
+ return;
611
1252
  this.sendNotification(McplMethod.InferenceChunk, params);
612
1253
  }
613
1254
  /** Send `inference/lifecycle` notification (§10.5). Best-effort. */
@@ -633,9 +1274,13 @@ export class McplServerConnection extends EventEmitter {
633
1274
  sendStateRollback(params) {
634
1275
  return this.sendRequest(McplMethod.StateRollback, params);
635
1276
  }
636
- /** Send `channels/open` request and await result. */
1277
+ /** Send `channels/open` request and await result. It keeps a deadline as
1278
+ * channels/publish does: a delivery opens a closed channel before it
1279
+ * publishes, so an open that is never answered would hold the delivery,
1280
+ * and the turn that waits for it, for good. */
637
1281
  sendChannelsOpen(params) {
638
- return this.sendRequest(McplMethod.ChannelsOpen, params);
1282
+ const timeoutMs = this.requestTimeoutMs > 0 ? this.requestTimeoutMs : this.publishDeadlineMs;
1283
+ return this.sendRequest(McplMethod.ChannelsOpen, params, { timeoutMs });
639
1284
  }
640
1285
  /** Send `channels/close` request and await result. */
641
1286
  sendChannelsClose(params) {
@@ -653,13 +1298,20 @@ export class McplServerConnection extends EventEmitter {
653
1298
  * Send `channels/publish`.
654
1299
  * May be sent as a request (if an ACK is desired) or notification.
655
1300
  * When `params.stream` is true, sends as a notification (no ACK).
1301
+ *
1302
+ * A request always has a deadline: the server's requestTimeoutMs, or
1303
+ * publishDeadlineMs when that is 0 (disabled). A publish is a side effect
1304
+ * whose outcome the resident is told and a held draft's attempt records,
1305
+ * so one that is never answered would hold both open for good. Its expiry
1306
+ * is a no-response, which the caller reads as unknown.
656
1307
  */
657
1308
  sendChannelsPublish(params) {
658
1309
  if (params.stream) {
659
1310
  this.sendNotification(McplMethod.ChannelsPublish, params);
660
1311
  return Promise.resolve();
661
1312
  }
662
- return this.sendRequest(McplMethod.ChannelsPublish, params);
1313
+ const timeoutMs = this.requestTimeoutMs > 0 ? this.requestTimeoutMs : this.publishDeadlineMs;
1314
+ return this.sendRequest(McplMethod.ChannelsPublish, params, { timeoutMs });
663
1315
  }
664
1316
  /** Send `channels/outgoing/chunk` notification. */
665
1317
  sendChannelsOutgoingChunk(params) {
@@ -685,9 +1337,45 @@ export class McplServerConnection extends EventEmitter {
685
1337
  // ==========================================================================
686
1338
  // Standard MCP methods
687
1339
  // ==========================================================================
688
- /** Send `tools/list` and return the server's tool definitions. */
689
- sendToolsList() {
690
- return this.sendRequest('tools/list', {});
1340
+ /**
1341
+ * Send `tools/list` and return the server's complete tool inventory,
1342
+ * following `nextCursor` across pages (2024-11-05 pagination). Cursors are
1343
+ * opaque strings, `''` included: only an absent or null `nextCursor` ends
1344
+ * the inventory (a null one is how some servers say "no more", and main
1345
+ * read it as a complete page). A page that isn't well formed (no `tools` array, a cursor
1346
+ * that isn't a string), or cursors that don't terminate (a repeat, or more
1347
+ * than {@link MAX_TOOL_LIST_PAGES} pages), are errors, never a partial or
1348
+ * empty inventory published as complete.
1349
+ */
1350
+ async sendToolsList() {
1351
+ const tools = [];
1352
+ const seen = new Set();
1353
+ let cursor;
1354
+ let first;
1355
+ for (let page = 0; page < MAX_TOOL_LIST_PAGES; page++) {
1356
+ const result = await this.sendRequest('tools/list', cursor === undefined ? {} : { cursor });
1357
+ const malformed = (why) => new Error(`MCPL server "${this.id}" sent a malformed tools/list page ${page + 1}: ${why}`);
1358
+ if (!result || typeof result !== 'object' || !Array.isArray(result.tools)) {
1359
+ throw malformed('no tools array');
1360
+ }
1361
+ first ??= result;
1362
+ tools.push(...result.tools);
1363
+ if (result.nextCursor === undefined || result.nextCursor === null) {
1364
+ // The first page's other fields are kept as they came, so a
1365
+ // single-page answer is returned exactly as before.
1366
+ const { nextCursor: _done, ...rest } = first;
1367
+ return { ...rest, tools };
1368
+ }
1369
+ if (typeof result.nextCursor !== 'string') {
1370
+ throw malformed(`nextCursor must be a string, got ${JSON.stringify(result.nextCursor)}`);
1371
+ }
1372
+ if (seen.has(result.nextCursor)) {
1373
+ throw new Error(`MCPL server "${this.id}" repeated tools/list cursor ${JSON.stringify(result.nextCursor)}`);
1374
+ }
1375
+ seen.add(result.nextCursor);
1376
+ cursor = result.nextCursor;
1377
+ }
1378
+ throw new Error(`MCPL server "${this.id}" tools/list did not finish within ${MAX_TOOL_LIST_PAGES} pages`);
691
1379
  }
692
1380
  /** Send `tools/call` and return the result. Optionally includes state/checkpoint for stateful tools. */
693
1381
  sendToolsCall(name, args, stateParams) {
@@ -716,43 +1404,61 @@ export class McplServerConnection extends EventEmitter {
716
1404
  // ==========================================================================
717
1405
  // Lifecycle
718
1406
  // ==========================================================================
719
- /** Close the connection: disable reconnect, kill the child process, and clean up resources. */
720
- async close() {
1407
+ /** The requests still waiting for an answer, for the operator's log line
1408
+ * when the connection goes: their methods and ids. */
1409
+ unansweredRequests() {
1410
+ const list = [...this.pendingRequests].map(([id, pending]) => `${pending.method} (id=${id})`).join(', ');
1411
+ return `${this.pendingRequests.size} request(s) unanswered: ${list}`;
1412
+ }
1413
+ /**
1414
+ * Close the connection for good: disable reconnect, kill the child
1415
+ * process, and clean up resources. At once, as before, reconnecting stops
1416
+ * and pending requests fail; a reconnect handshake in flight is aborted.
1417
+ *
1418
+ * Calls share one attempt: while it is pending, and once it has
1419
+ * succeeded, close() returns it. The attempt waits for the aborted
1420
+ * reconnect's own cleanup, closes the transport (its close is the reaping
1421
+ * verdict) and retries each launch an earlier cleanup couldn't reap. It
1422
+ * rejects while any child can't be confirmed exited, with that failure
1423
+ * itself, or an AggregateError when there are several. A failed attempt is
1424
+ * cleared, so the next close() retries what it couldn't reap. 'close' is
1425
+ * emitted once, when the teardown of a connection the host closed while
1426
+ * live completes.
1427
+ */
1428
+ close() {
721
1429
  // Disable reconnect before closing — explicit close means stop retrying
722
1430
  this.reconnectEnabled = false;
1431
+ this.closedByThisHost = true;
723
1432
  if (this.reconnectTimer) {
724
1433
  clearTimeout(this.reconnectTimer);
725
1434
  this.reconnectTimer = null;
726
1435
  }
1436
+ this.reconnecting?.abort.abort();
727
1437
  if (this.closePromise)
728
1438
  return this.closePromise;
729
- if (this.closed && !this.closeIncomplete && !this.reconnectPromise)
730
- return;
731
- this.closed = true;
732
- this.closeIncomplete = true;
733
- this.resetPolicyForTransportBoundary();
734
- // Reject all pending requests
735
- for (const [id, pending] of this.pendingRequests) {
736
- if (pending.timer)
737
- clearTimeout(pending.timer);
738
- pending.reject(new Error(`Connection to MCPL server "${this.id}" closed while awaiting response for ${pending.method} (id=${id})`));
1439
+ if (!this.closed) {
1440
+ this.closed = true;
1441
+ this.closeIncomplete = true;
1442
+ this.resetPolicyForTransportBoundary();
1443
+ // Reject all pending requests. Each message is the whole of what an
1444
+ // agent reads for a tools/call, and what follows a channel tool's own
1445
+ // words ("Failed to publish to channel: …") for the requests a channel
1446
+ // tool makes, so it names no method; the log line does. It doesn't say
1447
+ // whether this host reconnects: a restart closes the connection and then
1448
+ // connects again.
1449
+ if (this.pendingRequests.size > 0) {
1450
+ console.error(`MCPL server "${this.id}": this host closed its connection with ${this.unansweredRequests()}`);
1451
+ }
1452
+ for (const pending of this.pendingRequests.values()) {
1453
+ if (pending.timer)
1454
+ clearTimeout(pending.timer);
1455
+ pending.reject(new McplRequestError(`The "${this.id}" server didn't answer before this host closed its connection, so whether the request took effect is unknown. ` +
1456
+ 'Check its effect before trying again.', 'no-response'));
1457
+ }
1458
+ this.pendingRequests.clear();
1459
+ this.orphanedRequests.clear();
739
1460
  }
740
- this.pendingRequests.clear();
741
- this.orphanedRequests.clear();
742
- // Tear down the transport (kills the child / closes the socket). The
743
- // transport's own 'close' event is short-circuited by `this.closed` in
744
- // setupLifecycle, so this is the single source of the connection 'close'.
745
- const attempt = Promise.resolve().then(async () => {
746
- // An already opened handshake owns a transport too. It hands that
747
- // transport back without admission once reconnect has been disabled.
748
- // The registry bounds this wait; retry reuses this real close attempt.
749
- if (this.reconnectPromise)
750
- await this.reconnectPromise;
751
- if (this.transport)
752
- await this.transport.close();
753
- this.closeIncomplete = false;
754
- this.emit('close', null, null, this.closeInfoByHost());
755
- });
1461
+ const attempt = this.closeStep(this.transport, this.unreaped.map((held) => held.transport));
756
1462
  this.closePromise = attempt;
757
1463
  void attempt.catch(() => {
758
1464
  if (this.closePromise === attempt)
@@ -760,14 +1466,60 @@ export class McplServerConnection extends EventEmitter {
760
1466
  });
761
1467
  return attempt;
762
1468
  }
1469
+ /** One close attempt. `heldBefore` are the launches earlier cleanups
1470
+ * couldn't reap, which it retries. */
1471
+ async closeStep(transport, heldBefore) {
1472
+ // An aborted reconnect cleans up its own launch, holding it if it can't.
1473
+ // That cleanup was this attempt's try at it, so its failure is this
1474
+ // attempt's, not retried here.
1475
+ await this.reconnecting?.done;
1476
+ const heldMeanwhile = this.unreaped.filter((held) => !heldBefore.includes(held.transport));
1477
+ const failures = heldMeanwhile.map((held) => held.failure);
1478
+ const targets = [];
1479
+ for (const target of [transport, ...heldBefore]) {
1480
+ if (target && !targets.includes(target) && !heldMeanwhile.some((held) => held.transport === target))
1481
+ targets.push(target);
1482
+ }
1483
+ for (const target of targets) {
1484
+ try {
1485
+ // Kills the child / closes the socket.
1486
+ await target.close();
1487
+ this.unreaped = this.unreaped.filter((held) => held.transport !== target);
1488
+ }
1489
+ catch (error) {
1490
+ const failure = errorOf(error);
1491
+ failures.push(failure);
1492
+ if (!this.unreaped.some((held) => held.transport === target))
1493
+ this.unreaped.push({ transport: target, failure });
1494
+ }
1495
+ }
1496
+ if (failures.length === 1)
1497
+ throw failures[0];
1498
+ if (failures.length > 1) {
1499
+ throw new AggregateError(failures, `MCPL server "${this.id}" did not close cleanly: ${failures.map((f) => f.message).join('; ')}`);
1500
+ }
1501
+ // The transport's own 'close' event is short-circuited by `this.closed`
1502
+ // in setupLifecycle, so this is the single source of the connection
1503
+ // 'close' when the host tears down a live transport.
1504
+ if (this.closeIncomplete) {
1505
+ this.closeIncomplete = false;
1506
+ this.emit('close', null, null, this.closeInfoByHost());
1507
+ }
1508
+ }
763
1509
  /**
764
1510
  * Tear down a live transport after an awareness-accounting failure without
765
1511
  * disabling configured auto-reconnect. The normal transport close handler
766
1512
  * rejects requests, emits lifecycle state, and schedules the next bounded
767
- * reconnect attempt. With reconnect disabled this leaves the server closed.
1513
+ * reconnect attempt. With reconnect disabled the recycle ends the
1514
+ * connection for good, as close() does: 'close' comes once the child's
1515
+ * exit is confirmed, and a child it can't reap stays owned, holding the id.
768
1516
  */
769
1517
  async reconnectAfterFailure() {
770
1518
  this.pauseDataPlane();
1519
+ if (!this.reconnectEnabled) {
1520
+ await this.close();
1521
+ return;
1522
+ }
771
1523
  if (this.closed) {
772
1524
  this.scheduleReconnect();
773
1525
  return;
@@ -817,68 +1569,118 @@ export class McplServerConnection extends EventEmitter {
817
1569
  * Attempt to reconnect by opening a fresh transport and performing the
818
1570
  * handshake, then adopting that transport in place. Unlike the previous
819
1571
  * approach this builds no throwaway connection instance, so there are no
820
- * leaked listeners on a dead object.
1572
+ * leaked listeners on a dead object. The attempt is close()'s to abort,
1573
+ * and it settles only once its own launch is adopted, failed or cleaned up.
1574
+ * One attempt at a time: a request that arrives while one is in flight (a
1575
+ * recycle, say) starts nothing, since that attempt either adopts its
1576
+ * launch or schedules the next retry itself. A second attempt would take
1577
+ * close()'s ownership of the first (Iris-1827, room-293 #55410).
821
1578
  */
822
- attemptReconnect() {
823
- if (this.reconnectPromise)
824
- return this.reconnectPromise;
825
- const attempt = this.reconnectOnce();
826
- this.reconnectPromise = attempt;
827
- const clear = () => { if (this.reconnectPromise === attempt)
828
- this.reconnectPromise = null; };
829
- void attempt.then(clear, clear);
830
- return attempt;
831
- }
832
- async reconnectOnce() {
833
- // A queued recovery timer may outlive an earlier successful handshake.
834
- // Recheck disconnection before opening or replacing any owned transport.
835
- if (!this.closed || !this.reconnectEnabled || !this.config || !this.hostCapabilities)
1579
+ async attemptReconnect() {
1580
+ // A queued recovery timer may outlive an earlier successful handshake:
1581
+ // only a connection that is down reconnects, so it never replaces a live
1582
+ // transport.
1583
+ if (this.reconnecting || !this.closed || !this.reconnectEnabled || !this.config || !this.hostCapabilities)
836
1584
  return;
1585
+ const abort = new AbortController();
1586
+ const done = this.reconnectOnce(this.config, this.hostCapabilities, abort.signal);
1587
+ this.reconnecting = { abort, done };
1588
+ await done;
1589
+ if (this.reconnecting?.done === done)
1590
+ this.reconnecting = null;
1591
+ }
1592
+ /** One reconnect attempt. It never rejects: each outcome is an event, a
1593
+ * scheduled retry, or a launch held as unreaped. */
1594
+ async reconnectOnce(config, hostCapabilities, signal) {
837
1595
  // Ordinal of this attempt: 0 was the initial connect, so the Nth retry
838
1596
  // reports attempt N (reconnectAttempts counts failures so far).
839
1597
  const attempt = Math.max(1, this.reconnectAttempts);
840
- try {
841
- const { transport, capabilities, droppedCapabilities, mcpToolsAdvertised } = await McplServerConnection.handshake(this.config, this.hostCapabilities);
842
- if (!this.reconnectEnabled) {
843
- // The prior transport was disconnected before this retry began. Keep
844
- // the fresh, unadmitted transport owned by close(), including on a
845
- // failed transport cleanup, instead of reviving a stopped connection.
846
- // It isn't wired, and the handshake has removed its own listeners, so
847
- // an error it reports from here on, such as a malformed frame from a
848
- // WebSocket server while close() closes it, is logged here rather than
849
- // thrown where nothing catches it.
850
- transport.on('error', (err) => {
851
- console.error(`[mcpl] ${this.id}: a transport kept for close() reported an error: ${err.message}`);
852
- });
853
- this.transport = transport;
1598
+ // The previous launch must be gone before another starts. A transport
1599
+ // the host closed itself (reconnectAfterFailure) may still be reaping its
1600
+ // child; one whose child already exited resolves at once.
1601
+ const previous = this.transport;
1602
+ if (previous) {
1603
+ try {
1604
+ await previous.close();
1605
+ }
1606
+ catch (error) {
1607
+ this.holdUnreaped(previous, errorOf(error));
1608
+ if (!signal.aborted)
1609
+ this.emit('reconnect-failed', { error: this.unreapedFailure(), attempt, permanent: true });
854
1610
  return;
855
1611
  }
856
- // Close the data plane before the new transport can emit. The reconnect
857
- // lifecycle event bypasses buffering; its framework listener installs the
858
- // awareness barrier synchronously, then releases control traffic needed
859
- // to establish the server while data remains held.
860
- this.pauseDataPlane();
861
- this.resetPolicyForTransportBoundary();
862
- this.transport = transport;
863
- this.capabilities = capabilities;
864
- this.droppedCapabilities = droppedCapabilities;
865
- this.mcpToolsAdvertised = mcpToolsAdvertised;
866
- this.allowHostCommands = this.config?.allowHostCommands === true;
867
- this.closed = false;
868
- this.nextRequestId = 1;
869
- this.pendingRequests.clear();
870
- this.wireTransport(transport);
871
- console.error(`MCPL server "${this.id}" reconnected successfully`);
872
- this.reconnectAttempts = 0;
873
- this.emit('reconnect', { attempts: attempt });
1612
+ }
1613
+ if (signal.aborted)
1614
+ return;
1615
+ let established;
1616
+ try {
1617
+ established = await McplServerConnection.handshake(config, hostCapabilities, signal);
874
1618
  }
875
1619
  catch (error) {
876
- console.error(`MCPL server "${this.id}" reconnect failed: ${error.message}${withStderr(error)}`);
1620
+ const unreaped = error instanceof HandshakeCleanupError;
1621
+ if (unreaped)
1622
+ this.holdUnreaped(error.transport, error.cleanup);
1623
+ // Closed by the host meanwhile: not a failed attempt.
1624
+ if (signal.aborted)
1625
+ return;
1626
+ const failure = unreaped ? error.failure : errorOf(error);
1627
+ const redact = (text) => redactServerText(text, this.config ?? {});
1628
+ console.error(`MCPL server "${this.id}" reconnect failed: ${redact(failure.message)}${withStderr(failure, redact)}`);
877
1629
  this.reconnectAttempts = attempt + 1;
878
- const stderrTail = stderrTailOf(error);
879
- this.emit('reconnect-failed', { error: error.message, ...(stderrTail ? { stderrTail } : {}), attempt });
880
- this.scheduleReconnect();
1630
+ // No common protocol revision: every further attempt would restart the
1631
+ // server to hear the same answer. Stop retrying; the configuration is
1632
+ // what has to change. A launch that couldn't be reaped halts retrying
1633
+ // for good: close() can confirm its exit, but close() is terminal, so
1634
+ // nothing resumes this connection. Every other failure keeps the
1635
+ // backoff loop.
1636
+ if (error instanceof McplProtocolVersionError)
1637
+ this.reconnectEnabled = false;
1638
+ const permanent = unreaped || error instanceof McplProtocolVersionError;
1639
+ const message = unreaped ? `${failure.message}; ${this.unreapedFailure()}` : failure.message;
1640
+ const stderrTail = stderrTailOf(failure);
1641
+ this.emit('reconnect-failed', { error: message, ...(stderrTail ? { stderrTail } : {}), attempt, ...(permanent ? { permanent: true } : {}) });
1642
+ if (!permanent)
1643
+ this.scheduleReconnect();
1644
+ return;
1645
+ }
1646
+ const { transport, capabilities, droppedCapabilities, mcpToolsAdvertised, protocolVersion } = established;
1647
+ if (signal.aborted) {
1648
+ // Closed by the host while the handshake finished: this launch never
1649
+ // serves. close() is waiting on this attempt, so it hears the verdict.
1650
+ // The transport isn't wired, and the handshake has taken its own
1651
+ // listeners off, so an error it reports while it closes, such as a
1652
+ // malformed frame from a WebSocket server, is logged here rather than
1653
+ // thrown where nothing catches it.
1654
+ transport.on('error', (err) => {
1655
+ console.error(`[mcpl] ${this.id}: a transport closed before it served reported an error: ${redactServerText(err.message, config)}`);
1656
+ });
1657
+ await transport.close().catch((error) => this.holdUnreaped(transport, errorOf(error)));
1658
+ return;
881
1659
  }
1660
+ // Close both planes before the new transport can emit, as at initial
1661
+ // connect: its traffic is admitted against the grant, which this
1662
+ // boundary resets, so control traffic too (a server's channels/register
1663
+ // right after initialize) must wait for the new grant rather than be
1664
+ // refused against the empty one. The reconnect lifecycle event bypasses
1665
+ // buffering; its framework listener installs the awareness barrier,
1666
+ // re-establishes the grant (§5.3 policy responses are never buffered),
1667
+ // and then opens the planes.
1668
+ this.controlPlaneReady = false;
1669
+ this.dataPlaneReady = false;
1670
+ this.resetPolicyForTransportBoundary();
1671
+ this.transport = transport;
1672
+ this.capabilities = capabilities;
1673
+ this.droppedCapabilities = droppedCapabilities;
1674
+ this.mcpToolsAdvertised = mcpToolsAdvertised;
1675
+ this.protocolVersion = protocolVersion;
1676
+ this.allowHostCommands = this.config?.allowHostCommands === true;
1677
+ this.closed = false;
1678
+ this.nextRequestId = 1;
1679
+ this.pendingRequests.clear();
1680
+ this.wireTransport(transport);
1681
+ console.error(`MCPL server "${this.id}" reconnected successfully`);
1682
+ this.reconnectAttempts = 0;
1683
+ this.emit('reconnect', { attempts: attempt });
882
1684
  }
883
1685
  // ==========================================================================
884
1686
  // Private: message sending
@@ -896,7 +1698,29 @@ export class McplServerConnection extends EventEmitter {
896
1698
  */
897
1699
  sendRequest(method, params, options = {}) {
898
1700
  if (this.closed) {
899
- return Promise.reject(new Error(`Cannot send request: connection to "${this.id}" is closed`));
1701
+ // After this host's own close, which a restart makes before connecting
1702
+ // again and a removal makes for good, only that this host closed it
1703
+ // holds, and one next step holds for both: calling again reaches the
1704
+ // restarted server, or fails where it's gone.
1705
+ return Promise.reject(new McplRequestError(this.closedByThisHost
1706
+ ? `The request wasn't sent: this host closed its connection to the "${this.id}" server, so nothing ran. ` +
1707
+ 'If calling it again fails, ask whoever runs this host.'
1708
+ : `The request wasn't sent: the connection to the "${this.id}" server is closed, so nothing ran. ` +
1709
+ (this.willReconnect
1710
+ ? 'This host is reconnecting to it; try again later.'
1711
+ : "This host won't reconnect to it on its own. If you need it, ask whoever runs this host."), 'not-sent'));
1712
+ }
1713
+ // Once the child's stdin or stdout has failed, the transport is ending
1714
+ // it and writes nothing more to it, so a request is refused here as not
1715
+ // sent rather than left to wait out its timeout. The method and the
1716
+ // pipe's error are the operator's, so they go to the log.
1717
+ const pipeFailure = this.transport?.pipeFailure;
1718
+ if (pipeFailure) {
1719
+ console.error(`MCPL server "${this.id}": ${method} not sent, since its stdio pipe failed: ${logDetail(pipeFailure.message)}`);
1720
+ return Promise.reject(new McplRequestError(`The request wasn't sent: this host can no longer write to the "${this.id}" server, so nothing ran. ` +
1721
+ (this.willReconnect
1722
+ ? 'This host is reconnecting to it; try again later.'
1723
+ : "This host won't reconnect to it on its own. If you need it, ask whoever runs this host."), 'not-sent'));
900
1724
  }
901
1725
  const id = this.nextRequestId++;
902
1726
  const request = {
@@ -912,22 +1736,48 @@ export class McplServerConnection extends EventEmitter {
912
1736
  pending.timer = setTimeout(() => {
913
1737
  this.pendingRequests.delete(id);
914
1738
  if (options.surfaceOrphanedResponse) {
915
- this.orphanedRequests.set(id, { method, ...(method === 'push/render' ? { renderParams: { featureSet: params.featureSet, key: params.key, eventId: params.eventId } } : {}) });
1739
+ this.orphanedRequests.set(id, {
1740
+ method,
1741
+ ...(method === 'push/render'
1742
+ ? {
1743
+ renderParams: {
1744
+ featureSet: params.featureSet,
1745
+ key: params.key,
1746
+ eventId: params.eventId,
1747
+ ...(typeof params.channelId === 'string' ? { channelId: params.channelId } : {}),
1748
+ },
1749
+ }
1750
+ : {}),
1751
+ });
916
1752
  if (this.orphanedRequests.size > 4096)
917
1753
  this.orphanedRequests.delete(this.orphanedRequests.keys().next().value);
918
1754
  }
919
- reject(new Error(`MCPL server "${this.id}" did not respond to ${method} (id=${id}) ` +
920
- `within ${timeoutMs}ms — the server may be hung. The response ` +
921
- `outcome is unknown: the request was abandoned locally but was not ` +
922
- `cancelled, so the tool may still have completed server-side; verify ` +
923
- `state before retrying; a blind retry of a stateful/side-effecting ` +
924
- `tool may duplicate it.`));
1755
+ // The method and id are the operator's, so they go to the log.
1756
+ console.error(`MCPL server "${this.id}" did not respond to ${method} (id=${id}) within ${timeoutMs}ms; ` +
1757
+ 'the request was abandoned here, not cancelled');
1758
+ const seconds = timeoutMs / 1000;
1759
+ reject(new McplRequestError(`The "${this.id}" server didn't answer within ${seconds} second${seconds === 1 ? '' : 's'}, ` +
1760
+ 'so whether the request took effect is unknown: ' +
1761
+ 'it was abandoned here, not cancelled, and may still complete on the server. ' +
1762
+ 'Check its effect before trying again, since repeating something that changes state may do it twice.', 'no-response'));
925
1763
  }, timeoutMs);
926
1764
  // Don't hold the event loop open for the watchdog alone.
927
1765
  pending.timer.unref?.();
928
1766
  }
929
1767
  this.pendingRequests.set(id, pending);
930
- this.transport.writeLine(JSON.stringify(request));
1768
+ try {
1769
+ this.transport.writeLine(JSON.stringify(request));
1770
+ }
1771
+ catch (err) {
1772
+ // A synchronous write rejection sent no request. Remove its pending
1773
+ // state and watchdog before reporting that it is safe to retry. The
1774
+ // method, id and the transport's words are the operator's.
1775
+ this.pendingRequests.delete(id);
1776
+ if (pending.timer)
1777
+ clearTimeout(pending.timer);
1778
+ console.error(`MCPL server "${this.id}": writing ${method} (id=${id}) failed: ${logDetail(err instanceof Error ? err.message : err)}`);
1779
+ reject(new McplRequestError(`The request wasn't sent: this host couldn't write it to the "${this.id}" server, so nothing ran. Try again later.`, 'not-sent'));
1780
+ }
931
1781
  });
932
1782
  }
933
1783
  /**
@@ -1011,12 +1861,17 @@ export class McplServerConnection extends EventEmitter {
1011
1861
  this.setupLifecycle(transport);
1012
1862
  // Surface transport diagnostics (stdio child stderr) as 'stderr' events,
1013
1863
  // preserving the `{ line }` payload existing consumers expect.
1014
- transport.on('stderr', (line) => this.emit('stderr', { line }));
1864
+ // Through the same containment boundary as every inbound event, so a
1865
+ // stderr listener that throws is reported, not thrown into the transport.
1866
+ transport.on('stderr', (line) => this.dispatchInboundEvent('stderr', { line }));
1015
1867
  }
1016
1868
  /**
1017
1869
  * Wire up the transport's line stream to route incoming JSON-RPC messages.
1018
1870
  */
1019
1871
  setupMessageRouting(transport) {
1872
+ // The boundary this transport serves within: a line read once it has
1873
+ // passed is the dead process's (afterBoundary).
1874
+ const epoch = this.transportEpoch;
1020
1875
  transport.on('line', (line) => {
1021
1876
  let msg;
1022
1877
  try {
@@ -1026,63 +1881,191 @@ export class McplServerConnection extends EventEmitter {
1026
1881
  // Ignore non-JSON lines
1027
1882
  return;
1028
1883
  }
1029
- // Is this a response to one of our outbound requests?
1030
- if ('id' in msg && msg.id != null && !('method' in msg)) {
1031
- this.handleResponse(msg);
1884
+ // JSON-RPC messages must be objects. Ignore unrelated stdout/log values
1885
+ // before property access, just as malformed JSON lines are ignored.
1886
+ if (msg === null || typeof msg !== 'object' || Array.isArray(msg))
1032
1887
  return;
1888
+ try {
1889
+ this.dispatchMessage(msg, epoch);
1033
1890
  }
1034
- // It is an inbound request or notification from the server
1035
- const request = msg;
1036
- const eventName = McplServerConnection.METHOD_TO_EVENT[request.method];
1037
- // §7 is removed in 0.5.0: scope/elevate is no longer a method. Answer
1038
- // rather than hang (§6.6 — a method that will never be answered MUST
1039
- // return an error).
1040
- // featureSets/changed is REMOVED in 0.5.0 (§6.7 — it carried a
1041
- // server-authored change payload, the self-attestation defect; the
1042
- // need is met by §17's host-diffed manifest fetch). It was still
1043
- // routed, control-plane classified, and mutating declarations — a
1044
- // live ungated self-attestation path (PR #79 review blocker 4).
1045
- if (request.method === McplMethod.FeatureSetsChanged) {
1046
- if (request.id != null) {
1047
- this.sendErrorResponse(request.id, -32601, 'featureSets/changed removed in MCPL 0.5.0 (superseded by mcpl/manifestChanged, SPEC §17)');
1048
- }
1049
- else {
1050
- console.error(`[mcpl] ${this.id}: discarded featureSets/changed — removed in 0.5.0 (§6.7/§17)`);
1051
- }
1052
- return;
1891
+ catch (error) {
1892
+ const request = msg;
1893
+ this.reportInboundFailure(error, {
1894
+ ...(typeof request.method === 'string' ? { event: request.method } : {}),
1895
+ ...(typeof request.id === 'string' || typeof request.id === 'number' ? { requestId: request.id } : {}),
1896
+ });
1053
1897
  }
1054
- if (request.method === McplMethod.ScopeElevate) {
1055
- if (request.id != null) {
1056
- this.sendErrorResponse(request.id, -32601, 'scope/elevate removed in MCPL 0.5.0 (SPEC §7)');
1057
- }
1058
- return;
1898
+ });
1899
+ }
1900
+ /** Contain synchronous dispatch failures; async RPC handlers own their promises. */
1901
+ dispatchMessage(msg, epoch) {
1902
+ // Is this a response to one of our outbound requests?
1903
+ if ('id' in msg && msg.id != null && !('method' in msg)) {
1904
+ this.handleResponse(msg, epoch !== this.transportEpoch);
1905
+ return;
1906
+ }
1907
+ // It is an inbound request or notification from the server
1908
+ const request = msg;
1909
+ // Read after its transport's boundary, it is the dead process's: the
1910
+ // boundary's rule applies (afterBoundary).
1911
+ if (epoch !== this.transportEpoch) {
1912
+ this.afterBoundary(request, epoch);
1913
+ return;
1914
+ }
1915
+ // MCP's ping, which either side may send: its receiver answers promptly
1916
+ // with an empty result. It's answered as it arrives, before admission and
1917
+ // buffering, since it asks only whether the connection is alive, and a
1918
+ // paused plane mustn't hold that answer. A ping sent as a notification
1919
+ // has nothing to answer. The handshake answers one too.
1920
+ if (request.method === 'ping') {
1921
+ if (request.id != null)
1922
+ this.sendResponse(request.id, {});
1923
+ return;
1924
+ }
1925
+ const eventName = typeof request.method === 'string' && Object.hasOwn(McplServerConnection.METHOD_TO_EVENT, request.method)
1926
+ ? McplServerConnection.METHOD_TO_EVENT[request.method] : undefined;
1927
+ // §7 is removed in 0.5.0: scope/elevate is no longer a method. Answer
1928
+ // rather than hang (§6.6 — a method that will never be answered MUST
1929
+ // return an error).
1930
+ // featureSets/changed is REMOVED in 0.5.0 (§6.7 — it carried a
1931
+ // server-authored change payload, the self-attestation defect; the
1932
+ // need is met by §17's host-diffed manifest fetch). It was still
1933
+ // routed, control-plane classified, and mutating declarations — a
1934
+ // live ungated self-attestation path (PR #79 review blocker 4).
1935
+ if (request.method === McplMethod.FeatureSetsChanged) {
1936
+ if (request.id != null) {
1937
+ this.sendErrorResponse(request.id, -32601, 'featureSets/changed removed in MCPL 0.5.0 (superseded by mcpl/manifestChanged, SPEC §17)');
1059
1938
  }
1060
- // Positive-grant enforcement (§5.4, §14.1) happens at ADMISSION, in
1061
- // the emit() override below — the one choke-point that live routing
1062
- // and staged-buffer flushes share. See the comment there for why
1063
- // rejecting here, at line receipt, was wrong for staged connections.
1064
- if (eventName) {
1065
- // Emit the typed event with params and (for requests) a respond callback
1066
- if (request.id != null) {
1067
- // Server expects a response — provide a respond helper
1068
- this.emit(eventName, request.params, {
1069
- id: request.id,
1070
- respond: (result) => this.sendResponse(request.id, result),
1071
- respondError: (code, message, data) => this.sendErrorResponse(request.id, code, message, data),
1072
- });
1073
- }
1074
- else {
1075
- // Notification — no response expected
1076
- this.emit(eventName, request.params);
1077
- }
1939
+ else {
1940
+ console.error(`[mcpl] ${this.id}: discarded featureSets/changed — removed in 0.5.0 (§6.7/§17)`);
1078
1941
  }
1079
- });
1942
+ return;
1943
+ }
1944
+ if (request.method === McplMethod.ScopeElevate) {
1945
+ if (request.id != null) {
1946
+ this.sendErrorResponse(request.id, -32601, 'scope/elevate removed in MCPL 0.5.0 (SPEC §7)');
1947
+ }
1948
+ return;
1949
+ }
1950
+ // Positive-grant enforcement (§5.4, §14.1) happens at ADMISSION, in
1951
+ // the emit() override below — the one choke-point that live routing
1952
+ // and staged-buffer flushes share. See the comment there for why
1953
+ // rejecting here, at line receipt, was wrong for staged connections.
1954
+ if (!eventName) {
1955
+ // §6.6: a request that will never be answered MUST get an error, so a
1956
+ // request (it has an id) whose method isn't a string, or isn't one this
1957
+ // host handles, is answered -32601. A notification nobody handles is
1958
+ // ignored, as JSON-RPC asks.
1959
+ if (request.id != null) {
1960
+ this.sendErrorResponse(request.id, -32601, 'Method not found');
1961
+ console.error(`[mcpl] ${this.id}: answered -32601 to a request for ${describeMethod(request.method)}, which this host doesn't handle`);
1962
+ }
1963
+ return;
1964
+ }
1965
+ // Emit the typed event with params and (for requests) a respond callback
1966
+ if (request.id != null) {
1967
+ // Server expects a response — provide a respond helper
1968
+ this.dispatchInboundEvent(eventName, request.params, this.responderFor(request.id, epoch));
1969
+ }
1970
+ else {
1971
+ // Notification — no response expected
1972
+ this.dispatchInboundEvent(eventName, request.params);
1973
+ }
1974
+ }
1975
+ /**
1976
+ * A request's responder, bound to the transport it came on (`epoch`): an
1977
+ * answer, or a streamed inference's chunk, sent after that transport has
1978
+ * closed reaches nothing, never a server that didn't ask it.
1979
+ */
1980
+ responderFor(id, epoch) {
1981
+ return {
1982
+ id,
1983
+ respond: (result) => this.sendResponse(id, result, epoch),
1984
+ respondError: (code, message, data) => this.sendErrorResponse(id, code, message, data, epoch),
1985
+ sendChunk: (params) => this.sendInferenceChunk(params, epoch),
1986
+ };
1987
+ }
1988
+ /**
1989
+ * A request or notification read from a transport after its boundary has
1990
+ * passed: the boundary's rule (resetPolicyForTransportBoundary). An
1991
+ * incoming message or push is held as kept, for the next transport's
1992
+ * grant, and any other is dropped and logged. (A response read then goes to
1993
+ * handleResponse, flagged late.) A stdio
1994
+ * server's transport closes on the child's 'exit', when its stdout may
1995
+ * still be open, as it is while a process the server started holds it, and
1996
+ * it is read until the next reconnect attempt closes its reader. No
1997
+ * transport serves meanwhile, so what is held waits for the next grant
1998
+ * (flushHeld). A connection closed for good, which won't reconnect, reads
1999
+ * it while its writer lives, with no grant to come, so it drops and logs
2000
+ * all of it; hold's bounds cap what any other keeps.
2001
+ */
2002
+ afterBoundary(request, epoch) {
2003
+ const event = typeof request.method === 'string' && Object.hasOwn(McplServerConnection.METHOD_TO_EVENT, request.method)
2004
+ ? McplServerConnection.METHOD_TO_EVENT[request.method] : undefined;
2005
+ if (this.closedForGood || event === undefined || !McplServerConnection.WORLD_EVENTS.has(event)) {
2006
+ console.error(`[mcpl] ${this.id}: dropped a message read after its transport closed` +
2007
+ `${this.closedForGood ? ', with no transport to come' : ''}: ${describeMethod(request.method)}`);
2008
+ return;
2009
+ }
2010
+ this.hold(event, request.id != null ? [request.params, this.responderFor(request.id, epoch)] : [request.params], true);
2011
+ }
2012
+ /** Live delivery and delayed plane flushes share the same containment boundary. */
2013
+ dispatchInboundEvent(event, ...args) {
2014
+ try {
2015
+ this.emit(event, ...args);
2016
+ }
2017
+ catch (error) {
2018
+ const requestId = args[1]?.id;
2019
+ this.reportInboundFailure(error, {
2020
+ event,
2021
+ ...(typeof requestId === 'string' || typeof requestId === 'number' ? { requestId } : {}),
2022
+ });
2023
+ }
2024
+ }
2025
+ /** Report a failure the containment boundary caught, naming the event (or
2026
+ * method) and request it was dispatching, and what failed. The message is
2027
+ * what the framework's 'error' listener traces. */
2028
+ reportInboundFailure(cause, context = {}) {
2029
+ const where = (show) => (context.event === undefined ? '' : ` for ${show(context.event)}`) +
2030
+ (context.requestId === undefined ? '' : ` (request ${show(String(context.requestId))})`);
2031
+ // The error's message is what the 'error' listener traces, as text a
2032
+ // reader parses, so it is bounded rather than escaped (boundedForLog);
2033
+ // the log line prints the same through log-value.
2034
+ const error = new Error(`MCPL server "${this.id}" inbound dispatch failed${where(boundedForLog)}: ${boundedForLog(describeCause(cause))}`, { cause });
2035
+ console.error(`[mcpl] ${this.id}: inbound dispatch failed${where(logValue)}: ${logDetail(describeCause(cause))}`);
2036
+ this.emitToErrorListeners(error);
2037
+ }
2038
+ /**
2039
+ * Give an error to the connection's 'error' listeners, and say whether it
2040
+ * has any. EventEmitter throws an 'error' that nothing listens for, and
2041
+ * from a transport's callback, where these errors arise, nothing catches
2042
+ * that throw, so it ends the process. A bare connection has no listener:
2043
+ * the class is exported, and a probe, a playtest or a harness often attaches
2044
+ * none. So with no listener, the error is the caller's to log, unless it
2045
+ * already has. A listener that throws is caught and logged, with the error
2046
+ * it was handling.
2047
+ */
2048
+ emitToErrorListeners(error) {
2049
+ if (this.listenerCount('error') === 0)
2050
+ return false;
2051
+ try {
2052
+ this.emit('error', error);
2053
+ }
2054
+ catch (listenerError) {
2055
+ console.error(`[mcpl] ${this.id}: an 'error' listener failed: ${logDetail(describeCause(listenerError))}` +
2056
+ `, handling ${logDetail(error.message)}`);
2057
+ }
2058
+ return true;
1080
2059
  }
1081
2060
  /**
1082
2061
  * Handle a JSON-RPC response by resolving/rejecting the corresponding pending request.
1083
2062
  */
1084
- handleResponse(response) {
1085
- const pending = this.pendingRequests.get(response.id);
2063
+ handleResponse(response, late = false) {
2064
+ // One read after its transport's boundary answers a request the close
2065
+ // ended. It's matched against none of this connection's request records,
2066
+ // since the next transport numbers its requests afresh, and goes the way
2067
+ // of any late answer: a stateful one is surfaced as an orphaned response.
2068
+ const pending = late ? undefined : this.pendingRequests.get(response.id);
1086
2069
  if (!pending) {
1087
2070
  // Orphaned response — the request already timed out (or was settled) and
1088
2071
  // was removed from pendingRequests. A late STATEFUL result must be
@@ -1090,12 +2073,20 @@ export class McplServerConnection extends EventEmitter {
1090
2073
  // Selected side-effecting calls are also surfaced even without state so
1091
2074
  // their outcome-unknown receipt remains visible. We cannot safely
1092
2075
  // re-inject either result because the original dispatch context is gone.
1093
- const orphaned = this.orphanedRequests.get(response.id);
1094
- this.orphanedRequests.delete(response.id);
1095
- if (orphaned?.renderParams) {
1096
- this.emit('orphaned-render-response', { params: orphaned.renderParams, result: response.result, error: response.error });
1097
- return; // audit only: a timeout fallback can never be replaced
1098
- }
2076
+ const orphaned = late ? undefined : this.orphanedRequests.get(response.id);
2077
+ if (!late)
2078
+ this.orphanedRequests.delete(response.id);
2079
+ // A push/render answered after its timeout (RFC-006 §5.3) is reported
2080
+ // with the render's featureSet, key, eventId and, for a channel-scoped
2081
+ // subject, channelId, kept from its request when the timeout fired, so
2082
+ // its discarded answer can be recorded beside its batch's other
2083
+ // coalescing records (§11). It is only reported: the fallback the
2084
+ // timeout materialized stands (§5.4 rule 3). A late JSON-RPC error is
2085
+ // passed on as `error`, so its record can be told from a late result's,
2086
+ // and so is one beside a result, which JSON-RPC forbids: the live path
2087
+ // rejects such an answer as an error.
2088
+ const render = orphaned?.renderParams ?? {};
2089
+ const failure = response.error ? { error: { code: response.error.code, message: response.error.message } } : {};
1099
2090
  const result = response.result;
1100
2091
  if (result && typeof result === 'object') {
1101
2092
  const r = result;
@@ -1105,6 +2096,8 @@ export class McplServerConnection extends EventEmitter {
1105
2096
  method: orphaned?.method,
1106
2097
  hadState: r.state !== undefined,
1107
2098
  hadCheckpoint: r.checkpoint !== undefined,
2099
+ ...render,
2100
+ ...failure,
1108
2101
  });
1109
2102
  }
1110
2103
  }
@@ -1114,6 +2107,8 @@ export class McplServerConnection extends EventEmitter {
1114
2107
  method: orphaned.method,
1115
2108
  hadState: false,
1116
2109
  hadCheckpoint: false,
2110
+ ...render,
2111
+ ...failure,
1117
2112
  });
1118
2113
  }
1119
2114
  return; // Orphaned response — ignore
@@ -1122,7 +2117,27 @@ export class McplServerConnection extends EventEmitter {
1122
2117
  if (pending.timer)
1123
2118
  clearTimeout(pending.timer);
1124
2119
  if (response.error) {
1125
- pending.reject(new Error(`MCPL server "${this.id}" returned error for ${pending.method}: [${response.error.code}] ${response.error.message}`));
2120
+ // The server's own words, after a word on whose they are, bounded as
2121
+ // errors.ts says, since they're the server's text. Only a string is
2122
+ // words: a message of any other JSON value reads as none. The method,
2123
+ // id and JSON-RPC code are the operator's, so they go to the log, which
2124
+ // prints any value a server can send without throwing (logValue). The
2125
+ // caller's text keeps the server's words unredacted, as a JSON-RPC
2126
+ // answer passes (redactServerText's scope); the log is a record of the
2127
+ // server's text, so the message is redacted there, whatever JSON value
2128
+ // it is, as every other line this connection writes is.
2129
+ const said = typeof response.error.message === 'string' ? boundedForLog(response.error.message.trim()) : '';
2130
+ // The call is settled before its log line: its entry is gone and its
2131
+ // timer cleared, so nothing else would settle it, and a host's logger
2132
+ // can throw. The dispatch's containment reports such a throw. (A
2133
+ // configuration that can't be read withholds the message rather than
2134
+ // throw: redactServerText.)
2135
+ pending.reject(new McplRequestError(said
2136
+ ? `The "${this.id}" server answered with an error: ${said}`
2137
+ : `The "${this.id}" server answered with an error without saying what went wrong, so whether the request took effect is unknown. ` +
2138
+ 'Check its effect before trying again.', 'error-response', response.error.data));
2139
+ console.error(`MCPL server "${this.id}" answered ${pending.method} (id=${response.id}) with error ${logValue(response.error.code)}: ` +
2140
+ logValueRedacted(response.error.message, (text) => redactServerText(text, this.config ?? {})));
1126
2141
  }
1127
2142
  else {
1128
2143
  pending.resolve(response.result);
@@ -1131,17 +2146,30 @@ export class McplServerConnection extends EventEmitter {
1131
2146
  /**
1132
2147
  * Send a successful JSON-RPC response back to the server.
1133
2148
  */
1134
- sendResponse(id, result) {
1135
- if (this.closed)
2149
+ sendResponse(id, result, epoch = this.transportEpoch) {
2150
+ // A closed connection's epoch is past every request's, so a refusal
2151
+ // while it is closed is logged too.
2152
+ if (!this.sameTransport(id, epoch) || this.closed)
1136
2153
  return;
1137
2154
  const response = { jsonrpc: '2.0', id, result };
1138
2155
  this.transport?.writeLine(JSON.stringify(response));
1139
2156
  }
2157
+ /** Whether a response to a request received on `epoch` may go out now.
2158
+ * The id is the server's, so it's logged as a field; `answer`, which the
2159
+ * caller has formatted for the log, ends the line. */
2160
+ sameTransport(id, epoch, answer) {
2161
+ if (epoch === this.transportEpoch)
2162
+ return true;
2163
+ console.error(`[mcpl] ${this.id}: not answering request ${logValue(id)}: the transport it came on has closed${answer ? `, with ${answer}` : ''}`);
2164
+ return false;
2165
+ }
1140
2166
  /**
1141
2167
  * Send a JSON-RPC error response back to the server.
1142
2168
  */
1143
- sendErrorResponse(id, code, message, data) {
1144
- if (this.closed)
2169
+ sendErrorResponse(id, code, message, data, epoch = this.transportEpoch) {
2170
+ // A refusal that can't be answered still says what it refused: a held
2171
+ // message admitted under the next transport's grant is refused here.
2172
+ if (!this.sameTransport(id, epoch, `error ${code} ${logDetail(message)}`) || this.closed)
1145
2173
  return;
1146
2174
  const response = {
1147
2175
  jsonrpc: '2.0',
@@ -1161,26 +2189,41 @@ export class McplServerConnection extends EventEmitter {
1161
2189
  setupLifecycle(transport) {
1162
2190
  this.hostCloseRequested = false;
1163
2191
  transport.on('error', (err) => {
1164
- this.emit('error', new Error(`MCPL server "${this.id}" transport error: ${err.message}`));
2192
+ const error = new Error(`MCPL server "${this.id}" transport error: ${err.message}`);
2193
+ if (!this.emitToErrorListeners(error))
2194
+ console.error(`[mcpl] ${this.id}: transport error: ${logDetail(err.message)}`);
1165
2195
  });
1166
- transport.on('close', (info) => {
2196
+ const lost = (info) => {
1167
2197
  if (!this.closed) {
1168
2198
  this.closed = true;
1169
2199
  this.resetPolicyForTransportBoundary();
1170
- // Reject all pending requests
1171
- for (const [id, pending] of this.pendingRequests) {
2200
+ // A teardown the host asked for via reconnectAfterFailure() arrives
2201
+ // here too; it's reported as such below.
2202
+ const byHost = this.hostCloseRequested;
2203
+ this.hostCloseRequested = false;
2204
+ // Reject all pending requests. The words hold whether the server
2205
+ // dropped or this host closed the transport to recover; how it closed
2206
+ // is the operator's, so it goes to the log, not into the messages.
2207
+ if (this.pendingRequests.size > 0) {
2208
+ // Whatever the host asked for, the close's own details are kept, so
2209
+ // a drop that races a recycle loses nothing.
2210
+ const how = `${byHost ? 'closed by this host, ' : ''}` +
2211
+ `code=${info.code ?? 'n/a'}, signal=${info.signal ?? 'n/a'}, reason=${closeReasonText(info)}`;
2212
+ console.error(`MCPL server "${this.id}" disconnected (${how}) with ${this.unansweredRequests()}`);
2213
+ }
2214
+ for (const pending of this.pendingRequests.values()) {
1172
2215
  if (pending.timer)
1173
2216
  clearTimeout(pending.timer);
1174
- pending.reject(new Error(`MCPL server "${this.id}" disconnected unexpectedly (code=${info.code ?? 'n/a'}, signal=${info.signal ?? 'n/a'}, reason=${info.reason ?? 'unknown'}) while awaiting ${pending.method} (id=${id})`));
2217
+ pending.reject(new McplRequestError(`The connection to the "${this.id}" server closed before it answered, so whether the request took effect is unknown. ` +
2218
+ (this.reconnectEnabled
2219
+ ? 'Check its effect before trying again; this host is reconnecting to it.'
2220
+ : "This host won't reconnect to it on its own. If you need it, ask whoever runs this host."), 'no-response'));
1175
2221
  }
1176
2222
  this.pendingRequests.clear();
1177
2223
  this.orphanedRequests.clear();
1178
2224
  // Forward the transport's close info so listeners can tell a server
1179
2225
  // that refused the credential (WebSocket 4001) from a crash, and act
1180
- // before the reconnect below redials. A teardown the host asked for
1181
- // via reconnectAfterFailure() arrives here too; report it as such.
1182
- const byHost = this.hostCloseRequested;
1183
- this.hostCloseRequested = false;
2226
+ // before the reconnect below redials.
1184
2227
  const closeInfo = {
1185
2228
  transport: transport.kind,
1186
2229
  code: info.code ?? null,
@@ -1195,6 +2238,22 @@ export class McplServerConnection extends EventEmitter {
1195
2238
  this.scheduleReconnect();
1196
2239
  }
1197
2240
  }
2241
+ };
2242
+ transport.on('close', lost);
2243
+ // A stdio child whose stdin or stdout failed, and that outlived SIGKILL
2244
+ // too: the transport stays open, owning the child. The connection holds
2245
+ // it as a launch it can't reap, as a reconnect does, which halts
2246
+ // reconnecting, and is lost as on a close, with the permanent failure
2247
+ // that raises the alert and takes its tools off the list. Its id stays
2248
+ // held until close() confirms the exit. A close the host has begun
2249
+ // reports the failure itself. During a handshake nothing listens yet:
2250
+ // the handshake's own cleanup holds the launch, once the handshake fails.
2251
+ transport.on('unreaped', (failure) => {
2252
+ if (this.closed || this.transport !== transport)
2253
+ return;
2254
+ this.holdUnreaped(transport, failure);
2255
+ lost({ reason: `its stdio pipe failed with ${transport.pipeFailure?.message ?? 'an error'}, and its child outlived SIGKILL` });
2256
+ this.emit('reconnect-failed', { error: this.unreapedFailure(), attempt: Math.max(1, this.reconnectAttempts), permanent: true });
1198
2257
  });
1199
2258
  }
1200
2259
  }