@animalabs/agent-framework 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/README.md +145 -41
  2. package/dist/src/agent.d.ts +29 -2
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +73 -12
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  7. package/dist/src/api/mcp-server-core.js +3 -12
  8. package/dist/src/api/mcp-server-core.js.map +1 -1
  9. package/dist/src/api/server.d.ts.map +1 -1
  10. package/dist/src/api/server.js +11 -5
  11. package/dist/src/api/server.js.map +1 -1
  12. package/dist/src/auto-continue.d.ts +5 -2
  13. package/dist/src/auto-continue.d.ts.map +1 -1
  14. package/dist/src/auto-continue.js +9 -5
  15. package/dist/src/auto-continue.js.map +1 -1
  16. package/dist/src/code-execution/isolate/runner.d.ts +7 -5
  17. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
  18. package/dist/src/code-execution/isolate/runner.js +7 -5
  19. package/dist/src/code-execution/isolate/runner.js.map +1 -1
  20. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  21. package/dist/src/code-execution/isolate/scripts.js +3 -2
  22. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  23. package/dist/src/cut-off-round.d.ts +24 -0
  24. package/dist/src/cut-off-round.d.ts.map +1 -0
  25. package/dist/src/cut-off-round.js +87 -0
  26. package/dist/src/cut-off-round.js.map +1 -0
  27. package/dist/src/framework.d.ts +468 -125
  28. package/dist/src/framework.d.ts.map +1 -1
  29. package/dist/src/framework.js +3651 -1111
  30. package/dist/src/framework.js.map +1 -1
  31. package/dist/src/gate/event-gate.d.ts +133 -13
  32. package/dist/src/gate/event-gate.d.ts.map +1 -1
  33. package/dist/src/gate/event-gate.js +654 -59
  34. package/dist/src/gate/event-gate.js.map +1 -1
  35. package/dist/src/gate/index.d.ts +3 -2
  36. package/dist/src/gate/index.d.ts.map +1 -1
  37. package/dist/src/gate/index.js +2 -1
  38. package/dist/src/gate/index.js.map +1 -1
  39. package/dist/src/gate/reply-judge.d.ts +116 -0
  40. package/dist/src/gate/reply-judge.d.ts.map +1 -0
  41. package/dist/src/gate/reply-judge.js +467 -0
  42. package/dist/src/gate/reply-judge.js.map +1 -0
  43. package/dist/src/gate/types.d.ts +173 -2
  44. package/dist/src/gate/types.d.ts.map +1 -1
  45. package/dist/src/heartbeat/heartbeat.d.ts +202 -0
  46. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
  47. package/dist/src/heartbeat/heartbeat.js +778 -0
  48. package/dist/src/heartbeat/heartbeat.js.map +1 -0
  49. package/dist/src/heartbeat/reminder-time.d.ts +5 -0
  50. package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
  51. package/dist/src/heartbeat/reminder-time.js +107 -0
  52. package/dist/src/heartbeat/reminder-time.js.map +1 -0
  53. package/dist/src/held-writes.d.ts +3 -3
  54. package/dist/src/held-writes.js +3 -3
  55. package/dist/src/host-presentation.d.ts +170 -0
  56. package/dist/src/host-presentation.d.ts.map +1 -0
  57. package/dist/src/host-presentation.js +561 -0
  58. package/dist/src/host-presentation.js.map +1 -0
  59. package/dist/src/index.d.ts +5 -1
  60. package/dist/src/index.d.ts.map +1 -1
  61. package/dist/src/index.js +3 -1
  62. package/dist/src/index.js.map +1 -1
  63. package/dist/src/line-reader.d.ts +4 -0
  64. package/dist/src/line-reader.d.ts.map +1 -0
  65. package/dist/src/line-reader.js +59 -0
  66. package/dist/src/line-reader.js.map +1 -0
  67. package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
  68. package/dist/src/mcpl/capability-mask.js +3 -2
  69. package/dist/src/mcpl/capability-mask.js.map +1 -1
  70. package/dist/src/mcpl/channel-registry.d.ts +34 -6
  71. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  72. package/dist/src/mcpl/channel-registry.js +133 -54
  73. package/dist/src/mcpl/channel-registry.js.map +1 -1
  74. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  75. package/dist/src/mcpl/conversation-router.js +2 -1
  76. package/dist/src/mcpl/conversation-router.js.map +1 -1
  77. package/dist/src/mcpl/convert-block.d.ts +1 -0
  78. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  79. package/dist/src/mcpl/convert-block.js +16 -4
  80. package/dist/src/mcpl/convert-block.js.map +1 -1
  81. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  82. package/dist/src/mcpl/hook-orchestrator.js +4 -3
  83. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  84. package/dist/src/mcpl/inbound-source.d.ts +29 -11
  85. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  86. package/dist/src/mcpl/inbound-source.js +43 -70
  87. package/dist/src/mcpl/inbound-source.js.map +1 -1
  88. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  89. package/dist/src/mcpl/modern-connection.js +11 -7
  90. package/dist/src/mcpl/modern-connection.js.map +1 -1
  91. package/dist/src/mcpl/protocol-family.d.ts +3 -2
  92. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  93. package/dist/src/mcpl/protocol-family.js +25 -3
  94. package/dist/src/mcpl/protocol-family.js.map +1 -1
  95. package/dist/src/mcpl/push-handler.js +2 -2
  96. package/dist/src/mcpl/push-handler.js.map +1 -1
  97. package/dist/src/mcpl/references.d.ts +12 -40
  98. package/dist/src/mcpl/references.d.ts.map +1 -1
  99. package/dist/src/mcpl/references.js +29 -23
  100. package/dist/src/mcpl/references.js.map +1 -1
  101. package/dist/src/mcpl/server-connection.d.ts +18 -8
  102. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  103. package/dist/src/mcpl/server-connection.js +70 -30
  104. package/dist/src/mcpl/server-connection.js.map +1 -1
  105. package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
  106. package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
  107. package/dist/src/mcpl/skip-reply-wake.js +25 -0
  108. package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
  109. package/dist/src/mcpl/tags.d.ts +9 -2
  110. package/dist/src/mcpl/tags.d.ts.map +1 -1
  111. package/dist/src/mcpl/tags.js +12 -2
  112. package/dist/src/mcpl/tags.js.map +1 -1
  113. package/dist/src/mcpl/tool-presence.d.ts +66 -14
  114. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  115. package/dist/src/mcpl/tool-presence.js +150 -80
  116. package/dist/src/mcpl/tool-presence.js.map +1 -1
  117. package/dist/src/mcpl/tool-signature.d.ts +10 -3
  118. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  119. package/dist/src/mcpl/tool-signature.js +60 -30
  120. package/dist/src/mcpl/tool-signature.js.map +1 -1
  121. package/dist/src/mcpl/toolset-host.d.ts +88 -55
  122. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  123. package/dist/src/mcpl/toolset-host.js +220 -111
  124. package/dist/src/mcpl/toolset-host.js.map +1 -1
  125. package/dist/src/mcpl/toolsets-module.d.ts +27 -35
  126. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  127. package/dist/src/mcpl/toolsets-module.js +313 -91
  128. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  129. package/dist/src/mcpl/transport.d.ts +1 -1
  130. package/dist/src/mcpl/transport.d.ts.map +1 -1
  131. package/dist/src/mcpl/transport.js +16 -16
  132. package/dist/src/mcpl/transport.js.map +1 -1
  133. package/dist/src/mcpl/types.d.ts +31 -13
  134. package/dist/src/mcpl/types.d.ts.map +1 -1
  135. package/dist/src/mcpl/types.js.map +1 -1
  136. package/dist/src/module-registry.d.ts +10 -8
  137. package/dist/src/module-registry.d.ts.map +1 -1
  138. package/dist/src/module-registry.js +15 -9
  139. package/dist/src/module-registry.js.map +1 -1
  140. package/dist/src/modules/api/index.d.ts.map +1 -1
  141. package/dist/src/modules/api/index.js +4 -2
  142. package/dist/src/modules/api/index.js.map +1 -1
  143. package/dist/src/modules/discord/index.d.ts.map +1 -1
  144. package/dist/src/modules/discord/index.js +33 -25
  145. package/dist/src/modules/discord/index.js.map +1 -1
  146. package/dist/src/modules/history/index.d.ts.map +1 -1
  147. package/dist/src/modules/history/index.js +30 -17
  148. package/dist/src/modules/history/index.js.map +1 -1
  149. package/dist/src/modules/sender-name.d.ts +7 -0
  150. package/dist/src/modules/sender-name.d.ts.map +1 -0
  151. package/dist/src/modules/sender-name.js +10 -0
  152. package/dist/src/modules/sender-name.js.map +1 -0
  153. package/dist/src/modules/workspace/answers.d.ts +26 -0
  154. package/dist/src/modules/workspace/answers.d.ts.map +1 -0
  155. package/dist/src/modules/workspace/answers.js +138 -0
  156. package/dist/src/modules/workspace/answers.js.map +1 -0
  157. package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
  158. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  159. package/dist/src/modules/workspace/branch-intent.js +6 -2
  160. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  161. package/dist/src/modules/workspace/direct-write.d.ts +3 -0
  162. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
  163. package/dist/src/modules/workspace/direct-write.js +59 -24
  164. package/dist/src/modules/workspace/direct-write.js.map +1 -1
  165. package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
  166. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  167. package/dist/src/modules/workspace/disk-agreement.js +48 -17
  168. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  169. package/dist/src/modules/workspace/edit-text.d.ts +2 -1
  170. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
  171. package/dist/src/modules/workspace/edit-text.js +4 -2
  172. package/dist/src/modules/workspace/edit-text.js.map +1 -1
  173. package/dist/src/modules/workspace/effects.d.ts +4 -2
  174. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  175. package/dist/src/modules/workspace/effects.js +67 -50
  176. package/dist/src/modules/workspace/effects.js.map +1 -1
  177. package/dist/src/modules/workspace/index.d.ts +63 -51
  178. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  179. package/dist/src/modules/workspace/index.js +980 -547
  180. package/dist/src/modules/workspace/index.js.map +1 -1
  181. package/dist/src/modules/workspace/observe.d.ts +125 -82
  182. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  183. package/dist/src/modules/workspace/observe.js +465 -241
  184. package/dist/src/modules/workspace/observe.js.map +1 -1
  185. package/dist/src/modules/workspace/read-page.d.ts.map +1 -1
  186. package/dist/src/modules/workspace/read-page.js +7 -1
  187. package/dist/src/modules/workspace/read-page.js.map +1 -1
  188. package/dist/src/modules/workspace/reconcile.d.ts +7 -1
  189. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  190. package/dist/src/modules/workspace/reconcile.js +156 -50
  191. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  192. package/dist/src/modules/workspace/result-text.d.ts +1 -0
  193. package/dist/src/modules/workspace/result-text.d.ts.map +1 -1
  194. package/dist/src/modules/workspace/result-text.js.map +1 -1
  195. package/dist/src/modules/workspace/sync.d.ts +9 -0
  196. package/dist/src/modules/workspace/sync.d.ts.map +1 -1
  197. package/dist/src/modules/workspace/sync.js +14 -0
  198. package/dist/src/modules/workspace/sync.js.map +1 -1
  199. package/dist/src/modules/workspace/types.d.ts +96 -43
  200. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  201. package/dist/src/modules/workspace/types.js.map +1 -1
  202. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  203. package/dist/src/modules/workspace/watcher.js +8 -3
  204. package/dist/src/modules/workspace/watcher.js.map +1 -1
  205. package/dist/src/notice-tags.d.ts +79 -0
  206. package/dist/src/notice-tags.d.ts.map +1 -0
  207. package/dist/src/notice-tags.js +83 -0
  208. package/dist/src/notice-tags.js.map +1 -0
  209. package/dist/src/prose-drafts.d.ts +4 -3
  210. package/dist/src/prose-drafts.d.ts.map +1 -1
  211. package/dist/src/prose-drafts.js +3 -2
  212. package/dist/src/prose-drafts.js.map +1 -1
  213. package/dist/src/reasoning-loss-notice.d.ts +23 -0
  214. package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
  215. package/dist/src/reasoning-loss-notice.js +52 -0
  216. package/dist/src/reasoning-loss-notice.js.map +1 -0
  217. package/dist/src/reasoning-loss.d.ts +24 -0
  218. package/dist/src/reasoning-loss.d.ts.map +1 -0
  219. package/dist/src/reasoning-loss.js +102 -0
  220. package/dist/src/reasoning-loss.js.map +1 -0
  221. package/dist/src/send-tools.d.ts +3 -2
  222. package/dist/src/send-tools.d.ts.map +1 -1
  223. package/dist/src/send-tools.js +3 -2
  224. package/dist/src/send-tools.js.map +1 -1
  225. package/dist/src/sent-rows.js +1 -1
  226. package/dist/src/silent-heartbeat.d.ts +1 -53
  227. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  228. package/dist/src/silent-heartbeat.js +56 -3
  229. package/dist/src/silent-heartbeat.js.map +1 -1
  230. package/dist/src/supplied-text.d.ts +35 -0
  231. package/dist/src/supplied-text.d.ts.map +1 -0
  232. package/dist/src/supplied-text.js +141 -0
  233. package/dist/src/supplied-text.js.map +1 -0
  234. package/dist/src/thinking-drops.d.ts +4 -11
  235. package/dist/src/thinking-drops.d.ts.map +1 -1
  236. package/dist/src/thinking-drops.js +7 -14
  237. package/dist/src/thinking-drops.js.map +1 -1
  238. package/dist/src/tool-args.d.ts +12 -0
  239. package/dist/src/tool-args.d.ts.map +1 -1
  240. package/dist/src/tool-args.js +39 -4
  241. package/dist/src/tool-args.js.map +1 -1
  242. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  243. package/dist/src/tool-image-ledger.js +2 -1
  244. package/dist/src/tool-image-ledger.js.map +1 -1
  245. package/dist/src/tool-presentation.d.ts.map +1 -1
  246. package/dist/src/tool-presentation.js +5 -4
  247. package/dist/src/tool-presentation.js.map +1 -1
  248. package/dist/src/tool-result-guard.d.ts +8 -4
  249. package/dist/src/tool-result-guard.d.ts.map +1 -1
  250. package/dist/src/tool-result-guard.js +23 -11
  251. package/dist/src/tool-result-guard.js.map +1 -1
  252. package/dist/src/tune-out/coordinator.d.ts +3 -2
  253. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  254. package/dist/src/tune-out/coordinator.js +24 -26
  255. package/dist/src/tune-out/coordinator.js.map +1 -1
  256. package/dist/src/tune-out/tools.d.ts +4 -1
  257. package/dist/src/tune-out/tools.d.ts.map +1 -1
  258. package/dist/src/types/agent.d.ts +63 -18
  259. package/dist/src/types/agent.d.ts.map +1 -1
  260. package/dist/src/types/events.d.ts +18 -8
  261. package/dist/src/types/events.d.ts.map +1 -1
  262. package/dist/src/types/framework.d.ts +58 -0
  263. package/dist/src/types/framework.d.ts.map +1 -1
  264. package/dist/src/types/module.d.ts +27 -13
  265. package/dist/src/types/module.d.ts.map +1 -1
  266. package/dist/src/types/trace.d.ts +34 -1
  267. package/dist/src/types/trace.d.ts.map +1 -1
  268. package/package.json +3 -3
  269. package/src/agent.ts +76 -12
  270. package/src/api/mcp-server-core.ts +3 -14
  271. package/src/api/server.ts +11 -5
  272. package/src/auto-continue.ts +12 -5
  273. package/src/code-execution/isolate/runner.ts +7 -5
  274. package/src/code-execution/isolate/scripts.ts +3 -2
  275. package/src/cut-off-round.ts +98 -0
  276. package/src/framework.ts +3772 -1138
  277. package/src/gate/event-gate.ts +705 -60
  278. package/src/gate/index.ts +9 -1
  279. package/src/gate/reply-judge.ts +512 -0
  280. package/src/gate/types.ts +176 -3
  281. package/src/heartbeat/heartbeat.ts +843 -0
  282. package/src/heartbeat/reminder-time.ts +107 -0
  283. package/src/held-writes.ts +3 -3
  284. package/src/host-presentation.ts +605 -0
  285. package/src/index.ts +5 -1
  286. package/src/line-reader.ts +58 -0
  287. package/src/mcpl/capability-mask.ts +3 -2
  288. package/src/mcpl/channel-registry.ts +139 -53
  289. package/src/mcpl/conversation-router.ts +2 -1
  290. package/src/mcpl/convert-block.ts +17 -4
  291. package/src/mcpl/hook-orchestrator.ts +4 -3
  292. package/src/mcpl/inbound-source.ts +45 -70
  293. package/src/mcpl/modern-connection.ts +11 -7
  294. package/src/mcpl/protocol-family.ts +24 -3
  295. package/src/mcpl/push-handler.ts +2 -2
  296. package/src/mcpl/references.ts +31 -23
  297. package/src/mcpl/server-connection.ts +76 -34
  298. package/src/mcpl/skip-reply-wake.ts +35 -0
  299. package/src/mcpl/tags.ts +13 -2
  300. package/src/mcpl/tool-presence.ts +149 -82
  301. package/src/mcpl/tool-signature.ts +64 -32
  302. package/src/mcpl/toolset-host.ts +212 -110
  303. package/src/mcpl/toolsets-module.ts +277 -91
  304. package/src/mcpl/transport.ts +18 -14
  305. package/src/mcpl/types.ts +32 -13
  306. package/src/module-registry.ts +14 -9
  307. package/src/modules/api/index.ts +4 -2
  308. package/src/modules/discord/index.ts +37 -25
  309. package/src/modules/history/index.ts +30 -18
  310. package/src/modules/sender-name.ts +9 -0
  311. package/src/modules/workspace/answers.ts +151 -0
  312. package/src/modules/workspace/branch-intent.ts +10 -5
  313. package/src/modules/workspace/direct-write.ts +59 -23
  314. package/src/modules/workspace/disk-agreement.ts +52 -21
  315. package/src/modules/workspace/edit-text.ts +7 -5
  316. package/src/modules/workspace/effects.ts +69 -50
  317. package/src/modules/workspace/index.ts +966 -556
  318. package/src/modules/workspace/observe.ts +497 -242
  319. package/src/modules/workspace/read-page.ts +7 -1
  320. package/src/modules/workspace/reconcile.ts +184 -49
  321. package/src/modules/workspace/result-text.ts +2 -0
  322. package/src/modules/workspace/sync.ts +16 -0
  323. package/src/modules/workspace/types.ts +96 -43
  324. package/src/modules/workspace/watcher.ts +8 -3
  325. package/src/notice-tags.ts +84 -0
  326. package/src/prose-drafts.ts +4 -3
  327. package/src/reasoning-loss-notice.ts +64 -0
  328. package/src/reasoning-loss.ts +120 -0
  329. package/src/send-tools.ts +3 -2
  330. package/src/sent-rows.ts +1 -1
  331. package/src/silent-heartbeat.ts +4 -3
  332. package/src/supplied-text.ts +154 -0
  333. package/src/thinking-drops.ts +10 -16
  334. package/src/tool-args.ts +38 -4
  335. package/src/tool-image-ledger.ts +2 -1
  336. package/src/tool-presentation.ts +5 -4
  337. package/src/tool-result-guard.ts +26 -11
  338. package/src/tune-out/coordinator.ts +25 -27
  339. package/src/tune-out/tools.ts +4 -1
  340. package/src/types/agent.ts +63 -18
  341. package/src/types/events.ts +18 -8
  342. package/src/types/framework.ts +59 -0
  343. package/src/types/module.ts +28 -13
  344. package/src/types/trace.ts +36 -1
