@animalabs/agent-framework 0.22.1 → 0.22.2

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 (359) hide show
  1. package/README.md +1 -1
  2. package/dist/src/agent.d.ts +192 -19
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +404 -65
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/mcp-server-core.d.ts +41 -0
  7. package/dist/src/api/mcp-server-core.d.ts.map +1 -0
  8. package/dist/src/api/mcp-server-core.js +593 -0
  9. package/dist/src/api/mcp-server-core.js.map +1 -0
  10. package/dist/src/api/mcp-server.d.ts +2 -36
  11. package/dist/src/api/mcp-server.d.ts.map +1 -1
  12. package/dist/src/api/mcp-server.js +15 -595
  13. package/dist/src/api/mcp-server.js.map +1 -1
  14. package/dist/src/api/server.d.ts +9 -1
  15. package/dist/src/api/server.d.ts.map +1 -1
  16. package/dist/src/api/server.js +50 -26
  17. package/dist/src/api/server.js.map +1 -1
  18. package/dist/src/api/types.d.ts +9 -1
  19. package/dist/src/api/types.d.ts.map +1 -1
  20. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  21. package/dist/src/code-execution/py-runner.js.map +1 -1
  22. package/dist/src/framework.d.ts +393 -113
  23. package/dist/src/framework.d.ts.map +1 -1
  24. package/dist/src/framework.js +1748 -582
  25. package/dist/src/framework.js.map +1 -1
  26. package/dist/src/gate/event-gate.d.ts.map +1 -1
  27. package/dist/src/gate/gate-script.d.ts.map +1 -1
  28. package/dist/src/gate/gate-script.js.map +1 -1
  29. package/dist/src/index.d.ts +2 -2
  30. package/dist/src/index.d.ts.map +1 -1
  31. package/dist/src/index.js +2 -2
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
  34. package/dist/src/mcpl/channel-registry.d.ts +26 -7
  35. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  36. package/dist/src/mcpl/channel-registry.js +102 -22
  37. package/dist/src/mcpl/channel-registry.js.map +1 -1
  38. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  39. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  40. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  41. package/dist/src/mcpl/errors.js.map +1 -1
  42. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  43. package/dist/src/mcpl/feature-set-manager.js +4 -3
  44. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  45. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  46. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  47. package/dist/src/mcpl/log-value.d.ts +54 -0
  48. package/dist/src/mcpl/log-value.d.ts.map +1 -0
  49. package/dist/src/mcpl/log-value.js +109 -0
  50. package/dist/src/mcpl/log-value.js.map +1 -0
  51. package/dist/src/mcpl/prose-stream-router.d.ts +6 -6
  52. package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
  53. package/dist/src/mcpl/prose-stream-router.js +4 -4
  54. package/dist/src/mcpl/prose-stream-router.js.map +1 -1
  55. package/dist/src/mcpl/push-coalescer.d.ts +38 -3
  56. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  57. package/dist/src/mcpl/push-coalescer.js +81 -16
  58. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  59. package/dist/src/mcpl/push-handler.d.ts +26 -3
  60. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  61. package/dist/src/mcpl/push-handler.js +87 -23
  62. package/dist/src/mcpl/push-handler.js.map +1 -1
  63. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  64. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  65. package/dist/src/mcpl/references.d.ts +2 -2
  66. package/dist/src/mcpl/references.d.ts.map +1 -1
  67. package/dist/src/mcpl/references.js.map +1 -1
  68. package/dist/src/mcpl/refusal-lines.d.ts +37 -0
  69. package/dist/src/mcpl/refusal-lines.d.ts.map +1 -0
  70. package/dist/src/mcpl/refusal-lines.js +61 -0
  71. package/dist/src/mcpl/refusal-lines.js.map +1 -0
  72. package/dist/src/mcpl/scope-manager.d.ts.map +1 -1
  73. package/dist/src/mcpl/server-connection.d.ts +18 -2
  74. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  75. package/dist/src/mcpl/server-connection.js +66 -11
  76. package/dist/src/mcpl/server-connection.js.map +1 -1
  77. package/dist/src/mcpl/server-registry.d.ts.map +1 -1
  78. package/dist/src/mcpl/tool-classes.d.ts +1 -1
  79. package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
  80. package/dist/src/mcpl/tool-classes.js +3 -2
  81. package/dist/src/mcpl/tool-classes.js.map +1 -1
  82. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  83. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  84. package/dist/src/mcpl/transport.d.ts +23 -5
  85. package/dist/src/mcpl/transport.d.ts.map +1 -1
  86. package/dist/src/mcpl/transport.js +103 -10
  87. package/dist/src/mcpl/transport.js.map +1 -1
  88. package/dist/src/mcpl/types.d.ts +30 -26
  89. package/dist/src/mcpl/types.d.ts.map +1 -1
  90. package/dist/src/mcpl/types.js.map +1 -1
  91. package/dist/src/module-registry.d.ts.map +1 -1
  92. package/dist/src/module-registry.js.map +1 -1
  93. package/dist/src/modules/api/index.d.ts.map +1 -1
  94. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
  95. package/dist/src/modules/discord/discord-js-client.js.map +1 -1
  96. package/dist/src/modules/discord/index.d.ts.map +1 -1
  97. package/dist/src/modules/discord/index.js +4 -2
  98. package/dist/src/modules/discord/index.js.map +1 -1
  99. package/dist/src/modules/health/index.d.ts.map +1 -1
  100. package/dist/src/modules/health/index.js.map +1 -1
  101. package/dist/src/modules/history/index.d.ts.map +1 -1
  102. package/dist/src/modules/history/index.js.map +1 -1
  103. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  104. package/dist/src/modules/history/semantic.js.map +1 -1
  105. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  106. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  107. package/dist/src/modules/workspace/disk-agreement.d.ts +22 -4
  108. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  109. package/dist/src/modules/workspace/disk-agreement.js +63 -25
  110. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  111. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  112. package/dist/src/modules/workspace/index.d.ts +8 -4
  113. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  114. package/dist/src/modules/workspace/index.js +39 -12
  115. package/dist/src/modules/workspace/index.js.map +1 -1
  116. package/dist/src/modules/workspace/observe.d.ts +43 -8
  117. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  118. package/dist/src/modules/workspace/observe.js +264 -39
  119. package/dist/src/modules/workspace/observe.js.map +1 -1
  120. package/dist/src/modules/workspace/reconcile.d.ts +17 -1
  121. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  122. package/dist/src/modules/workspace/reconcile.js +57 -14
  123. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  124. package/dist/src/modules/workspace/types.d.ts +27 -2
  125. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  126. package/dist/src/modules/workspace/types.js.map +1 -1
  127. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  128. package/dist/src/modules/workspace/watcher.js.map +1 -1
  129. package/dist/src/operator-log.d.ts.map +1 -1
  130. package/dist/src/operator-log.js.map +1 -1
  131. package/dist/src/provider-waits.d.ts.map +1 -1
  132. package/dist/src/provider-waits.js.map +1 -1
  133. package/dist/src/queue.d.ts.map +1 -1
  134. package/dist/src/record-journal.d.ts.map +1 -1
  135. package/dist/src/record-journal.js.map +1 -1
  136. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  137. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  138. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  139. package/dist/src/recovery/offline-branch.js +29 -24
  140. package/dist/src/recovery/offline-branch.js.map +1 -1
  141. package/dist/src/recovery/recover-cli.js.map +1 -1
  142. package/dist/src/runtime/liveness-watchdog.d.ts.map +1 -1
  143. package/dist/src/runtime/liveness-watchdog.js.map +1 -1
  144. package/dist/src/shutdown.d.ts.map +1 -1
  145. package/dist/src/shutdown.js.map +1 -1
  146. package/dist/src/stored-removal.d.ts +73 -0
  147. package/dist/src/stored-removal.d.ts.map +1 -0
  148. package/dist/src/stored-removal.js +100 -0
  149. package/dist/src/stored-removal.js.map +1 -0
  150. package/dist/src/thinking-token-stamp.js.map +1 -1
  151. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  152. package/dist/src/tool-image-ledger.js.map +1 -1
  153. package/dist/src/tool-presentation.d.ts.map +1 -1
  154. package/dist/src/tool-presentation.js.map +1 -1
  155. package/dist/src/tool-result-guard.d.ts.map +1 -1
  156. package/dist/src/tool-result-guard.js.map +1 -1
  157. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  158. package/dist/src/tune-out/coordinator.js.map +1 -1
  159. package/dist/src/tune-out/tools.d.ts +1 -1
  160. package/dist/src/tune-out/tools.d.ts.map +1 -1
  161. package/dist/src/types/agent.d.ts +42 -6
  162. package/dist/src/types/agent.d.ts.map +1 -1
  163. package/dist/src/types/framework.d.ts +10 -0
  164. package/dist/src/types/framework.d.ts.map +1 -1
  165. package/dist/src/types/module.d.ts +8 -4
  166. package/dist/src/types/module.d.ts.map +1 -1
  167. package/dist/src/types/trace.d.ts +61 -10
  168. package/dist/src/types/trace.d.ts.map +1 -1
  169. package/dist/src/usage/usage-tracker.d.ts.map +1 -1
  170. package/dist/test/agent-runtime-settings.test.js +1139 -3
  171. package/dist/test/agent-runtime-settings.test.js.map +1 -1
  172. package/dist/test/batched-wake-locus.test.js.map +1 -1
  173. package/dist/test/channel-label-history.test.js.map +1 -1
  174. package/dist/test/code-execution.test.js.map +1 -1
  175. package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
  176. package/dist/test/conversation-router.test.js.map +1 -1
  177. package/dist/test/conversation-routing.test.js +240 -6
  178. package/dist/test/conversation-routing.test.js.map +1 -1
  179. package/dist/test/discord-awareness-outbox.test.js +104 -0
  180. package/dist/test/discord-awareness-outbox.test.js.map +1 -1
  181. package/dist/test/discord-typing-keepalive.test.d.ts +2 -0
  182. package/dist/test/discord-typing-keepalive.test.d.ts.map +1 -0
  183. package/dist/test/discord-typing-keepalive.test.js +77 -0
  184. package/dist/test/discord-typing-keepalive.test.js.map +1 -0
  185. package/dist/test/ephemeral-settle.test.js.map +1 -1
  186. package/dist/test/event-gate-passthrough.test.js.map +1 -1
  187. package/dist/test/event-gate-policy-mutation.test.js.map +1 -1
  188. package/dist/test/event-gate-rate-limit.test.js.map +1 -1
  189. package/dist/test/event-gate-script.test.js.map +1 -1
  190. package/dist/test/event-gate-selfwake.test.js.map +1 -1
  191. package/dist/test/event-gate-tags.test.js.map +1 -1
  192. package/dist/test/event-gate.test.js.map +1 -1
  193. package/dist/test/explicit-prose-routing.test.js +363 -42
  194. package/dist/test/explicit-prose-routing.test.js.map +1 -1
  195. package/dist/test/framework.test.js.map +1 -1
  196. package/dist/test/helpers/coalescing-fixture.d.ts +2 -0
  197. package/dist/test/helpers/coalescing-fixture.d.ts.map +1 -1
  198. package/dist/test/helpers/coalescing-fixture.js +8 -2
  199. package/dist/test/helpers/coalescing-fixture.js.map +1 -1
  200. package/dist/test/helpers/mock-membrane.d.ts.map +1 -1
  201. package/dist/test/helpers/mock-membrane.js.map +1 -1
  202. package/dist/test/hide-then-turn.test.d.ts +2 -0
  203. package/dist/test/hide-then-turn.test.d.ts.map +1 -0
  204. package/dist/test/hide-then-turn.test.js +101 -0
  205. package/dist/test/hide-then-turn.test.js.map +1 -0
  206. package/dist/test/history-module-overview.test.js.map +1 -1
  207. package/dist/test/history-module-semantic.test.js.map +1 -1
  208. package/dist/test/history-module.test.js.map +1 -1
  209. package/dist/test/history-semantic-review-findings.test.js.map +1 -1
  210. package/dist/test/host-quiesce.test.js +125 -0
  211. package/dist/test/host-quiesce.test.js.map +1 -1
  212. package/dist/test/inference-exhaustion-rewind.test.js +81 -3
  213. package/dist/test/inference-exhaustion-rewind.test.js.map +1 -1
  214. package/dist/test/inference-failure-bounds.test.js +1 -0
  215. package/dist/test/inference-failure-bounds.test.js.map +1 -1
  216. package/dist/test/kv-unified-open-flight-retry.test.js.map +1 -1
  217. package/dist/test/liveness-watchdog.test.js.map +1 -1
  218. package/dist/test/maintenance-timer.test.js +1564 -2
  219. package/dist/test/maintenance-timer.test.js.map +1 -1
  220. package/dist/test/mcp-server-bin.test.d.ts +2 -0
  221. package/dist/test/mcp-server-bin.test.d.ts.map +1 -0
  222. package/dist/test/mcp-server-bin.test.js +142 -0
  223. package/dist/test/mcp-server-bin.test.js.map +1 -0
  224. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
  225. package/dist/test/mcpl-child-env.test.js +1 -1
  226. package/dist/test/mcpl-child-env.test.js.map +1 -1
  227. package/dist/test/mcpl-coalesced-content-parity.test.d.ts +2 -0
  228. package/dist/test/mcpl-coalesced-content-parity.test.d.ts.map +1 -0
  229. package/dist/test/mcpl-coalesced-content-parity.test.js +532 -0
  230. package/dist/test/mcpl-coalesced-content-parity.test.js.map +1 -0
  231. package/dist/test/mcpl-coalescing.test.js.map +1 -1
  232. package/dist/test/mcpl-config-diagnostics.test.js.map +1 -1
  233. package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
  234. package/dist/test/mcpl-manifest-changed.test.js.map +1 -1
  235. package/dist/test/mcpl-reconnect-events.test.js.map +1 -1
  236. package/dist/test/mcpl-reconnect-reregistration.test.js +2 -0
  237. package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
  238. package/dist/test/mcpl-reference-fetcher.test.js.map +1 -1
  239. package/dist/test/mcpl-reference-fields.test.js.map +1 -1
  240. package/dist/test/mcpl-rejection-log-lines.test.d.ts +2 -0
  241. package/dist/test/mcpl-rejection-log-lines.test.d.ts.map +1 -0
  242. package/dist/test/mcpl-rejection-log-lines.test.js +349 -0
  243. package/dist/test/mcpl-rejection-log-lines.test.js.map +1 -0
  244. package/dist/test/mcpl-stderr-tail.test.d.ts +2 -0
  245. package/dist/test/mcpl-stderr-tail.test.d.ts.map +1 -0
  246. package/dist/test/mcpl-stderr-tail.test.js +210 -0
  247. package/dist/test/mcpl-stderr-tail.test.js.map +1 -0
  248. package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +1 -1
  249. package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -1
  250. package/dist/test/mcpl-tool-lifecycle-script.test.js.map +1 -1
  251. package/dist/test/mcpl-tool-lifecycle.test.js.map +1 -1
  252. package/dist/test/mcpl-transport-close.test.d.ts +2 -0
  253. package/dist/test/mcpl-transport-close.test.d.ts.map +1 -0
  254. package/dist/test/mcpl-transport-close.test.js +373 -0
  255. package/dist/test/mcpl-transport-close.test.js.map +1 -0
  256. package/dist/test/mcpl-transport-spawn.test.d.ts +2 -0
  257. package/dist/test/mcpl-transport-spawn.test.d.ts.map +1 -0
  258. package/dist/test/mcpl-transport-spawn.test.js +58 -0
  259. package/dist/test/mcpl-transport-spawn.test.js.map +1 -0
  260. package/dist/test/mcpl-ws-transport.test.js +1 -0
  261. package/dist/test/mcpl-ws-transport.test.js.map +1 -1
  262. package/dist/test/offline-recovery-branch.test.js +344 -0
  263. package/dist/test/offline-recovery-branch.test.js.map +1 -1
  264. package/dist/test/operator-surgery.test.js +197 -0
  265. package/dist/test/operator-surgery.test.js.map +1 -1
  266. package/dist/test/overbudget-drain-breaker.test.js +1 -0
  267. package/dist/test/overbudget-drain-breaker.test.js.map +1 -1
  268. package/dist/test/pack-installs-alone.test.d.ts +2 -0
  269. package/dist/test/pack-installs-alone.test.d.ts.map +1 -0
  270. package/dist/test/pack-installs-alone.test.js +284 -0
  271. package/dist/test/pack-installs-alone.test.js.map +1 -0
  272. package/dist/test/physical-window-restart.test.d.ts +6 -4
  273. package/dist/test/physical-window-restart.test.d.ts.map +1 -1
  274. package/dist/test/physical-window-restart.test.js +275 -10
  275. package/dist/test/physical-window-restart.test.js.map +1 -1
  276. package/dist/test/placed-reaction-baseline.test.js.map +1 -1
  277. package/dist/test/present-while-acting.test.js +949 -6
  278. package/dist/test/present-while-acting.test.js.map +1 -1
  279. package/dist/test/provider-acceleration-cooldown.test.js.map +1 -1
  280. package/dist/test/provider-waits.test.js +67 -4
  281. package/dist/test/provider-waits.test.js.map +1 -1
  282. package/dist/test/puppet-tool-call.test.js +2 -0
  283. package/dist/test/puppet-tool-call.test.js.map +1 -1
  284. package/dist/test/reaction-baseline-platform.test.js.map +1 -1
  285. package/dist/test/record-journal.test.js.map +1 -1
  286. package/dist/test/refusal-rewind.test.js +9 -0
  287. package/dist/test/refusal-rewind.test.js.map +1 -1
  288. package/dist/test/refusal-single-call.test.js.map +1 -1
  289. package/dist/test/release-bump.test.d.ts +2 -0
  290. package/dist/test/release-bump.test.d.ts.map +1 -0
  291. package/dist/test/release-bump.test.js +248 -0
  292. package/dist/test/release-bump.test.js.map +1 -0
  293. package/dist/test/release-changelog.test.js.map +1 -1
  294. package/dist/test/release-holds.test.d.ts +2 -0
  295. package/dist/test/release-holds.test.d.ts.map +1 -0
  296. package/dist/test/release-holds.test.js +456 -0
  297. package/dist/test/release-holds.test.js.map +1 -0
  298. package/dist/test/same-round-think-policy.test.js.map +1 -1
  299. package/dist/test/shutdown-errors.test.js.map +1 -1
  300. package/dist/test/silent-heartbeat-rows.test.js.map +1 -1
  301. package/dist/test/stop-with-host-operations.test.d.ts +20 -0
  302. package/dist/test/stop-with-host-operations.test.d.ts.map +1 -0
  303. package/dist/test/stop-with-host-operations.test.js +1174 -0
  304. package/dist/test/stop-with-host-operations.test.js.map +1 -0
  305. package/dist/test/stop-with-work-in-flight.test.js +162 -50
  306. package/dist/test/stop-with-work-in-flight.test.js.map +1 -1
  307. package/dist/test/surgery-marks-receipt.test.js.map +1 -1
  308. package/dist/test/tool-batch-hook.test.js.map +1 -1
  309. package/dist/test/tool-result-guard.test.js +131 -9
  310. package/dist/test/tool-result-guard.test.js.map +1 -1
  311. package/dist/test/tool-result-spill.test.js.map +1 -1
  312. package/dist/test/tool-wrapper-prose-guard-integration.test.js.map +1 -1
  313. package/dist/test/trunk-channel-routing.test.js +330 -7
  314. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  315. package/dist/test/tune-out-e2e.test.js.map +1 -1
  316. package/dist/test/unconvertible-tool-result.test.js.map +1 -1
  317. package/dist/test/unsent-compiles.test.d.ts +2 -0
  318. package/dist/test/unsent-compiles.test.d.ts.map +1 -0
  319. package/dist/test/unsent-compiles.test.js +139 -0
  320. package/dist/test/unsent-compiles.test.js.map +1 -0
  321. package/dist/test/workspace-branch-continuation.test.js.map +1 -1
  322. package/dist/test/workspace-materialize-freshness.test.js.map +1 -1
  323. package/dist/test/workspace-read-text.test.js.map +1 -1
  324. package/dist/test/workspace-three-way.test.js +907 -7
  325. package/dist/test/workspace-three-way.test.js.map +1 -1
  326. package/dist/test/workspace-watcher.test.js.map +1 -1
  327. package/dist/test/xml-round-persist.test.js.map +1 -1
  328. package/dist/tsconfig.tsbuildinfo +1 -1
  329. package/package.json +8 -6
  330. package/src/agent.ts +411 -66
  331. package/src/api/mcp-server-core.ts +681 -0
  332. package/src/api/mcp-server.ts +16 -685
  333. package/src/api/server.ts +51 -29
  334. package/src/api/types.ts +9 -1
  335. package/src/framework.ts +1899 -641
  336. package/src/index.ts +2 -2
  337. package/src/mcpl/channel-registry.ts +126 -31
  338. package/src/mcpl/feature-set-manager.ts +4 -3
  339. package/src/mcpl/log-value.ts +116 -0
  340. package/src/mcpl/prose-stream-router.ts +6 -6
  341. package/src/mcpl/push-coalescer.ts +76 -12
  342. package/src/mcpl/push-handler.ts +95 -23
  343. package/src/mcpl/refusal-lines.ts +59 -0
  344. package/src/mcpl/server-connection.ts +70 -11
  345. package/src/mcpl/tool-classes.ts +3 -2
  346. package/src/mcpl/transport.ts +105 -14
  347. package/src/mcpl/types.ts +5 -1
  348. package/src/modules/discord/index.ts +4 -2
  349. package/src/modules/workspace/disk-agreement.ts +74 -18
  350. package/src/modules/workspace/index.ts +41 -11
  351. package/src/modules/workspace/observe.ts +265 -39
  352. package/src/modules/workspace/reconcile.ts +71 -14
  353. package/src/modules/workspace/types.ts +26 -1
  354. package/src/recovery/offline-branch.ts +32 -28
  355. package/src/stored-removal.ts +126 -0
  356. package/src/types/agent.ts +42 -6
  357. package/src/types/framework.ts +10 -0
  358. package/src/types/module.ts +8 -4
  359. package/src/types/trace.ts +62 -11
