@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
@@ -81,8 +81,9 @@ function anyMatches(patterns: string[], path: string): boolean {
81
81
  return patterns.some((pattern) => patternMatchesPath(pattern, path));
82
82
  }
83
83
 
84
- /** Keys that are negotiation metadata, not grantable capabilities. */
85
- const UNMASKABLE_KEYS = new Set(['version', 'featureSets']);
84
+ /** Keys that are negotiation metadata, or RFC-014 declarations (toolHints,
85
+ * toolsets, surface), not grantable capabilities. */
86
+ const UNMASKABLE_KEYS = new Set(['version', 'featureSets', 'toolHints', 'toolsets', 'surface']);
86
87
 
87
88
  /**
88
89
  * Interior object paths whose object value is itself the capability, with
@@ -13,8 +13,10 @@
13
13
  * - Build channel context for beforeInference params
14
14
  */
15
15
 
16
+ import { NOTICE_TAG } from '../notice-tags.js';
16
17
  import type { ContentBlock } from '@animalabs/membrane';
17
- import { convertBlock } from './convert-block.js';
18
+ import { convertBlocks } from './convert-block.js';
19
+ import { cutToFit, markedText, markNoticeOpenings, nameToTypeBack, quotedValue, showsAsItself, suppliedValue } from '../supplied-text.js';
18
20
  import type { JsStore } from '@animalabs/chronicle';
19
21
  import { isDeepStrictEqual } from 'node:util';
20
22
  import { ensureStateRegistered } from '../module-registry.js';
@@ -45,7 +47,7 @@ import { RefusalLines } from './refusal-lines.js';
45
47
  import { CapabilityGrant } from './capability-grant.js';
46
48
  import type { InboundSource } from './inbound-source.js';
47
49
  import { McplRequestError, type McplServerConnection } from './server-connection.js';
48
- import { readSkipReplyWake } from './skip-reply-wake.js';
50
+ import { readSkipReplyWake, skipReplyNote, type SkipReplyEarlierWake } from './skip-reply-wake.js';
49
51
  import { isProseTargetToken } from './prose-grammar.js';
50
52
 
51
53
  // ============================================================================
@@ -643,19 +645,21 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
643
645
  // Constructor Options
644
646
  // ============================================================================
645
647
 
646
- /** Bound connector values for notices, logs and an outcome's reason. A
647
- * PublishOutcome keeps the connector's structured data (`detail`) as the
648
- * connection gives it, its strings redacted (redactedData). */
648
+ /** Bound connector values for notices, logs and an outcome's reason, on one
649
+ * line with their notice openings marked (suppliedValue). A PublishOutcome
650
+ * keeps the connector's structured data (`detail`) as the connection gives
651
+ * it, its strings redacted (redactedData). */
649
652
  export function connectorText(value: unknown): string {
650
653
  let text: string;
651
654
  try {
652
655
  text = typeof value === 'string' ? value : JSON.stringify(value) ?? String(value);
653
656
  } catch {
654
- return '[unserializable connector value]';
657
+ return `[${NOTICE_TAG.unserializableConnectorValue}]`;
655
658
  }
656
659
  const limit = 2000;
657
- const suffix = '… [truncated]';
658
- return text.length > limit ? text.slice(0, limit - suffix.length) + suffix : text;
660
+ const suffix = `… [${NOTICE_TAG.truncated}]`;
661
+ const shown = suppliedValue(text);
662
+ return shown.length > limit ? cutToFit(shown, limit - suffix.length) + suffix : shown;
659
663
  }
660
664
 
