@animalabs/agent-framework 0.22.0 → 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 (418) hide show
  1. package/README.md +12 -5
  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 +407 -66
  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 +84 -23
  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 +682 -103
  23. package/dist/src/framework.d.ts.map +1 -1
  24. package/dist/src/framework.js +3202 -718
  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 +4 -4
  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/index.d.ts +2 -0
  47. package/dist/src/mcpl/index.d.ts.map +1 -1
  48. package/dist/src/mcpl/index.js.map +1 -1
  49. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  50. package/dist/src/mcpl/log-value.d.ts +54 -0
  51. package/dist/src/mcpl/log-value.d.ts.map +1 -0
  52. package/dist/src/mcpl/log-value.js +109 -0
  53. package/dist/src/mcpl/log-value.js.map +1 -0
  54. package/dist/src/mcpl/prose-stream-router.d.ts +6 -6
  55. package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
  56. package/dist/src/mcpl/prose-stream-router.js +4 -4
  57. package/dist/src/mcpl/prose-stream-router.js.map +1 -1
  58. package/dist/src/mcpl/push-coalescer.d.ts +38 -3
  59. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  60. package/dist/src/mcpl/push-coalescer.js +81 -16
  61. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  62. package/dist/src/mcpl/push-handler.d.ts +26 -3
  63. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  64. package/dist/src/mcpl/push-handler.js +87 -23
  65. package/dist/src/mcpl/push-handler.js.map +1 -1
  66. package/dist/src/mcpl/reference-fetcher.d.ts +2 -1
  67. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  68. package/dist/src/mcpl/reference-fetcher.js +20 -9
  69. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  70. package/dist/src/mcpl/references.d.ts +29 -6
  71. package/dist/src/mcpl/references.d.ts.map +1 -1
  72. package/dist/src/mcpl/references.js +57 -16
  73. package/dist/src/mcpl/references.js.map +1 -1
  74. package/dist/src/mcpl/refusal-lines.d.ts +37 -0
  75. package/dist/src/mcpl/refusal-lines.d.ts.map +1 -0
  76. package/dist/src/mcpl/refusal-lines.js +61 -0
  77. package/dist/src/mcpl/refusal-lines.js.map +1 -0
  78. package/dist/src/mcpl/scope-manager.d.ts.map +1 -1
  79. package/dist/src/mcpl/server-connection.d.ts +46 -3
  80. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  81. package/dist/src/mcpl/server-connection.js +98 -15
  82. package/dist/src/mcpl/server-connection.js.map +1 -1
  83. package/dist/src/mcpl/server-registry.d.ts.map +1 -1
  84. package/dist/src/mcpl/tool-classes.d.ts +1 -1
  85. package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
  86. package/dist/src/mcpl/tool-classes.js +3 -2
  87. package/dist/src/mcpl/tool-classes.js.map +1 -1
  88. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  89. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  90. package/dist/src/mcpl/transport.d.ts +23 -5
  91. package/dist/src/mcpl/transport.d.ts.map +1 -1
  92. package/dist/src/mcpl/transport.js +103 -10
  93. package/dist/src/mcpl/transport.js.map +1 -1
  94. package/dist/src/mcpl/types.d.ts +30 -26
  95. package/dist/src/mcpl/types.d.ts.map +1 -1
  96. package/dist/src/mcpl/types.js.map +1 -1
  97. package/dist/src/module-registry.d.ts +9 -3
  98. package/dist/src/module-registry.d.ts.map +1 -1
  99. package/dist/src/module-registry.js +3 -2
  100. package/dist/src/module-registry.js.map +1 -1
  101. package/dist/src/modules/api/index.d.ts.map +1 -1
  102. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
  103. package/dist/src/modules/discord/discord-js-client.js.map +1 -1
  104. package/dist/src/modules/discord/index.d.ts.map +1 -1
  105. package/dist/src/modules/discord/index.js +4 -2
  106. package/dist/src/modules/discord/index.js.map +1 -1
  107. package/dist/src/modules/health/index.d.ts.map +1 -1
  108. package/dist/src/modules/health/index.js.map +1 -1
  109. package/dist/src/modules/history/index.d.ts.map +1 -1
  110. package/dist/src/modules/history/index.js.map +1 -1
  111. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  112. package/dist/src/modules/history/semantic.js.map +1 -1
  113. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  114. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  115. package/dist/src/modules/workspace/disk-agreement.d.ts +22 -4
  116. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  117. package/dist/src/modules/workspace/disk-agreement.js +63 -25
  118. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  119. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  120. package/dist/src/modules/workspace/index.d.ts +8 -4
  121. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  122. package/dist/src/modules/workspace/index.js +85 -16
  123. package/dist/src/modules/workspace/index.js.map +1 -1
  124. package/dist/src/modules/workspace/observe.d.ts +56 -11
  125. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  126. package/dist/src/modules/workspace/observe.js +288 -43
  127. package/dist/src/modules/workspace/observe.js.map +1 -1
  128. package/dist/src/modules/workspace/reconcile.d.ts +31 -1
  129. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  130. package/dist/src/modules/workspace/reconcile.js +63 -12
  131. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  132. package/dist/src/modules/workspace/types.d.ts +27 -2
  133. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  134. package/dist/src/modules/workspace/types.js.map +1 -1
  135. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  136. package/dist/src/modules/workspace/watcher.js.map +1 -1
  137. package/dist/src/operator-log.d.ts +41 -0
  138. package/dist/src/operator-log.d.ts.map +1 -1
  139. package/dist/src/operator-log.js.map +1 -1
  140. package/dist/src/provider-waits.d.ts +216 -0
  141. package/dist/src/provider-waits.d.ts.map +1 -0
  142. package/dist/src/provider-waits.js +482 -0
  143. package/dist/src/provider-waits.js.map +1 -0
  144. package/dist/src/queue.d.ts.map +1 -1
  145. package/dist/src/record-journal.d.ts.map +1 -1
  146. package/dist/src/record-journal.js.map +1 -1
  147. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  148. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  149. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  150. package/dist/src/recovery/offline-branch.js +29 -24
  151. package/dist/src/recovery/offline-branch.js.map +1 -1
  152. package/dist/src/recovery/recover-cli.js.map +1 -1
  153. package/dist/src/refusal-reactions.d.ts +14 -12
  154. package/dist/src/refusal-reactions.d.ts.map +1 -1
  155. package/dist/src/refusal-reactions.js +14 -12
  156. package/dist/src/refusal-reactions.js.map +1 -1
  157. package/dist/src/runtime/liveness-watchdog.d.ts.map +1 -1
  158. package/dist/src/runtime/liveness-watchdog.js.map +1 -1
  159. package/dist/src/shutdown.d.ts.map +1 -1
  160. package/dist/src/shutdown.js.map +1 -1
  161. package/dist/src/stored-removal.d.ts +73 -0
  162. package/dist/src/stored-removal.d.ts.map +1 -0
  163. package/dist/src/stored-removal.js +100 -0
  164. package/dist/src/stored-removal.js.map +1 -0
  165. package/dist/src/thinking-token-stamp.js.map +1 -1
  166. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  167. package/dist/src/tool-image-ledger.js.map +1 -1
  168. package/dist/src/tool-presentation.d.ts.map +1 -1
  169. package/dist/src/tool-presentation.js.map +1 -1
  170. package/dist/src/tool-result-guard.d.ts +30 -13
  171. package/dist/src/tool-result-guard.d.ts.map +1 -1
  172. package/dist/src/tool-result-guard.js +71 -25
  173. package/dist/src/tool-result-guard.js.map +1 -1
  174. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  175. package/dist/src/tune-out/coordinator.js.map +1 -1
  176. package/dist/src/tune-out/tools.d.ts +1 -1
  177. package/dist/src/tune-out/tools.d.ts.map +1 -1
  178. package/dist/src/types/agent.d.ts +42 -6
  179. package/dist/src/types/agent.d.ts.map +1 -1
  180. package/dist/src/types/events.d.ts +12 -0
  181. package/dist/src/types/events.d.ts.map +1 -1
  182. package/dist/src/types/framework.d.ts +10 -0
  183. package/dist/src/types/framework.d.ts.map +1 -1
  184. package/dist/src/types/module.d.ts +20 -8
  185. package/dist/src/types/module.d.ts.map +1 -1
  186. package/dist/src/types/trace.d.ts +75 -13
  187. package/dist/src/types/trace.d.ts.map +1 -1
  188. package/dist/src/usage/usage-tracker.d.ts.map +1 -1
  189. package/dist/test/agent-runtime-settings.test.js +1139 -3
  190. package/dist/test/agent-runtime-settings.test.js.map +1 -1
  191. package/dist/test/batched-wake-locus.test.js.map +1 -1
  192. package/dist/test/channel-label-history.test.js.map +1 -1
  193. package/dist/test/code-execution.test.js.map +1 -1
  194. package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
  195. package/dist/test/conversation-router.test.js.map +1 -1
  196. package/dist/test/conversation-routing.test.js +240 -6
  197. package/dist/test/conversation-routing.test.js.map +1 -1
  198. package/dist/test/discord-awareness-outbox.test.js +107 -0
  199. package/dist/test/discord-awareness-outbox.test.js.map +1 -1
  200. package/dist/test/discord-typing-keepalive.test.d.ts +2 -0
  201. package/dist/test/discord-typing-keepalive.test.d.ts.map +1 -0
  202. package/dist/test/discord-typing-keepalive.test.js +77 -0
  203. package/dist/test/discord-typing-keepalive.test.js.map +1 -0
  204. package/dist/test/ephemeral-settle.test.js.map +1 -1
  205. package/dist/test/event-gate-passthrough.test.js.map +1 -1
  206. package/dist/test/event-gate-policy-mutation.test.js.map +1 -1
  207. package/dist/test/event-gate-rate-limit.test.js.map +1 -1
  208. package/dist/test/event-gate-script.test.js.map +1 -1
  209. package/dist/test/event-gate-selfwake.test.js.map +1 -1
  210. package/dist/test/event-gate-tags.test.js.map +1 -1
  211. package/dist/test/event-gate.test.js.map +1 -1
  212. package/dist/test/explicit-prose-routing.test.js +363 -44
  213. package/dist/test/explicit-prose-routing.test.js.map +1 -1
  214. package/dist/test/framework.test.js +160 -1
  215. package/dist/test/framework.test.js.map +1 -1
  216. package/dist/test/helpers/coalescing-fixture.d.ts +2 -0
  217. package/dist/test/helpers/coalescing-fixture.d.ts.map +1 -1
  218. package/dist/test/helpers/coalescing-fixture.js +11 -4
  219. package/dist/test/helpers/coalescing-fixture.js.map +1 -1
  220. package/dist/test/helpers/mock-membrane.d.ts.map +1 -1
  221. package/dist/test/helpers/mock-membrane.js.map +1 -1
  222. package/dist/test/hide-then-turn.test.d.ts +2 -0
  223. package/dist/test/hide-then-turn.test.d.ts.map +1 -0
  224. package/dist/test/hide-then-turn.test.js +101 -0
  225. package/dist/test/hide-then-turn.test.js.map +1 -0
  226. package/dist/test/history-module-overview.test.js.map +1 -1
  227. package/dist/test/history-module-semantic.test.js.map +1 -1
  228. package/dist/test/history-module.test.js.map +1 -1
  229. package/dist/test/history-semantic-review-findings.test.js.map +1 -1
  230. package/dist/test/host-quiesce.test.js +125 -0
  231. package/dist/test/host-quiesce.test.js.map +1 -1
  232. package/dist/test/inference-exhaustion-rewind.test.js +220 -14
  233. package/dist/test/inference-exhaustion-rewind.test.js.map +1 -1
  234. package/dist/test/inference-failure-bounds.test.d.ts +2 -0
  235. package/dist/test/inference-failure-bounds.test.d.ts.map +1 -0
  236. package/dist/test/inference-failure-bounds.test.js +1202 -0
  237. package/dist/test/inference-failure-bounds.test.js.map +1 -0
  238. package/dist/test/kv-unified-open-flight-retry.test.js.map +1 -1
  239. package/dist/test/late-events-after-stop.test.d.ts +2 -0
  240. package/dist/test/late-events-after-stop.test.d.ts.map +1 -0
  241. package/dist/test/late-events-after-stop.test.js +122 -0
  242. package/dist/test/late-events-after-stop.test.js.map +1 -0
  243. package/dist/test/liveness-watchdog.test.js.map +1 -1
  244. package/dist/test/maintenance-timer.test.js +1564 -2
  245. package/dist/test/maintenance-timer.test.js.map +1 -1
  246. package/dist/test/mcp-server-bin.test.d.ts +2 -0
  247. package/dist/test/mcp-server-bin.test.d.ts.map +1 -0
  248. package/dist/test/mcp-server-bin.test.js +142 -0
  249. package/dist/test/mcp-server-bin.test.js.map +1 -0
  250. package/dist/test/mcpl-awareness-barrier.test.js +141 -0
  251. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
  252. package/dist/test/mcpl-child-env.test.js +1 -1
  253. package/dist/test/mcpl-child-env.test.js.map +1 -1
  254. package/dist/test/mcpl-coalesced-content-parity.test.d.ts +2 -0
  255. package/dist/test/mcpl-coalesced-content-parity.test.d.ts.map +1 -0
  256. package/dist/test/mcpl-coalesced-content-parity.test.js +532 -0
  257. package/dist/test/mcpl-coalesced-content-parity.test.js.map +1 -0
  258. package/dist/test/mcpl-coalescing.test.js +78 -16
  259. package/dist/test/mcpl-coalescing.test.js.map +1 -1
  260. package/dist/test/mcpl-config-diagnostics.test.js +6 -3
  261. package/dist/test/mcpl-config-diagnostics.test.js.map +1 -1
  262. package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
  263. package/dist/test/mcpl-manifest-changed.test.js +2 -1
  264. package/dist/test/mcpl-manifest-changed.test.js.map +1 -1
  265. package/dist/test/mcpl-reconnect-events.test.js.map +1 -1
  266. package/dist/test/mcpl-reconnect-reregistration.test.js +2 -0
  267. package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
  268. package/dist/test/mcpl-reference-fetcher.test.js +232 -4
  269. package/dist/test/mcpl-reference-fetcher.test.js.map +1 -1
  270. package/dist/test/mcpl-reference-fields.test.js +69 -2
  271. package/dist/test/mcpl-reference-fields.test.js.map +1 -1
  272. package/dist/test/mcpl-rejection-log-lines.test.d.ts +2 -0
  273. package/dist/test/mcpl-rejection-log-lines.test.d.ts.map +1 -0
  274. package/dist/test/mcpl-rejection-log-lines.test.js +349 -0
  275. package/dist/test/mcpl-rejection-log-lines.test.js.map +1 -0
  276. package/dist/test/mcpl-stderr-tail.test.d.ts +2 -0
  277. package/dist/test/mcpl-stderr-tail.test.d.ts.map +1 -0
  278. package/dist/test/mcpl-stderr-tail.test.js +210 -0
  279. package/dist/test/mcpl-stderr-tail.test.js.map +1 -0
  280. package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +1 -1
  281. package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -1
  282. package/dist/test/mcpl-tool-lifecycle-script.test.js.map +1 -1
  283. package/dist/test/mcpl-tool-lifecycle.test.js.map +1 -1
  284. package/dist/test/mcpl-transport-close.test.d.ts +2 -0
  285. package/dist/test/mcpl-transport-close.test.d.ts.map +1 -0
  286. package/dist/test/mcpl-transport-close.test.js +373 -0
  287. package/dist/test/mcpl-transport-close.test.js.map +1 -0
  288. package/dist/test/mcpl-transport-spawn.test.d.ts +2 -0
  289. package/dist/test/mcpl-transport-spawn.test.d.ts.map +1 -0
  290. package/dist/test/mcpl-transport-spawn.test.js +58 -0
  291. package/dist/test/mcpl-transport-spawn.test.js.map +1 -0
  292. package/dist/test/mcpl-ws-transport.test.js +104 -0
  293. package/dist/test/mcpl-ws-transport.test.js.map +1 -1
  294. package/dist/test/offline-recovery-branch.test.js +344 -0
  295. package/dist/test/offline-recovery-branch.test.js.map +1 -1
  296. package/dist/test/operator-surgery.test.js +197 -0
  297. package/dist/test/operator-surgery.test.js.map +1 -1
  298. package/dist/test/ops-alert.test.js +2 -1
  299. package/dist/test/ops-alert.test.js.map +1 -1
  300. package/dist/test/overbudget-drain-breaker.test.js +1 -0
  301. package/dist/test/overbudget-drain-breaker.test.js.map +1 -1
  302. package/dist/test/pack-installs-alone.test.d.ts +2 -0
  303. package/dist/test/pack-installs-alone.test.d.ts.map +1 -0
  304. package/dist/test/pack-installs-alone.test.js +284 -0
  305. package/dist/test/pack-installs-alone.test.js.map +1 -0
  306. package/dist/test/physical-window-restart.test.d.ts +6 -4
  307. package/dist/test/physical-window-restart.test.d.ts.map +1 -1
  308. package/dist/test/physical-window-restart.test.js +275 -10
  309. package/dist/test/physical-window-restart.test.js.map +1 -1
  310. package/dist/test/placed-reaction-baseline.test.d.ts +2 -0
  311. package/dist/test/placed-reaction-baseline.test.d.ts.map +1 -0
  312. package/dist/test/placed-reaction-baseline.test.js +312 -0
  313. package/dist/test/placed-reaction-baseline.test.js.map +1 -0
  314. package/dist/test/present-while-acting.test.js +949 -6
  315. package/dist/test/present-while-acting.test.js.map +1 -1
  316. package/dist/test/provider-acceleration-cooldown.test.js +5 -1
  317. package/dist/test/provider-acceleration-cooldown.test.js.map +1 -1
  318. package/dist/test/provider-waits.test.d.ts +2 -0
  319. package/dist/test/provider-waits.test.d.ts.map +1 -0
  320. package/dist/test/provider-waits.test.js +2259 -0
  321. package/dist/test/provider-waits.test.js.map +1 -0
  322. package/dist/test/puppet-tool-call.test.js +2 -0
  323. package/dist/test/puppet-tool-call.test.js.map +1 -1
  324. package/dist/test/reaction-baseline-platform.test.d.ts +2 -0
  325. package/dist/test/reaction-baseline-platform.test.d.ts.map +1 -0
  326. package/dist/test/reaction-baseline-platform.test.js +124 -0
  327. package/dist/test/reaction-baseline-platform.test.js.map +1 -0
  328. package/dist/test/record-journal.test.js.map +1 -1
  329. package/dist/test/refusal-reaction-baseline.test.js +10 -3
  330. package/dist/test/refusal-reaction-baseline.test.js.map +1 -1
  331. package/dist/test/refusal-rewind.test.js +9 -0
  332. package/dist/test/refusal-rewind.test.js.map +1 -1
  333. package/dist/test/refusal-single-call.test.js.map +1 -1
  334. package/dist/test/release-bump.test.d.ts +2 -0
  335. package/dist/test/release-bump.test.d.ts.map +1 -0
  336. package/dist/test/release-bump.test.js +248 -0
  337. package/dist/test/release-bump.test.js.map +1 -0
  338. package/dist/test/release-changelog.test.js.map +1 -1
  339. package/dist/test/release-holds.test.d.ts +2 -0
  340. package/dist/test/release-holds.test.d.ts.map +1 -0
  341. package/dist/test/release-holds.test.js +456 -0
  342. package/dist/test/release-holds.test.js.map +1 -0
  343. package/dist/test/same-round-think-policy.test.js.map +1 -1
  344. package/dist/test/save-recent-image.test.js +138 -2
  345. package/dist/test/save-recent-image.test.js.map +1 -1
  346. package/dist/test/shutdown-errors.test.js.map +1 -1
  347. package/dist/test/silent-heartbeat-rows.test.js.map +1 -1
  348. package/dist/test/stop-with-host-operations.test.d.ts +20 -0
  349. package/dist/test/stop-with-host-operations.test.d.ts.map +1 -0
  350. package/dist/test/stop-with-host-operations.test.js +1174 -0
  351. package/dist/test/stop-with-host-operations.test.js.map +1 -0
  352. package/dist/test/stop-with-work-in-flight.test.js +162 -50
  353. package/dist/test/stop-with-work-in-flight.test.js.map +1 -1
  354. package/dist/test/surgery-marks-receipt.test.d.ts +2 -0
  355. package/dist/test/surgery-marks-receipt.test.d.ts.map +1 -0
  356. package/dist/test/surgery-marks-receipt.test.js +540 -0
  357. package/dist/test/surgery-marks-receipt.test.js.map +1 -0
  358. package/dist/test/tool-batch-hook.test.js.map +1 -1
  359. package/dist/test/tool-result-guard.test.js +352 -16
  360. package/dist/test/tool-result-guard.test.js.map +1 -1
  361. package/dist/test/tool-result-spill.test.js.map +1 -1
  362. package/dist/test/tool-wrapper-prose-guard-integration.test.js.map +1 -1
  363. package/dist/test/trunk-channel-routing.test.js +330 -7
  364. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  365. package/dist/test/tune-out-e2e.test.js.map +1 -1
  366. package/dist/test/unconvertible-tool-result.test.js.map +1 -1
  367. package/dist/test/unsent-compiles.test.d.ts +2 -0
  368. package/dist/test/unsent-compiles.test.d.ts.map +1 -0
  369. package/dist/test/unsent-compiles.test.js +139 -0
  370. package/dist/test/unsent-compiles.test.js.map +1 -0
  371. package/dist/test/workspace-branch-continuation.test.js.map +1 -1
  372. package/dist/test/workspace-materialize-freshness.test.js.map +1 -1
  373. package/dist/test/workspace-read-text.test.js.map +1 -1
  374. package/dist/test/workspace-three-way.test.js +1022 -7
  375. package/dist/test/workspace-three-way.test.js.map +1 -1
  376. package/dist/test/workspace-watcher.test.js.map +1 -1
  377. package/dist/test/xml-round-persist.test.js.map +1 -1
  378. package/dist/tsconfig.tsbuildinfo +1 -1
  379. package/package.json +8 -6
  380. package/src/agent.ts +415 -67
  381. package/src/api/mcp-server-core.ts +681 -0
  382. package/src/api/mcp-server.ts +16 -685
  383. package/src/api/server.ts +87 -26
  384. package/src/api/types.ts +9 -1
  385. package/src/framework.ts +3389 -782
  386. package/src/index.ts +9 -4
  387. package/src/mcpl/channel-registry.ts +126 -31
  388. package/src/mcpl/feature-set-manager.ts +4 -3
  389. package/src/mcpl/index.ts +2 -0
  390. package/src/mcpl/log-value.ts +116 -0
  391. package/src/mcpl/prose-stream-router.ts +6 -6
  392. package/src/mcpl/push-coalescer.ts +76 -12
  393. package/src/mcpl/push-handler.ts +95 -23
  394. package/src/mcpl/reference-fetcher.ts +18 -8
  395. package/src/mcpl/references.ts +57 -13
  396. package/src/mcpl/refusal-lines.ts +59 -0
  397. package/src/mcpl/server-connection.ts +125 -15
  398. package/src/mcpl/tool-classes.ts +3 -2
  399. package/src/mcpl/transport.ts +105 -14
  400. package/src/mcpl/types.ts +5 -1
  401. package/src/module-registry.ts +17 -9
  402. package/src/modules/discord/index.ts +4 -2
  403. package/src/modules/workspace/disk-agreement.ts +74 -18
  404. package/src/modules/workspace/index.ts +85 -15
  405. package/src/modules/workspace/observe.ts +295 -43
  406. package/src/modules/workspace/reconcile.ts +90 -12
  407. package/src/modules/workspace/types.ts +26 -1
  408. package/src/operator-log.ts +30 -0
  409. package/src/provider-waits.ts +523 -0
  410. package/src/recovery/offline-branch.ts +32 -28
  411. package/src/refusal-reactions.ts +14 -12
  412. package/src/stored-removal.ts +126 -0
  413. package/src/tool-result-guard.ts +72 -24
  414. package/src/types/agent.ts +42 -6
  415. package/src/types/events.ts +12 -0
  416. package/src/types/framework.ts +10 -0
  417. package/src/types/module.ts +20 -8
  418. package/src/types/trace.ts +76 -14