@@ -18,8 +18,10 @@ import type {
18
18
  import type { FeatureSetManager } from './feature-set-manager.js';
19
19
  import { McplFeatureSetError } from './feature-set-manager.js';
20
20
  import { expandCoreTags } from './tags.js';
21
- import { EmptyContentError, validateCoalescedContent } from './push-coalescer.js';
21
+ import { coalesceRejectionReason, EmptyContentError, validateCoalescedContent } from './push-coalescer.js';
22
22
  import { isSilentHeartbeatMarker, isVisiblyEmptyContent } from './visible-content.js';
23
+ import { logDetail, logValue } from './log-value.js';
24
+ import { RefusalLines } from './refusal-lines.js';
23
25
 
24
26
  // ============================================================================
25
27
  // McplPushEvent (the ProcessEvent shape pushed to the queue)
@@ -55,13 +57,17 @@ export interface McplPushEvent {
55
57
  // ============================================================================
56
58
 
57
59
  /**
58
- * Convert a single MCPL wire-format content block to a membrane ContentBlock.
59
- * Same logic as hook-orchestrator.ts.
60
+ * Convert a single MCPL wire-format content block to a membrane ContentBlock,
61
+ * as the channel lane's convertBlock (channel-registry.ts) does, apart from
62
+ * the provenance its reference stubs name. Hook context injections convert
63
+ * through hook-orchestrator.ts's own.
60
64
  */
61
65
  export function convertBlock(block: McplContentBlock): ContentBlock {
62
66
  switch (block.type) {
63
67
  case 'text':
64
- return { type: 'text', text: block.text };
68
+ // Text that isn't a string is no text, as the request sanitizer and
69
+ // the visible-content check already read it.
70
+ return { type: 'text', text: typeof block.text === 'string' ? block.text : '' };
65
71
 
66
72
  case 'image':
67
73
  if (isInlineContradiction(block)) {
@@ -69,24 +75,24 @@ export function convertBlock(block: McplContentBlock): ContentBlock {
69
75
  // closed, withhold the data (checked BEFORE the data branch).
70
76
  return { type: 'text', text: INLINE_WITHHELD_TEXT };
71
77
  }
72
- if (block.uri && block.disposition) {
78
+ if (isFilledString(block.uri) && block.disposition) {
73
79
  // RFC-005 uri-form media with a disposition claim: stub, do not
74
80
  // hand the URI to the provider or inline it.
75
81
  return { type: 'text', text: referenceStubOrNull(block, 'attachment on push event') ?? '[reference]' };
76
82
  }
77
- if (block.data && block.mimeType) {
83
+ if (isFilledString(block.data) && isFilledString(block.mimeType)) {
78
84
  return {
79
85
  type: 'image',
80
86
  source: { type: 'base64', data: block.data, mediaType: block.mimeType },
81
87
  } as ContentBlock;
82
88
  }
83
- if (block.uri) {
89
+ if (isFilledString(block.uri)) {
84
90
  return {
85
91
  type: 'image',
86
92
  source: { type: 'url', url: block.uri },
87
93
  } as ContentBlock;
88
94
  }
89
- return { type: 'text', text: '[Image: no data]' };
95
+ return { type: 'text', text: isFilledString(block.data) ? '[Image: no media type]' : '[Image: no data]' };
90
96
 
91
97
  case 'audio':
92
98
  if (isInlineContradiction(block)) {
@@ -94,31 +100,43 @@ export function convertBlock(block: McplContentBlock): ContentBlock {
94
100
  // closed, withhold the data (checked BEFORE the data branch).
95
101
  return { type: 'text', text: INLINE_WITHHELD_TEXT };
96
102
  }
97
- if (block.uri && block.disposition) {
103
+ if (isFilledString(block.uri) && block.disposition) {
98
104
  // RFC-005 uri-form media with a disposition claim: stub, do not
99
105
  // hand the URI to the provider or inline it.
100
106
  return { type: 'text', text: referenceStubOrNull(block, 'attachment on push event') ?? '[reference]' };
101
107
  }
102
- if (block.data && block.mimeType) {
108
+ if (isFilledString(block.data) && isFilledString(block.mimeType)) {
103
109
  return {
104
110
  type: 'audio',
105
111
  source: { type: 'base64', data: block.data, mediaType: block.mimeType },
106
112
  } as ContentBlock;
107
113
  }
108
- return { type: 'text', text: '[Audio: no data]' };
114
+ return { type: 'text', text: isFilledString(block.data) ? '[Audio: no media type]' : '[Audio: no data]' };
109
115
 
110
116
  case 'resource':
111
117
  // RFC-005: reference blocks become bounded stubs — never raw URIs
112
118
  // (a signed URL is a bearer credential that looks like a location).
113
119
  return { type: 'text', text: referenceStubOrNull(block, 'attachment on push event') ?? '[reference]' };
114
120
 
115
- default:
121
+ default: {
116
122
  // Unknown wire block types previously fell off the exhaustive switch
117
- // and propagated `undefined` into ContentBlock[]. Fail visibly.
118
- return { type: 'text', text: `[unrecognized content block: ${(block as { type?: string }).type ?? 'untyped'}]` };
123
+ // and propagated `undefined` into ContentBlock[]. Fail visibly. A type
124
+ // that isn't a string is no type, and is never converted to one: an
125
+ // object can throw when it is.
126
+ const type: unknown = (block as { type?: unknown }).type;
127
+ return { type: 'text', text: `[unrecognized content block: ${typeof type === 'string' ? type : 'untyped'}]` };
128
+ }
119
129
  }
120
130
  }
121
131
 
132
+ /** A string with something in it: the only data, mimeType or uri a block's
133
+ * source is built from, and the only uri that makes it a reference. Anything
134
+ * else a sender puts there leaves the block without it, so conversion never
135
+ * throws and never copies a non-string. */
136
+ function isFilledString(value: unknown): value is string {
137
+ return typeof value === 'string' && value !== '';
138
+ }
139
+
122
140
  // ============================================================================
123
141
  // LRU Dedup Set
124
142
  // ============================================================================
@@ -171,6 +189,9 @@ export class PushHandler {
171
189
  private emitTraceFn: (event: { type: string; [key: string]: unknown }) => void;
172
190
  private shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean;
173
191
  private dedup = new DedupSet(1000);
192
+ /** Holds back repeats of the line for a channel-scoped push refused because
193
+ * its channel isn't declared (-32023) or may not be used (-32017). */
194
+ private channelRefusalLines = new RefusalLines();
174
195
 
175
196
  constructor(
176
197
  featureSetManager: FeatureSetManager,
@@ -221,7 +242,7 @@ export class PushHandler {
221
242
  : 'Feature set validation failed';
222
243
  // Loud rejection — a rejected push event is an agent that silently
223
244
  // never hears the message. (2026-07-09 diagnosability pass.)
224
- console.error(`[push-event-rejected] server=${serverId} eventId=${params.eventId} reason=${reason}`);
245
+ console.error(`[push-event-rejected] server=${serverId} eventId=${logValue(params.eventId ?? '')} reason=${logValue(reason)}`);
225
246
  if (err instanceof McplFeatureSetError && responder?.respondError) {
226
247
  responder.respondError(err.code, reason, { featureSet: err.featureSet });
227
248
  } else {
@@ -240,11 +261,13 @@ export class PushHandler {
240
261
  if (coalesced) {
241
262
  // RFC-006 §13: malformed content is a -32602, checked before conversion.
242
263
  try {
243
- validateCoalescedContent(params.payload?.content, undefined, { allowEmpty: emptyAllowed });
264
+ validateCoalescedContent(params.payload?.content, { allowEmpty: emptyAllowed });
244
265
  } catch (error) {
245
266
  const err = error as Error & { code?: number; field?: string };
267
+ const code = err.code ?? -32602;
246
268
  if (err instanceof EmptyContentError) this.traceEmptyRejection(serverId, params);
247
- if (responder?.respondError) responder.respondError(err.code ?? -32602, err.message, { field: err.field });
269
+ else this.traceCoalesceRejection(serverId, params, err, code);
270
+ if (responder?.respondError) responder.respondError(code, err.message, { field: err.field });
248
271
  else responder?.respond({ accepted: false, reason: err instanceof EmptyContentError ? err.reason : err.message });
249
272
  return;
250
273
  }
@@ -265,7 +288,7 @@ export class PushHandler {
265
288
  // coalescer's receipts instead (RFC-006 §3.1: a retry within the window
266
289
  // gets its original result, which this set could not return).
267
290
  if (!coalesced && this.dedup.checkAndAdd(params.eventId)) {
268
- console.error(`[push-event-rejected] server=${serverId} eventId=${params.eventId} reason=duplicate`);
291
+ console.error(`[push-event-rejected] server=${serverId} eventId=${logValue(params.eventId ?? '')} reason=duplicate`);
269
292
  responder?.respond({ accepted: false, reason: 'duplicate' });
270
293
  return;
271
294
  }
@@ -314,8 +337,9 @@ export class PushHandler {
314
337
  responder?.respond(result);
315
338
  } catch (error) {
316
339
  const err = error as Error & { code?: number; field?: string };
317
- console.error(`[push-event-rejected] server=${serverId} eventId=${params.eventId} reason=${err.message}`);
318
- if (responder?.respondError) responder.respondError(typeof err.code === 'number' ? err.code : -32603, err.message, { field: err.field });
340
+ const code = typeof err.code === 'number' ? err.code : -32603;
341
+ this.traceCoalesceRejection(serverId, params, err, code);
342
+ if (responder?.respondError) responder.respondError(code, err.message, { field: err.field });
319
343
  else responder?.respond({ accepted: false, reason: err.message });
320
344
  }
321
345
  return;
@@ -334,13 +358,61 @@ export class PushHandler {
334
358
  responder?.respond({ accepted: true, inferenceId });
335
359
  }
336
360
 
337
- /** Loud, like every other push rejection: the producer's wake went nowhere. */
361
+ /**
362
+ * A coalesced occurrence the host refused, as loud as every other push
363
+ * rejection: its producer is told on the wire, and until now the host said
364
+ * nothing, so a dropped message left no trace where operators look
365
+ * (agent-framework#266). The producer gets the wire answer this lane gives
366
+ * (`answered`): the thrown error's code and message, or -32602 for a
367
+ * malformed occurrence without one. The reason follows that answer
368
+ * (coalesceRejectionReason): `coalesce-failed` when the host itself failed,
369
+ * `coalesce-invalid` when the producer's occurrence was refused, with the
370
+ * failing `field` and the error's message as `detail`. A missing eventId
371
+ * prints as `""` and traces as `''`, as the channel lane's missing id does.
372
+ * A channel-scoped push refused because its channel isn't declared or may
373
+ * not be used names the channel on its line and its trace, and is refused
374
+ * for as long as that lasts, so its line prints at most once a minute per
375
+ * channel (RefusalLines); every refusal is traced.
376
+ */
377
+ private traceCoalesceRejection(
378
+ serverId: string,
379
+ params: PushEventParams,
380
+ error: { field?: unknown; message: string },
381
+ answered: number,
382
+ ): void {
383
+ const reason = coalesceRejectionReason(answered);
384
+ const field = typeof error.field === 'string' && error.field ? error.field : undefined;
385
+ const scoped = params.coalesce?.channelId;
386
+ const channelId = (answered === -32023 || answered === -32017) && typeof scoped === 'string' ? scoped : undefined;
387
+ const held = channelId === undefined ? 0 : this.channelRefusalLines.admit(serverId, channelId);
388
+ if (held !== null) {
389
+ console.error(
390
+ `[push-event-rejected] server=${serverId} featureSet=${logValue(params.featureSet ?? '')} eventId=${logValue(params.eventId ?? '')} ` +
391
+ (channelId === undefined ? '' : `channel=${logValue(channelId)} `) +
392
+ `reason=${reason}${field ? ` field=${logValue(field)}` : ''}: ${logDetail(error.message)}` +
393
+ (held ? ` (${held} more since the last line)` : ''),
394
+ );
395
+ }
396
+ this.emitTraceFn({
397
+ type: 'mcpl:push-event-rejected',
398
+ serverId,
399
+ eventId: typeof params.eventId === 'string' ? params.eventId : '',
400
+ featureSet: params.featureSet,
401
+ reason,
402
+ ...(field ? { field } : {}),
403
+ ...(channelId === undefined ? {} : { channelId }),
404
+ detail: error.message,
405
+ });
406
+ }
407
+
408
+ /** Loud, like every other push rejection: the producer's wake went nowhere.
409
+ * A missing eventId prints as `""` and traces as `''`, as on every push line. */
338
410
  private traceEmptyRejection(serverId: string, params: PushEventParams): void {
339
- console.error(`[push-event-rejected] server=${serverId} eventId=${params.eventId} reason=empty-content`);
411
+ console.error(`[push-event-rejected] server=${serverId} eventId=${logValue(params.eventId ?? '')} reason=empty-content`);
340
412
  this.emitTraceFn({
341
413
  type: 'mcpl:push-event-rejected',
342
414
  serverId,
343
- eventId: params.eventId,
415
+ eventId: typeof params.eventId === 'string' ? params.eventId : '',
344
416
  featureSet: params.featureSet,
345
417
  reason: 'empty-content',
346
418
  });
@@ -0,0 +1,59 @@
1
+ import { logValue } from './log-value.js';
2
+
3
+ /**
4
+ * At most one host line a minute for a refusal a server keeps repeating for
5
+ * as long as a standing condition lasts: messages for a channel it never
6
+ * registered, or channel-scoped pushes to a channel it hasn't declared or may
7
+ * not use. A server that never registered a busy channel would otherwise
8
+ * print a line for every message in it. Only the line is held back: every
9
+ * refusal is still answered and traced. Each lane keeps its own records, as
10
+ * its lines are its own.
11
+ *
12
+ * Per server and channel, the first refusal prints. After that a refusal
13
+ * prints only once a minute has passed since that channel's last line, and
14
+ * its line names how many were held back since. The minute is timed with a
15
+ * monotonic clock, so a step of the wall clock can't silence a channel. A
16
+ * refusal within a minute of the channel's last line is counted into its
17
+ * next line, whatever happens to the channel in between (it may be
18
+ * registered, unregistered or reconnected).
19
+ *
20
+ * A record is keyed by the channel as its line prints it (logValue), so two
21
+ * ids share one exactly when their lines name them alike, which only ids
22
+ * over 256 units can. The records cover the 1,000 most recently refused
23
+ * channels: a new channel evicts the least recently refused one, with its
24
+ * minute and any count it held, and that channel's next refusal prints as a
25
+ * first.
26
+ */
27
+ export class RefusalLines {
28
+ /** By key, from the least recently refused channel to the most. */
29
+ private readonly records = new Map<string, { at: number; since: number }>();
30
+
31
+ constructor(private readonly now: () => number = () => performance.now()) {}
32
+
33
+ /**
34
+ * Record one refusal for a server's channel. Returns null when its line is
35
+ * held back, or else how many of the channel's refusals were held back
36
+ * since its last line (0 for its first).
37
+ */
38
+ admit(serverId: string, channelId: string): number | null {
39
+ // logValue never prints a NUL, so the key's last NUL keeps the two apart
40
+ // whatever a server id holds.
41
+ const key = `${serverId}\u0000${logValue(channelId)}`;
42
+ const now = this.now();
43
+ const record = this.records.get(key);
44
+ if (record) this.records.delete(key);
45
+ else if (this.records.size >= MAX_CHANNELS) this.records.delete(this.records.keys().next().value!);
46
+ if (!record || now - record.at >= EVERY_MS) {
47
+ this.records.set(key, { at: now, since: 0 });
48
+ return record?.since ?? 0;
49
+ }
50
+ record.since++;
51
+ this.records.set(key, record);
52
+ return null;
53
+ }
54
+ }
55
+
56
+ /** A channel's line prints at most this often. */
57
+ const EVERY_MS = 60_000;
58
+ /** The channels the records cover at once. */
59
+ const MAX_CHANNELS = 1_000;
@@ -15,6 +15,7 @@ import { openTransport, type McplTransport, type TransportCloseInfo } from './tr
15
15
  import { maskNegotiatedCapabilities } from './capability-mask.js';
16
16
  import { CapabilityGrant, expandAdvertisementShorthand } from './capability-grant.js';
17
17
  import { CAPABILITY_DISABLED } from './errors.js';
18
+ import { logDetail, logValue } from './log-value.js';
18
19
 
19
20
  import type {
20
21
  McplServerConfig,
@@ -65,6 +66,9 @@ export interface McplConnectionCloseInfo {
65
66
  signal: NodeJS.Signals | null;
66
67
  /** WebSocket close reason, or a short description of the cause. */
67
68
  reason?: string;
69
+ /** True when `reason` is the peer's own text, a WebSocket server's close
70
+ * reason, rather than the transport's or the host's words. */
71
+ reasonFromPeer?: boolean;
68
72
  /** True when the host closed the link (`close()` / `reconnectAfterFailure()`),
69
73
  * false when the transport dropped unexpectedly. */
70
74
  byHost: boolean;
@@ -74,6 +78,23 @@ export interface McplConnectionCloseInfo {
74
78
  * Spring Boot + JDA servers can take 5-10s to boot, so 30s is safe. */
75
79
  const INITIALIZE_TIMEOUT_MS = 30_000;
76
80
 
81
+ /** A failed connect's `stderrTail`, the last of the server's stderr that its
82
+ * handshake's error keeps, when there is one. */
83
+ export function stderrTailOf(error: unknown): string | undefined {
84
+ const tail = (error as { stderrTail?: unknown } | null | undefined)?.stderrTail;
85
+ return typeof tail === 'string' && tail !== '' ? tail : undefined;
86
+ }
87
+
88
+ /** For the host's own log line about a failed connect: the server's stderr
89
+ * that the error keeps as `stderrTail`, one marked line per line of it,
90
+ * each through log-value, since it's the server's text. '' when there's
91
+ * none. */
92
+ export function withStderr(error: unknown): string {
93
+ const tail = stderrTailOf(error);
94
+ if (tail === undefined) return '';
95
+ return '. The last of its stderr:' + tail.split('\n').map((line) => `\n stderr: ${logDetail(line)}`).join('');
96
+ }
97
+
77
98
  /** Default per-request timeout in milliseconds (see McplServerConfig.requestTimeoutMs).
78
99
  * A live-but-wedged server (accepts requests, never answers, never closes its
79
100
  * transport) would otherwise freeze the awaiting agent turn forever — no
@@ -116,8 +137,13 @@ interface RequestOptions {
116
137
  * - `'channels-incoming'` — Server sent `channels/incoming`
117
138
  * - `'error'` — Connection-level error
118
139
  * - `'close'` — Connection closed: `(code, signal, info: McplConnectionCloseInfo)`
119
- * - `'connect-failed'` — Initial connect failed; background retry scheduled
120
- * - `'reconnect-failed'` — A background reconnect attempt failed; next retry scheduled
140
+ * - `'connect-failed'` — Initial connect failed; background retry scheduled:
141
+ * `{ error, stderrTail?, attempt }`
142
+ * - `'reconnect-failed'` — A background reconnect attempt failed; next retry scheduled:
143
+ * `{ error, stderrTail?, attempt }`. `stderrTail` is the last of the
144
+ * server's stderr when its handshake failed by its transport closing, a
145
+ * transport error or its timeout and it had written any; `error` never
146
+ * carries it.
121
147
  * - `'reconnect'` — Background reconnect succeeded
122
148
  */
123
149
  export class McplServerConnection extends EventEmitter {
@@ -403,7 +429,7 @@ export class McplServerConnection extends EventEmitter {
403
429
  if (responder?.respondError) {
404
430
  responder.respondError(CAPABILITY_DISABLED, `${name} rejected: capability "${requiredCap}" ${phase}`, { capability: requiredCap });
405
431
  } else {
406
- console.error(`[mcpl] ${this.id}: discarded ${name} — "${requiredCap}" ${phase}`);
432
+ console.error(`[mcpl] ${this.id}: discarded ${logValue(name)} — "${requiredCap}" ${phase}`);
407
433
  }
408
434
  return true;
409
435
  }
@@ -531,6 +557,15 @@ export class McplServerConnection extends EventEmitter {
531
557
  hostCapabilities: McplHostCapabilities,
532
558
  ): Promise<{ transport: McplTransport; capabilities: McplCapabilities | null; droppedCapabilities: ReadonlySet<string>; mcpToolsAdvertised: boolean }> {
533
559
  const transport = await openTransport(config);
560
+ // Once the handshake stops listening for 'error', this logs what the
561
+ // transport reports, until the handshake hands the transport on, or for
562
+ // good if the handshake fails. EventEmitter throws an 'error' that has no
563
+ // listener, and from an I/O callback nothing catches it. A malformed
564
+ // WebSocket frame that arrives with the initialize response, or while a
565
+ // failed handshake's close() runs, is such an error.
566
+ const logAfterSettled = (err: Error) => {
567
+ console.error(`[mcpl] ${config.id}: transport error after the handshake settled: ${err.message}`);
568
+ };
534
569
 
535
570
  try {
536
571
  const initId = 0; // use id=0 for the handshake
@@ -548,10 +583,20 @@ export class McplServerConnection extends EventEmitter {
548
583
  // Await the initialize response, racing an early transport close/error and
549
584
  // a timeout. All three paths clean up their listeners.
550
585
  const { mcpl: rawCapabilities, mcpToolsAdvertised } = await new Promise<{ mcpl: McplCapabilities | null; mcpToolsAdvertised: boolean }>((resolve, reject) => {
586
+ // When the transport fails, the end of the server's stderr often says
587
+ // why, such as a missing token or a crash at startup. It goes on the
588
+ // error as `stderrTail`, never into the message: an error's message
589
+ // travels to traces and tool results, and a server's stderr is its
590
+ // own text, which can hold a credential.
591
+ const failed = (failure: string): Error & { stderrTail?: string } => {
592
+ const tail = transport.stderrTail.trimEnd();
593
+ return Object.assign(new Error(failure), tail ? { stderrTail: tail } : {});
594
+ };
551
595
  const cleanup = () => {
552
596
  transport.off('line', onLine);
553
597
  transport.off('close', onClose);
554
598
  transport.off('error', onError);
599
+ transport.on('error', logAfterSettled);
555
600
  clearTimeout(timer);
556
601
  };
557
602
  const onLine = (line: string) => {
@@ -580,15 +625,15 @@ export class McplServerConnection extends EventEmitter {
580
625
  };
581
626
  const onClose = (info: TransportCloseInfo) => {
582
627
  cleanup();
583
- reject(new Error(`MCPL server "${config.id}" transport closed before handshake completed (${info.reason ?? 'unknown'})`));
628
+ reject(failed(`MCPL server "${config.id}" transport closed before handshake completed (${info.reason ?? 'unknown'})`));
584
629
  };
585
630
  const onError = (err: Error) => {
586
631
  cleanup();
587
- reject(new Error(`MCPL server "${config.id}" transport error during handshake: ${err.message}`));
632
+ reject(failed(`MCPL server "${config.id}" transport error during handshake: ${err.message}`));
588
633
  };
589
634
  const timer = setTimeout(() => {
590
635
  cleanup();
591
- reject(new Error(`MCPL server "${config.id}" initialize handshake timed out`));
636
+ reject(failed(`MCPL server "${config.id}" initialize handshake timed out`));
592
637
  }, INITIALIZE_TIMEOUT_MS);
593
638
 
594
639
  transport.on('line', onLine);
@@ -610,9 +655,13 @@ export class McplServerConnection extends EventEmitter {
610
655
  // had never been advertised (hooks, streaming, queries all read this).
611
656
  const { capabilities: scoped, dropped } = maskNegotiatedCapabilities(capabilities, config);
612
657
  if (dropped.length > 0) {
613
- console.error(`MCPL server "${config.id}" capabilities masked by host config: ${dropped.join(', ')}`);
658
+ console.error(`MCPL server "${config.id}" capabilities masked by host config: ${dropped.map(logValue).join(', ')}`);
614
659
  }
615
660
 
661
+ // The caller wires the transport, or keeps it for close() with a
662
+ // listener of its own, in the continuation of this return, before any
663
+ // I/O callback can emit.
664
+ transport.off('error', logAfterSettled);
616
665
  return { transport, capabilities: scoped, droppedCapabilities: new Set(dropped), mcpToolsAdvertised };
617
666
  } catch (err) {
618
667
  // Never leak the child / socket if the handshake fails.
@@ -639,12 +688,13 @@ export class McplServerConnection extends EventEmitter {
639
688
  }
640
689
 
641
690
  // Non-blocking start: create a disconnected stub that will reconnect in background
642
- console.error(`MCPL server "${config.id}" initial connect failed, will retry:`, (error as Error).message);
691
+ console.error(`MCPL server "${config.id}" initial connect failed, will retry: ${(error as Error).message}${withStderr(error)}`);
643
692
  const stub = McplServerConnection.createDisconnectedStub(config, hostCapabilities);
644
693
  // Surface the initial failure. The event is buffered by the emit()
645
694
  // override until the host wires listeners and calls ready(), so it is
646
695
  // not lost in the window before wireMcplEvents runs.
647
- stub.emit('connect-failed', { error: (error as Error).message, attempt: 0 });
696
+ const stderrTail = stderrTailOf(error);
697
+ stub.emit('connect-failed', { error: (error as Error).message, ...(stderrTail ? { stderrTail } : {}), attempt: 0 });
648
698
  return stub;
649
699
  }
650
700
  }
@@ -989,6 +1039,13 @@ export class McplServerConnection extends EventEmitter {
989
1039
  // The prior transport was disconnected before this retry began. Keep
990
1040
  // the fresh, unadmitted transport owned by close(), including on a
991
1041
  // failed transport cleanup, instead of reviving a stopped connection.
1042
+ // It isn't wired, and the handshake has removed its own listeners, so
1043
+ // an error it reports from here on, such as a malformed frame from a
1044
+ // WebSocket server while close() closes it, is logged here rather than
1045
+ // thrown where nothing catches it.
1046
+ transport.on('error', (err: Error) => {
1047
+ console.error(`[mcpl] ${this.id}: a transport kept for close() reported an error: ${err.message}`);
1048
+ });
992
1049
  this.transport = transport;
993
1050
  return;
994
1051
  }
@@ -1013,9 +1070,10 @@ export class McplServerConnection extends EventEmitter {
1013
1070
  this.reconnectAttempts = 0;
1014
1071
  this.emit('reconnect', { attempts: attempt });
1015
1072
  } catch (error) {
1016
- console.error(`MCPL server "${this.id}" reconnect failed:`, (error as Error).message);
1073
+ console.error(`MCPL server "${this.id}" reconnect failed: ${(error as Error).message}${withStderr(error)}`);
1017
1074
  this.reconnectAttempts = attempt + 1;
1018
- this.emit('reconnect-failed', { error: (error as Error).message, attempt });
1075
+ const stderrTail = stderrTailOf(error);
1076
+ this.emit('reconnect-failed', { error: (error as Error).message, ...(stderrTail ? { stderrTail } : {}), attempt });
1019
1077
  this.scheduleReconnect();
1020
1078
  }
1021
1079
  }
@@ -1355,6 +1413,7 @@ export class McplServerConnection extends EventEmitter {
1355
1413
  code: info.code ?? null,
1356
1414
  signal: info.signal ?? null,
1357
1415
  reason: info.reason ?? (byHost ? 'closed by host' : undefined),
1416
+ ...(info.reasonFromPeer ? { reasonFromPeer: true } : {}),
1358
1417
  byHost,
1359
1418
  };
1360
1419
  this.emit('close', closeInfo.code, closeInfo.signal, closeInfo);
@@ -21,6 +21,7 @@
21
21
  */
22
22
 
23
23
  import { globMatch } from './tool-glob.js';
24
+ import { logValue } from './log-value.js';
24
25
 
25
26
  export const TOOL_CLASSES = [
26
27
  'comms',
@@ -116,7 +117,7 @@ export function parseDeclaredClasses(meta: unknown, toolName = '(unnamed)'): Too
116
117
  const raw = (meta as Record<string, unknown>)[TOOL_CLASS_META_KEY];
117
118
  if (raw === undefined) return undefined;
118
119
  if (!Array.isArray(raw)) {
119
- console.error(`[mcpl] ${toolName}: _meta["${TOOL_CLASS_META_KEY}"] is not an array — treated as unclassed`);
120
+ console.error(`[mcpl] ${logValue(toolName)}: _meta["${TOOL_CLASS_META_KEY}"] is not an array — treated as unclassed`);
120
121
  return undefined;
121
122
  }
122
123
  const out: ToolClass[] = [];
@@ -124,7 +125,7 @@ export function parseDeclaredClasses(meta: unknown, toolName = '(unnamed)'): Too
124
125
  if (isToolClass(value)) {
125
126
  if (!out.includes(value)) out.push(value);
126
127
  } else {
127
- console.error(`[mcpl] ${toolName}: unknown tool class ${JSON.stringify(value)} ignored (RFC-008 §3)`);
128
+ console.error(`[mcpl] ${logValue(toolName)}: unknown tool class ${logValue(value)} ignored (RFC-008 §3)`);
128
129
  }
129
130
  }
130
131
  return out.length > 0 ? out : undefined;