661
665
  /**
@@ -688,6 +692,9 @@ export function sameChannel(a: ChannelRef | null | undefined, b: ChannelRef | nu
688
692
  return a.serverId === b.serverId && a.channelId === b.channelId;
689
693
  }
690
694
 
695
+ export const NO_CHANNEL_EXAMPLE = '#<channel>';
696
+ export const NO_PERSON_EXAMPLE = '@<person>';
697
+
691
698
  /**
692
699
  * A channel's server-qualified name, `serverId:channelId`. A name given as a
693
700
  * string (a tool's channelId, a `>>` target, a host's call) may take this
@@ -699,12 +706,32 @@ export function qualifiedChannelName(ref: ChannelRef): string {
699
706
  return `${ref.serverId}:${ref.channelId}`;
700
707
  }
701
708
 
709
+ function markedHistory(history: readonly unknown[]): unknown[] {
710
+ return history.map((item) => {
711
+ if (item === null || typeof item !== 'object' || Array.isArray(item) || !('content' in item)) return item;
712
+ const { content } = item as { content: unknown };
713
+ if (Array.isArray(content)) return { ...item, content: markNoticeOpenings(content.map(withEmbeddedTextMarked) as object[]) };
714
+ return typeof content === 'string' ? { ...item, content: markedText(content) } : item;
715
+ });
716
+ }
717
+
718
+ function withEmbeddedTextMarked(block: unknown): unknown {
719
+ if (block === null || typeof block !== 'object' || (block as { type?: unknown }).type !== 'resource') return block;
720
+ const { resource } = block as { resource?: unknown };
721
+ if (resource === null || typeof resource !== 'object' || typeof (resource as { text?: unknown }).text !== 'string') return block;
722
+ return { ...block, resource: { ...resource, text: markedText((resource as { text: string }).text) } };
723
+ }
724
+
702
725
  /** A channel told apart by its id and its server, for one no string picks
703
- * alone: `"b:room" on server "a"`. No input takes it for a name. */
726
+ * alone or whose name wouldn't show as itself: `"b:room" on server "a"`,
727
+ * each a JSON literal a channel tool's channelId and serverId take back
728
+ * (quotedValue). No input takes the whole form for a name. */
704
729
  function channelOnServer(ref: ChannelRef): string {
705
- return `${JSON.stringify(ref.channelId)} on server ${JSON.stringify(ref.serverId)}`;
730
+ return `${quotedValue(ref.channelId)} on server ${quotedValue(ref.serverId)}`;
706
731
  }
707
732
 
733
+ const CHANNEL_ON_SERVER_AT_END = /("(?:[^"\\]|\\.)*") on server ("(?:[^"\\]|\\.)*")$/;
734
+
708
735
  /**
709
736
  * What one channels/publish attempt established (ChannelRegistry.publish).
710
737
  * - `delivered`: the connector returned `delivered: true`. A historical
@@ -1089,6 +1116,10 @@ export type ChannelToolOrigin =
1089
1116
  * name one of them answers to by its id means that channel
1090
1117
  * (ChannelRegistry.ownChannelNamed). */
1091
1118
  own?: readonly ChannelRef[];
1119
+ /** For a skip_reply whose self-wake the framework armed: the heartbeat's
1120
+ * wake due before it, when there is one, which its confirmation names
1121
+ * (skipReplyNote). */
1122
+ earlierWake?: SkipReplyEarlierWake;
1092
1123
  };
1093
1124
 
1094
1125
  /** The machine decision sources a module-origin channel_close may record.
@@ -1894,7 +1925,7 @@ export class ChannelRegistry {
1894
1925
  }
1895
1926
  }
1896
1927
  if (message.tags) message.tags = expandCoreTags(message.tags);
1897
- const convertedContent: ContentBlock[] = message.content.map((block) => convertBlock(block, 'attachment on channel message', serverId));
1928
+ const convertedContent: ContentBlock[] = convertBlocks(message.content, 'attachment on channel message', serverId);
1898
1929
  if (!emptyAllowed && isVisiblyEmptyConversion(message.content, convertedContent)) {
1899
1930
  rejectEmpty();
1900
1931
  continue;
@@ -1909,7 +1940,8 @@ export class ChannelRegistry {
1909
1940
  const incomingEntry = this.channels.get(incomingKey)!;
1910
1941
  const markAccepted = (firstView = false) => {
1911
1942
  // Non-conversational markers (a reaction added or removed, an edit, a
1912
- // deletion) are ABOUT a message, not one the agent replies to: they
1943
+ // deletion, and a chat:system notice such as a join or the weather)
1944
+ // are ABOUT a message or the place, not one the agent replies to: they
1913
1945
  // must not retarget the default publish locus or become
1914
1946
  // `context.incoming`'s reply anchor. An edit the coalescer delivered
1915
1947
  // as the agent's first view counts as the message it replaced.
@@ -2385,7 +2417,10 @@ export class ChannelRegistry {
2385
2417
  return this.handleToolJournal(input as { content?: string });
2386
2418
 
2387
2419
  case 'skip_reply':
2388
- return this.handleToolSkipReply(input as { reason?: string; wake_in_seconds?: unknown });
2420
+ return this.handleToolSkipReply(
2421
+ input as { reason?: string; wake_in_seconds?: unknown },
2422
+ origin?.kind === 'agent' ? origin.earlierWake : undefined,
2423
+ );
2389
2424
 
2390
2425
  default:
2391
2426
  // Not every agent is given every channel tool, so the refusal points
@@ -2441,7 +2476,8 @@ export class ChannelRegistry {
2441
2476
  // declared it inbound-only, since speech isn't posted there): a conversation
2442
2477
  // fork's home channel, else this turn's channel (single trunk agent — item-3
2443
2478
  // redux: its locus, else its triggering channel), else the global default
2444
- // (heartbeats). Without this, a fork
2479
+ // (a no-trigger turn's, but not a heartbeat's, whose turn takes none).
2480
+ // Without this, a fork
2445
2481
  // was advertised the global locus but published somewhere else; and a trunk
2446
2482
  // agent was told the wrong channel under concurrency.
2447
2483
  // Each lookup is keyed on whether a name was given, not its truthiness:
@@ -3920,12 +3956,20 @@ export class ChannelRegistry {
3920
3956
  * name, only where no registered channel takes that name, as a channel
3921
3957
  * tool reads it or a `>>` target does (by label or raw id too), so the
3922
3958
  * name can't send anything to another channel; else by its id and server.
3959
+ * A name that wouldn't show as itself (showsAsItself: one holding a line
3960
+ * break, a notice opening, a quote, a backslash or a lone surrogate, or
3961
+ * whitespace at its edges) is shown by its id and its server too, which
3962
+ * the channel tools take exactly and the prose resolver doesn't, so no name
3963
+ * shown can be another channel's.
3923
3964
  */
