@animalabs/agent-framework 0.32.0 → 0.34.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 (368) hide show
  1. package/README.md +340 -22
  2. package/dist/src/admission/admit.d.ts +18 -0
  3. package/dist/src/admission/admit.d.ts.map +1 -1
  4. package/dist/src/admission/admit.js +16 -3
  5. package/dist/src/admission/admit.js.map +1 -1
  6. package/dist/src/agent.d.ts +208 -80
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +345 -113
  9. package/dist/src/agent.js.map +1 -1
  10. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  11. package/dist/src/api/mcp-server-core.js +50 -12
  12. package/dist/src/api/mcp-server-core.js.map +1 -1
  13. package/dist/src/api/server.d.ts.map +1 -1
  14. package/dist/src/api/server.js +4 -20
  15. package/dist/src/api/server.js.map +1 -1
  16. package/dist/src/append-all.d.ts +5 -0
  17. package/dist/src/append-all.d.ts.map +1 -0
  18. package/dist/src/append-all.js +8 -0
  19. package/dist/src/append-all.js.map +1 -0
  20. package/dist/src/code-execution/isolate/scripts.d.ts +12 -8
  21. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  22. package/dist/src/code-execution/isolate/scripts.js +24 -0
  23. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  24. package/dist/src/config-checks.d.ts +7 -0
  25. package/dist/src/config-checks.d.ts.map +1 -1
  26. package/dist/src/config-checks.js +14 -0
  27. package/dist/src/config-checks.js.map +1 -1
  28. package/dist/src/context-manager-errors.d.ts +25 -0
  29. package/dist/src/context-manager-errors.d.ts.map +1 -0
  30. package/dist/src/context-manager-errors.js +31 -0
  31. package/dist/src/context-manager-errors.js.map +1 -0
  32. package/dist/src/context-options.d.ts +70 -0
  33. package/dist/src/context-options.d.ts.map +1 -0
  34. package/dist/src/context-options.js +90 -0
  35. package/dist/src/context-options.js.map +1 -0
  36. package/dist/src/cut-off-round.js +4 -4
  37. package/dist/src/deferred-import.d.ts +93 -0
  38. package/dist/src/deferred-import.d.ts.map +1 -0
  39. package/dist/src/deferred-import.js +177 -0
  40. package/dist/src/deferred-import.js.map +1 -0
  41. package/dist/src/dendrite/audience.d.ts +223 -0
  42. package/dist/src/dendrite/audience.d.ts.map +1 -0
  43. package/dist/src/dendrite/audience.js +202 -0
  44. package/dist/src/dendrite/audience.js.map +1 -0
  45. package/dist/src/dendrite/index.d.ts +5 -3
  46. package/dist/src/dendrite/index.d.ts.map +1 -1
  47. package/dist/src/dendrite/index.js +2 -1
  48. package/dist/src/dendrite/index.js.map +1 -1
  49. package/dist/src/dendrite/presets.d.ts +10 -5
  50. package/dist/src/dendrite/presets.d.ts.map +1 -1
  51. package/dist/src/dendrite/presets.js +17 -5
  52. package/dist/src/dendrite/presets.js.map +1 -1
  53. package/dist/src/dendrite/registry.d.ts +62 -7
  54. package/dist/src/dendrite/registry.d.ts.map +1 -1
  55. package/dist/src/dendrite/registry.js +183 -12
  56. package/dist/src/dendrite/registry.js.map +1 -1
  57. package/dist/src/dendrite/types.d.ts +91 -8
  58. package/dist/src/dendrite/types.d.ts.map +1 -1
  59. package/dist/src/failure-text.d.ts +36 -0
  60. package/dist/src/failure-text.d.ts.map +1 -0
  61. package/dist/src/failure-text.js +89 -0
  62. package/dist/src/failure-text.js.map +1 -0
  63. package/dist/src/foreign-rounds.d.ts +69 -0
  64. package/dist/src/foreign-rounds.d.ts.map +1 -0
  65. package/dist/src/foreign-rounds.js +152 -0
  66. package/dist/src/foreign-rounds.js.map +1 -0
  67. package/dist/src/framework.d.ts +2064 -842
  68. package/dist/src/framework.d.ts.map +1 -1
  69. package/dist/src/framework.js +8586 -3739
  70. package/dist/src/framework.js.map +1 -1
  71. package/dist/src/gate/event-gate.d.ts +125 -21
  72. package/dist/src/gate/event-gate.d.ts.map +1 -1
  73. package/dist/src/gate/event-gate.js +487 -72
  74. package/dist/src/gate/event-gate.js.map +1 -1
  75. package/dist/src/gate/index.d.ts +1 -1
  76. package/dist/src/gate/index.d.ts.map +1 -1
  77. package/dist/src/gate/index.js.map +1 -1
  78. package/dist/src/gate/reply-judge.d.ts +4 -0
  79. package/dist/src/gate/reply-judge.d.ts.map +1 -1
  80. package/dist/src/gate/reply-judge.js +12 -3
  81. package/dist/src/gate/reply-judge.js.map +1 -1
  82. package/dist/src/gate/rule-scope.d.ts +30 -0
  83. package/dist/src/gate/rule-scope.d.ts.map +1 -0
  84. package/dist/src/gate/rule-scope.js +41 -0
  85. package/dist/src/gate/rule-scope.js.map +1 -0
  86. package/dist/src/gate/suppress-script-worker.d.ts +2 -0
  87. package/dist/src/gate/suppress-script-worker.d.ts.map +1 -0
  88. package/dist/src/gate/suppress-script-worker.js +103 -0
  89. package/dist/src/gate/suppress-script-worker.js.map +1 -0
  90. package/dist/src/gate/suppress-scripts.d.ts +105 -0
  91. package/dist/src/gate/suppress-scripts.d.ts.map +1 -0
  92. package/dist/src/gate/suppress-scripts.js +403 -0
  93. package/dist/src/gate/suppress-scripts.js.map +1 -0
  94. package/dist/src/gate/suppression-review.d.ts +143 -0
  95. package/dist/src/gate/suppression-review.d.ts.map +1 -0
  96. package/dist/src/gate/suppression-review.js +463 -0
  97. package/dist/src/gate/suppression-review.js.map +1 -0
  98. package/dist/src/gate/suppression-stats.d.ts +125 -0
  99. package/dist/src/gate/suppression-stats.d.ts.map +1 -0
  100. package/dist/src/gate/suppression-stats.js +214 -0
  101. package/dist/src/gate/suppression-stats.js.map +1 -0
  102. package/dist/src/gate/types.d.ts +142 -7
  103. package/dist/src/gate/types.d.ts.map +1 -1
  104. package/dist/src/gate/wake-onboarding.d.ts +119 -0
  105. package/dist/src/gate/wake-onboarding.d.ts.map +1 -0
  106. package/dist/src/gate/wake-onboarding.js +277 -0
  107. package/dist/src/gate/wake-onboarding.js.map +1 -0
  108. package/dist/src/heartbeat/heartbeat.js +1 -1
  109. package/dist/src/heartbeat/heartbeat.js.map +1 -1
  110. package/dist/src/held-writes.d.ts +31 -23
  111. package/dist/src/held-writes.d.ts.map +1 -1
  112. package/dist/src/held-writes.js +44 -59
  113. package/dist/src/held-writes.js.map +1 -1
  114. package/dist/src/index.d.ts +6 -2
  115. package/dist/src/index.d.ts.map +1 -1
  116. package/dist/src/index.js +4 -0
  117. package/dist/src/index.js.map +1 -1
  118. package/dist/src/inference-bounds.d.ts +2 -1
  119. package/dist/src/inference-bounds.d.ts.map +1 -1
  120. package/dist/src/inference-bounds.js +9 -6
  121. package/dist/src/inference-bounds.js.map +1 -1
  122. package/dist/src/jsonrpc-envelope.d.ts +69 -0
  123. package/dist/src/jsonrpc-envelope.d.ts.map +1 -0
  124. package/dist/src/jsonrpc-envelope.js +66 -0
  125. package/dist/src/jsonrpc-envelope.js.map +1 -0
  126. package/dist/src/mcpl/channel-registry.d.ts +188 -28
  127. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  128. package/dist/src/mcpl/channel-registry.js +426 -70
  129. package/dist/src/mcpl/channel-registry.js.map +1 -1
  130. package/dist/src/mcpl/checkpoint-manager.d.ts +2 -4
  131. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  132. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  133. package/dist/src/mcpl/feature-set-manager.d.ts +2 -4
  134. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  135. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  136. package/dist/src/mcpl/inbound-source.d.ts +2 -1
  137. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  138. package/dist/src/mcpl/inbound-source.js +2 -1
  139. package/dist/src/mcpl/inbound-source.js.map +1 -1
  140. package/dist/src/mcpl/inference-router.d.ts +6 -6
  141. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  142. package/dist/src/mcpl/inference-router.js.map +1 -1
  143. package/dist/src/mcpl/modern-connection.d.ts +109 -15
  144. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  145. package/dist/src/mcpl/modern-connection.js +593 -86
  146. package/dist/src/mcpl/modern-connection.js.map +1 -1
  147. package/dist/src/mcpl/protocol-family.d.ts +3 -1
  148. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  149. package/dist/src/mcpl/protocol-family.js +15 -9
  150. package/dist/src/mcpl/protocol-family.js.map +1 -1
  151. package/dist/src/mcpl/push-coalescer.d.ts +132 -22
  152. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  153. package/dist/src/mcpl/push-coalescer.js +105 -15
  154. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  155. package/dist/src/mcpl/push-handler.d.ts +9 -7
  156. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  157. package/dist/src/mcpl/push-handler.js.map +1 -1
  158. package/dist/src/mcpl/server-connection.d.ts +50 -28
  159. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  160. package/dist/src/mcpl/server-connection.js +165 -72
  161. package/dist/src/mcpl/server-connection.js.map +1 -1
  162. package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
  163. package/dist/src/mcpl/tool-classes.js +3 -0
  164. package/dist/src/mcpl/tool-classes.js.map +1 -1
  165. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  166. package/dist/src/mcpl/tool-presence.js +10 -6
  167. package/dist/src/mcpl/tool-presence.js.map +1 -1
  168. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  169. package/dist/src/mcpl/toolset-host.js +7 -6
  170. package/dist/src/mcpl/toolset-host.js.map +1 -1
  171. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  172. package/dist/src/mcpl/toolsets-module.js +2 -1
  173. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  174. package/dist/src/mcpl/types.d.ts +33 -6
  175. package/dist/src/mcpl/types.d.ts.map +1 -1
  176. package/dist/src/mcpl/types.js.map +1 -1
  177. package/dist/src/module-registry.d.ts +24 -1
  178. package/dist/src/module-registry.d.ts.map +1 -1
  179. package/dist/src/module-registry.js +68 -18
  180. package/dist/src/module-registry.js.map +1 -1
  181. package/dist/src/modules/discord/index.d.ts +45 -8
  182. package/dist/src/modules/discord/index.d.ts.map +1 -1
  183. package/dist/src/modules/discord/index.js +238 -87
  184. package/dist/src/modules/discord/index.js.map +1 -1
  185. package/dist/src/modules/health/index.js +2 -2
  186. package/dist/src/modules/health/index.js.map +1 -1
  187. package/dist/src/modules/history/index.d.ts +59 -0
  188. package/dist/src/modules/history/index.d.ts.map +1 -1
  189. package/dist/src/modules/history/index.js +432 -67
  190. package/dist/src/modules/history/index.js.map +1 -1
  191. package/dist/src/modules/history/semantic.d.ts +6 -5
  192. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  193. package/dist/src/modules/history/semantic.js +18 -25
  194. package/dist/src/modules/history/semantic.js.map +1 -1
  195. package/dist/src/modules/workspace/disk-agreement.d.ts +1 -1
  196. package/dist/src/modules/workspace/disk-agreement.js +1 -1
  197. package/dist/src/modules/workspace/index.d.ts +28 -7
  198. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  199. package/dist/src/modules/workspace/index.js +80 -25
  200. package/dist/src/modules/workspace/index.js.map +1 -1
  201. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  202. package/dist/src/modules/workspace/observe.js +2 -1
  203. package/dist/src/modules/workspace/observe.js.map +1 -1
  204. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  205. package/dist/src/modules/workspace/reconcile.js +3 -2
  206. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  207. package/dist/src/notice-tags.d.ts +20 -9
  208. package/dist/src/notice-tags.d.ts.map +1 -1
  209. package/dist/src/notice-tags.js +20 -9
  210. package/dist/src/notice-tags.js.map +1 -1
  211. package/dist/src/operator-log.d.ts +36 -3
  212. package/dist/src/operator-log.d.ts.map +1 -1
  213. package/dist/src/operator-log.js +2 -2
  214. package/dist/src/operator-log.js.map +1 -1
  215. package/dist/src/presentation.d.ts +747 -0
  216. package/dist/src/presentation.d.ts.map +1 -0
  217. package/dist/src/presentation.js +1437 -0
  218. package/dist/src/presentation.js.map +1 -0
  219. package/dist/src/prose-drafts.d.ts +15 -11
  220. package/dist/src/prose-drafts.d.ts.map +1 -1
  221. package/dist/src/prose-drafts.js +16 -12
  222. package/dist/src/prose-drafts.js.map +1 -1
  223. package/dist/src/read-marks.d.ts +192 -0
  224. package/dist/src/read-marks.d.ts.map +1 -0
  225. package/dist/src/read-marks.js +242 -0
  226. package/dist/src/read-marks.js.map +1 -0
  227. package/dist/src/reasoning-loss.d.ts +5 -0
  228. package/dist/src/reasoning-loss.d.ts.map +1 -0
  229. package/dist/src/reasoning-loss.js +8 -0
  230. package/dist/src/reasoning-loss.js.map +1 -0
  231. package/dist/src/recovery/offline-branch.d.ts +91 -6
  232. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  233. package/dist/src/recovery/offline-branch.js +277 -32
  234. package/dist/src/recovery/offline-branch.js.map +1 -1
  235. package/dist/src/recovery/recover-cli.js +24 -4
  236. package/dist/src/recovery/recover-cli.js.map +1 -1
  237. package/dist/src/send-tools.d.ts +5 -9
  238. package/dist/src/send-tools.d.ts.map +1 -1
  239. package/dist/src/send-tools.js +40 -7
  240. package/dist/src/send-tools.js.map +1 -1
  241. package/dist/src/sent-rows.js +1 -1
  242. package/dist/src/stored-injections.d.ts +2 -2
  243. package/dist/src/stored-injections.js +2 -2
  244. package/dist/src/stored-removal.d.ts +17 -4
  245. package/dist/src/stored-removal.d.ts.map +1 -1
  246. package/dist/src/stored-removal.js +29 -5
  247. package/dist/src/stored-removal.js.map +1 -1
  248. package/dist/src/supplied-text.d.ts +1 -1
  249. package/dist/src/supplied-text.js +3 -3
  250. package/dist/src/supplied-text.js.map +1 -1
  251. package/dist/src/thinking-drops.d.ts +2 -0
  252. package/dist/src/thinking-drops.d.ts.map +1 -1
  253. package/dist/src/thinking-drops.js.map +1 -1
  254. package/dist/src/tool-presentation.d.ts.map +1 -1
  255. package/dist/src/tool-presentation.js +38 -9
  256. package/dist/src/tool-presentation.js.map +1 -1
  257. package/dist/src/tool-result-guard.d.ts +12 -1
  258. package/dist/src/tool-result-guard.d.ts.map +1 -1
  259. package/dist/src/tool-result-guard.js +42 -8
  260. package/dist/src/tool-result-guard.js.map +1 -1
  261. package/dist/src/tune-out/coordinator.d.ts +1170 -81
  262. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  263. package/dist/src/tune-out/coordinator.js +3732 -369
  264. package/dist/src/tune-out/coordinator.js.map +1 -1
  265. package/dist/src/tune-out/tools.d.ts +114 -17
  266. package/dist/src/tune-out/tools.d.ts.map +1 -1
  267. package/dist/src/tune-out/tools.js +89 -28
  268. package/dist/src/tune-out/tools.js.map +1 -1
  269. package/dist/src/tune-out/window-anchor.d.ts +143 -0
  270. package/dist/src/tune-out/window-anchor.d.ts.map +1 -0
  271. package/dist/src/tune-out/window-anchor.js +202 -0
  272. package/dist/src/tune-out/window-anchor.js.map +1 -0
  273. package/dist/src/types/agent.d.ts +121 -25
  274. package/dist/src/types/agent.d.ts.map +1 -1
  275. package/dist/src/types/events.d.ts +2 -0
  276. package/dist/src/types/events.d.ts.map +1 -1
  277. package/dist/src/types/framework.d.ts +72 -17
  278. package/dist/src/types/framework.d.ts.map +1 -1
  279. package/dist/src/types/inference-log.d.ts +1 -0
  280. package/dist/src/types/inference-log.d.ts.map +1 -1
  281. package/dist/src/types/module.d.ts +180 -69
  282. package/dist/src/types/module.d.ts.map +1 -1
  283. package/dist/src/types/trace.d.ts +573 -39
  284. package/dist/src/types/trace.d.ts.map +1 -1
  285. package/package.json +4 -4
  286. package/src/admission/admit.ts +32 -3
  287. package/src/agent.ts +392 -132
  288. package/src/api/mcp-server-core.ts +54 -17
  289. package/src/api/server.ts +4 -23
  290. package/src/append-all.ts +6 -0
  291. package/src/code-execution/isolate/scripts.ts +28 -3
  292. package/src/config-checks.ts +14 -0
  293. package/src/context-manager-errors.ts +37 -0
  294. package/src/context-options.ts +106 -0
  295. package/src/cut-off-round.ts +4 -4
  296. package/src/deferred-import.ts +239 -0
  297. package/src/dendrite/audience.ts +315 -0
  298. package/src/dendrite/index.ts +14 -2
  299. package/src/dendrite/presets.ts +18 -6
  300. package/src/dendrite/registry.ts +213 -14
  301. package/src/dendrite/types.ts +92 -8
  302. package/src/failure-text.ts +88 -0
  303. package/src/foreign-rounds.ts +199 -0
  304. package/src/framework.ts +8796 -3813
  305. package/src/gate/event-gate.ts +518 -91
  306. package/src/gate/index.ts +3 -0
  307. package/src/gate/reply-judge.ts +12 -3
  308. package/src/gate/rule-scope.ts +45 -0
  309. package/src/gate/suppress-script-worker.ts +89 -0
  310. package/src/gate/suppress-scripts.ts +414 -0
  311. package/src/gate/suppression-review.ts +517 -0
  312. package/src/gate/suppression-stats.ts +314 -0
  313. package/src/gate/types.ts +148 -7
  314. package/src/gate/wake-onboarding.ts +309 -0
  315. package/src/heartbeat/heartbeat.ts +1 -1
  316. package/src/held-writes.ts +49 -60
  317. package/src/index.ts +10 -2
  318. package/src/inference-bounds.ts +11 -6
  319. package/src/jsonrpc-envelope.ts +81 -0
  320. package/src/mcpl/channel-registry.ts +513 -116
  321. package/src/mcpl/checkpoint-manager.ts +3 -2
  322. package/src/mcpl/feature-set-manager.ts +2 -1
  323. package/src/mcpl/inbound-source.ts +2 -1
  324. package/src/mcpl/inference-router.ts +7 -4
  325. package/src/mcpl/modern-connection.ts +629 -91
  326. package/src/mcpl/protocol-family.ts +15 -9
  327. package/src/mcpl/push-coalescer.ts +193 -34
  328. package/src/mcpl/push-handler.ts +10 -5
  329. package/src/mcpl/server-connection.ts +169 -75
  330. package/src/mcpl/tool-classes.ts +3 -0
  331. package/src/mcpl/tool-presence.ts +10 -5
  332. package/src/mcpl/toolset-host.ts +7 -6
  333. package/src/mcpl/toolsets-module.ts +2 -1
  334. package/src/mcpl/types.ts +33 -6
  335. package/src/module-registry.ts +73 -17
  336. package/src/modules/discord/index.ts +230 -82
  337. package/src/modules/health/index.ts +2 -2
  338. package/src/modules/history/index.ts +439 -70
  339. package/src/modules/history/semantic.ts +18 -24
  340. package/src/modules/workspace/disk-agreement.ts +1 -1
  341. package/src/modules/workspace/index.ts +89 -26
  342. package/src/modules/workspace/observe.ts +2 -1
  343. package/src/modules/workspace/reconcile.ts +3 -2
  344. package/src/notice-tags.ts +21 -9
  345. package/src/operator-log.ts +36 -3
  346. package/src/presentation.ts +1678 -0
  347. package/src/prose-drafts.ts +16 -12
  348. package/src/read-marks.ts +328 -0
  349. package/src/reasoning-loss.ts +7 -0
  350. package/src/recovery/offline-branch.ts +350 -35
  351. package/src/recovery/recover-cli.ts +23 -4
  352. package/src/send-tools.ts +40 -7
  353. package/src/sent-rows.ts +1 -1
  354. package/src/stored-injections.ts +2 -2
  355. package/src/stored-removal.ts +31 -7
  356. package/src/supplied-text.ts +3 -3
  357. package/src/thinking-drops.ts +2 -0
  358. package/src/tool-presentation.ts +34 -9
  359. package/src/tool-result-guard.ts +41 -7
  360. package/src/tune-out/coordinator.ts +4121 -472
  361. package/src/tune-out/tools.ts +162 -44
  362. package/src/tune-out/window-anchor.ts +277 -0
  363. package/src/types/agent.ts +122 -25
  364. package/src/types/events.ts +2 -0
  365. package/src/types/framework.ts +73 -17
  366. package/src/types/inference-log.ts +1 -0
  367. package/src/types/module.ts +181 -68
  368. package/src/types/trace.ts +607 -43