@@ -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,
@@ -48,10 +49,52 @@ import type {
48
49
  import { McplMethod } from './types.js';
49
50
  import type { ToolLifecycleParams, ToolObserveRule } from './tool-lifecycle.js';
50
51
 
52
+ /**
53
+ * Payload of the connection `'close'` event (third argument; the first two
54
+ * stay `(code, signal)` for existing listeners). Lets a host see WHY the
55
+ * link went down — in particular a WebSocket close code such as 4001 from
56
+ * a server that refused or expired the credential — so it can refresh what
57
+ * `accessProvider` will hand the next dial instead of replaying it.
58
+ */
59
+ export interface McplConnectionCloseInfo {
60
+ /** Transport kind that closed; null when the connection never had one
61
+ * (a disconnected stub torn down before its first reconnect). */
62
+ transport: McplTransport['kind'] | null;
63
+ /** WebSocket close code (e.g. 4001) or stdio child exit code; null when unknown. */
64
+ code: number | null;
65
+ /** Signal that ended a stdio child; null otherwise. */
66
+ signal: NodeJS.Signals | null;
67
+ /** WebSocket close reason, or a short description of the cause. */
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;
72
+ /** True when the host closed the link (`close()` / `reconnectAfterFailure()`),
73
+ * false when the transport dropped unexpectedly. */
74
+ byHost: boolean;
75
+ }
76
+
51
77
  /** Timeout for the initialize handshake in milliseconds.
52
78
  * Spring Boot + JDA servers can take 5-10s to boot, so 30s is safe. */
53
79
  const INITIALIZE_TIMEOUT_MS = 30_000;
54
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
+
55
98
  /** Default per-request timeout in milliseconds (see McplServerConfig.requestTimeoutMs).
56
99
  * A live-but-wedged server (accepts requests, never answers, never closes its
57
100
  * transport) would otherwise freeze the awaiting agent turn forever — no
@@ -93,9 +136,14 @@ interface RequestOptions {
93
136
  * - `'channels-changed'` — Server sent `channels/changed`
94
137
  * - `'channels-incoming'` — Server sent `channels/incoming`
95
138
  * - `'error'` — Connection-level error
96
- * - `'close'` — Connection closed
97
- * - `'connect-failed'` — Initial connect failed; background retry scheduled
98
- * - `'reconnect-failed'` — A background reconnect attempt failed; next retry scheduled
139
+ * - `'close'` — Connection closed: `(code, signal, info: McplConnectionCloseInfo)`
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.
99
147
  * - `'reconnect'` — Background reconnect succeeded
100
148
  */
101
149
  export class McplServerConnection extends EventEmitter {
@@ -209,6 +257,9 @@ export class McplServerConnection extends EventEmitter {
209
257
  * observable even though its original promise has already been rejected. */
210
258
  private orphanedRequests = new Map<string | number, { method: string; renderParams?: Record<string, unknown> }>();
211
259
  private closed = false;
260
+ /** Set by `reconnectAfterFailure()` before it closes a live transport, so
261
+ * the transport close handler reports that teardown as `byHost: true`. */
262
+ private hostCloseRequested = false;
212
263
  private closePromise: Promise<void> | null = null;
213
264
  private closeIncomplete = false;
214
265
  private reconnectPromise: Promise<void> | null = null;
@@ -378,7 +429,7 @@ export class McplServerConnection extends EventEmitter {
378
429
  if (responder?.respondError) {
379
430
  responder.respondError(CAPABILITY_DISABLED, `${name} rejected: capability "${requiredCap}" ${phase}`, { capability: requiredCap });
380
431
  } else {
381
- console.error(`[mcpl] ${this.id}: discarded ${name} — "${requiredCap}" ${phase}`);
432
+ console.error(`[mcpl] ${this.id}: discarded ${logValue(name)} — "${requiredCap}" ${phase}`);
382
433
  }
383
434
  return true;
384
435
  }
@@ -506,6 +557,15 @@ export class McplServerConnection extends EventEmitter {
506
557
  hostCapabilities: McplHostCapabilities,
507
558
  ): Promise<{ transport: McplTransport; capabilities: McplCapabilities | null; droppedCapabilities: ReadonlySet<string>; mcpToolsAdvertised: boolean }> {
508
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
+ };
509
569
 
510
570
  try {
511
571
  const initId = 0; // use id=0 for the handshake
@@ -523,10 +583,20 @@ export class McplServerConnection extends EventEmitter {
523
583
  // Await the initialize response, racing an early transport close/error and
524
584
  // a timeout. All three paths clean up their listeners.
525
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
+ };
526
595
  const cleanup = () => {
527
596
  transport.off('line', onLine);
528
597
  transport.off('close', onClose);
529
598
  transport.off('error', onError);
599
+ transport.on('error', logAfterSettled);
530
600
  clearTimeout(timer);
531
601
  };
532
602
  const onLine = (line: string) => {
@@ -555,15 +625,15 @@ export class McplServerConnection extends EventEmitter {
555
625
  };
556
626
  const onClose = (info: TransportCloseInfo) => {
557
627
  cleanup();
558
- 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'})`));
559
629
  };
560
630
  const onError = (err: Error) => {
561
631
  cleanup();
562
- 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}`));
563
633
  };
564
634
  const timer = setTimeout(() => {
565
635
  cleanup();
566
- reject(new Error(`MCPL server "${config.id}" initialize handshake timed out`));
636
+ reject(failed(`MCPL server "${config.id}" initialize handshake timed out`));
567
637
  }, INITIALIZE_TIMEOUT_MS);
568
638
 
569
639
  transport.on('line', onLine);
@@ -585,9 +655,13 @@ export class McplServerConnection extends EventEmitter {
585
655
  // had never been advertised (hooks, streaming, queries all read this).
586
656
  const { capabilities: scoped, dropped } = maskNegotiatedCapabilities(capabilities, config);
587
657
  if (dropped.length > 0) {
588
- 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(', ')}`);
589
659
  }
590
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);
591
665
  return { transport, capabilities: scoped, droppedCapabilities: new Set(dropped), mcpToolsAdvertised };
592
666
  } catch (err) {
593
667
  // Never leak the child / socket if the handshake fails.
@@ -614,12 +688,13 @@ export class McplServerConnection extends EventEmitter {
614
688
  }
615
689
 
616
690
  // Non-blocking start: create a disconnected stub that will reconnect in background
617
- 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)}`);
618
692
  const stub = McplServerConnection.createDisconnectedStub(config, hostCapabilities);
619
693
  // Surface the initial failure. The event is buffered by the emit()
620
694
  // override until the host wires listeners and calls ready(), so it is
621
695
  // not lost in the window before wireMcplEvents runs.
622
- 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 });
623
698
  return stub;
624
699
  }
625
700
  }
@@ -864,7 +939,7 @@ export class McplServerConnection extends EventEmitter {
864
939
  if (this.reconnectPromise) await this.reconnectPromise;
865
940
  if (this.transport) await this.transport.close();
866
941
  this.closeIncomplete = false;
867
- this.emit('close');
942
+ this.emit('close', null, null, this.closeInfoByHost());
868
943
  });
869
944
  this.closePromise = attempt;
870
945
  void attempt.catch(() => {
@@ -886,14 +961,26 @@ export class McplServerConnection extends EventEmitter {
886
961
  return;
887
962
  }
888
963
  if (this.transport) {
964
+ this.hostCloseRequested = true;
889
965
  await this.transport.close();
890
966
  return;
891
967
  }
892
968
  this.closed = true;
893
- this.emit('close');
969
+ this.emit('close', null, null, this.closeInfoByHost());
894
970
  this.scheduleReconnect();
895
971
  }
896
972
 
973
+ /** 'close' payload for a host-initiated teardown (no transport close code). */
974
+ private closeInfoByHost(): McplConnectionCloseInfo {
975
+ return {
976
+ transport: this.transport?.kind ?? null,
977
+ code: null,
978
+ signal: null,
979
+ reason: 'closed by host',
980
+ byHost: true,
981
+ };
982
+ }
983
+
897
984
  /**
898
985
  * Schedule a background reconnection attempt.
899
986
  *
@@ -952,6 +1039,13 @@ export class McplServerConnection extends EventEmitter {
952
1039
  // The prior transport was disconnected before this retry began. Keep
953
1040
  // the fresh, unadmitted transport owned by close(), including on a
954
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
+ });
955
1049
  this.transport = transport;
956
1050
  return;
957
1051
  }
@@ -976,9 +1070,10 @@ export class McplServerConnection extends EventEmitter {
976
1070
  this.reconnectAttempts = 0;
977
1071
  this.emit('reconnect', { attempts: attempt });
978
1072
  } catch (error) {
979
- 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)}`);
980
1074
  this.reconnectAttempts = attempt + 1;