3924
3965
  nameShown(ref: ChannelRef): string {
3925
3966
  const entry = this.entryAt(ref);
3926
- if (entry) return this.nameFor(entry) ?? channelOnServer(ref);
3967
+ if (entry) {
3968
+ const name = this.nameFor(entry);
3969
+ return name !== undefined && showsAsItself(name) ? name : channelOnServer(ref);
3970
+ }
3927
3971
  for (const name of [ref.channelId, qualifiedChannelName(ref)]) {
3928
- if (this.entriesNamed(name).length === 0 && !('channelId' in this.resolveProseTarget(name))) return name;
3972
+ if (showsAsItself(name) && this.entriesNamed(name).length === 0 && !('channelId' in this.resolveProseTarget(name))) return name;
3929
3973
  }
3930
3974
  return channelOnServer(ref);
3931
3975
  }
@@ -3965,7 +4009,7 @@ export class ChannelRegistry {
3965
4009
  private tuneOutEndText(entry: ChannelEntry, tool?: string): string {
3966
4010
  const close = tool === 'channel_close';
3967
4011
  return this.nameFor(entry) !== undefined
3968
- ? `Call tune_out with channelId ${this.quotedShown(refOf(entry))} and mode "cancel" for it to resume normal ` +
4012
+ ? `Call tune_out with channelId ${quotedValue(this.nameFor(entry)!)} and mode "cancel" for it to resume normal ` +
3969
4013
  `attention; cancelling delivers the diverted backlog${close ? ', and channel_close closes it after that.' : '.'}`
3970
4014
  : 'No channelId names it alone, so tune_out can\'t cancel it: its tune-out ends by itself when its duration ' +
3971
4015
  'runs out, or at the first wake past its wake budget, and its backlog is delivered then.' +
@@ -3978,7 +4022,7 @@ export class ChannelRegistry {
3978
4022
  */
3979
4023
  quotedShown(ref: ChannelRef): string {
3980
4024
  const shown = this.nameShown(ref);
3981
- return shown === channelOnServer(ref) ? shown : JSON.stringify(shown);
4025
+ return shown === channelOnServer(ref) ? shown : quotedValue(shown);
3982
4026
  }
3983
4027
 
3984
4028
  /**
@@ -4016,7 +4060,7 @@ export class ChannelRegistry {
4016
4060
  const nothing = opts.nothing ?? 'nothing ran';
4017
4061
  const names = entries.map((e) => this.nameFor(e));
4018
4062
  if (names.every((n) => n !== undefined)) {
4019
- const listed = names.map((n) => JSON.stringify(n)).join(', ');
4063
+ const listed = names.map((n) => quotedValue(n!)).join(', ');
4020
4064
  const servers = [...new Set(entries.map((e) => JSON.stringify(e.serverId)))];
4021
4065
  // One server's two channels can answer to one name too, as `s`'s
4022
4066
  // `s:room` and `room` both do to `s:room`; its server picks neither.
@@ -4037,8 +4081,8 @@ export class ChannelRegistry {
4037
4081
  const own = names[i];
4038
4082
  if (own === undefined && !opts.takesServerId) return `- ${where}: no channelId reaches it alone.`;
4039
4083
  return own !== undefined
4040
- ? `- ${where}: call again with channelId ${JSON.stringify(own)}.`
4041
- : `- ${where}: call again with channelId ${JSON.stringify(e.descriptor.id)} and serverId ${JSON.stringify(e.serverId)}.`;
4084
+ ? `- ${where}: call again with channelId ${quotedValue(own)}.`
4085
+ : `- ${where}: call again with channelId ${quotedValue(e.descriptor.id)} and serverId ${JSON.stringify(e.serverId)}.`;
4042
4086
  });
4043
4087
  const count = entries.length === 2 ? 'either of two channels' : `any of ${entries.length} channels`;
4044
4088
  return `Channel ${JSON.stringify(name)} could mean ${count}, so ${nothing}:\n${lines.join('\n')}`;
@@ -4077,7 +4121,7 @@ export class ChannelRegistry {
4077
4121
  // so the text says whose.
4078
4122
  if (!alone) return 'channel_list shows the channels and their servers.';
4079
4123
  const one = named[0]!;
4080
- const names = `${JSON.stringify(channelId)} alone names channel ${JSON.stringify(one.descriptor.id)} on server `
4124
+ const names = `${JSON.stringify(channelId)} alone names channel ${quotedValue(one.descriptor.id)} on server `
4081
4125
  + JSON.stringify(one.serverId);
4082
4126
  return takesServerId ? `${names}: for that one, call again with serverId left out.` : `${names}.`;
4083
4127
  }
@@ -4089,7 +4133,7 @@ export class ChannelRegistry {
4089
4133
  const places = elsewhere.map((e) => {
4090
4134
  const at = `on server ${JSON.stringify(e.serverId)}`;
4091
4135
  const name = this.nameFor(e);
4092
- return name === undefined ? `${at} (no channelId reaches it alone)` : `${at} as ${JSON.stringify(name)}`;
4136
+ return name === undefined ? `${at} (no channelId reaches it alone)` : `${at} as ${quotedValue(name)}`;
4093
4137
  });
4094
4138
  return `It's ${places.length === 1 ? places[0] : `${places.slice(0, -1).join(', ')} and ${places.at(-1)}`}.`;
4095
4139
  }
@@ -4393,17 +4437,19 @@ export class ChannelRegistry {
4393
4437
  spec: string,
4394
4438
  opts?: { forSpeech?: boolean; own?: readonly ChannelRef[] },
4395
4439
  ): PublishDestination | { error: string; candidates?: string[]; refusedChannelId?: string } {
4396
- const own = this.ownChannelNamed(spec.trim(), opts?.own);
4440
+ const name = spec.trim();
4441
+ if (name === NO_CHANNEL_EXAMPLE || name === NO_PERSON_EXAMPLE) return { error: `${name} is the example's placeholder: write the name it stands for in its place` };
4442
+ const own = this.ownChannelNamed(name, opts?.own);
4397
4443
  let resolved: PublishDestination | { error: string; candidates?: string[] };
4398
4444
  if (own === undefined) {
4399
- resolved = this.resolveTargetByName(spec);
4445
+ resolved = this.resolveTargetByName(name);
4400
4446
  } else if ('neither' in own) {
4401
- resolved = { error: `"${spec.trim()}" is the id of this turn's channels on more than one server`, candidates: this.ownCandidates(own.neither).map((c) => c.text) };
4447
+ resolved = { error: `"${name}" is the id of this turn's channels on more than one server`, candidates: this.ownCandidates(own.neither).map((c) => c.prose) };
4402
4448
  } else {
4403
4449
  const entry = this.entryAt(own.channel);
4404
4450
  resolved = entry
4405
4451
  ? { channelId: entry.descriptor.id, serverId: entry.serverId, label: entry.descriptor.label }
4406
- : { error: `${this.ownChannelGoneText(spec.trim(), own.channel)}, and nothing can be sent there until that server registers it again` };
4452
+ : { error: `${this.ownChannelGoneText(name, own.channel)}, and nothing can be sent there until that server registers it again` };
4407
4453
  }
4408
4454
  if (!opts?.forSpeech || 'error' in resolved) return resolved;
4409
4455
  const entry = this.entryAt(resolved);
@@ -4462,7 +4508,7 @@ export class ChannelRegistry {
4462
4508
  if (!mine) return undefined;
4463
4509
  if ('neither' in mine) {
4464
4510
  const done = tool === 'publish' ? 'was posted' : tool === 'deliver' ? 'was delivered' : 'ran';
4465
- return { error: this.ownNeitherRefusal(name, mine.neither, done) };
4511
+ return { error: this.ownNeitherRefusal(name, mine.neither, done, false) };
4466
4512
  }
4467
4513
  return this.entryAt(mine.channel) ? { channel: mine.channel } : { error: this.ownChannelGoneRefusal(name, mine.channel, tool) };
4468
4514
  }
@@ -4481,32 +4527,73 @@ export class ChannelRegistry {
4481
4527
  /**
4482
4528
  * The turn's channels a name means neither of (ownChannelNamed), each by a
4483
4529
  * string that reaches it outside the turn's rule: its server-qualified
4484
- * name, where that picks it alone, since its id is the name refused. One
4485
- * no string picks alone, and one its server no longer registers, is named
4486
- * by its id and server, saying so; `reaches` says which a name reaches.
4530
+ * name, where that picks it alone, since its id is the name refused. `text`
4531
+ * is that name, which a refusal writes as JSON; `prose`, which a `>>`
4532
+ * target reads, is that name where it shows as itself, else its id and
4533
+ * server. One no string picks alone, and one its server no longer
4534
+ * registers, are named by id and server in both, saying so. `reaches` says
4535
+ * which a name reaches.
4487
4536
  */
4488
- private ownCandidates(channels: readonly ChannelRef[]): Array<{ text: string; reaches: boolean }> {
4537
+ private ownCandidates(channels: readonly ChannelRef[]): Array<{ text: string; prose: string; reaches: boolean; byServer: boolean }> {
4489
4538
  return channels.map((c) => {
4490
4539
  const entry = this.entryAt(c);
4491
- if (!entry) return { text: `${channelOnServer(c)} (its server no longer registers it)`, reaches: false };
4540
+ if (!entry) {
4541
+ const text = `${channelOnServer(c)} (its server no longer registers it)`;
4542
+ return { text, prose: text, reaches: false, byServer: false };
4543
+ }
4492
4544
  const qualified = qualifiedChannelName(c);
4493
4545
  const picked = this.entriesNamed(qualified);
4494
- return picked.length === 1 && picked[0] === entry
4495
- ? { text: qualified, reaches: true }
4496
- : { text: `${channelOnServer(c)} (no channelId reaches it alone)`, reaches: false };
4546
+ if (picked.length !== 1 || picked[0] !== entry) {
4547
+ const text = `${channelOnServer(c)} (no channelId reaches it alone)`;
4548
+ return { text, prose: text, reaches: false, byServer: true };
4549
+ }
4550
+ return { text: qualified, prose: showsAsItself(qualified) ? qualified : channelOnServer(c), reaches: true, byServer: false };
4497
4551
  });
4498
4552
  }
4499
4553
 
4554
+ /**
4555
+ * What a refusal listing these candidates says of each one that is a
4556
+ * channel shown by its id and server: the channelId channel_publish reaches
4557
+ * it by, or that no channelId reaches it alone. That is its server-qualified
4558
+ * name where that picks it alone, since a bare id is what a turn's own
4559
+ * channels answer to first, whether or not their server still registers
4560
+ * them (ownChannelNamed), and else its id where that picks it alone and
4561
+ * isn't empty (channel_publish refuses an empty one). It leaves out a channel no
4562
+ * longer registered and a candidate that already says no channelId reaches
4563
+ * it alone, and is empty when nothing is left.
4564
+ */
4565
+ publishRouteFor(candidates: readonly string[]): string {
4566
+ const parts = new Set<string>();
4567
+ for (const candidate of candidates) {
4568
+ const shown = CHANNEL_ON_SERVER_AT_END.exec(candidate);
4569
+ if (!shown) continue;
4570
+ const ref: ChannelRef = { channelId: JSON.parse(shown[1]!) as string, serverId: JSON.parse(shown[2]!) as string };
4571
+ const entry = this.entryAt(ref);
4572
+ if (!entry) continue;
4573
+ const name = [qualifiedChannelName(ref), ref.channelId].find((n) => {
4574
+ const named = n === '' ? [] : this.entriesNamed(n);
4575
+ return named.length === 1 && named[0] === entry;
4576
+ });
4577
+ parts.add(name !== undefined
4578
+ ? `channel_publish reaches ${channelOnServer(ref)} with channelId ${quotedValue(name)}`
4579
+ : `no channelId reaches ${channelOnServer(ref)} alone`);
4580
+ }
4581
+ return parts.size ? ` For a channel shown by its id and server: ${[...parts].join('; ')}.` : '';
4582
+ }
4583
+
4500
4584
  /** A tool's refusal of a name the turn's channels on more than one server
4501
4585
  * answer to by their id (ownChannelNamed's neither). */
4502
- private ownNeitherRefusal(name: string, channels: readonly ChannelRef[], done: string): string {
4586
+ private ownNeitherRefusal(name: string, channels: readonly ChannelRef[], done: string, takesServer = true): string {
4503
4587
  const candidates = this.ownCandidates(channels);
4504
- const named = candidates.filter((c) => c.reaches).map((c) => JSON.stringify(c.text));
4588
+ const named = candidates.filter((c) => c.reaches).map((c) => quotedValue(c.text));
4505
4589
  const pick = named.length === candidates.length ? ' Call again with channelId set to one of them.'
4506
4590
  : named.length === 1 ? ` Call again with channelId ${named[0]}.`
4507
4591
  : named.length > 1 ? ` Call again with channelId set to one of ${named.join(', ')}.` : '';
4508
- const listed = candidates.map((c) => (c.reaches ? JSON.stringify(c.text) : c.text)).join(', ');
4509
- return `"${name}" is the id of this turn's channels on more than one server (${listed}), so nothing ${done}.${pick}`;
4592
+ const listed = candidates.map((c) => (c.reaches ? quotedValue(c.text) : c.text)).join(', ');
4593
+ const byServer = takesServer && candidates.some((c) => c.byServer)
4594
+ ? ' For a channel shown by its id and server, call again with that channelId and that serverId.'
4595
+ : '';
4596
+ return `"${name}" is the id of this turn's channels on more than one server (${listed}), so nothing ${done}.${pick}${byServer}`;
4510
4597
  }
4511
4598
 
4512
4599
  private resolveTargetByName(
@@ -4520,7 +4607,7 @@ export class ChannelRegistry {
4520
4607
  // A candidate is named by the token that picks it alone (nameFor), so
4521
4608
  // two servers' channels with one id or one label can be told apart, or,
4522
4609
  // where no string picks it alone, by its id and its server.
4523
- const token = (e: ChannelEntry): string => this.nameFor(e) ?? channelOnServer(refOf(e));
4610
+ const token = (e: ChannelEntry): string => this.nameShown(refOf(e));
4524
4611
  // A DM is shown by its label, the person, where that label, given as a
4525
4612
  // target, picks that DM. It doesn't where another channel answers to that
4526
4613
  // label too, as two servers' DMs with one person do (a label is matched
@@ -4536,7 +4623,7 @@ export class ChannelRegistry {
4536
4623
  };
4537
4624
  const dmShown = (list: ChannelEntry[]): string[] => list.map((e) => {
4538
4625
  const label = e.descriptor.label ?? e.descriptor.id;
4539
- return labelPicks(e, label) ? label : token(e);
4626
+ return showsAsItself(label) && labelPicks(e, label) ? label : token(e);
4540
4627
  });
4541
4628
 
4542
4629
  // A channel id, or its server-qualified name (entriesNamed).
@@ -4654,7 +4741,7 @@ export class ChannelRegistry {
4654
4741
  if (byName.length > 1) {
4655
4742
  return {
4656
4743
  error: `"${trimmed}" matches several channels`,
4657
- candidates: byName.map((e) => `${e.descriptor.label} = ${token(e)}`),
4744
+ candidates: byName.map((e) => `${nameToTypeBack(e.descriptor.label ?? '')} = ${token(e)}`),
4658
4745
  };
4659
4746
  }
4660
4747
 
@@ -4663,7 +4750,7 @@ export class ChannelRegistry {
4663
4750
  const near = entries
4664
4751
  .filter((e) => norm(e.descriptor.label ?? '').includes(norm(trimmed)))
4665
4752
  .slice(0, 5)
4666
- .map((e) => `${e.descriptor.label} = ${token(e)}`);
4753
+ .map((e) => `${nameToTypeBack(e.descriptor.label ?? '')} = ${token(e)}`);
4667
4754
  return { error: `no channel matches "${trimmed}"`, ...(near.length ? { candidates: near } : {}) };
4668
4755
  }
4669
4756
 
@@ -4700,7 +4787,7 @@ export class ChannelRegistry {
4700
4787
  qualifiedChannelName(ref),
4701
4788
  ];
4702
4789
  for (const c of candidates) {
4703
- if (!isProseTargetToken(c) || c === '#') continue;
4790
+ if (!isProseTargetToken(c) || c === '#' || !showsAsItself(c)) continue;
4704
4791
  const r = this.resolveProseTarget(c, { forSpeech: true });
4705
4792
  if ('channelId' in r && sameChannel(r, ref)) return c;
4706
4793
  }
@@ -4989,7 +5076,7 @@ export class ChannelRegistry {
4989
5076
  channelId: input.channelId,
4990
5077
  serverId: entry.serverId,
4991
5078
  status: alreadyDesiredOpen ? 'reconciled' : 'opened',
4992
- ...(result.history ? { history: result.history } : {}),
5079
+ ...(result.history ? { history: markedHistory(result.history) } : {}),
4993
5080
  ...(result.historyTruncated ? { historyTruncated: true } : {}),
4994
5081
  },
4995
5082
  };
@@ -5478,13 +5565,14 @@ export class ChannelRegistry {
5478
5565
  * tool, so any trailing prose this turn is NOT posted. Replaces the old
5479
5566
  * overloaded use of `think` for staying silent.
5480
5567
  */
5481
- private handleToolSkipReply(input: { reason?: string; wake_in_seconds?: unknown }): ToolResult {
5568
+ private handleToolSkipReply(input: { reason?: string; wake_in_seconds?: unknown }, earlierWake?: SkipReplyEarlierWake): ToolResult {
5482
5569
  // The self-wake itself is armed by the framework (which owns the
5483
5570
  // EventGate) BEFORE its dispatch reaches this handler, from the same
5484
5571
  // reading, and the framework refuses a wake the host can't set before
5485
5572
  // dispatching. A caller of this registry's handleChannelToolCall
5486
5573
  // (AgentFramework.channels) goes past both, so its note names a wake
5487
- // nothing armed.
5574
+ // nothing armed. The framework, which also owns the heartbeat, says
5575
+ // which of its wakes is due before the self-wake (`earlierWake`).
5488
5576
  const wake = readSkipReplyWake(input);
5489
5577
  const selfWake = wake.kind === 'wake' ? wake.seconds : undefined;
5490
5578
  return {
@@ -5500,9 +5588,7 @@ export class ChannelRegistry {
5500
5588
  // on Fable: ~60% of skip_reply result bytes were verbatim input echo).
5501
5589
  data: {
5502
5590
  skipped: true,
5503
- note: selfWake !== undefined
5504
- ? `Turn ended; nothing sent. Self-wake in ~${selfWake}s unless something wakes you first.`
5505
- : 'Turn ended; nothing sent.',
5591
+ note: skipReplyNote(selfWake, selfWake !== undefined ? earlierWake : undefined),
5506
5592
  },
5507
5593
  };
5508
5594
  }
@@ -6095,7 +6181,7 @@ export class ChannelRegistry {
6095
6181
  entry = this.entryAt(serverGiven ? { serverId: input.serverId!, channelId: fallback!.channelId } : fallback!);
6096
6182
  } else if (own && 'neither' in own) {
6097
6183
  if (seen) seen.refusal = 'shared-id';
6098
- return { success: false, error: this.ownNeitherRefusal(input.channelId, own.neither, 'was posted'), isError: true };
6184
+ return { success: false, error: this.ownNeitherRefusal(input.channelId, own.neither, 'was posted', false), isError: true };
6099
6185
  } else if (own) {
6100
6186
  entry = this.entryAt(own.channel);
6101
6187
  if (!entry) {
@@ -6126,7 +6212,7 @@ export class ChannelRegistry {
6126
6212
  // channel with its id isn't where its message came from.
6127
6213
  error: input.channelId === undefined
6128
6214
  ? serverGiven
6129
- ? `Server "${input.serverId}" has no channel "${fallback!.channelId}", the id of the channel this call would default to, so nothing was posted.`
6215
+ ? `Server "${input.serverId}" has no channel ${quotedValue(fallback!.channelId)}, the id of the channel this call would default to, so nothing was posted.`
6130
6216
  : `The channel this would default to, ${this.quotedShown(fallback!)}, isn't registered now, so nothing was posted.`
6131
6217
  // This tool's schema has no serverId, so one given here is a host's
6132
6218
  // ref (publishForAgent), or one an agent added unasked, and the
@@ -34,6 +34,7 @@
34
34
  * follow-up.
35
35
  */
36
36
 
37
+ import { NOTICE_TAG } from '../notice-tags.js';
37
38
  import { createHash } from 'node:crypto';
38
39
  import type { ContextStrategy } from '@animalabs/context-manager';
39
40
  import type { ChannelDescriptor } from './types.js';
@@ -144,7 +145,7 @@ export type RoutingDecision =
144
145
  const DEFAULT_IDLE_TTL_MS = 12 * 60 * 60 * 1000;
145
146
 
146
147
  export const DEFAULT_CLOSURE_PROMPT =
147
- '[system] This engagement is closing due to inactivity. Finalize your ' +
148
+ `[${NOTICE_TAG.system}] This engagement is closing due to inactivity. Finalize your ` +
148
149
  'work: post any promised results to the channel and record outstanding ' +
149
150
  'threads as unresolved. Do not ask follow-up questions.';
150
151
 
@@ -4,6 +4,7 @@
4
4
  * about a malformed block holds on all three.
5
5
  */
6
6
 
7
+ import { NOTICE_TAG } from '../notice-tags.js';
7
8
  import type { ContentBlock } from '@animalabs/membrane';
8
9
  import {
9
10
  AUDIO_NO_DATA_TEXT,
@@ -18,6 +19,7 @@ import {
18
19
  } from './references.js';
19
20
  import { NON_STRING_TEXT_PLACEHOLDER } from './visible-content.js';
20
21
  import type { McplContentBlock } from './types.js';
22
+ import { markNoticeOpenings } from '../supplied-text.js';
21
23
 
22
24
  /**
23
25
  * Convert a single MCPL wire-format content block to a membrane ContentBlock.
@@ -50,7 +52,7 @@ export function convertBlock(block: McplContentBlock, provenance: string, server
50
52
  // is a claim, one the reference rejects included, as classifyBlock
51
53
  // and isInlineContradiction read it: a falsy one such as 0, false
52
54
  // or '' stubs too, rather than sending the URI as the image's source.
53
- return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? '[reference]' };
55
+ return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? `[${NOTICE_TAG.reference}]` };
54
56
  }
55
57
  if (isFilledString(block.data) && isFilledString(block.mimeType)) {
56
58
  return {
@@ -76,7 +78,7 @@ export function convertBlock(block: McplContentBlock, provenance: string, server
76
78
  // RFC-005 uri-form media with a disposition claim, a falsy one
77
79
  // included, as for an image (above): stub, do not hand the URI to
78
80
  // the provider or inline it.
79
- return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? '[reference]' };
81
+ return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? `[${NOTICE_TAG.reference}]` };
80
82
  }
81
83
  if (isFilledString(block.data) && isFilledString(block.mimeType)) {
82
84
  return {
@@ -95,14 +97,14 @@ export function convertBlock(block: McplContentBlock, provenance: string, server
95
97
  }
96
98
  // RFC-005: reference blocks become bounded stubs — never raw URIs
97
99
  // (a signed URL is a bearer credential that looks like a location).
98
- return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? '[reference]' };
100
+ return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? `[${NOTICE_TAG.reference}]` };
99
101
  }
100
102
 
101
103
  default: {
102
104
  // MCP's link to a resource isn't one of MCPL's own blocks, but a server
103
105
  // can send one, and RFC-005 §8.1 makes it a reference, shown as one.
104
106
  if ((block as { type?: unknown }).type === 'resource_link') {
105
- return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? '[reference]' };
107
+ return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? `[${NOTICE_TAG.reference}]` };
106
108
  }
107
109
  // Unknown wire block types previously fell off the exhaustive switch
108
110
  // and propagated `undefined` into ContentBlock[]. Fail visibly. A type
@@ -113,6 +115,17 @@ export function convertBlock(block: McplContentBlock, provenance: string, server
113
115
  }
114
116
  }
115
117
 
118
+ export function convertBlocks(blocks: readonly McplContentBlock[], provenance: string, serverId: string): ContentBlock[] {
119
+ const converted = blocks.map((block) => convertBlock(block, provenance, serverId));
120
+ return markNoticeOpenings(converted, (index) => sendersText(blocks[index]));
121
+ }
122
+
123
+ function sendersText(block: McplContentBlock | undefined): boolean {
124
+ if (block?.type === 'text') return typeof block.text === 'string';
125
+ const c = classifyBlock(block);
126
+ return c.kind === 'embedded' && c.contents.kind === 'block' && c.contents.block.type === 'text';
127
+ }
128
+
116
129
  /** A string with something in it: the only data, mimeType or uri a block's
117
130
  * source is built from, and the only uri that makes it a reference. Anything
118
131
  * else a sender puts there leaves the block without it, so conversion never
@@ -14,7 +14,8 @@
14
14
  */
15
15
 
16
16
  import type { ContentBlock } from '@animalabs/membrane';
17
- import { convertBlock } from './convert-block.js';
17
+ import { convertBlocks } from './convert-block.js';
18
+ import { markedText } from '../supplied-text.js';
18
19
  import type { ContextInjection } from '@animalabs/context-manager';
19
20
 
20
21
  import type {
@@ -64,9 +65,9 @@ function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise
64
65
  */
65
66
  function convertContent(content: string | McplContentBlock[], serverId: string): ContentBlock[] {
66
67
  if (typeof content === 'string') {
67
- return [{ type: 'text', text: content }];
68
+ return [{ type: 'text', text: markedText(content) }];
68
69
  }
69
- return content.map((block) => convertBlock(block, 'attachment on context injection', serverId));
70
+ return convertBlocks(content, 'attachment on context injection', serverId);
70
71
  }
71
72
 
72
73
  /**