@@ -22,6 +22,8 @@
22
22
  */
23
23
 
24
24
  import { createHash } from 'node:crypto';
25
+ import { blockAsStored, MalformedContentError } from '@animalabs/context-manager';
26
+ import { logDetail, logValue } from './log-value.js';
25
27
 
26
28
  /** The MCPL admission lane that accepted an item. Its contract decides what
27
29
  * an `eventId` is worth: `push/event` deduplicates by eventId, and RFC-006
@@ -192,92 +194,65 @@ export function canonicalJson(value: unknown): string {
192
194
  }
193
195
 
194
196
  /**
195
- * A block as the store hands it back. Context-manager keeps inline media as
196
- * blobs (blob-manager.ts): an image whose source isn't a URL, and every
197
- * document, audio or video block, comes back as `{ type, source: { type:
198
- * 'base64', data, mediaType } }` and nothing else, its data re-encoded from
199
- * the decoded bytes, and an image's media type taken from its bytes'
200
- * signature where they have one. Any other block comes back as it was given,
201
- * except a store-internal `blob_ref` from an unresolved read, which resolves
202
- * to inline media by default. It is digested as given here, so an unresolved
203
- * read and its resolved form differ. Context-manager refuses caller-supplied
204
- * blob_ref writes; the MCPL lanes never store one.
197
+ * blockAsStored throws a `MalformedContentError` for a block the store
198
+ * refuses on write, a caller-supplied `blob_ref` among them. Such a block
199
+ * has no stored form, and whatever is thrown, it is digested as given.
205
200
  */