981
- 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 });
982
1077
  this.scheduleReconnect();
983
1078
  }
984
1079
  }
@@ -1285,6 +1380,7 @@ export class McplServerConnection extends EventEmitter {
1285
1380
  * and — when enabled — schedules a background reconnect.
1286
1381
  */
1287
1382
  private setupLifecycle(transport: McplTransport): void {
1383
+ this.hostCloseRequested = false;
1288
1384
  transport.on('error', (err: Error) => {
1289
1385
  this.emit('error', new Error(`MCPL server "${this.id}" transport error: ${err.message}`));
1290
1386
  });
@@ -1306,7 +1402,21 @@ export class McplServerConnection extends EventEmitter {
1306
1402
  this.pendingRequests.clear();
1307
1403
  this.orphanedRequests.clear();
1308
1404
 
1309
- this.emit('close', info.code ?? null, info.signal ?? null);
1405
+ // Forward the transport's close info so listeners can tell a server
1406
+ // that refused the credential (WebSocket 4001) from a crash, and act
1407
+ // before the reconnect below redials. A teardown the host asked for
1408
+ // via reconnectAfterFailure() arrives here too; report it as such.
1409
+ const byHost = this.hostCloseRequested;
1410
+ this.hostCloseRequested = false;
1411
+ const closeInfo: McplConnectionCloseInfo = {
1412
+ transport: transport.kind,
1413
+ code: info.code ?? null,
1414
+ signal: info.signal ?? null,
1415
+ reason: info.reason ?? (byHost ? 'closed by host' : undefined),
1416
+ ...(info.reasonFromPeer ? { reasonFromPeer: true } : {}),
1417
+ byHost,
1418
+ };
1419
+ this.emit('close', closeInfo.code, closeInfo.signal, closeInfo);
1310
1420
 
1311
1421
  // Schedule reconnect if enabled (unexpected disconnect triggers auto-reconnect)
1312
1422
  if (this.reconnectEnabled) {
@@ -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;
@@ -19,6 +19,7 @@
19
19
  import { spawn, type ChildProcess } from 'node:child_process';
20
20
  import { createInterface, type Interface as ReadlineInterface } from 'node:readline';
21
21
  import { EventEmitter } from 'node:events';
22
+ import { StringDecoder } from 'node:string_decoder';
22
23
  import WebSocket from 'ws';
23
24
 
24
25
  import type { McplServerConfig } from './types.js';
@@ -29,6 +30,9 @@ export interface TransportCloseInfo {
29
30
  code?: number | null;
30
31
  signal?: NodeJS.Signals | null;
31
32
  reason?: string;
33
+ /** True when `reason` is the peer's own text, a WebSocket server's close
34
+ * reason, rather than the transport's words. */
35
+ reasonFromPeer?: boolean;
32
36
  }
33
37
 
34
38
  /**
@@ -71,6 +75,16 @@ export abstract class McplTransport extends EventEmitter {
71
75
  return this._closed;
72
76
  }
73
77
 
78
+ /**
79
+ * The last of what the server wrote to its diagnostic stream: at most its
80
+ * last {@link STDERR_TAIL_CHARS} UTF-16 code units, never opening inside a
81
+ * surrogate pair, after a '…' when earlier ones were dropped. A stdio
82
+ * child's stderr, and '' for a transport that has none.
83
+ */
84
+ get stderrTail(): string {
85
+ return '';
86
+ }
87
+
74
88
  /** Deliver an inbound line, buffering it if nobody is listening yet. */
75
89
  protected pushLine(line: string): void {
76
90
  if (this.listenerCount('line') > 0) this.emit('line', line);
@@ -167,8 +181,19 @@ export function buildChildEnv(
167
181
  return merged;
168
182
  }
169
183
 
184
+ /** The most of a stdio child's stderr a transport keeps for {@link McplTransport.stderrTail}. */
185
+ export const STDERR_TAIL_CHARS = 2_048;
186
+
170
187
  export class StdioTransport extends McplTransport {
171
188
  readonly kind = 'stdio' as const;
189
+ /** The first error on the child's stdin or stdout, named in the close reason. */
190
+ private pipeError: Error | undefined;
191
+ private stderrEnd = '';
192
+ private stderrCut = false;
193
+
194
+ override get stderrTail(): string {
195
+ return (this.stderrCut ? '…' : '') + this.stderrEnd;
196
+ }
172
197
 
173
198
  private constructor(
174
199
  private readonly child: ChildProcess,
@@ -178,26 +203,70 @@ export class StdioTransport extends McplTransport {
178
203
 
179
204
  this.rl.on('line', (line: string) => this.pushLine(line));
180
205
 
181
- // Stitch stderr chunks into whole lines (a chunk can split a line).
206
+ // Stitch stderr chunks into whole lines (a chunk can split a line, and a
207
+ // character's bytes), and keep the stream's last characters for
208
+ // stderrTail.
182
209
  let carry = '';
183
- this.child.stderr?.on('data', (chunk: Buffer) => {
184
- const text = carry + chunk.toString('utf8');
185
- const lines = text.split('\n');
210
+ const decoder = new StringDecoder('utf8');
211
+ const received = (text: string) => {
212
+ const end = this.stderrEnd + text;
213
+ this.stderrCut ||= end.length > STDERR_TAIL_CHARS;
214
+ let kept = end.slice(-STDERR_TAIL_CHARS);
215
+ // A cut between a surrogate pair's halves drops the low half too.
216
+ if (/^[\uDC00-\uDFFF]/.test(kept) && end.length > STDERR_TAIL_CHARS) kept = kept.slice(1);
217
+ this.stderrEnd = kept;
218
+ const lines = (carry + text).split('\n');
186
219
  carry = lines.pop() ?? '';
187
220
  for (const l of lines) if (l.length > 0) this.pushStderr(l);
221
+ };
222
+ this.child.stderr?.on('data', (chunk: Buffer) => received(decoder.write(chunk)));
223
+ // A last line with no newline is complete once the stream ends.
224
+ this.child.stderr?.on('end', () => {
225
+ received(decoder.end());
226
+ if (carry.length > 0) this.pushStderr(carry);
188
227
  });
189
228
 
190
229
  this.child.on('error', (err) => this.emit('error', err));
191
- this.child.on('exit', (code, signal) =>
192
- this.markClosed({ code, signal, reason: 'child process exited' }),
193
- );
230
+ // The child's stdin and stdout carry the protocol, so after an error on
231
+ // either, a write's EPIPE or a read error, this transport can't reach the
232
+ // child even if it's still running. The handler sends a running child
233
+ // SIGTERM, and the exit's close names the error, so the connection handles
234
+ // it as any other exit. The close is the report: no 'error' is emitted,
235
+ // nothing is thrown. Nothing here escalates to SIGKILL, so a child that
236
+ // ignores SIGTERM keeps running, unreported. A child that stops reading
237
+ // without closing its stdin raises no error: the writes are buffered, and
238
+ // only request timeouts bound it (McplServerConfig.requestTimeoutMs).
239
+ const endOnPipeError = (err: Error) => {
240
+ this.pipeError ??= err;
241
+ if (this.child.exitCode === null && this.child.signalCode === null) this.child.kill();
242
+ };
243
+ this.child.stdin?.on('error', endOnPipeError);
244
+ // On the stream itself, since close() closes readline, which takes its own
245
+ // listener off the stream. While readline is open, its listener, which
246
+ // came first, re-emits the error on the interface, where an empty
247
+ // listener absorbs that copy.
248
+ this.child.stdout?.on('error', endOnPipeError);
249
+ this.rl.on('error', () => { /* readline's copy of a stdout error, handled above */ });
250
+ // stderr carries diagnostics only: a read error on it is reported if
251
+ // anyone listens and never thrown. It ends nothing once the connection
252
+ // is wired; during a handshake the listener is the handshake's, which
253
+ // fails it.
254
+ this.child.stderr?.on('error', (err) => {
255
+ if (this.listenerCount('error') > 0) this.emit('error', err);
256
+ });
257
+ this.child.on('exit', (code, signal) => this.markClosed({
258
+ code,
259
+ signal,
260
+ reason: this.pipeError ? `child process exited, and its pipe failed with ${this.pipeError.message}` : 'child process exited',
261
+ }));
194
262
  }
195
263
 
196
264
  /**
197
- * Spawn `config.command` and return a ready stdio transport. Rejects if the
198
- * command is missing (a config bug) or the process fails to spawn.
265
+ * Spawn `config.command` and resolve with a ready stdio transport once the
266
+ * child has spawned. Rejects if the command is missing (a config bug) or the
267
+ * process fails to spawn.
199
268
  */
200
- static spawn(config: McplServerConfig): StdioTransport {
269
+ static async spawn(config: McplServerConfig): Promise<StdioTransport> {
201
270
  if (!config.command) {
202
271
  throw new Error(
203
272
  `MCPL server "${config.id}": stdio transport requires "command" (none provided)`,
@@ -207,6 +276,22 @@ export class StdioTransport extends McplTransport {
207
276
  stdio: ['pipe', 'pipe', 'pipe'],
208
277
  env: buildChildEnv(config),
209
278
  });
279
+ // Node reports a failed spawn (a command that doesn't exist, say) as an
280
+ // 'error' on a tick after spawn() returns, and no 'exit' follows. It is
281
+ // settled here, where it is listened for. A caller that listens only once
282
+ // this resolves, as the handshake does, would be too late when reached
283
+ // from a timer callback, as a reconnect is: Node runs that tick before the
284
+ // caller's promise continuation, and throws an 'error' nothing listens for.
285
+ await new Promise<void>((resolve, reject) => {
286
+ const failed = (err: Error) => {
287
+ reject(new Error(`MCPL server "${config.id}" could not be started: ${err.message}`, { cause: err }));
288
+ };
289
+ child.once('error', failed);
290
+ child.once('spawn', () => {
291
+ child.off('error', failed);
292
+ resolve();
293
+ });
294
+ });
210
295
  const rl = createInterface({ input: child.stdout! });
211
296
  return new StdioTransport(child, rl);
212
297
  }
@@ -223,7 +308,12 @@ export class StdioTransport extends McplTransport {
223
308
  this.child.kill();
224
309
  }
225
310
  await new Promise<void>((resolve) => {
226
- if (!this.child || this.child.exitCode !== null || this.child.killed) {
311
+ // Once Node has seen a child exit, it has its exitCode set, or its
312
+ // signalCode when a signal ended it, and its 'exit' event has fired. A
313
+ // signal that didn't come through kill() (an OOM kill, a crash) leaves
314
+ // `killed` false. A child that has exited but isn't reaped yet still
315
+ // takes the kill() above, which sets `killed`.
316
+ if (!this.child || this.child.exitCode !== null || this.child.signalCode !== null || this.child.killed) {
227
317
  resolve();
228
318
  } else {
229
319
  this.child.once('exit', () => resolve());
@@ -256,9 +346,10 @@ export class WebSocketTransport extends McplTransport {
256
346
  });
257
347
 
258
348
  this.ws.on('error', (err: Error) => this.emit('error', err));
259
- this.ws.on('close', (code: number, reason: Buffer) =>
260
- this.markClosed({ code, reason: reason?.toString('utf8') || 'websocket closed' }),
261
- );
349
+ this.ws.on('close', (code: number, reason: Buffer) => {
350
+ const text = reason?.toString('utf8');
351
+ this.markClosed(text ? { code, reason: text, reasonFromPeer: true } : { code, reason: 'websocket closed' });
352
+ });
262
353
  }
263
354
 
264
355
  /**
package/src/mcpl/types.ts CHANGED
@@ -374,7 +374,11 @@ export interface McplServerConfig {
374
374
  * accepts a request and never responds would otherwise freeze the awaiting
375
375
  * agent turn forever; with the timeout the pending request rejects with a
376
376
  * descriptive error, which the framework surfaces as a normal isError
377
- * tool_result. Set 0 to disable. Default: 60000 (60 seconds).
377
+ * tool_result. Request timeouts are the only bound for a stdio server that
378
+ * stops reading its stdin without closing it: the host's writes are
379
+ * buffered, no error is raised, and the connection stays up. This is the
380
+ * timeout of every request that names none; the handshake's initialize has
381
+ * its own. Set 0 to disable. Default: 60000 (60 seconds).
378
382
  */
379
383
  requestTimeoutMs?: number;
380
384
 
@@ -26,6 +26,12 @@ import type {
26
26
  import type { Agent } from './agent.js';
27
27
  import { DEFAULT_SHUTDOWN_TIMEOUT_MS, validateShutdownTimeout, waitForShutdown } from './shutdown.js';
28
28
 
29
+ /** How the registry queues a module's event: its push names the module. A
30
+ * `ProcessQueue` serves too, ignoring the name. */
31
+ export interface ModuleQueue {
32
+ push(event: ProcessEvent, moduleName?: string): void;
33
+ }
34
+
29
35
  const MODULE_STATE_PREFIX = 'modules/';
30
36
 
31
37
  /**
@@ -78,7 +84,7 @@ export class ModuleRegistry {
78
84
  private moduleContexts: Map<string, ModuleContextImpl> = new Map();
79
85
  private speechHandlers: SpeechHandler[] = [];
80
86
  private store: JsStore;
81
- private queue: ProcessQueue;
87
+ private queue: ModuleQueue;
82
88
  private getAgents: () => Agent[];
83
89
  private addMessageFn: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
84
90
  private editMessageFn: (id: MessageId, content: ContentBlock[]) => void;
@@ -86,7 +92,7 @@ export class ModuleRegistry {
86
92
  private getMessageFn: (id: MessageId) => StoredMessage | null;
87
93
  private queryMessagesFn: (filter: MessageQuery) => MessageQueryResult;
88
94
 
89
- private pushEventFn: (event: ProcessEvent) => void;
95
+ private pushEventFn: (event: ProcessEvent, moduleName?: string) => void;
90
96
  private onTraceFn: (listener: TraceEventListener) => () => void;
91
97
  private callToolFn: (call: ToolCall) => Promise<ToolResult>;
92
98
  /** Framework ops-alert channel, surfaced to modules via
@@ -101,7 +107,7 @@ export class ModuleRegistry {
101
107
 
102
108
  constructor(
103
109
  store: JsStore,
104
- queue: ProcessQueue,
110
+ queue: ModuleQueue,
105
111
  options: {
106
112
  getAgents: () => Agent[];
107
113
  addMessage: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
@@ -109,7 +115,8 @@ export class ModuleRegistry {
109
115
  removeMessage: (id: MessageId) => void;
110
116
  getMessage: (id: MessageId) => StoredMessage | null;
111
117
  queryMessages: (filter: MessageQuery) => MessageQueryResult;
112
- pushEvent: (event: ProcessEvent) => void;
118
+ /** Called with the pushing module's name. */
119
+ pushEvent: (event: ProcessEvent, moduleName?: string) => void;
113
120
  onTrace: (listener: TraceEventListener) => () => void;
114
121
  callTool: (call: ToolCall) => Promise<ToolResult>;
115
122
  notifyOps?: (kind: string, agentName: string, message: string, data?: Record<string, unknown>) => void;
@@ -537,7 +544,7 @@ class ModuleContextImpl implements ModuleContext {
537
544
  private removeMessageFn: (id: MessageId) => void;
538
545
  private getMessageFn: (id: MessageId) => StoredMessage | null;
539
546
  private queryMessagesFn: (filter: MessageQuery) => MessageQueryResult;
540
- private pushEventFn: (event: ProcessEvent) => void;
547
+ private pushEventFn: (event: ProcessEvent, moduleName?: string) => void;
541
548
  private onTraceFn: (listener: TraceEventListener) => () => void;
542
549
  private callToolFn: (call: ToolCall) => Promise<ToolResult>;
543
550
 
@@ -548,7 +555,7 @@ class ModuleContextImpl implements ModuleContext {
548
555
  moduleName: string,
549
556
  store: JsStore,
550
557
  stateId: string,
551
- queue: ProcessQueue,
558
+ queue: ModuleQueue,
552
559
  registry: ModuleRegistry,
553
560
  isRestart: boolean,
554
561
  getAgents: () => Agent[],
@@ -557,14 +564,15 @@ class ModuleContextImpl implements ModuleContext {
557
564
  removeMessage: (id: MessageId) => void,
558
565
  getMessage: (id: MessageId) => StoredMessage | null,
559
566
  queryMessages: (filter: MessageQuery) => MessageQueryResult,
560
- pushEvent: (event: ProcessEvent) => void,
567
+ pushEvent: (event: ProcessEvent, moduleName?: string) => void,
561
568
  onTrace: (listener: TraceEventListener) => () => void,
562
569
  callTool: (call: ToolCall) => Promise<ToolResult>
563
570
  ) {
564
571
  this.moduleName = moduleName;
565
572
  this.store = store;
566
573
  this.stateId = stateId;
567
- this.queue = queue;
574
+ // The deprecated handle: a push names this module, as pushEvent() does.
575
+ this.queue = { push: (event) => queue.push(event, moduleName) };
568
576
  this.registry = registry;
569
577
  this.isRestart = isRestart;
570
578
  this.getAgentsFn = getAgents;
@@ -747,7 +755,7 @@ class ModuleContextImpl implements ModuleContext {
747
755
  }
748
756
 
749
757
  pushEvent(event: ProcessEvent): void {
750
- this.pushEventFn(event);
758
+ this.pushEventFn(event, this.moduleName);
751
759
  }
752
760
 
753
761
  private persistExternalIdMap(): void {
@@ -67,9 +67,11 @@ const TYPING_REFRESH_MS = 8_000;
67
67
  // stuck on forever.
68
68
  const TYPING_MAX_MS = 5 * 60_000;
69
69
 
70
- // Trace event types that terminate an agent's turn — any of these stops the
71
- // typing keepalive. They fire once per turn, after all tool-call cycles.
70
+ // Trace event types that end an agent's turn (onTrace's contract) — any of
71
+ // these stops the typing keepalive. A turn's tool-call cycles stay inside its
72
+ // stream, so they come after them. inference:completed is the normal end.
72
73
  const TURN_TERMINAL_TRACES = new Set([
74
+ 'inference:completed',
73
75
  'inference:turn_ended',
74
76
  'inference:aborted',
75
77
  'inference:failed',