@@ -16,6 +16,7 @@ import { maskNegotiatedCapabilities } from './capability-mask.js';
16
16
  import { CapabilityGrant, expandAdvertisementShorthand } from './capability-grant.js';
17
17
  import { CAPABILITY_DISABLED, boundedForLog, describeCause } from './errors.js';
18
18
  import { logDetail, logValue, logValueRedacted, loggedCode } from './log-value.js';
19
+ import { jsonKind, readEnvelope } from '../jsonrpc-envelope.js';
19
20
  import {
20
21
  LEGACY_MCP_PROTOCOL_VERSION,
21
22
  MODERN_MCP_PROTOCOL_VERSION,
@@ -156,20 +157,27 @@ const UNSUPPORTED_PROTOCOL_VERSION_CODE = -32022;
156
157
  const MAX_TOOL_LIST_PAGES = 100;
157
158
 
158
159
  /**
159
- * The server and this engine have no protocol revision in common, so the
160
- * connection was never established. This is a protocol-negotiation fact, not
161
- * a request outcome. Retrying cannot change it: each engine offers one
162
- * revision, and only configuration can fix the mismatch. A connection that
163
- * gets this error does not schedule reconnects. The legacy engine and the
164
- * modern one (ModernMcpConnection, whose pinned revision the server refused)
165
- * both throw it, so its readers treat them alike.
160
+ * This engine and the server could not negotiate a protocol revision, so the
161
+ * connect that throws it established no connection. Usually they share none. Over HTTP it
162
+ * can also be the SDK's own refusal: after a -32022 whose list names a
163
+ * revision from 2026-07-28 on that this client doesn't speak, the SDK
164
+ * negotiates no further, even when the list also names a 2025 revision both
165
+ * speak. This is a protocol-negotiation fact, not a request outcome.
166
+ * Retrying cannot change it: each engine offers the same revisions every
167
+ * time (`offeredRevisions`), and only configuration, or the server's
168
+ * operator, can fix the mismatch. A connection that gets this error does not
169
+ * schedule reconnects. The legacy engine and the modern one
170
+ * (ModernMcpConnection) both throw it, so its readers treat them alike.
166
171
  *
167
- * - `rejected`: the server refused the offered revision: the legacy engine's
168
- * `initialize` answered with error -32022, or a modern server's discovery
169
- * or error naming no common revision. `supported` is the server's own list,
170
- * when it gave one.
171
- * - `mismatch`: the server answered the legacy engine's `initialize` with a
172
- * revision other than 2024-11-05, or with none.
172
+ * - `rejected`: the server refused the revision offered. The legacy
173
+ * engine's `initialize` was answered with error -32022. A modern server's
174
+ * discovery or -32022 error named no revision in common (over HTTP, a
175
+ * discovery's list counts once the server has refused the `initialize`
176
+ * tried after it too), or, over HTTP, named one this client doesn't speak,
177
+ * as above. `supported` is the server's own list, when it gave one.
178
+ * - `mismatch`: the server answered `initialize` with a revision that
179
+ * handshake doesn't accept, or with none: the legacy engine's, or a modern
180
+ * connection's 2025 handshake over HTTP.
173
181
  */
174
182
  export class McplProtocolVersionError extends Error {
175
183
  constructor(
@@ -184,9 +192,15 @@ export class McplProtocolVersionError extends Error {
184
192
  * redacted (`rejected`). */
185
193
  readonly code?: number,
186
194
  readonly data?: unknown,
187
- /** The revision this client offered: the legacy engine's, unless a
188
- * modern connection's. */
195
+ /** The revision this client offered first, in its order of preference:
196
+ * the legacy engine's (2024-11-05), a modern connection's over stdio
197
+ * (2026-07-28), or over HTTP 2026-07-28, the first of
198
+ * `offeredRevisions`. */
189
199
  readonly offered: string = MCP_PROTOCOL_VERSION,
200
+ /** Every revision this client offered, in its order of preference: the
201
+ * legacy engine's one, a modern connection's one over stdio, or its
202
+ * list over HTTP. */
203
+ readonly offeredRevisions: readonly string[] = [offered],
190
204
  ) {
191
205
  super(message);
192
206
  this.name = 'McplProtocolVersionError';
@@ -367,11 +381,32 @@ interface HeldItem {
367
381
  kept?: true;
368
382
  }
369
383
 
370
- /** A request's method for a log line, whatever the server sent there. */
371
- function describeMethod(method: unknown): string {
372
- return typeof method === 'string'
373
- ? `method ${logValue(method)}`
374
- : `a method of type ${method === null ? 'null' : typeof method}`;
384
+ /** Whether a response that can't be matched to a request of ours, its id
385
+ * being neither a string nor a number or missing, is logged: only a
386
+ * JSON-RPC 2.0 error is, since that's how a server says it couldn't read a
387
+ * message of ours (JSON-RPC 2.0 §5). Anything else id-less, such as a
388
+ * server's own JSON log line with `"id": null`, is ignored silently, as it
389
+ * was before #475, so a server's log output can't fill the host's. */
390
+ function isJsonRpcError(response: object): boolean {
391
+ return (response as { jsonrpc?: unknown }).jsonrpc === '2.0' && Object.hasOwn(response, 'error');
392
+ }
393
+
394
+ /** The log line for such an error. The host's own request ids are numbers,
395
+ * so it can't be matched to one. Once connected it may be the answer to a
396
+ * request of ours, which gets nothing from it and ends as any unanswered
397
+ * request does, by its timeout where it has one. (During the handshake an
398
+ * error with id null or no id answers initialize.) Its error member is the
399
+ * server's text, whatever JSON value it is, so it's printed redacted with
400
+ * `redact`, the server's redaction (logValueRedacted), as handleResponse
401
+ * prints a matched error's code and message. */
402
+ function unmatchedResponse(
403
+ serverId: string,
404
+ response: { id?: unknown; error?: unknown },
405
+ redact: (text: string) => string,
406
+ ): string {
407
+ const which = Object.hasOwn(response, 'id') ? `whose id is ${jsonKind(response.id)}` : 'with no id';
408
+ return `[mcpl] ${serverId}: ignored a response ${which}, which can't be matched to a request of ours; ` +
409
+ `its error: ${logValueRedacted(response.error, redact)}`;
375
410
  }
376
411
 
377
412
  /**
@@ -700,13 +735,14 @@ export class McplServerConnection extends EventEmitter {
700
735
  // Reconnect state (adapted from Anarchid/agent-framework@mcpl-module-proto)
701
736
  private config: McplServerConfig | null = null;
702
737
  /** The redactor of the server's text for `config`, made at its first error
703
- * answer, by when `config` is set, and kept: a framework-launched child's
704
- * `env` is a getter that reads the awareness ledger, so a redactor built
705
- * per answer would read it per answer (#540's review, R1-2). An answer
706
- * takes one build of it for its words, data and log line (`now()`), so
707
- * it reads the ledger at most once. It builds again only after a build
708
- * that failed, which withholds that answer's text, or once a dial notes
709
- * something new (serverTextRedactor). */
738
+ * answer or response it can't match (handleResponse), by when `config`
739
+ * is set, and kept: a framework-launched child's `env` is a getter that
740
+ * reads the awareness ledger, so a redactor built per answer would read
741
+ * it per answer (#540's review, R1-2). An answer takes one build of it
742
+ * for its words, data and log line (`now()`), so it reads the ledger at
743
+ * most once. It builds again only after a build that failed, which
744
+ * withholds that answer's text, or once a dial notes something new
745
+ * (serverTextRedactor). */
710
746
  private redactor: ServerTextRedactor | null = null;
711
747
  private hostCapabilities: McplHostCapabilities | null = null;
712
748
  private reconnectEnabled = false;
@@ -1358,15 +1394,35 @@ export class McplServerConnection extends EventEmitter {
1358
1394
  // initialize response, whatever its id: a server's own ids can start
1359
1395
  // at 0, this handshake's. MCP lets a server ping before
1360
1396
  // initialization completes, and the ping gets its empty result, as
1361
- // on a connected transport. Anything else it sends now is ignored.
1362
- if ('method' in msg) {
1363
- const request = msg as unknown as JsonRpcRequest;
1364
- if (request.method === 'ping' && request.id != null) {
1365
- transport.writeLine(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: {} }));
1397
+ // on a connected transport, `"id": null` included
1398
+ // (src/jsonrpc-envelope.ts). Anything else it sends now is ignored,
1399
+ // an invalid request included. Of its responses, the one with
1400
+ // initialize's id answers it, and so does a JSON-RPC 2.0 error with
1401
+ // id null or with no id: a peer answers a request it couldn't read
1402
+ // that way (JSON-RPC 2.0 §5; MCP 2025-11-25, Basic), initialize is
1403
+ // the only request this host has out, and a peer never answers a
1404
+ // response. Such an error means the server can't read this host's
1405
+ // messages, so the handshake fails at once, with the error's message.
1406
+ // `"jsonrpc": "2.0"` keeps a server's own JSON log line with an
1407
+ // error key and id null, or no id, from failing it; one with
1408
+ // initialize's id is taken for the answer, whatever its jsonrpc. Of
1409
+ // the other responses, a JSON-RPC 2.0 error whose id is neither a
1410
+ // string nor a number is logged, as on a connected transport, and
1411
+ // any other is ignored.
1412
+ const envelope = readEnvelope(msg as unknown as Record<string, unknown>);
1413
+ if (envelope.kind === 'request' && envelope.method === 'ping') {
1414
+ transport.writeLine(JSON.stringify({ jsonrpc: '2.0', id: envelope.id, result: {} }));
1415
+ return;
1416
+ }
1417
+ if (envelope.kind !== 'response') return;
1418
+ const namesNoRequest = msg.id === null || !Object.hasOwn(msg, 'id');
1419
+ const answersInitialize = msg.id === initId || (namesNoRequest && msg.jsonrpc === '2.0' && Boolean(msg.error));
1420
+ if (!answersInitialize) {
1421
+ if (typeof msg.id !== 'string' && typeof msg.id !== 'number' && isJsonRpcError(msg)) {
1422
+ console.error(unmatchedResponse(config.id, msg, (text) => redactServerText(text, config)));
1366
1423
  }
1367
1424
  return;
1368
1425
  }
1369
- if (msg.id !== initId) return;
1370
1426
  cleanup();
1371
1427
  if (msg.error) {
1372
1428
  // describeCause rather than the template's own conversion: the
@@ -1739,8 +1795,9 @@ export class McplServerConnection extends EventEmitter {
1739
1795
  * following `nextCursor` across pages (2024-11-05 pagination). Cursors are
1740
1796
  * opaque strings, `''` included: only an absent or null `nextCursor` ends
1741
1797
  * the inventory (a null one is how some servers say "no more", and main
1742
- * read it as a complete page). A page that isn't well formed (no `tools` array, a cursor
1743
- * that isn't a string), or cursors that don't terminate (a repeat, or more
1798
+ * read it as a complete page). A page that isn't well formed (no `tools` array, an entry
1799
+ * that isn't an object with a string `name`, a cursor that isn't a string), or cursors
1800
+ * that don't terminate (a repeat, or more
1744
1801
  * than {@link MAX_TOOL_LIST_PAGES} pages), are errors, never a partial or
1745
1802
  * empty inventory published as complete. The read tap (RFC-014) is handed
1746
1803
  * the complete inventory once, with the last page's `_meta`, as the last
@@ -1755,9 +1812,12 @@ export class McplServerConnection extends EventEmitter {
1755
1812
  let first: Record<string, unknown> | undefined;
1756
1813
  const read: McpToolDefinition[] = [];
1757
1814
  const revs = new Set<unknown>();
1815
+ const isTool = (entry: unknown): boolean =>
1816
+ !!entry && typeof entry === 'object' && typeof (entry as { name?: unknown }).name === 'string';
1758
1817
  const onRead = (result: unknown) => {
1759
1818
  if (!result || typeof result !== 'object' || !Array.isArray((result as { tools?: unknown }).tools)) return;
1760
1819
  const r = result as { tools: McpToolDefinition[]; nextCursor?: unknown; _meta?: unknown };
1820
+ if (!r.tools.every(isTool)) return;
1761
1821
  for (const t of r.tools) read.push(t);
1762
1822
  const rev = (r._meta as Record<string, unknown> | undefined)?.['mcpl/rev'];
1763
1823
  if (typeof rev === 'number') revs.add(rev);
@@ -1774,6 +1834,8 @@ export class McplServerConnection extends EventEmitter {
1774
1834
  if (!result || typeof result !== 'object' || !Array.isArray(result.tools)) {
1775
1835
  throw malformed('no tools array');
1776
1836
  }
1837
+ const unreadable = (result.tools as unknown[]).findIndex((entry) => !isTool(entry));
1838
+ if (unreadable !== -1) throw malformed(`entry ${unreadable + 1} isn't an object with a string name`);
1777
1839
  first ??= result;
1778
1840
  for (const t of result.tools as McpToolDefinition[]) tools.push(t);
1779
1841
  if (result.nextCursor === undefined || result.nextCursor === null) {
@@ -2389,10 +2451,10 @@ export class McplServerConnection extends EventEmitter {
2389
2451
  // passed is the dead process's (afterBoundary).
2390
2452
  const epoch = this.transportEpoch;
2391
2453
  transport.on('line', (line: string) => {
2392
- const msg = messageFromLine(this.id, line, transport) as JsonRpcRequest | JsonRpcResponse | undefined;
2454
+ const msg = messageFromLine(this.id, line, transport);
2393
2455
  if (!msg) return;
2394
2456
  try {
2395
- this.dispatchMessage(msg, epoch);
2457
+ this.dispatchMessage(msg as Record<string, unknown>, epoch);
2396
2458
  } catch (error) {
2397
2459
  const request = msg as { method?: unknown; id?: unknown };
2398
2460
  this.reportInboundFailure(error, {
@@ -2404,25 +2466,47 @@ export class McplServerConnection extends EventEmitter {
2404
2466
  }
2405
2467
 
2406
2468
  /** Contain synchronous dispatch failures; async RPC handlers own their promises. */
2407
- private dispatchMessage(msg: JsonRpcRequest | JsonRpcResponse, epoch: number): void {
2408
- // Is this a response to one of our outbound requests?
2409
- if ('id' in msg && msg.id != null && !('method' in msg)) {
2410
- const late = epoch !== this.transportEpoch;
2411
- // RFC-014: its hints apply as it's read, whether or not anyone still
2412
- // waits for it. A dead transport's revs are its own, so a response
2413
- // read after its boundary is never tapped.
2414
- if (!late) this.tapResponse(msg as JsonRpcResponse);
2415
- this.handleResponse(msg as JsonRpcResponse, late);
2416
- return;
2469
+ private dispatchMessage(msg: Record<string, unknown>, epoch: number): void {
2470
+ // The envelope is read once, here (src/jsonrpc-envelope.ts says how).
2471
+ const envelope = readEnvelope(msg);
2472
+ switch (envelope.kind) {
2473
+ case 'response': {
2474
+ // Never answered, so a server that answers what it can't read, with
2475
+ // id null, can't start an exchange of errors with us. One read after
2476
+ // its transport's boundary is late (handleResponse).
2477
+ const late = epoch !== this.transportEpoch;
2478
+ // RFC-014: its hints apply as it's read, whether or not anyone still
2479
+ // waits for it. A dead transport's revs are its own, so a response
2480
+ // read after its boundary is never tapped.
2481
+ if (!late) this.tapResponse(msg as unknown as JsonRpcResponse);
2482
+ this.handleResponse(msg as unknown as JsonRpcResponse, late);
2483
+ return;
2484
+ }
2485
+ case 'neither':
2486
+ // Not JSON-RPC: ignored like the server's other unrelated output.
2487
+ return;
2488
+ case 'invalid': {
2489
+ // Answered on the transport it came on, as every request is. One
2490
+ // read after that transport's boundary is the dead process's and
2491
+ // isn't answered: sendErrorResponse logs the answer it didn't send.
2492
+ this.sendErrorResponse(envelope.id, envelope.error.code, envelope.error.message, undefined, epoch);
2493
+ if (epoch !== this.transportEpoch) return;
2494
+ const forMethod = envelope.method !== undefined ? ` for method ${logValue(envelope.method)}` : '';
2495
+ console.error(`[mcpl] ${this.id}: answered -32600 to an invalid request${forMethod}: ${envelope.problems}`);
2496
+ return;
2497
+ }
2417
2498
  }
2418
2499
 
2419
- // It is an inbound request or notification from the server
2420
- const request = msg as JsonRpcRequest;
2500
+ // It is an inbound request or notification from the server, its method
2501
+ // a string. A request's id is a string, a number or null, and `"id": null`
2502
+ // is answered with id null; a notification has none.
2503
+ const request = msg as unknown as JsonRpcRequest;
2504
+ const requestId = envelope.kind === 'request' ? envelope.id : undefined;
2421
2505
 
2422
2506
  // Read after its transport's boundary, it is the dead process's: the
2423
2507
  // boundary's rule applies (afterBoundary).
2424
2508
  if (epoch !== this.transportEpoch) {
2425
- this.afterBoundary(request, epoch);
2509
+ this.afterBoundary(request, requestId, epoch);
2426
2510
  return;
2427
2511
  }
2428
2512
 
@@ -2432,7 +2516,7 @@ export class McplServerConnection extends EventEmitter {
2432
2516
  // paused plane mustn't hold that answer. A ping sent as a notification
2433
2517
  // has nothing to answer. The handshake answers one too.
2434
2518
  if (request.method === 'ping') {
2435
- if (request.id != null) this.sendResponse(request.id, {});
2519
+ if (requestId !== undefined) this.sendResponse(requestId, {});
2436
2520
  return;
2437
2521
  }
2438
2522
 
@@ -2443,11 +2527,11 @@ export class McplServerConnection extends EventEmitter {
2443
2527
  if (request.method === 'push/event') this.tap({ kind: 'push', params: request.params });
2444
2528
  else if (request.method === 'tools/hints' && this.readTap) {
2445
2529
  this.tap({ kind: 'hints', params: request.params });
2446
- if (request.id != null) this.sendResponse(request.id, {});
2530
+ if (requestId !== undefined) this.sendResponse(requestId, {});
2447
2531
  return;
2448
2532
  }
2449
2533
 
2450
- const eventName = typeof request.method === 'string' && Object.hasOwn(McplServerConnection.METHOD_TO_EVENT, request.method)
2534
+ const eventName = Object.hasOwn(McplServerConnection.METHOD_TO_EVENT, request.method)
2451
2535
  ? McplServerConnection.METHOD_TO_EVENT[request.method] : undefined;
2452
2536
 
2453
2537
  // §7 is removed in 0.5.0: scope/elevate is no longer a method. Answer
@@ -2459,8 +2543,8 @@ export class McplServerConnection extends EventEmitter {
2459
2543
  // routed, control-plane classified, and mutating declarations — a
2460
2544
  // live ungated self-attestation path (PR #79 review blocker 4).
2461
2545
  if (request.method === McplMethod.FeatureSetsChanged) {
2462
- if (request.id != null) {
2463
- this.sendErrorResponse(request.id, -32601, 'featureSets/changed removed in MCPL 0.5.0 (superseded by mcpl/manifestChanged, SPEC §17)');
2546
+ if (requestId !== undefined) {
2547
+ this.sendErrorResponse(requestId, -32601, 'featureSets/changed removed in MCPL 0.5.0 (superseded by mcpl/manifestChanged, SPEC §17)');
2464
2548
  } else {
2465
2549
  console.error(`[mcpl] ${this.id}: discarded featureSets/changed — removed in 0.5.0 (§6.7/§17)`);
2466
2550
  }
@@ -2468,8 +2552,8 @@ export class McplServerConnection extends EventEmitter {
2468
2552
  }
2469
2553
 
2470
2554
  if (request.method === McplMethod.ScopeElevate) {
2471
- if (request.id != null) {
2472
- this.sendErrorResponse(request.id, -32601, 'scope/elevate removed in MCPL 0.5.0 (SPEC §7)');
2555
+ if (requestId !== undefined) {
2556
+ this.sendErrorResponse(requestId, -32601, 'scope/elevate removed in MCPL 0.5.0 (SPEC §7)');
2473
2557
  }
2474
2558
  return;
2475
2559
  }
@@ -2481,20 +2565,19 @@ export class McplServerConnection extends EventEmitter {
2481
2565
 
2482
2566
  if (!eventName) {
2483
2567
  // §6.6: a request that will never be answered MUST get an error, so a
2484
- // request (it has an id) whose method isn't a string, or isn't one this
2485
- // host handles, is answered -32601. A notification nobody handles is
2486
- // ignored, as JSON-RPC asks.
2487
- if (request.id != null) {
2488
- this.sendErrorResponse(request.id, -32601, 'Method not found');
2489
- console.error(`[mcpl] ${this.id}: answered -32601 to a request for ${describeMethod(request.method)}, which this host doesn't handle`);
2568
+ // request whose method isn't one this host handles is answered -32601.
2569
+ // A notification nobody handles is ignored, as JSON-RPC asks.
2570
+ if (requestId !== undefined) {
2571
+ this.sendErrorResponse(requestId, -32601, 'Method not found');
2572
+ console.error(`[mcpl] ${this.id}: answered -32601 to a request for method ${logValue(request.method)}, which this host doesn't handle`);
2490
2573
  }
2491
2574
  return;
2492
2575
  }
2493
2576
 
2494
2577
  // Emit the typed event with params and (for requests) a respond callback
2495
- if (request.id != null) {
2578
+ if (requestId !== undefined) {
2496
2579
  // Server expects a response — provide a respond helper
2497
- this.dispatchInboundEvent(eventName, request.params, this.responderFor(request.id, epoch));
2580
+ this.dispatchInboundEvent(eventName, request.params, this.responderFor(requestId, epoch));
2498
2581
  } else {
2499
2582
  // Notification — no response expected
2500
2583
  this.dispatchInboundEvent(eventName, request.params);
@@ -2504,9 +2587,10 @@ export class McplServerConnection extends EventEmitter {
2504
2587
  /**
2505
2588
  * A request's responder, bound to the transport it came on (`epoch`): an
2506
2589
  * answer, or a streamed inference's chunk, sent after that transport has
2507
- * closed reaches nothing, never a server that didn't ask it.
2590
+ * closed reaches nothing, never a server that didn't ask it. A request
2591
+ * sent with id null is answered with id null.
2508
2592
  */
2509
- private responderFor(id: string | number, epoch: number) {
2593
+ private responderFor(id: string | number | null, epoch: number) {
2510
2594
  return {
2511
2595
  id,
2512
2596
  signal: epoch === this.transportEpoch ? this.transportEnded.signal : AbortSignal.abort(),
@@ -2531,17 +2615,17 @@ export class McplServerConnection extends EventEmitter {
2531
2615
  * it while its writer lives, with no grant to come, so it drops and logs
2532
2616
  * all of it; hold's bounds cap what any other keeps.
2533
2617
  */
2534
- private afterBoundary(request: JsonRpcRequest, epoch: number): void {
2535
- const event = typeof request.method === 'string' && Object.hasOwn(McplServerConnection.METHOD_TO_EVENT, request.method)
2618
+ private afterBoundary(request: JsonRpcRequest, requestId: string | number | null | undefined, epoch: number): void {
2619
+ const event = Object.hasOwn(McplServerConnection.METHOD_TO_EVENT, request.method)
2536
2620
  ? McplServerConnection.METHOD_TO_EVENT[request.method] : undefined;
2537
2621
  if (this.closedForGood || event === undefined || !McplServerConnection.WORLD_EVENTS.has(event)) {
2538
2622
  console.error(
2539
2623
  `[mcpl] ${this.id}: dropped a message read after its transport closed` +
2540
- `${this.closedForGood ? ', with no transport to come' : ''}: ${describeMethod(request.method)}`,
2624
+ `${this.closedForGood ? ', with no transport to come' : ''}: method ${logValue(request.method)}`,
2541
2625
  );
2542
2626
  return;
2543
2627
  }
2544
- this.hold(event, request.id != null ? [request.params, this.responderFor(request.id, epoch)] : [request.params], true);
2628
+ this.hold(event, requestId !== undefined ? [request.params, this.responderFor(requestId, epoch)] : [request.params], true);
2545
2629
  }
2546
2630
 
2547
2631
  /** Live delivery and delayed plane flushes share the same containment boundary. */
@@ -2596,7 +2680,7 @@ export class McplServerConnection extends EventEmitter {
2596
2680
  /** Tap a response to a hinted request (RFC-014): its result's hints, or a
2597
2681
  * list's statement, apply now, awaited or not. */
2598
2682
  private tapResponse(response: JsonRpcResponse): void {
2599
- if (!this.readTap) return;
2683
+ if (!this.readTap || response.id === null) return;
2600
2684
  const sent = this.hintedRequests.get(response.id);
2601
2685
  if (!sent) return;
2602
2686
  this.hintedRequests.delete(response.id);
@@ -2609,6 +2693,16 @@ export class McplServerConnection extends EventEmitter {
2609
2693
  * Handle a JSON-RPC response by resolving/rejecting the corresponding pending request.
2610
2694
  */
2611
2695
  private handleResponse(response: JsonRpcResponse, late = false): void {
2696
+ // A response whose id is neither a string nor a number, or that has
2697
+ // none, can't be matched to a request of ours, on this transport or an
2698
+ // earlier one. It isn't an orphan, which is a late answer to one, so
2699
+ // it's ignored, and logged when it's a JSON-RPC 2.0 error.
2700
+ if (typeof response.id !== 'string' && typeof response.id !== 'number') {
2701
+ if (isJsonRpcError(response)) {
2702
+ console.error(unmatchedResponse(this.id, response, (this.redactor ??= serverTextRedactor(this.config ?? {})).now()));
2703
+ }
2704
+ return;
2705
+ }
2612
2706
  // One read after its transport's boundary answers a request the close
2613
2707
  // ended. It's matched against none of this connection's request records,
2614
2708
  // since the next transport numbers its requests afresh, and goes the way
@@ -2706,7 +2800,7 @@ export class McplServerConnection extends EventEmitter {
2706
2800
  /**
2707
2801
  * Send a successful JSON-RPC response back to the server.
2708
2802
  */
2709
- private sendResponse(id: string | number, result: unknown, epoch = this.transportEpoch): void {
2803
+ private sendResponse(id: string | number | null, result: unknown, epoch = this.transportEpoch): void {
2710
2804
  // A closed connection's epoch is past every request's, so a refusal
2711
2805
  // while it is closed is logged too.
2712
2806
  if (!this.sameTransport(id, epoch) || this.closed) return;
@@ -2717,7 +2811,7 @@ export class McplServerConnection extends EventEmitter {
2717
2811
  /** Whether a response to a request received on `epoch` may go out now.
2718
2812
  * The id is the server's, so it's logged as a field; `answer`, which the
2719
2813
  * caller has formatted for the log, ends the line. */
2720
- private sameTransport(id: string | number, epoch: number, answer?: string): boolean {
2814
+ private sameTransport(id: string | number | null, epoch: number, answer?: string): boolean {
2721
2815
  if (epoch === this.transportEpoch) return true;
2722
2816
  console.error(
2723
2817
  `[mcpl] ${this.id}: not answering request ${logValue(id)}: the transport it came on has closed${answer ? `, with ${answer}` : ''}`,
@@ -2729,7 +2823,7 @@ export class McplServerConnection extends EventEmitter {
2729
2823
  * Send a JSON-RPC error response back to the server.
2730
2824
  */
2731
2825
  private sendErrorResponse(
2732
- id: string | number,
2826
+ id: string | number | null,
2733
2827
  code: number,
2734
2828
  message: string,
2735
2829
  data?: unknown,
@@ -87,8 +87,11 @@ export const BUILTIN_TOOL_CLASSES: Readonly<Record<string, readonly ToolClass[]>
87
87
  wake: ['control'],
88
88
  wake_add_rule: ['control'],
89
89
  wake_remove_rule: ['control'],
90
+ wake_accept_suggestions: ['control'],
91
+ wake_decline: ['control'],
90
92
  event_tags: ['control'],
91
93
  prose_help: ['control'],
94
+ prose_locus: ['control'],
92
95
  // Files and blobs in the agent's own stores.
93
96
  save_recent_image: ['files'],
94
97
  read_image: ['files'],
@@ -27,6 +27,7 @@
27
27
  */
28
28
  import { objectSignature, renderToolLines, toolDigest, type HintedTool, type Shown } from './tool-signature.js';
29
29
  import { nameToTypeBack, quotedValue, suppliedValue } from '../supplied-text.js';
30
+ import { appendAll } from '../append-all.js';
30
31
 
31
32
  export interface ToolsetDecl {
32
33
  title?: string;
@@ -632,7 +633,7 @@ export class ToolPresence {
632
633
  continue;
633
634
  }
634
635
  const title = (c.opened || c.retitled) && !bare ? c.title : undefined;
635
- if (showsBlock(c)) lines.push(...this.toolBlock(c.key, c.now, { headed: !bare, title, seen, describeAll: opts.describeAll }));
636
+ if (showsBlock(c)) appendAll(lines, this.toolBlock(c.key, c.now, { headed: !bare, title, seen, describeAll: opts.describeAll }));
636
637
  else if (title) lines.push(`${this.label(c.key)} ${suppliedValue(title)}`);
637
638
  if (c.gone.length) lines.push(`now unavailable: ${c.gone.map(suppliedValue).join(', ')}`);
638
639
  if (c.opened && !bare) {
@@ -641,7 +642,8 @@ export class ToolPresence {
641
642
  }
642
643
  }
643
644
  for (const s of cs.suggest) lines.push(this.renderSuggestion(s));
644
- lines.push(...this.untoldNotes(cs), ...cs.notes);
645
+ appendAll(lines, this.untoldNotes(cs));
646
+ appendAll(lines, cs.notes);
645
647
  return lines.join('\n');
646
648
  }
647
649
 
@@ -663,7 +665,7 @@ export class ToolPresence {
663
665
  if (!v.size) return null;
664
666
  const lines: string[] = [];
665
667
  for (const [key, e] of v) {
666
- lines.push(...this.toolBlock(key, [...e.tools.values()], { describeAll: true, title: e.title }));
668
+ appendAll(lines, this.toolBlock(key, [...e.tools.values()], { describeAll: true, title: e.title }));
667
669
  const close = this.closeHint(key);
668
670
  if (close) lines.push(` ${close} closes it.`);
669
671
  }
@@ -679,7 +681,7 @@ export class ToolPresence {
679
681
  const v = this.view();
680
682
  if (v.size) {
681
683
  for (const [key, e] of v) {
682
- lines.push(...this.toolBlock(key, [...e.tools.values()], { describeAll: true, title: e.title, seen: opts.mark === false ? new Set<string>() : undefined }).map((l) => ` ${l}`));
684
+ appendAll(lines, this.toolBlock(key, [...e.tools.values()], { describeAll: true, title: e.title, seen: opts.mark === false ? new Set<string>() : undefined }).map((l) => ` ${l}`));
683
685
  const close = this.closeHint(key);
684
686
  if (close) lines.push(` ${close} closes it.`);
685
687
  }
@@ -700,7 +702,10 @@ export class ToolPresence {
700
702
  const names = this.allTools.filter((t) => this.toolsetOf(t.name) === g).map((t) => t.name);
701
703
  openable.push(` ${nameToTypeBack(g)}: ${names.map(nameToTypeBack).join(', ')}`);
702
704
  }
703
- if (openable.length) lines.push(`can open — ${this.names.open} {server:"${this.server}", toolset, instance?, options?}:`, ...openable);
705
+ if (openable.length) {
706
+ lines.push(`can open — ${this.names.open} {server:"${this.server}", toolset, instance?, options?}:`);
707
+ appendAll(lines, openable);
708
+ }
704
709
  return lines.join('\n');
705
710
  }
706
711
 
@@ -35,6 +35,7 @@ import {
35
35
  type ChangeSet, type MetaToolNames, type Presence,
36
36
  } from './tool-presence.js';
37
37
  import type { HintedTool } from './tool-signature.js';
38
+ import { appendAll } from '../append-all.js';
38
39
 
39
40
  /** The meta-tools' module, whose tools are `toolsets--open` and so on. */
40
41
  export const TOOLSETS_MODULE = 'toolsets';
@@ -246,7 +247,7 @@ export class ToolsetController {
246
247
  const toolset = typeof res.toolset === 'string' ? res.toolset : String(params.toolset ?? '');
247
248
  const instance = typeof res.instance === 'string' && res.instance ? res.instance : typeof params.instance === 'string' ? params.instance : undefined;
248
249
  this.presence.noteOpened(toolset, instance, typeof res.title === 'string' && res.title ? res.title : undefined);
249
- cs.changes.push(...this.presence.diff(before, this.presence.view()).changes);
250
+ appendAll(cs.changes, this.presence.diff(before, this.presence.view()).changes);
250
251
  break;
251
252
  }
252
253
  case 'toolsets/close': {
@@ -255,7 +256,7 @@ export class ToolsetController {
255
256
  cs = this.applyHints(metaHints(res), 'toolsets/close');
256
257
  const before = this.presence.view();
257
258
  this.presence.noteClosed(String(params.toolset ?? ''), typeof params.instance === 'string' ? params.instance : undefined);
258
- cs.changes.push(...this.presence.diff(before, this.presence.view()).changes);
259
+ appendAll(cs.changes, this.presence.diff(before, this.presence.view()).changes);
259
260
  break;
260
261
  }
261
262
  default:
@@ -298,7 +299,7 @@ export class ToolsetController {
298
299
  this.scheduleRecovery();
299
300
  }
300
301
  const cs = this.presence.diff(before, this.presence.view());
301
- cs.suggest.push(...r.suggest);
302
+ appendAll(cs.suggest, r.suggest);
302
303
  return cs;
303
304
  }
304
305
 
@@ -332,11 +333,11 @@ export class ToolsetController {
332
333
  this.scheduleRecovery();
333
334
  }
334
335
  const cs = this.presence.diff(before, this.presence.view());
335
- cs.suggest.push(...suggest);
336
+ appendAll(cs.suggest, suggest);
336
337
  // A lost toolset gets its note, not also a "closed" line.
337
338
  cs.changes = cs.changes.filter((c) => !lost.some((o) => keyOf(o.toolset, o.instance) === c.key));
338
339
  const told = this.presence.adoptionNotes(lost, ended);
339
- cs.notes.push(...told.notes);
340
+ appendAll(cs.notes, told.notes);
340
341
  if (told.ended.length) cs.ended = [...(cs.ended ?? []), ...told.ended];
341
342
  return cs;
342
343
  }
@@ -625,7 +626,7 @@ export class ToolsetController {
625
626
  const moved = this.presence.applyMoves(moves);
626
627
  if (!moved.length) return;
627
628
  const cs = emptyChange();
628
- cs.notes.push(...moved.map(({ name, to }) => to === 'dynamic'
629
+ appendAll(cs.notes, moved.map(({ name, to }) => to === 'dynamic'
629
630
  ? `${nameToTypeBack(name)} left your tool list: it's announced when available, and called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:${quotedValue(name)}, args}.`
630
631
  : `${nameToTypeBack(name)} is in your tool list now, as ${nameToTypeBack(this.deps.listedName(this.serverId, name))}.`));
631
632
  this.deliver(cs);
@@ -32,6 +32,7 @@ import { keyOf, type ChangeSet, type ToolPresence } from './tool-presence.js';
32
32
  import { checkArgs, jsonText, renderToolDefinition, type HintedTool } from './tool-signature.js';
33
33
  import { McplRequestError } from './server-connection.js';
34
34
  import { BUILTIN_TOOLSET, type HostPresentation } from '../host-presentation.js';
35
+ import { appendAll } from '../append-all.js';
35
36
 
36
37
  /** The namespace a reminder's stored injection carries. */
37
38
  export const TOOLSETS_REMINDER_NAMESPACE = 'mcpl-toolsets';
@@ -516,7 +517,7 @@ export class ToolsetsModule implements Module {
516
517
  return said;
517
518
  })
518
519
  : [];
519
- lines.push(...unread);
520
+ appendAll(lines, unread);
520
521
  return lines.length ? textResult(`${result.data}\n${lines.join('\n')}`) : result;
521
522
  }
522
523