206
201
  function storedBlock(block: unknown): unknown {
207
- const media = block as { type?: unknown; source?: { type?: unknown; data?: unknown; mediaType?: unknown } } | null;
208
- const blob = media?.type === 'image'
209
- ? media.source?.type !== 'url'
210
- : media?.type === 'document' || media?.type === 'audio' || media?.type === 'video';
211
- const source = media?.source;
212
- if (!blob || typeof source?.data !== 'string') return block;
213
- const data = canonicalBase64(source.data);
214
- const mediaType = media!.type === 'image'
215
- ? sniffRasterImageMediaType(Buffer.from(data.slice(0, 32), 'base64')) ?? source.mediaType
216
- : source.mediaType;
217
- return { type: media!.type, source: { type: 'base64', data, mediaType } };
218
- }
219
-
220
- /**
221
- * Base64 as the store writes it back from the decoded bytes: padded, with no
222
- * whitespace, the standard alphabet and zero unused bits. Data already in
223
- * that form, as every stored copy's is, is returned without decoding it all.
224
- */
225
- function canonicalBase64(data: string): string {
226
- if (data.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(data)) {
227
- const last = data.slice(-4);
228
- if (Buffer.from(last, 'base64').toString('base64') === last) return data;
229
- }
230
- return Buffer.from(data, 'base64').toString('base64');
231
- }
232
-
233
- /**
234
- * The store's own test for raster image types with an unambiguous byte
235
- * signature, copied from context-manager's blob-manager.ts (it isn't
236
- * exported): the store relabels an image whose bytes match one of these.
237
- */
238
- function sniffRasterImageMediaType(bytes: Uint8Array): string | null {
239
- if (bytes.length >= 8 &&
240
- bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47 &&
241
- bytes[4] === 0x0d && bytes[5] === 0x0a && bytes[6] === 0x1a && bytes[7] === 0x0a) {
242
- return 'image/png';
243
- }
244
- if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
245
- return 'image/jpeg';
246
- }
247
- if (bytes.length >= 6) {
248
- const signature = Buffer.from(bytes.subarray(0, 6)).toString('ascii');
249
- if (signature === 'GIF87a' || signature === 'GIF89a') return 'image/gif';
250
- }
251
- if (bytes.length >= 12 &&
252
- Buffer.from(bytes.subarray(0, 4)).toString('ascii') === 'RIFF' &&
253
- Buffer.from(bytes.subarray(8, 12)).toString('ascii') === 'WEBP') {
254
- return 'image/webp';
202
+ try {
203
+ return blockAsStored(block as Parameters<typeof blockAsStored>[0]);
204
+ } catch (err) {
205
+ if (!(err instanceof MalformedContentError)) {
206
+ const { name, message } = (err ?? {}) as { name?: unknown; message?: unknown };
207
+ console.error(
208
+ `[stored-digest-as-given] error=${logValue(typeof name === 'string' ? name : typeof err)}: ` +
209
+ logDetail(typeof message === 'string' ? message : ''),
210
+ );
211
+ }
212
+ return block;
255
213
  }
256
- return null;
257
214
  }
258
215
 
259
216
  /**
260
217
  * The version identity of a delivered body (agreed with the receipts lane,
261
218
  * room-220 #46752–#47210): SHA-256 hex of `canonicalJson([blocks])` over the
262
219
  * ContentBlock[] the framework stores for the body — after MCPL conversion,
263
- * before any host decoration is added and before storage shards it — with
264
- * each block as the store keeps it (storedBlock, canonicalJson). So a body
265
- * hashes alike as delivered, as handed to storage, and as read back from
266
- * the store. The one-element array keeps the framing an undecorated,
267
- * unsharded stored copy has always hashed to, so a stamped copy and an
268
- * older copy of the same body share a version.
220
+ * which marks the server's text at the framework's notice openings
221
+ * (supplied-text.ts), before any host decoration is added and before storage
222
+ * shards it — with
223
+ * each block as the store keeps it, which context-manager's `blockAsStored`
224
+ * gives. So a body hashes alike as delivered, as handed to storage, and as
225
+ * read back unsharded from the store with its blobs resolved, except one
226
+ * whose blocks the store changes together: when it mends a `tool_use` whose
227
+ * input isn't an object, it also releases a raw replay form (`rawXml`,
228
+ * `rawItem`) that the message's other blocks share, which no one block's
229
+ * stored form shows. A body an MCPL lane stamps is never one. A block the
230
+ * store refuses on write, a `blob_ref` among them, is digested as given. The
231
+ * one-element array keeps the framing an undecorated, unsharded stored copy
232
+ * has always hashed to, so a stamped copy and an older copy of the same body
233
+ * share a version where the store keeps the body as it did then.
234
+ *
235
+ * A digest is a digest under the context-manager release that computed it: a
236
+ * release that changes what the store keeps, such as its set of image
237
+ * signatures, changes the digest of a body it touches.
269
238
  *
270
239
  * Ingestion stamps it as `metadata.sourceBodyDigest`, and the same function
271
240
  * over exactly the blocks handed to storage (decorations included) as
272
241
  * `metadata.storedBodyDigest`, on both MCPL lanes. Both live outside the
273
242
  * frozen admission envelope: they describe the body actually delivered (a
274
243
  * materialization, a correction), not the admission. Neither proves later
275
- * presence or completeness; an unsharded stored copy that no longer hashes
244
+ * presence or completeness. An unsharded stored copy that no longer hashes
276
245
  * to its storedBodyDigest was changed after delivery (editMessage keeps
277
- * metadata). A body the store shards carries the whole body's two digests
278
- * on every shard, and no shard hashes to them. Check a copy read back with
279
- * its blobs resolved (getAllMessages, getMessage, or getMessageWindow
280
- * without `resolveBlobs: false`): a blob reference never matches.
246
+ * metadata), or holds blocks the store changes together, or its digest was
247
+ * taken under a release that kept some block of it differently: a
248
+ * context-manager release that changed a stored form, or an agent-framework
249
+ * release from 0.24.0 up to the one that brought in blockAsStored, which
250
+ * took stored forms from a copy of the store's transform. Digests already
251
+ * stamped are never recomputed. A body the store shards carries the whole
252
+ * body's two digests on every shard, and no shard hashes to them. Check a
253
+ * copy read back with its blobs resolved (getAllMessages, getMessage, or
254
+ * getMessageWindow without `resolveBlobs: false`): a blob reference never
255
+ * matches.
281
256
  *
282
257
  * Exported from the package root, so a consumer checks a copy against either
283
258
  * field with this same function rather than a copy of it.
@@ -68,7 +68,7 @@ import {
68
68
 
69
69
  import { StdioTransport, type McplTransport } from './transport.js';
70
70
  import { McplProtocolVersionError, McplRequestError, McplUnreapedLaunchError } from './server-connection.js';
71
- import { LEGACY_MCP_PROTOCOL_VERSION, MAX_TIMER_MS, MODERN_MCP_PROTOCOL_VERSION, checkServerConfig, describeCredentialError, maskedError, noteDialSent, redactServerText, redactedData, serverTextRedactor, type ServerTextRedactor } from './protocol-family.js';
71
+ import { LEGACY_MCP_PROTOCOL_VERSION, MAX_TIMER_MS, MODERN_MCP_PROTOCOL_VERSION, checkServerConfig, describeCredentialError, logDroppedLine, maskedError, noteDialSent, redactServerText, redactedData, serverTextRedactor, type ServerTextRedactor } from './protocol-family.js';
72
72
  import type { McplServerConfig, McpToolDefinition } from './types.js';
73
73
  import { boundedForLog } from './errors.js';
74
74
  import { logDetail, logValue, logValueRedacted, loggedCode } from './log-value.js';
@@ -297,8 +297,9 @@ export function malformedLineDiagnostic(
297
297
 
298
298
  /**
299
299
  * SDK `Transport` over Connectome's stdio spawner. Framing is the same
300
- * newline-delimited JSON-RPC as legacy. Lines that are not JSON are ignored,
301
- * as the legacy engine ignores them. Because this is a custom transport, the
300
+ * newline-delimited JSON-RPC as legacy. Lines that are not JSON are dropped,
301
+ * and the first three on a connection that aren't blank are logged by their
302
+ * length, as the legacy engine does. Because this is a custom transport, the
302
303
  * SDK runs `server/discover` on this very process instead of starting a
303
304
  * throwaway copy, and cancels with `notifications/cancelled`, which is the
304
305
  * stdio binding's mechanism.
@@ -332,14 +333,17 @@ class SpawnerStdioTransport implements Transport {
332
333
  const line = await this.spawning;
333
334
  this.line = line;
334
335
  line.on('line', (text: string) => {
335
- // A line that isn't JSON is ignored, as the legacy engine ignores it.
336
- // A line that is JSON but not a JSON-RPC message (`null`, an array, a
337
- // bare value) is a diagnostic. It must never reach a handler that
338
- // assumes the shape: thrown from this callback, it would end the host.
336
+ // A line that isn't JSON is dropped, and the first three on a
337
+ // connection that aren't blank are logged by their length, as the
338
+ // legacy engine does. A line that is JSON but not a JSON-RPC message
339
+ // (`null`, an array, a bare value) is a diagnostic. It must never reach
340
+ // a handler that assumes the shape: thrown from this callback, it would
341
+ // end the host.
339
342
  let parsed: unknown;
340
343
  try {
341
344
  parsed = JSON.parse(text);
342
345
  } catch {
346
+ logDroppedLine(this.config.id, text, 'unparsed', line);
343
347
  return;
344
348
  }
345
349
  let message: JSONRPCMessage;
@@ -22,6 +22,7 @@
22
22
  * isn't `initialize`), the SDK's stdio probe launches the server twice, and a
23
23
  * server's era is a stable fact its configuration can state.
24
24
  */
25
+ import { NOTICE_TAG } from '../notice-tags.js';
25
26
  import { DENY_BY_DEFAULT, capabilityPatternMatches } from './capability-grant.js';
26
27
  import { DEFAULT_EAGER_FETCH_MAX_BYTES, DEFAULT_MAX_TOTAL_BYTES } from './reference-fetcher.js';
27
28
  import { DEFAULT_MAX_INPUT_BYTES } from './tool-lifecycle.js';
@@ -198,11 +199,11 @@ export function serverTextRedactor(config: Pick<McplServerConfig, 'url' | 'token
198
199
 
199
200
  /** What a redaction gives in place of text when the configuration it
200
201
  * redacts with can't be read ({@link redactServerText}). */
201
- export const WITHHELD_TEXT = "[withheld: the server's configuration couldn't be read to redact this]";
202
+ export const WITHHELD_TEXT = `[${NOTICE_TAG.withheld}: the server's configuration couldn't be read to redact this]`;
202
203
 
203
204
  /** What {@link redactedData} gives in place of data nested deeper than it
204
205
  * walks. */
205
- export const TOO_DEEP_TEXT = '[withheld: nested too deep to redact]';
206
+ export const TOO_DEEP_TEXT = `[${NOTICE_TAG.withheld}: nested too deep to redact]`;
206
207
 
207
208
  /** How deep {@link redactedData} walks. A server's JSON can nest deeper than
208
209
  * a recursive walk's stack holds (10,000 levels overflow it), and the walk
@@ -528,7 +529,7 @@ export function credentialRedactor(patterns: readonly CredentialPattern[]): Reda
528
529
  /** Any value as text: a value that can't be printed reads as a placeholder. */
529
530
  export function asText(text: unknown): string {
530
531
  if (typeof text === 'string') return text;
531
- try { return String(text); } catch { return '[a value that cannot be printed]'; }
532
+ try { return String(text); } catch { return `[${NOTICE_TAG.unprintableValue}]`; }
532
533
  }
533
534
 
534
535
  /** A part of an environment variable's name that ends with a credential word, in the singular or plural, with any digits after it. */
@@ -869,3 +870,23 @@ export function checkServerConfig(config: McplServerConfig): ServerBinding {
869
870
  for (const warning of serverConfigWarnings(config)) console.warn(`[mcp] ${warning}`);
870
871
  return resolveServerBinding(config);
871
872
  }
873
+
874
+ const DROPPED_LINES_LOGGED = 3;
875
+ const droppedLinesLogged = new WeakMap<object, number>();
876
+
877
+ export function logDroppedLine(serverId: string, line: string, reason: 'unparsed' | 'not-object', connection: object): void {
878
+ if (/^[ \t]*$/.test(line)) return;
879
+ const logged = droppedLinesLogged.get(connection) ?? 0;
880
+ if (logged > DROPPED_LINES_LOGGED) return;
881
+ droppedLinesLogged.set(connection, logged + 1);
882
+ if (logged === DROPPED_LINES_LOGGED) {
883
+ console.error(`MCP server "${serverId}" sent another line that isn't a JSON object, so it was dropped; more such lines on this connection are dropped without a log line`);
884
+ return;
885
+ }
886
+ let length = 0;
887
+ for (const _ of line) length++;
888
+ const why = reason === 'not-object' ? 'is JSON but not an object'
889
+ : /^[ \t]*\{/.test(line) ? "starts like a JSON object but doesn't parse"
890
+ : "isn't JSON";
891
+ console.error(`MCP server "${serverId}" sent a line of length ${length} that ${why}, so it was dropped`);
892
+ }
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { ContentBlock } from '@animalabs/membrane';
11
- import { convertBlock } from './convert-block.js';
11
+ import { convertBlocks } from './convert-block.js';
12
12
 
13
13
  import type {
14
14
  PushEventParams,
@@ -204,7 +204,7 @@ export class PushHandler {
204
204
 
205
205
  // 2. Convert content blocks, and refuse content with nothing visible in
206
206
  // it — before dedup, so a refused eventId is not burned for a retry.
207
- const content: ContentBlock[] = params.payload.content.map((block) => convertBlock(block, 'attachment on push event', serverId));
207
+ const content: ContentBlock[] = convertBlocks(params.payload.content, 'attachment on push event', serverId);
208
208
  if (!emptyAllowed && isVisiblyEmptyConversion(params.payload.content, content)) {
209
209
  const err = new EmptyContentError();
210
210
  this.traceEmptyRejection(serverId, params);
@@ -26,6 +26,9 @@
26
26
  * the registry is what makes a future host-mediated fetcher and the
27
27
  * code-execution materialization hook possible.
28
28
  */
29
+ import { cutToFit, suppliedValue } from '../supplied-text.js';
30
+ import { NOTICE_TAG } from '../notice-tags.js';
31
+
29
32
 
30
33
  export const REFERENCE_LIMITS = { uri: 4096, mimeType: 255, name: 255, expiresAt: 64 } as const;
31
34
  export const DIGEST_PATTERN = /^sha256:[A-Za-z0-9_-]{43}$/;
@@ -210,7 +213,7 @@ export function isInlineContradiction(block: unknown): boolean {
210
213
  }
211
214
 
212
215
  export const INLINE_WITHHELD_TEXT =
213
- '[Content not shown: its inline data arrived marked as a reference too, so this host withheld it.]';
216
+ `[${NOTICE_TAG.contentNotShown}: its inline data arrived marked as a reference too, so this host withheld it.]`;
214
217
 
215
218
  /*
216
219
  * What the model-visible lanes (push, channel, context injection and tool
@@ -220,28 +223,28 @@ export const INLINE_WITHHELD_TEXT =
220
223
  * agent to do about any of them. The text marker is NON_STRING_TEXT_PLACEHOLDER
221
224
  * (visible-content.ts).
222
225
  */
223
- export const IMAGE_NO_MEDIA_TYPE_TEXT = '[Image not shown: its data arrived without a media type.]';
224
- export const IMAGE_NO_DATA_TEXT = '[Image not shown: it arrived with no usable data or link.]';
225
- export const AUDIO_NO_MEDIA_TYPE_TEXT = '[Audio not shown: its data arrived without a media type.]';
226
+ export const IMAGE_NO_MEDIA_TYPE_TEXT = `[${NOTICE_TAG.imageNotShown}: its data arrived without a media type.]`;
227
+ export const IMAGE_NO_DATA_TEXT = `[${NOTICE_TAG.imageNotShown}: it arrived with no usable data or link.]`;
228
+ export const AUDIO_NO_MEDIA_TYPE_TEXT = `[${NOTICE_TAG.audioNotShown}: its data arrived without a media type.]`;
226
229
  /** On the push, channel and context-injection lanes, audio shows only as
227
230
  * inline data, so a plain link isn't enough either. (A tool result's audio
228
231
  * with a link is a reference, as any block with one is there.) */
229
- export const AUDIO_NO_DATA_TEXT = '[Audio not shown: it arrived with no usable inline data.]';
232
+ export const AUDIO_NO_DATA_TEXT = `[${NOTICE_TAG.audioNotShown}: it arrived with no usable inline data.]`;
230
233
  /** MCP's embedded resource whose contents are neither text nor a blob, or
231
234
  * both. */
232
- export const EMBEDDED_RESOURCE_UNREADABLE_TEXT = '[Resource not shown: its contents arrived as neither text nor data, or as both.]';
235
+ export const EMBEDDED_RESOURCE_UNREADABLE_TEXT = `[${NOTICE_TAG.resourceNotShown}: its contents arrived as neither text nor data, or as both.]`;
233
236
  /** MCP's embedded resource whose blob has no media type, so it can't be
234
237
  * shown as an image or audio. */
235
- export const EMBEDDED_RESOURCE_NO_MEDIA_TYPE_TEXT = '[Resource not shown: its data arrived without a media type.]';
238
+ export const EMBEDDED_RESOURCE_NO_MEDIA_TYPE_TEXT = `[${NOTICE_TAG.resourceNotShown}: its data arrived without a media type.]`;
236
239
 
237
240
  /** MCP's embedded resource whose blob is of a type no lane can show: what
238
241
  * it was, by its type and size. Made well formed (a lone UTF-16 surrogate
239
242
  * becomes U+FFFD), as a stub is (#695), since it names the block's media
240
- * type, which sanitizeLabel leaves as it is: every lane then shows the same
241
- * text. */
243
+ * type, whose lone surrogates referenceTypeLabel leaves as they are: every
244
+ * lane then shows the same text. */
242
245
  export function embeddedBinaryText(mimeType: string, blob: string): string {
243
246
  const what = [referenceTypeLabel(mimeType), formatSizeBytes(Math.floor(blob.length * 3 / 4))].filter(Boolean).join(', ');
244
- return `[Resource not shown: this host can't display its data (${what}).]`.toWellFormed();
247
+ return `[${NOTICE_TAG.resourceNotShown}: this host can't display its data (${what}).]`.toWellFormed();
245
248
  }
246
249
  /**
247
250
  * What the converter (convertBlock, on every lane) shows for a text
@@ -250,10 +253,10 @@ export function embeddedBinaryText(mimeType: string, blob: string): string {
250
253
  * sender's, so it never makes content visible (isVisiblyEmptyConversion in
251
254
  * visible-content.ts, which re-exports it).
252
255
  */
253
- export const NON_STRING_TEXT_PLACEHOLDER = '[Text not shown: a text part arrived without readable text.]';
254
- export const REFERENCE_NO_LINK_TEXT = '[Reference not shown: it arrived without a link.]';
255
- export const REFERENCE_LINK_TOO_LONG_TEXT = '[Reference not shown: its link is longer than this host accepts.]';
256
- const UNREADABLE_BLOCK_TEXT = "[Content not shown: a part arrived in a form this host can't read.]";
256
+ export const NON_STRING_TEXT_PLACEHOLDER = `[${NOTICE_TAG.textNotShown}: a text part arrived without readable text.]`;
257
+ export const REFERENCE_NO_LINK_TEXT = `[${NOTICE_TAG.referenceNotShown}: it arrived without a link.]`;
258
+ export const REFERENCE_LINK_TOO_LONG_TEXT = `[${NOTICE_TAG.referenceNotShown}: its link is longer than this host accepts.]`;
259
+ const UNREADABLE_BLOCK_TEXT = `[${NOTICE_TAG.contentNotShown}: a part arrived in a form this host can't read.]`;
257
260
 
258
261
  /** A block of a type the host doesn't show, of no type at all, or no block.
259
262
  * The type is the sender's word, so it's shown as a label is: bounded, with
@@ -261,10 +264,10 @@ const UNREADABLE_BLOCK_TEXT = "[Content not shown: a part arrived in a form this
261
264
  * converted to one. */
262
265
  export function unknownBlockText(block: unknown): string {
263
266
  if (!isRecord(block)) return UNREADABLE_BLOCK_TEXT;
264
- const label = typeof block.type === 'string' ? sanitizeLabel(block.type, STUB_FIELD_CHARS) : '';
267
+ const label = typeof block.type === 'string' ? suppliedLabel(block.type, STUB_FIELD_CHARS) : '';
265
268
  return label
266
- ? `[Content not shown: this host can't display "${label}" content.]`
267
- : '[Content not shown: a part arrived with no type.]';
269
+ ? `[${NOTICE_TAG.contentNotShown}: this host can't display "${label}" content.]`
270
+ : `[${NOTICE_TAG.contentNotShown}: a part arrived with no type.]`;
268
271
  }
269
272
 
270
273
  /**
@@ -654,6 +657,11 @@ export function sanitizeLabel(s: string, maxChars: number): string {
654
657
  return cut + '…';
655
658
  }
656
659
 
660
+ function suppliedLabel(s: string, maxChars: number): string {
661
+ const shown = suppliedValue(s).replace(/[\p{Cc}\p{Bidi_Control}]/gu, '').trim();
662
+ return shown.length <= maxChars ? shown : `${cutToFit(shown, maxChars - 1)}…`;
663
+ }
664
+
657
665
  export function formatSizeBytes(n: number): string {
658
666
  if (n < 1024) return `${n}B`;
659
667
  if (n < 1024 * 1024) return `~${Math.round(n / 1024)}KB`;
@@ -786,7 +794,7 @@ export class ReferenceRegistry {
786
794
  return existing;
787
795
  }
788
796
  }
789
- const refId = `ref_${(++this.counter).toString(36)}_${Math.random().toString(36).slice(2, 2 + REF_ID_SUFFIX_CHARS)}`;
797
+ const refId = `${NOTICE_TAG.referenceId}_${(++this.counter).toString(36)}_${Math.random().toString(36).slice(2, 2 + REF_ID_SUFFIX_CHARS)}`;
790
798
  const record: ReferenceRecord = { refId, serverIds: serverId === undefined ? [] : [serverId], testimony, receivedAt: Date.now() };
791
799
  this.records.set(refId, record);
792
800
  this.byKey.set(testimony.uri, refId);
@@ -871,7 +879,7 @@ export interface ScriptLine {
871
879
  /** A reference's type as its stub shows one, or '' when nothing is left of
872
880
  * it once sanitized (the stub then leaves it out). */
873
881
  export function referenceTypeLabel(mimeType: string | undefined): string {
874
- return mimeType ? sanitizeLabel(mimeType, STUB_FIELD_CHARS) : '';
882
+ return mimeType ? suppliedLabel(mimeType, STUB_FIELD_CHARS) : '';
875
883
  }
876
884
 
877
885
  /**
@@ -886,7 +894,7 @@ export function referenceTypeLabel(mimeType: string | undefined): string {
886
894
  * stub, so it's no slot whatever its length.
887
895
  */
888
896
  export const REFERENCE_STUB_MAX_CHARS =
889
- `[ref_${MAX_SAFE.toString(36)}_] `.length + REF_ID_SUFFIX_CHARS
897
+ `[${NOTICE_TAG.referenceId}_${MAX_SAFE.toString(36)}_] `.length + REF_ID_SUFFIX_CHARS
890
898
  + STUB_FIELD_CHARS
891
899
  + ' — '.length + STUB_FIELD_CHARS + ', '.length + `${formatSizeBytes(MAX_SAFE)} claimed`.length
892
900
  + ' — '.length + STUB_FIELD_CHARS
@@ -1099,7 +1107,7 @@ export function buildReferenceStub(t: ReferenceTestimony, provenance?: string, s
1099
1107
  const nowMs = Date.now();
1100
1108
  const parts: string[] = [];
1101
1109
  // A label is shown only when something is left of it once sanitized.
1102
- const name = t.name ? sanitizeLabel(t.name, STUB_FIELD_CHARS) : '';
1110
+ const name = t.name ? suppliedLabel(t.name, STUB_FIELD_CHARS) : '';
1103
1111
  if (name) parts.push(name);
1104
1112
  const meta: string[] = [];
1105
1113
  const mimeType = referenceTypeLabel(t.mimeType);
@@ -1130,9 +1138,9 @@ export function buildReferenceStub(t: ReferenceTestimony, provenance?: string, s
1130
1138
  * server's link shows (StubOptions.unfetchedLinkUri).
1131
1139
  */
1132
1140
  function unfetchedLinkText(t: ReferenceTestimony, uri: string): string {
1133
- const name = t.name ? sanitizeLabel(t.name, STUB_FIELD_CHARS) : '';
1141
+ const name = t.name ? suppliedLabel(t.name, STUB_FIELD_CHARS) : '';
1134
1142
  const mimeType = referenceTypeLabel(t.mimeType);
1135
- return `[resource link${name ? ` "${name}"` : ''}: ${uri}${mimeType ? `, ${mimeType}` : ''}; not fetched]`;
1143
+ return `[${NOTICE_TAG.resourceLink}${name ? ` "${name}"` : ''}: ${uri}${mimeType ? `, ${mimeType}` : ''}; not fetched]`;
1136
1144
  }
1137
1145
 
1138
1146
  /**
@@ -20,6 +20,7 @@ import {
20
20
  LEGACY_MCP_PROTOCOL_VERSION,
21
21
  MODERN_MCP_PROTOCOL_VERSION,
22
22
  checkServerConfig,
23
+ logDroppedLine,
23
24
  redactServerText,
24
25
  redactedData,
25
26
  serverTextRedactor,
@@ -240,19 +241,29 @@ export function toolsetDeclarationsOf(advertised: unknown): Record<string, unkno
240
241
  /** What the RFC-014 read tap is handed as the connection reads a message
241
242
  * that can carry hint changes (see McplServerConnection.setReadTap). */
242
243
  export type HintRead =
243
- /** A response to a request that can carry hints (RFC-014 §6.1 carrier 1).
244
- * `awaited`: whoever sent it still waits for it; false for a result that
245
- * arrives after its timeout. A JSON-RPC error carries no state and isn't
246
- * tapped. */
247
- | { kind: 'result'; method: string; params: Record<string, unknown>; result: unknown; awaited: boolean }
244
+ /** A response to a request that can carry hints (RFC-014 §6.1 carrier 1),
245
+ * or a `tools/list`'s whole inventory (§4.1), its pages joined by
246
+ * sendToolsList, `straddled` when its pages carry different revs.
247
+ * `awaited`: whoever sent it still waits for it; false for a
248
+ * result that arrives after its timeout. A JSON-RPC error carries no state
249
+ * and isn't tapped. */
250
+ | { kind: 'result'; method: string; params: Record<string, unknown>; result: unknown; awaited: boolean; straddled?: boolean }
248
251
  /** A push/event (carrier 2), as read, before the plane gate or the grant
249
252
  * admit or refuse it. */
250
253
  | { kind: 'push'; params: unknown }
251
254
  /** A standalone tools/hints (carrier 3). */
252
255
  | { kind: 'hints'; params: unknown };
253
256
 
254
- /** Requests whose results can carry hint state (RFC-014 §6.1, §4.1). */
255
- const HINTED_METHODS: ReadonlySet<string> = new Set(['tools/call', 'toolsets/open', 'toolsets/close', 'toolsets/list', 'tools/list']);
257
+ /** Requests whose results can carry hint state, remembered for a late answer (RFC-014 §6.1). */
258
+ const HINTED_METHODS: ReadonlySet<string> = new Set(['tools/call', 'toolsets/open', 'toolsets/close', 'toolsets/list']);
259
+
260
+ function hintedParams(method: string, params: Record<string, unknown>): { toolset?: string; instance?: string } {
261
+ if (method !== 'toolsets/open' && method !== 'toolsets/close') return {};
262
+ return {
263
+ ...(typeof params.toolset === 'string' ? { toolset: params.toolset } : {}),
264
+ ...(typeof params.instance === 'string' ? { instance: params.instance } : {}),
265
+ };
266
+ }
256
267
 
257
268
  /**
258
269
  * A handshake that failed, and whose cleanup couldn't confirm that the child
@@ -330,6 +341,7 @@ interface PendingRequest {
330
341
  resolve: (result: unknown) => void;
331
342
  reject: (error: Error) => void;
332
343
  method: string;
344
+ onRead?: (result: unknown) => void;
333
345
  /** Per-request timeout timer; cleared on response/close/timeout. */
334
346
  timer?: ReturnType<typeof setTimeout>;
335
347
  }
@@ -339,6 +351,9 @@ interface RequestOptions {
339
351
  timeoutMs?: number;
340
352
  /** Surface a late response even when it carries no checkpoint state. */
341
353
  surfaceOrphanedResponse?: boolean;
354
+ /** Called with a successful result as its response is read, before
355
+ * anything read after it and before the request's promise settles. */
356
+ onRead?: (result: unknown) => void;
342
357
  }
343
358
 
344
359
  /** A server→host message held while its plane is closed. `kept`: it came on
@@ -452,15 +467,18 @@ export class McplServerConnection extends EventEmitter {
452
467
  * the message: a push the plane holds or the grant refuses, a result its
453
468
  * caller stopped waiting for. The tap sees every such message of the
454
469
  * current transport, synchronously, before anything routes it; a late
455
- * answer's request is remembered past its timeout (hintedRequests) so its
456
- * hints still apply. Null (the default) taps nothing, and the connection
470
+ * answer's request is remembered past its timeout (hintedRequests, while
471
+ * no more than 1,000 hinted requests await an answer) so its hints still
472
+ * apply. Null (the default) taps nothing, and the connection
457
473
  * behaves as it always has.
458
474
  */
459
475
  private readTap: ((read: HintRead, epoch: number) => void) | null = null;
460
476
  /** The hinted requests sent on this transport, by id, kept past a timeout
461
- * so a late result is still tapped; bounded. Cleared at each boundary,
477
+ * so a late result is still tapped: each one's method, and an open's or
478
+ * close's toolset and instance, which are all the read of its result
479
+ * uses; never a call's arguments. Bounded. Cleared at each boundary,
462
480
  * since the next transport numbers its requests afresh. */
463
- private hintedRequests = new Map<string | number, { method: string; params: Record<string, unknown> }>();
481
+ private hintedRequests = new Map<string | number, { method: string; params: { toolset?: string; instance?: string } }>();
464
482
 
465
483
  /** Install (or, with null, remove) the RFC-014 read tap (see readTap). */
466
484
  setReadTap(tap: ((read: HintRead, epoch: number) => void) | null): void {
@@ -1313,13 +1331,8 @@ export class McplServerConnection extends EventEmitter {
1313
1331
  reject(closedWhileConnecting());
1314
1332
  };
1315
1333
  const onLine = (line: string) => {
1316
- let msg: JsonRpcResponse;
1317
- try {
1318
- msg = JSON.parse(line) as JsonRpcResponse;
1319
- } catch {
1320
- return; // Ignore non-JSON lines (e.g. logback output from Java servers)
1321
- }
1322
- if (msg === null || typeof msg !== 'object' || Array.isArray(msg)) return;
1334
+ const msg = messageFromLine(config.id, line, transport) as JsonRpcResponse | undefined;
1335
+ if (!msg) return;
1323
1336
  // A request or a notification from the server is never the
1324
1337
  // initialize response, whatever its id: a server's own ids can start
1325
1338
  // at 0, this handshake's. MCP lets a server ping before
@@ -1708,22 +1721,40 @@ export class McplServerConnection extends EventEmitter {
1708
1721
  * read it as a complete page). A page that isn't well formed (no `tools` array, a cursor
1709
1722
  * that isn't a string), or cursors that don't terminate (a repeat, or more
1710
1723
  * than {@link MAX_TOOL_LIST_PAGES} pages), are errors, never a partial or
1711
- * empty inventory published as complete.
1724
+ * empty inventory published as complete. The read tap (RFC-014) is handed
1725
+ * the complete inventory once, with the last page's `_meta`, as the last
1726
+ * page's response is read, before anything read after it; a list that
1727
+ * fails or times out hands it nothing. It's `straddled` when its pages
1728
+ * carry different `mcpl/rev`s.
1712
1729
  */
1713
1730
  async sendToolsList(): Promise<{ tools: McpToolDefinition[] }> {
1714
1731
  const tools: McpToolDefinition[] = [];
1715
1732
  const seen = new Set<string>();
1716
1733
  let cursor: string | undefined;
1717
1734
  let first: Record<string, unknown> | undefined;
1735
+ const read: McpToolDefinition[] = [];
1736
+ const revs = new Set<unknown>();
1737
+ const onRead = (result: unknown) => {
1738
+ if (!result || typeof result !== 'object' || !Array.isArray((result as { tools?: unknown }).tools)) return;
1739
+ const r = result as { tools: McpToolDefinition[]; nextCursor?: unknown; _meta?: unknown };
1740
+ for (const t of r.tools) read.push(t);
1741
+ const rev = (r._meta as Record<string, unknown> | undefined)?.['mcpl/rev'];
1742
+ if (typeof rev === 'number') revs.add(rev);
1743
+ if (r.nextCursor !== undefined && r.nextCursor !== null) return;
1744
+ this.tap({
1745
+ kind: 'result', method: 'tools/list', params: {}, ...(revs.size > 1 ? { straddled: true } : {}),
1746
+ result: { tools: read, ...(r._meta !== undefined ? { _meta: r._meta } : {}) }, awaited: true,
1747
+ });
1748
+ };
1718
1749
  for (let page = 0; page < MAX_TOOL_LIST_PAGES; page++) {
1719
- const result = await this.sendRequest('tools/list', cursor === undefined ? {} : { cursor }) as Record<string, unknown> | null;
1750
+ const result = await this.sendRequest('tools/list', cursor === undefined ? {} : { cursor }, { onRead }) as Record<string, unknown> | null;
1720
1751
  const malformed = (why: string) =>
1721
1752
  new Error(`MCPL server "${this.id}" sent a malformed tools/list page ${page + 1}: ${why}`);
1722
1753
  if (!result || typeof result !== 'object' || !Array.isArray(result.tools)) {
1723
1754
  throw malformed('no tools array');
1724
1755
  }
1725
1756
  first ??= result;
1726
- tools.push(...(result.tools as McpToolDefinition[]));
1757
+ for (const t of result.tools as McpToolDefinition[]) tools.push(t);
1727
1758
  if (result.nextCursor === undefined || result.nextCursor === null) {
1728
1759
  // The first page's other fields are kept as they came, so a
1729
1760
  // single-page answer is returned exactly as before.
@@ -2176,7 +2207,7 @@ export class McplServerConnection extends EventEmitter {
2176
2207
  }
2177
2208
 
2178
2209
  return new Promise((resolve, reject) => {
2179
- const pending: PendingRequest = { resolve, reject, method };
2210
+ const pending: PendingRequest = { resolve, reject, method, ...(options.onRead ? { onRead: options.onRead } : {}) };
2180
2211
  const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs;
2181
2212
  if (timeoutMs > 0) {
2182
2213
  pending.timer = setTimeout(() => {
@@ -2218,7 +2249,7 @@ export class McplServerConnection extends EventEmitter {
2218
2249
  // RFC-014: a request whose result can carry hints is remembered past
2219
2250
  // its timeout, so a late result's hints still apply (readTap).
2220
2251
  if (this.readTap && HINTED_METHODS.has(method)) {
2221
- this.hintedRequests.set(id, { method, params });
2252
+ this.hintedRequests.set(id, { method, params: hintedParams(method, params) });
2222
2253
  if (this.hintedRequests.size > 1000) this.hintedRequests.delete(this.hintedRequests.keys().next().value!);
2223
2254
  }
2224
2255
  try {
@@ -2341,17 +2372,8 @@ export class McplServerConnection extends EventEmitter {
2341
2372
  // passed is the dead process's (afterBoundary).
2342
2373
  const epoch = this.transportEpoch;
2343
2374
  transport.on('line', (line: string) => {
2344
- let msg: JsonRpcRequest | JsonRpcResponse;
2345
- try {
2346
- msg = JSON.parse(line);
2347
- } catch {
2348
- // Ignore non-JSON lines
2349
- return;
2350
- }
2351
-
2352
- // JSON-RPC messages must be objects. Ignore unrelated stdout/log values
2353
- // before property access, just as malformed JSON lines are ignored.
2354
- if (msg === null || typeof msg !== 'object' || Array.isArray(msg)) return;
2375
+ const msg = messageFromLine(this.id, line, transport) as JsonRpcRequest | JsonRpcResponse | undefined;
2376
+ if (!msg) return;
2355
2377
  try {
2356
2378
  this.dispatchMessage(msg, epoch);
2357
2379
  } catch (error) {
@@ -2654,6 +2676,11 @@ export class McplServerConnection extends EventEmitter {
2654
2676
  logValueRedacted(response.error.message, redact),
2655
2677
  );
2656
2678
  } else {
2679
+ try {
2680
+ pending.onRead?.(response.result);
2681
+ } catch (error) {
2682
+ console.error(`MCPL server "${this.id}": reading ${pending.method}'s result failed: ${logDetail(describeCause(error))}`);
2683
+ }
2657
2684
  pending.resolve(response.result);
2658
2685
  }
2659
2686
  }
@@ -2789,3 +2816,18 @@ export class McplServerConnection extends EventEmitter {
2789
2816
  });
2790
2817
  }
2791
2818
  }
2819
+
2820
+ function messageFromLine(serverId: string, line: string, transport: McplTransport): object | undefined {
2821
+ let value: unknown;
2822
+ try {
2823
+ value = JSON.parse(line);
2824
+ } catch {
2825
+ logDroppedLine(serverId, line, 'unparsed', transport);
2826
+ return undefined;
2827
+ }
2828
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
2829
+ logDroppedLine(serverId, line, 'not-object', transport);
2830
+ return undefined;
2831
+ }
2832
+ return value;
2833
+ }