@animalabs/agent-framework 0.23.3 → 0.24.1

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 (355) hide show
  1. package/README.md +212 -42
  2. package/dist/src/agent.d.ts +65 -9
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +98 -12
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/server.d.ts.map +1 -1
  7. package/dist/src/api/server.js +14 -12
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/clock-rows.d.ts +115 -0
  10. package/dist/src/clock-rows.d.ts.map +1 -0
  11. package/dist/src/clock-rows.js +222 -0
  12. package/dist/src/clock-rows.js.map +1 -0
  13. package/dist/src/code-execution/engine.d.ts +177 -0
  14. package/dist/src/code-execution/engine.d.ts.map +1 -0
  15. package/dist/src/code-execution/engine.js +494 -0
  16. package/dist/src/code-execution/engine.js.map +1 -0
  17. package/dist/src/code-execution/index.d.ts +5 -10
  18. package/dist/src/code-execution/index.d.ts.map +1 -1
  19. package/dist/src/code-execution/index.js +5 -9
  20. package/dist/src/code-execution/index.js.map +1 -1
  21. package/dist/src/code-execution/isolate/calls.d.ts +114 -0
  22. package/dist/src/code-execution/isolate/calls.d.ts.map +1 -0
  23. package/dist/src/code-execution/isolate/calls.js +145 -0
  24. package/dist/src/code-execution/isolate/calls.js.map +1 -0
  25. package/dist/src/code-execution/isolate/child-entry.d.ts +2 -0
  26. package/dist/src/code-execution/isolate/child-entry.d.ts.map +1 -0
  27. package/dist/src/code-execution/isolate/child-entry.js +5 -0
  28. package/dist/src/code-execution/isolate/child-entry.js.map +1 -0
  29. package/dist/src/code-execution/isolate/child-main.d.ts +26 -0
  30. package/dist/src/code-execution/isolate/child-main.d.ts.map +1 -0
  31. package/dist/src/code-execution/isolate/child-main.js +132 -0
  32. package/dist/src/code-execution/isolate/child-main.js.map +1 -0
  33. package/dist/src/code-execution/isolate/child.d.ts +44 -0
  34. package/dist/src/code-execution/isolate/child.d.ts.map +1 -0
  35. package/dist/src/code-execution/isolate/child.js +315 -0
  36. package/dist/src/code-execution/isolate/child.js.map +1 -0
  37. package/dist/src/code-execution/isolate/frames.d.ts +80 -0
  38. package/dist/src/code-execution/isolate/frames.d.ts.map +1 -0
  39. package/dist/src/code-execution/isolate/frames.js +210 -0
  40. package/dist/src/code-execution/isolate/frames.js.map +1 -0
  41. package/dist/src/code-execution/isolate/output.d.ts +68 -0
  42. package/dist/src/code-execution/isolate/output.d.ts.map +1 -0
  43. package/dist/src/code-execution/isolate/output.js +143 -0
  44. package/dist/src/code-execution/isolate/output.js.map +1 -0
  45. package/dist/src/code-execution/isolate/prelude.d.ts +109 -0
  46. package/dist/src/code-execution/isolate/prelude.d.ts.map +1 -0
  47. package/dist/src/code-execution/isolate/prelude.js +401 -0
  48. package/dist/src/code-execution/isolate/prelude.js.map +1 -0
  49. package/dist/src/code-execution/isolate/protocol.d.ts +151 -0
  50. package/dist/src/code-execution/isolate/protocol.d.ts.map +1 -0
  51. package/dist/src/code-execution/isolate/protocol.js +150 -0
  52. package/dist/src/code-execution/isolate/protocol.js.map +1 -0
  53. package/dist/src/code-execution/isolate/render.d.ts +57 -0
  54. package/dist/src/code-execution/isolate/render.d.ts.map +1 -0
  55. package/dist/src/code-execution/isolate/render.js +214 -0
  56. package/dist/src/code-execution/isolate/render.js.map +1 -0
  57. package/dist/src/code-execution/isolate/runner.d.ts +250 -0
  58. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -0
  59. package/dist/src/code-execution/isolate/runner.js +854 -0
  60. package/dist/src/code-execution/isolate/runner.js.map +1 -0
  61. package/dist/src/code-execution/isolate/scripts.d.ts +175 -0
  62. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -0
  63. package/dist/src/code-execution/isolate/scripts.js +357 -0
  64. package/dist/src/code-execution/isolate/scripts.js.map +1 -0
  65. package/dist/src/code-execution/quickjs.wasm +0 -0
  66. package/dist/src/code-execution/tool-definition.d.ts +93 -43
  67. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  68. package/dist/src/code-execution/tool-definition.js +176 -142
  69. package/dist/src/code-execution/tool-definition.js.map +1 -1
  70. package/dist/src/deadline-timer.d.ts +3 -0
  71. package/dist/src/deadline-timer.d.ts.map +1 -1
  72. package/dist/src/deadline-timer.js +1 -1
  73. package/dist/src/deadline-timer.js.map +1 -1
  74. package/dist/src/framework.d.ts +1120 -457
  75. package/dist/src/framework.d.ts.map +1 -1
  76. package/dist/src/framework.js +4635 -2505
  77. package/dist/src/framework.js.map +1 -1
  78. package/dist/src/gate/event-gate.d.ts +28 -4
  79. package/dist/src/gate/event-gate.d.ts.map +1 -1
  80. package/dist/src/gate/event-gate.js +89 -13
  81. package/dist/src/gate/event-gate.js.map +1 -1
  82. package/dist/src/held-writes.d.ts +16 -3
  83. package/dist/src/held-writes.d.ts.map +1 -1
  84. package/dist/src/held-writes.js +16 -3
  85. package/dist/src/held-writes.js.map +1 -1
  86. package/dist/src/index.d.ts +8 -6
  87. package/dist/src/index.d.ts.map +1 -1
  88. package/dist/src/index.js +10 -8
  89. package/dist/src/index.js.map +1 -1
  90. package/dist/src/mcpl/channel-registry.d.ts +483 -115
  91. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  92. package/dist/src/mcpl/channel-registry.js +1735 -425
  93. package/dist/src/mcpl/channel-registry.js.map +1 -1
  94. package/dist/src/mcpl/conversation-router.d.ts +19 -39
  95. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  96. package/dist/src/mcpl/conversation-router.js +27 -59
  97. package/dist/src/mcpl/conversation-router.js.map +1 -1
  98. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  99. package/dist/src/mcpl/convert-block.js +9 -5
  100. package/dist/src/mcpl/convert-block.js.map +1 -1
  101. package/dist/src/mcpl/inbound-source.d.ts +50 -0
  102. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  103. package/dist/src/mcpl/inbound-source.js +130 -0
  104. package/dist/src/mcpl/inbound-source.js.map +1 -1
  105. package/dist/src/mcpl/index.d.ts +2 -1
  106. package/dist/src/mcpl/index.d.ts.map +1 -1
  107. package/dist/src/mcpl/index.js +3 -2
  108. package/dist/src/mcpl/index.js.map +1 -1
  109. package/dist/src/mcpl/log-value.d.ts +6 -0
  110. package/dist/src/mcpl/log-value.d.ts.map +1 -1
  111. package/dist/src/mcpl/log-value.js +8 -0
  112. package/dist/src/mcpl/log-value.js.map +1 -1
  113. package/dist/src/mcpl/modern-connection.d.ts +15 -6
  114. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  115. package/dist/src/mcpl/modern-connection.js +168 -53
  116. package/dist/src/mcpl/modern-connection.js.map +1 -1
  117. package/dist/src/mcpl/prose-grammar.d.ts +4 -5
  118. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  119. package/dist/src/mcpl/prose-grammar.js +4 -5
  120. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  121. package/dist/src/mcpl/protocol-family.d.ts +97 -10
  122. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  123. package/dist/src/mcpl/protocol-family.js +174 -17
  124. package/dist/src/mcpl/protocol-family.js.map +1 -1
  125. package/dist/src/mcpl/push-coalescer.d.ts +132 -14
  126. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  127. package/dist/src/mcpl/push-coalescer.js +227 -50
  128. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  129. package/dist/src/mcpl/push-handler.d.ts +9 -20
  130. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  131. package/dist/src/mcpl/push-handler.js +7 -49
  132. package/dist/src/mcpl/push-handler.js.map +1 -1
  133. package/dist/src/mcpl/reference-fetcher.d.ts +2 -2
  134. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  135. package/dist/src/mcpl/reference-fetcher.js +4 -4
  136. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  137. package/dist/src/mcpl/server-connection.d.ts +50 -21
  138. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  139. package/dist/src/mcpl/server-connection.js +158 -74
  140. package/dist/src/mcpl/server-connection.js.map +1 -1
  141. package/dist/src/mcpl/tags.d.ts +24 -0
  142. package/dist/src/mcpl/tags.d.ts.map +1 -1
  143. package/dist/src/mcpl/tags.js +33 -0
  144. package/dist/src/mcpl/tags.js.map +1 -1
  145. package/dist/src/mcpl/tool-result-normalize.d.ts +2 -1
  146. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  147. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  148. package/dist/src/mcpl/transport.d.ts +55 -7
  149. package/dist/src/mcpl/transport.d.ts.map +1 -1
  150. package/dist/src/mcpl/transport.js +163 -34
  151. package/dist/src/mcpl/transport.js.map +1 -1
  152. package/dist/src/mcpl/types.d.ts +22 -6
  153. package/dist/src/mcpl/types.d.ts.map +1 -1
  154. package/dist/src/mcpl/types.js.map +1 -1
  155. package/dist/src/module-registry.d.ts +4 -2
  156. package/dist/src/module-registry.d.ts.map +1 -1
  157. package/dist/src/module-registry.js +100 -13
  158. package/dist/src/module-registry.js.map +1 -1
  159. package/dist/src/modules/discord/index.d.ts.map +1 -1
  160. package/dist/src/modules/discord/index.js +8 -13
  161. package/dist/src/modules/discord/index.js.map +1 -1
  162. package/dist/src/modules/history/index.d.ts.map +1 -1
  163. package/dist/src/modules/history/index.js +27 -4
  164. package/dist/src/modules/history/index.js.map +1 -1
  165. package/dist/src/modules/history/semantic.d.ts +15 -3
  166. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  167. package/dist/src/modules/history/semantic.js +27 -5
  168. package/dist/src/modules/history/semantic.js.map +1 -1
  169. package/dist/src/modules/index.d.ts +1 -1
  170. package/dist/src/modules/index.d.ts.map +1 -1
  171. package/dist/src/modules/index.js.map +1 -1
  172. package/dist/src/modules/unknown-tool.d.ts +4 -1
  173. package/dist/src/modules/unknown-tool.d.ts.map +1 -1
  174. package/dist/src/modules/unknown-tool.js +4 -1
  175. package/dist/src/modules/unknown-tool.js.map +1 -1
  176. package/dist/src/modules/workspace/branch-intent.d.ts +3 -1
  177. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  178. package/dist/src/modules/workspace/branch-intent.js +3 -1
  179. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  180. package/dist/src/modules/workspace/edit-plan.d.ts +265 -0
  181. package/dist/src/modules/workspace/edit-plan.d.ts.map +1 -0
  182. package/dist/src/modules/workspace/edit-plan.js +794 -0
  183. package/dist/src/modules/workspace/edit-plan.js.map +1 -0
  184. package/dist/src/modules/workspace/edit-text.d.ts +117 -0
  185. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -0
  186. package/dist/src/modules/workspace/edit-text.js +584 -0
  187. package/dist/src/modules/workspace/edit-text.js.map +1 -0
  188. package/dist/src/modules/workspace/effects.d.ts +5 -4
  189. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  190. package/dist/src/modules/workspace/effects.js +78 -25
  191. package/dist/src/modules/workspace/effects.js.map +1 -1
  192. package/dist/src/modules/workspace/index.d.ts +493 -62
  193. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  194. package/dist/src/modules/workspace/index.js +2216 -652
  195. package/dist/src/modules/workspace/index.js.map +1 -1
  196. package/dist/src/modules/workspace/observe.d.ts +213 -38
  197. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  198. package/dist/src/modules/workspace/observe.js +416 -36
  199. package/dist/src/modules/workspace/observe.js.map +1 -1
  200. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  201. package/dist/src/modules/workspace/reconcile.js +15 -8
  202. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  203. package/dist/src/modules/workspace/text.d.ts +32 -0
  204. package/dist/src/modules/workspace/text.d.ts.map +1 -0
  205. package/dist/src/modules/workspace/text.js +67 -0
  206. package/dist/src/modules/workspace/text.js.map +1 -0
  207. package/dist/src/modules/workspace/types.d.ts +210 -6
  208. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  209. package/dist/src/modules/workspace/types.js.map +1 -1
  210. package/dist/src/modules/workspace/watcher.d.ts +3 -2
  211. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  212. package/dist/src/modules/workspace/watcher.js.map +1 -1
  213. package/dist/src/operator-log.d.ts +52 -24
  214. package/dist/src/operator-log.d.ts.map +1 -1
  215. package/dist/src/operator-log.js.map +1 -1
  216. package/dist/src/recovery/discord-awareness-outbox.d.ts +496 -72
  217. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  218. package/dist/src/recovery/discord-awareness-outbox.js +1196 -289
  219. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  220. package/dist/src/recovery/offline-branch.d.ts +26 -1
  221. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  222. package/dist/src/recovery/offline-branch.js +164 -39
  223. package/dist/src/recovery/offline-branch.js.map +1 -1
  224. package/dist/src/recovery/recover-cli.js +23 -4
  225. package/dist/src/recovery/recover-cli.js.map +1 -1
  226. package/dist/src/send-tools.d.ts +11 -0
  227. package/dist/src/send-tools.d.ts.map +1 -0
  228. package/dist/src/send-tools.js +11 -0
  229. package/dist/src/send-tools.js.map +1 -0
  230. package/dist/src/timezone.d.ts +4 -2
  231. package/dist/src/timezone.d.ts.map +1 -1
  232. package/dist/src/timezone.js +4 -2
  233. package/dist/src/timezone.js.map +1 -1
  234. package/dist/src/tool-image-ledger.d.ts +46 -7
  235. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  236. package/dist/src/tool-image-ledger.js +60 -7
  237. package/dist/src/tool-image-ledger.js.map +1 -1
  238. package/dist/src/tool-result-guard.d.ts +14 -11
  239. package/dist/src/tool-result-guard.d.ts.map +1 -1
  240. package/dist/src/tool-result-guard.js +17 -14
  241. package/dist/src/tool-result-guard.js.map +1 -1
  242. package/dist/src/tool-result-history.d.ts +3 -2
  243. package/dist/src/tool-result-history.d.ts.map +1 -1
  244. package/dist/src/tool-result-history.js +3 -2
  245. package/dist/src/tool-result-history.js.map +1 -1
  246. package/dist/src/tune-out/coordinator.d.ts +21 -0
  247. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  248. package/dist/src/tune-out/coordinator.js +142 -40
  249. package/dist/src/tune-out/coordinator.js.map +1 -1
  250. package/dist/src/types/agent.d.ts +31 -6
  251. package/dist/src/types/agent.d.ts.map +1 -1
  252. package/dist/src/types/events.d.ts +66 -9
  253. package/dist/src/types/events.d.ts.map +1 -1
  254. package/dist/src/types/framework.d.ts +69 -62
  255. package/dist/src/types/framework.d.ts.map +1 -1
  256. package/dist/src/types/module.d.ts +40 -8
  257. package/dist/src/types/module.d.ts.map +1 -1
  258. package/dist/src/types/trace.d.ts +111 -10
  259. package/dist/src/types/trace.d.ts.map +1 -1
  260. package/dist/src/usage/types.d.ts +16 -0
  261. package/dist/src/usage/types.d.ts.map +1 -1
  262. package/dist/src/usage/usage-tracker.d.ts +14 -1
  263. package/dist/src/usage/usage-tracker.d.ts.map +1 -1
  264. package/dist/src/usage/usage-tracker.js +81 -41
  265. package/dist/src/usage/usage-tracker.js.map +1 -1
  266. package/package.json +6 -8
  267. package/src/agent.ts +99 -12
  268. package/src/api/server.ts +14 -12
  269. package/src/clock-rows.ts +302 -0
  270. package/src/code-execution/engine.ts +595 -0
  271. package/src/code-execution/index.ts +5 -17
  272. package/src/code-execution/isolate/calls.ts +199 -0
  273. package/src/code-execution/isolate/child-entry.ts +5 -0
  274. package/src/code-execution/isolate/child-main.ts +132 -0
  275. package/src/code-execution/isolate/child.ts +327 -0
  276. package/src/code-execution/isolate/frames.ts +209 -0
  277. package/src/code-execution/isolate/output.ts +161 -0
  278. package/src/code-execution/isolate/prelude.ts +409 -0
  279. package/src/code-execution/isolate/protocol.ts +240 -0
  280. package/src/code-execution/isolate/render.ts +230 -0
  281. package/src/code-execution/isolate/runner.ts +911 -0
  282. package/src/code-execution/isolate/scripts.ts +479 -0
  283. package/src/code-execution/quickjs.wasm +0 -0
  284. package/src/code-execution/quickjs.wasm.LICENSES +149 -0
  285. package/src/code-execution/tool-definition.ts +236 -167
  286. package/src/deadline-timer.ts +1 -1
  287. package/src/framework.ts +4978 -2671
  288. package/src/gate/event-gate.ts +79 -17
  289. package/src/held-writes.ts +17 -3
  290. package/src/index.ts +32 -22
  291. package/src/mcpl/channel-registry.ts +1892 -471
  292. package/src/mcpl/conversation-router.ts +26 -72
  293. package/src/mcpl/convert-block.ts +9 -5
  294. package/src/mcpl/inbound-source.ts +131 -0
  295. package/src/mcpl/index.ts +4 -2
  296. package/src/mcpl/log-value.ts +9 -0
  297. package/src/mcpl/modern-connection.ts +162 -53
  298. package/src/mcpl/prose-grammar.ts +4 -5
  299. package/src/mcpl/protocol-family.ts +200 -22
  300. package/src/mcpl/push-coalescer.ts +306 -62
  301. package/src/mcpl/push-handler.ts +12 -54
  302. package/src/mcpl/reference-fetcher.ts +4 -4
  303. package/src/mcpl/server-connection.ts +180 -79
  304. package/src/mcpl/tags.ts +36 -0
  305. package/src/mcpl/tool-result-normalize.ts +2 -1
  306. package/src/mcpl/transport.ts +162 -33
  307. package/src/mcpl/types.ts +23 -6
  308. package/src/module-registry.ts +92 -13
  309. package/src/modules/discord/index.ts +8 -13
  310. package/src/modules/history/index.ts +25 -3
  311. package/src/modules/history/semantic.ts +29 -6
  312. package/src/modules/index.ts +1 -0
  313. package/src/modules/unknown-tool.ts +4 -1
  314. package/src/modules/workspace/branch-intent.ts +3 -1
  315. package/src/modules/workspace/edit-plan.ts +954 -0
  316. package/src/modules/workspace/edit-text.ts +618 -0
  317. package/src/modules/workspace/effects.ts +70 -19
  318. package/src/modules/workspace/index.ts +2293 -650
  319. package/src/modules/workspace/observe.ts +465 -56
  320. package/src/modules/workspace/reconcile.ts +14 -7
  321. package/src/modules/workspace/text.ts +68 -0
  322. package/src/modules/workspace/types.ts +211 -6
  323. package/src/modules/workspace/watcher.ts +3 -2
  324. package/src/operator-log.ts +47 -19
  325. package/src/recovery/discord-awareness-outbox.ts +1500 -356
  326. package/src/recovery/offline-branch.ts +189 -55
  327. package/src/recovery/recover-cli.ts +25 -4
  328. package/src/send-tools.ts +11 -0
  329. package/src/timezone.ts +4 -2
  330. package/src/tool-image-ledger.ts +62 -8
  331. package/src/tool-result-guard.ts +18 -15
  332. package/src/tool-result-history.ts +3 -2
  333. package/src/tune-out/coordinator.ts +166 -45
  334. package/src/types/agent.ts +32 -6
  335. package/src/types/events.ts +66 -9
  336. package/src/types/framework.ts +69 -62
  337. package/src/types/module.ts +40 -8
  338. package/src/types/trace.ts +121 -15
  339. package/src/usage/types.ts +16 -0
  340. package/src/usage/usage-tracker.ts +86 -43
  341. package/dist/src/code-execution/py-runner.d.ts +0 -191
  342. package/dist/src/code-execution/py-runner.d.ts.map +0 -1
  343. package/dist/src/code-execution/py-runner.js +0 -655
  344. package/dist/src/code-execution/py-runner.js.map +0 -1
  345. package/dist/src/code-execution/runtime-py.d.ts +0 -50
  346. package/dist/src/code-execution/runtime-py.d.ts.map +0 -1
  347. package/dist/src/code-execution/runtime-py.js +0 -677
  348. package/dist/src/code-execution/runtime-py.js.map +0 -1
  349. package/dist/src/mcpl/prose-stream-router.d.ts +0 -88
  350. package/dist/src/mcpl/prose-stream-router.d.ts.map +0 -1
  351. package/dist/src/mcpl/prose-stream-router.js +0 -291
  352. package/dist/src/mcpl/prose-stream-router.js.map +0 -1
  353. package/src/code-execution/py-runner.ts +0 -791
  354. package/src/code-execution/runtime-py.ts +0 -677
  355. package/src/mcpl/prose-stream-router.ts +0 -268
@@ -1,13 +1,19 @@
1
1
  import { JsStore } from '@animalabs/chronicle';
2
+ import type { SurgeryMarkerReceipt } from '../operator-log.js';
2
3
  import { ContextManager, continuesBody, type BodyShard } from '@animalabs/context-manager';
3
4
  import { removeStoredInterval } from '../stored-removal.js';
4
5
  import { firstFreeBranchName } from '../branch-names.js';
5
6
  import {
6
7
  DEFAULT_DISCORD_AWARENESS_EMOJI,
7
8
  DiscordAwarenessOutbox,
9
+ boundDiscordAwarenessText,
8
10
  defaultDiscordAwarenessOutboxPath,
11
+ checkedDiscordAwarenessMarks,
9
12
  extractDiscordAwarenessRefs,
13
+ selectDiscordAwarenessRefs,
14
+ type DiscordAwarenessMarks,
10
15
  type DiscordAwarenessRef,
16
+ type DiscordMessageMetadataCarrier,
11
17
  } from './discord-awareness-outbox.js';
12
18
 
13
19
  export interface OfflineRecoveryBranchOptions {
@@ -29,8 +35,14 @@ export interface OfflineRecoveryBranchOptions {
29
35
  namespace?: string;
30
36
  /** Use for old message records that do not carry metadata.serverId. */
31
37
  discordServerId?: string;
38
+ /** The awareness ledger (default: under the store). */
32
39
  outboxPath?: string;
33
40
  emoji?: string;
41
+ /**
42
+ * Awareness marks on the removed Discord messages: an explicit publication
43
+ * choice, `none` by default. The recovery itself stays local either way.
44
+ */
45
+ marks?: DiscordAwarenessMarks;
34
46
  dryRun?: boolean;
35
47
  }
36
48
 
@@ -40,9 +52,27 @@ export interface OfflineRecoveryBranchResult {
40
52
  targetBranch: string;
41
53
  messagesRemoved: number;
42
54
  messagesSuppressed: number;
55
+ /** Removed messages that carry a Discord address. */
56
+ discordAddressable: number;
57
+ /** The publication scope recorded for this recovery. */
58
+ marksScope: 'none' | 'addressed' | 'all';
59
+ /** Marks queued for delivery (planned, for a dry run); not delivered. */
43
60
  discordMarkersQueued: number;
61
+ /** What became of the marks once the branch was made (absent on a dry
62
+ * run): scheduling only, never Discord acceptance. */
63
+ markers?: SurgeryMarkerReceipt;
64
+ /** The refs that will be marked (empty unless marks were chosen). */
44
65
  refs: DiscordAwarenessRef[];
66
+ /** Removed addressable messages the marks choice leaves unmarked (outside
67
+ * its scope); `markers` says whether the chosen ones are scheduled. */
68
+ unmarked: number;
69
+ /** Authorized refs (marks.refs) this recovery does not remove. */
70
+ notRemoved: number;
71
+ /** The awareness ledger the batch is recorded in. */
45
72
  outboxPath: string;
73
+ /** Set when a suppression-only batch's resume record couldn't be closed:
74
+ * what of it is left, as the host's surgeries report it. */
75
+ suppressionRecord?: string;
46
76
  }
47
77
 
48
78
  /**
@@ -65,6 +95,10 @@ export async function createOfflineRecoveryBranch(
65
95
  if (count !== undefined && (!Number.isFinite(count) || count < 1)) {
66
96
  throw new Error('messages must be a positive integer');
67
97
  }
98
+ // The marks choice is checked as the live methods check it, before the
99
+ // store is opened: a malformed one would otherwise mark every removed
100
+ // message.
101
+ const marks = checkedDiscordAwarenessMarks(options.marks);
68
102
 
69
103
  const store = JsStore.openOrCreate({ path: options.storePath });
70
104
  let contextManager: ContextManager | null = null;
@@ -84,7 +118,7 @@ export async function createOfflineRecoveryBranch(
84
118
  let target: StoredWindowMessage;
85
119
  let targetIndex: number;
86
120
  let removedCount: number;
87
- let refs: DiscordAwarenessRef[];
121
+ let carriers: DiscordMessageMetadataCarrier[];
88
122
 
89
123
  if (options.messageId) {
90
124
  // Search backward in bounded windows: recovery anchors are normally near
@@ -101,12 +135,7 @@ export async function createOfflineRecoveryBranch(
101
135
  target = located.message;
102
136
  targetIndex = located.index;
103
137
  removedCount = total - located.index - 1;
104
- refs = collectDiscordRefs(
105
- contextManager,
106
- located.index + 1,
107
- total,
108
- options.discordServerId,
109
- );
138
+ carriers = collectDiscordCarriers(contextManager, located.index + 1, total);
110
139
  } else if (options.contextId) {
111
140
  const located = findInternalMessageFromTail(contextManager, total, options.contextId.trim());
112
141
  if (!located) {
@@ -115,12 +144,7 @@ export async function createOfflineRecoveryBranch(
115
144
  target = located.message;
116
145
  targetIndex = located.index;
117
146
  removedCount = total - located.index - 1;
118
- refs = collectDiscordRefs(
119
- contextManager,
120
- located.index + 1,
121
- total,
122
- options.discordServerId,
123
- );
147
+ carriers = collectDiscordCarriers(contextManager, located.index + 1, total);
124
148
  } else {
125
149
  // Count mode remains for compatibility. Read only the target plus suffix,
126
150
  // avoiding full-history materialization and attachment blob inflation.
@@ -135,7 +159,7 @@ export async function createOfflineRecoveryBranch(
135
159
  target = countTarget;
136
160
  targetIndex = total - count! - 1;
137
161
  removedCount = count!;
138
- refs = extractDiscordAwarenessRefs(discarded, options.discordServerId);
162
+ carriers = discarded.map(addressingOnly);
139
163
  }
140
164
 
141
165
  validateAnchorBoundary(contextManager, targetIndex, total, target);
@@ -145,7 +169,6 @@ export async function createOfflineRecoveryBranch(
145
169
  targetIndex,
146
170
  options.suppressMessageIds ?? [],
147
171
  options.suppressRanges ?? [],
148
- options.discordServerId,
149
172
  );
150
173
  validateRemovalIntegrity(
151
174
  contextManager,
@@ -160,7 +183,10 @@ export async function createOfflineRecoveryBranch(
160
183
  ...suppression.intervals,
161
184
  ],
162
185
  );
163
- refs = dedupeDiscordRefs([...refs, ...suppression.refs]);
186
+ carriers = [...carriers, ...suppression.carriers];
187
+ const addressable = extractDiscordAwarenessRefs(carriers, options.discordServerId).length;
188
+ const selection = selectDiscordAwarenessRefs(carriers, marks, options.discordServerId);
189
+ const refs = selection.refs;
164
190
 
165
191
  const sourceBranch = store.currentBranch().name;
166
192
  const held = new Set(store.listBranches().map((branch) => branch.name));
@@ -169,6 +195,10 @@ export async function createOfflineRecoveryBranch(
169
195
  if (targetBranch === sourceBranch) {
170
196
  throw new Error('Recovery branch name must differ from the active source branch');
171
197
  }
198
+ // A known collision is refused before anything is prepared.
199
+ if (store.listBranches().some((branch) => branch.name === targetBranch)) {
200
+ throw new Error(`Recovery branch ${targetBranch} already exists; choose another --branch`);
201
+ }
172
202
  const outboxPath = options.outboxPath
173
203
  ?? defaultDiscordAwarenessOutboxPath(options.storePath);
174
204
 
@@ -178,33 +208,44 @@ export async function createOfflineRecoveryBranch(
178
208
  targetBranch,
179
209
  messagesRemoved: removedCount,
180
210
  messagesSuppressed: suppression.messageCount,
211
+ discordAddressable: addressable,
212
+ marksScope: marks === 'none' ? 'none' : marks.scope,
181
213
  discordMarkersQueued: refs.length,
182
214
  refs,
215
+ unmarked: selection.unmarked,
216
+ notRemoved: selection.notRemoved,
183
217
  outboxPath,
184
218
  };
185
219
  if (options.dryRun) return result;
186
220
 
187
- const outbox = new DiscordAwarenessOutbox(outboxPath);
188
- const batch = outbox.prepare({
221
+ // The host is stopped, so this process is the ledger's only writer.
222
+ const outbox = new DiscordAwarenessOutbox(outboxPath, { store });
223
+ const batch = await outbox.prepare({
189
224
  agentName: options.agentName,
190
225
  sourceBranch,
191
226
  targetBranch,
192
227
  refs,
228
+ scope: marks === 'none' ? 'none' : marks.scope,
229
+ unmarked: selection.unmarked,
230
+ notRemoved: selection.notRemoved,
193
231
  emoji: options.emoji ?? DEFAULT_DISCORD_AWARENESS_EMOJI,
194
232
  // Merely seeing targetBranch active does not prove branch-local
195
- // suppressions finished. Only the recovery operation may activate this
196
- // batch after every removal below succeeds.
197
- activationPolicy: suppression.intervals.length > 0 ? 'explicit' : 'target-branch',
233
+ // suppressions finished: a start on it applies them again while the
234
+ // batch is prepared. Only the recovery may activate it, after every
235
+ // removal below succeeds.
198
236
  suppressionIntervals: suppression.intervals.map((interval) => ({
199
237
  fromId: String(interval.fromId),
200
238
  toId: String(interval.toId),
201
239
  })),
202
240
  });
203
241
 
204
- // branchAt uses the target message's origin sequence. No message content is
205
- // compiled or submitted to Membrane during this operation.
206
- const createdBranch = contextManager.branchAt(target.id, targetBranch);
242
+ let createdBranch: string | undefined;
207
243
  try {
244
+ // branchAt uses the target message's origin sequence. No message
245
+ // content is compiled or submitted to Membrane during this operation.
246
+ // Creating the branch is part of the body: a failure here settles the
247
+ // batch like a failed switch or redaction.
248
+ createdBranch = contextManager.branchAt(target.id, targetBranch);
208
249
  await contextManager.switchBranch(createdBranch);
209
250
  // Each interval is removed by its messages' ids, so the order doesn't
210
251
  // change the result. removeStoredInterval takes a single message outside
@@ -213,18 +254,109 @@ export async function createOfflineRecoveryBranch(
213
254
  for (const interval of [...suppression.intervals].reverse()) {
214
255
  removeStoredInterval(contextManager, interval.fromId, interval.toId);
215
256
  }
216
- if (batch) outbox.activate(batch.id);
217
257
  } catch (error) {
218
258
  // A partially suppressed branch is not safe to boot into. Preserve it
219
- // for diagnosis, but leave Chronicle on the untouched source branch and
220
- // keep the explicit outbox batch non-deliverable.
221
- if (store.currentBranch().name === createdBranch) {
222
- await contextManager.switchBranch(sourceBranch);
259
+ // for diagnosis, but put Chronicle back on the untouched source branch.
260
+ // Once the source is confirmed, the batch is retired (discard syncs the
261
+ // restored state before it records that): its surgery never happened.
262
+ // If it can't be confirmed, the batch stays prepared as the body's
263
+ // record: a start on its target branch finishes a suppression's
264
+ // removals, since its body is in effect there, and a start retires the
265
+ // batch, none of its marks sent. The error says which, and names the
266
+ // branch.
267
+ const failure = error instanceof Error ? error.message : String(error);
268
+ let restore = '';
269
+ try {
270
+ if (store.currentBranch().name !== sourceBranch) await contextManager.switchBranch(sourceBranch);
271
+ } catch (restoreError) {
272
+ restore = `; restoring ${sourceBranch} failed: ${boundDiscordAwarenessText(
273
+ restoreError instanceof Error ? restoreError.message : String(restoreError),
274
+ )}`;
275
+ }
276
+ const sourceConfirmed = store.currentBranch().name === sourceBranch;
277
+ let retire = '';
278
+ // A batch with marks never sends them once kept: the recovery, run
279
+ // again, chooses them again.
280
+ const redo = batch && batch.refs.length > 0
281
+ ? '. To apply its marks, run the recovery again onto a new --branch, choosing them again' : '';
282
+ if (batch) {
283
+ if (!sourceConfirmed) {
284
+ // The recovery starts from the store's current branch, which this
285
+ // one couldn't put back: a run from there would start from the fork.
286
+ const redoFromSource = batch.refs.length > 0
287
+ ? `. To apply its marks, redo it from ${sourceBranch}, choosing them again: the recovery starts from the ` +
288
+ `store's current branch, which isn't ${sourceBranch} now, so start the host, switch to ${sourceBranch} ` +
289
+ 'through the host API\'s branch.switch, stop the host, and run the recovery again onto a new --branch'
290
+ : '';
291
+ retire = `; awareness batch ${batch.id} kept, since the source branch could not be confirmed: ` +
292
+ (suppression.intervals.length > 0
293
+ ? `a start on ${targetBranch} finishes its removals and retires it`
294
+ : 'the next start retires it') + `, none of its marks sent${redoFromSource}`;
295
+ } else {
296
+ try {
297
+ await outbox.discard(batch.id);
298
+ retire = `; awareness batch ${batch.id} retired, none of its marks sent${redo}`;
299
+ } catch (discardError) {
300
+ // None of its marks is ever sent: a start retires it. A write that
301
+ // fails leaves the batch as it was, prepared, as in the host's
302
+ // retireAfterRestore. A suppression's removals stay pending for
303
+ // its target branch: a start on a branch with that name finishes
304
+ // them.
305
+ const detail = boundDiscordAwarenessText(
306
+ discardError instanceof Error ? discardError.message : String(discardError),
307
+ );
308
+ const kept = `; awareness batch ${batch.id} could not be retired (${detail}); it stays prepared, none of ` +
309
+ 'its marks to be sent';
310
+ if (suppression.intervals.length === 0) {
311
+ retire = `${kept}, until the next start retires it${redo}`;
312
+ } else {
313
+ let forked = true;
314
+ try {
315
+ forked = store.listBranches().some((branch) => branch.name === targetBranch);
316
+ } catch { /* unknown: say what a start on an existing branch would do */ }
317
+ retire = `${kept}. Its removals stay pending for ${targetBranch}: a start on a branch with that ` +
318
+ 'name would finish them there and retire the batch' + (forked
319
+ ? `, and ${targetBranch} is kept, so delete it unless you want that, through the host API's ` +
320
+ 'branch.delete once the host runs'
321
+ : '') + redo;
322
+ }
323
+ }
324
+ }
223
325
  }
224
- throw error;
326
+ // A partly suppressed branch is unsafe to start on, unless its batch was
327
+ // kept because the source couldn't be confirmed: a start on it then
328
+ // completes the suppression, as the batch's note says.
329
+ const kept = createdBranch === undefined ? ''
330
+ : suppression.intervals.length === 0 ? `; branch ${createdBranch} is kept for diagnosis`
331
+ : batch && !sourceConfirmed ? `; branch ${createdBranch} is kept: it may be partly suppressed`
332
+ : `; branch ${createdBranch} is kept for diagnosis: it may be partly suppressed, so don't start on it, and ` +
333
+ 'delete it once it has been looked at (the host API\'s branch.delete, once the host runs)';
334
+ if (!restore && !retire && !kept) throw error;
335
+ throw new Error(`${failure}${restore}${kept}${retire}`, { cause: error });
225
336
  }
226
337
 
227
- return result;
338
+ // The body change landed. Marker bookkeeping is reported apart from it
339
+ // and never undoes it.
340
+ const facts = {
341
+ scope: (marks === 'none' ? 'none' : marks.scope) as SurgeryMarkerReceipt['scope'],
342
+ unmarked: selection.unmarked,
343
+ notRemoved: selection.notRemoved,
344
+ };
345
+ let markers: SurgeryMarkerReceipt = { ...facts, status: 'none', queued: 0 };
346
+ let suppressionRecord: string | undefined;
347
+ if (batch) {
348
+ const settled = await outbox.settleActivation(batch.id);
349
+ if (batch.refs.length > 0) {
350
+ markers = settled.status === 'queued'
351
+ ? { ...facts, status: 'queued', queued: settled.queued, batchId: batch.id }
352
+ : { ...facts, status: 'not-scheduled', queued: 0, error: settled.error };
353
+ } else if (settled.status === 'not-scheduled') {
354
+ // A suppression-only batch carries no marks, and its activation
355
+ // closes the body's resume record: if that failed, say what became of it.
356
+ suppressionRecord = `the resume record (awareness batch ${batch.id}, no marks) could not be closed: ${settled.error}`;
357
+ }
358
+ }
359
+ return { ...result, discordMarkersQueued: markers.queued, markers, ...(suppressionRecord ? { suppressionRecord } : {}) };
228
360
  } finally {
229
361
  contextManager?.close();
230
362
  store.close();
@@ -372,8 +504,7 @@ function buildSuppressionPlan(
372
504
  targetIndex: number,
373
505
  rawMessageIds: string[],
374
506
  rawRanges: Array<{ fromMessageId: string; toMessageId: string }>,
375
- forcedServerId?: string,
376
- ): { intervals: SuppressionInterval[]; refs: DiscordAwarenessRef[]; messageCount: number } {
507
+ ): { intervals: SuppressionInterval[]; carriers: DiscordMessageMetadataCarrier[]; messageCount: number } {
377
508
  const messageIds = rawMessageIds.map(normalizeDiscordMessageId);
378
509
  const ranges = rawRanges.map((range) => ({
379
510
  fromMessageId: normalizeDiscordMessageId(range.fromMessageId),
@@ -383,7 +514,7 @@ function buildSuppressionPlan(
383
514
  ...messageIds,
384
515
  ...ranges.flatMap((range) => [range.fromMessageId, range.toMessageId]),
385
516
  ]);
386
- if (wanted.size === 0) return { intervals: [], refs: [], messageCount: 0 };
517
+ if (wanted.size === 0) return { intervals: [], carriers: [], messageCount: 0 };
387
518
 
388
519
  const locations = new Map<string, MessageLocation>();
389
520
  for (let offset = 0; offset <= targetIndex; offset += RECOVERY_SCAN_WINDOW) {
@@ -473,45 +604,48 @@ function buildSuppressionPlan(
473
604
  }
474
605
  }
475
606
 
476
- const refs: DiscordAwarenessRef[] = [];
607
+ const carriers: DiscordMessageMetadataCarrier[] = [];
477
608
  for (const interval of merged) {
478
- refs.push(...collectDiscordRefs(
479
- contextManager,
480
- interval.start,
481
- interval.end + 1,
482
- forcedServerId,
483
- ));
609
+ carriers.push(...collectDiscordCarriers(contextManager, interval.start, interval.end + 1));
484
610
  }
485
611
  return {
486
612
  intervals: merged,
487
- refs: dedupeDiscordRefs(refs),
613
+ carriers,
488
614
  messageCount: merged.reduce((sum, interval) => sum + interval.end - interval.start + 1, 0),
489
615
  };
490
616
  }
491
617
 
492
- function collectDiscordRefs(
618
+ /**
619
+ * The addressing metadata of removed messages, read in bounded windows: only
620
+ * the Discord address and the MCPL tags (for `addressed` selection) are kept,
621
+ * never content. A sharded message may repeat its metadata across adjacent
622
+ * records; selection dedupes by address.
623
+ */
624
+ function collectDiscordCarriers(
493
625
  contextManager: ContextManager,
494
626
  start: number,
495
627
  end: number,
496
- forcedServerId?: string,
497
- ): DiscordAwarenessRef[] {
498
- const refs: DiscordAwarenessRef[] = [];
628
+ ): DiscordMessageMetadataCarrier[] {
629
+ const carriers: DiscordMessageMetadataCarrier[] = [];
499
630
  for (let offset = start; offset < end; offset += RECOVERY_SCAN_WINDOW) {
500
631
  const messages = contextManager.getMessageWindow(
501
632
  offset,
502
633
  Math.min(RECOVERY_SCAN_WINDOW, end - offset),
503
634
  { resolveBlobs: false },
504
635
  ).messages;
505
- refs.push(...extractDiscordAwarenessRefs(messages, forcedServerId));
636
+ carriers.push(...messages.map(addressingOnly));
506
637
  }
507
- // A sharded message may repeat Discord metadata across adjacent records.
508
- return dedupeDiscordRefs(refs);
638
+ return carriers;
509
639
  }
510
640
 
511
- function dedupeDiscordRefs(refs: DiscordAwarenessRef[]): DiscordAwarenessRef[] {
512
- const deduped = new Map<string, DiscordAwarenessRef>();
513
- for (const ref of refs) {
514
- deduped.set(`${ref.serverId}\0${ref.channelId}\0${ref.messageId}`, ref);
515
- }
516
- return [...deduped.values()];
641
+ function addressingOnly(message: { metadata?: Record<string, unknown> }): DiscordMessageMetadataCarrier {
642
+ const metadata = message.metadata ?? {};
643
+ return {
644
+ metadata: {
645
+ serverId: metadata.serverId,
646
+ channelId: metadata.channelId,
647
+ messageId: metadata.messageId,
648
+ tags: metadata.tags,
649
+ },
650
+ };
517
651
  }
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { createOfflineRecoveryBranch } from './offline-branch.js';
4
+ import type { DiscordAwarenessMarks } from './discord-awareness-outbox.js';
4
5
 
5
6
  interface CliOptions {
6
7
  storePath?: string;
@@ -15,6 +16,7 @@ interface CliOptions {
15
16
  discordServerId?: string;
16
17
  outboxPath?: string;
17
18
  emoji?: string;
19
+ marks: 'none' | 'addressed' | 'all';
18
20
  dryRun: boolean;
19
21
  }
20
22
 
@@ -22,9 +24,12 @@ function usage(): string {
22
24
  return `Usage:
23
25
  agent-framework-recover --store <path> --agent <name> <anchor> [options]
24
26
 
25
- Creates and activates a Chronicle recovery branch while the normal host is down,
26
- then queues Discord reactions for addressable messages removed from awareness.
27
- Message content is never compiled or written to the recovery outbox.
27
+ Creates and activates a Chronicle recovery branch while the normal host is
28
+ down. The recovery is local to the resident: by default no Discord reaction is
29
+ placed. --marks chooses to mark removed Discord messages with an awareness
30
+ reaction (queued here, delivered when the host and its Discord MCPL server
31
+ next connect). Message content is never compiled or written to the awareness
32
+ ledger.
28
33
 
29
34
  Options:
30
35
  --message-id <id|link> Last Discord message the agent should still see
@@ -35,6 +40,10 @@ Options:
35
40
  --branch <name> Recovery branch name (generated by default)
36
41
  --namespace <namespace> Context namespace (default: agents/<agent>)
37
42
  --discord-server <id> Override/inject the Discord MCPL server id
43
+ --marks <none|addressed|all>
44
+ Awareness marks on removed Discord messages
45
+ (default none; addressed = mentions, replies to
46
+ the bot and DMs; all = every addressable message)
38
47
  --outbox <path> Override the branch-independent outbox path
39
48
  --emoji <emoji> Marker reaction (default: 💤)
40
49
  --dry-run Inspect counts/addresses without writing
@@ -44,7 +53,7 @@ The agent host must be stopped before running this command.`;
44
53
  }
45
54
 
46
55
  function parseArgs(args: string[]): CliOptions {
47
- const options: CliOptions = { dryRun: false, suppressMessageIds: [], suppressRanges: [] };
56
+ const options: CliOptions = { dryRun: false, suppressMessageIds: [], suppressRanges: [], marks: 'none' };
48
57
  for (let i = 0; i < args.length; i++) {
49
58
  const arg = args[i];
50
59
  const value = () => {
@@ -80,6 +89,14 @@ function parseArgs(args: string[]): CliOptions {
80
89
  case '--discord-server': options.discordServerId = value(); break;
81
90
  case '--outbox': options.outboxPath = value(); break;
82
91
  case '--emoji': options.emoji = value(); break;
92
+ case '--marks': {
93
+ const marks = value();
94
+ if (marks !== 'none' && marks !== 'addressed' && marks !== 'all') {
95
+ throw new Error('--marks must be none, addressed or all');
96
+ }
97
+ options.marks = marks;
98
+ break;
99
+ }
83
100
  case '--dry-run': options.dryRun = true; break;
84
101
  case '--help':
85
102
  case '-h':
@@ -101,6 +118,9 @@ async function main(): Promise<void> {
101
118
  throw new Error(`--store, --agent, and one anchor (--message-id, --context-id, or --messages) are required\n\n${usage()}`);
102
119
  }
103
120
 
121
+ // A CLI choice and its application are the same moment (the host is
122
+ // stopped), so the scope is evaluated now rather than frozen to refs.
123
+ const marks: DiscordAwarenessMarks = options.marks === 'none' ? 'none' : { scope: options.marks };
104
124
  const result = await createOfflineRecoveryBranch({
105
125
  storePath: options.storePath,
106
126
  agentName: options.agentName,
@@ -114,6 +134,7 @@ async function main(): Promise<void> {
114
134
  discordServerId: options.discordServerId,
115
135
  outboxPath: options.outboxPath,
116
136
  emoji: options.emoji,
137
+ marks,
117
138
  dryRun: options.dryRun,
118
139
  });
119
140
 
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Tools whose call delivers the resident's message to a channel or person,
3
+ * by bare name (an MCPL server's `<prefix>--` stripped). The framework reads
4
+ * a call of one as an explicit send that silences the round's auto-routed
5
+ * prose; the history module's semantic index reads its message argument as
6
+ * what the resident wrote to someone, whether or not it was delivered.
7
+ */
8
+ export const SEND_TOOLS = new Set(['channel_publish', 'send_message', 'reply_message', 'send_dm']);
9
+
10
+ /** Strip the `server--` MCPL prefix from a tool name. */
11
+ export const bareToolName = (n: string): string => n.split('--').pop()!;
package/src/timezone.ts CHANGED
@@ -2,8 +2,10 @@
2
2
  * Agent-visible wall-clock formatting.
3
3
  *
4
4
  * Persistence and protocol timestamps should remain epoch milliseconds or UTC
5
- * ISO strings. These helpers are only for presentation boundaries where a
6
- * timestamp is rendered into an agent's context or a tool result.
5
+ * ISO strings. These helpers are for presentation boundaries where a
6
+ * timestamp is rendered into an agent's context or a tool result, and the
7
+ * zone resolveTimeZone gives also places clock rows' boundaries
8
+ * (clock-rows.ts).
7
9
  */
8
10
 
9
11
  export function isValidTimeZone(timeZone: string): boolean {
@@ -316,7 +316,8 @@ export interface StoredCut {
316
316
  head: string;
317
317
  /** The image slots past the cut that the stored copy re-appends after its
318
318
  * truncation notice, in text order: the newest MAX_KEPT_IMAGE_SLOTS of
319
- * those the live copy showed. */
319
+ * those the live copy showed, or under a readers' bound the newest of
320
+ * those that fit it, down to none (cutStoredToolResultToFit). */
320
321
  kept: string[];
321
322
  /** How many image slots past the cut the live copy showed. */
322
323
  fell: number;
@@ -326,8 +327,10 @@ export interface StoredCut {
326
327
  * Where a tool result's text longer than `cap` is cut: by its stored copy
327
328
  * (cutStoredToolResult), by history's own truncation (truncateForHistory),
328
329
  * and by its live copy, for each text block beside an image
329
- * (tryNativeToolResultContent, framework.ts), so the stored copy and the live
330
- * copy cut alike. The text's length when it isn't longer than `cap`.
330
+ * (tryNativeToolResultContent, framework.ts), so at one cap the stored copy
331
+ * and the live copy cut alike. Under a readers' bound the stored copy's cut
332
+ * can come earlier (cutStoredToolResultToFit). The text's length when it
333
+ * isn't longer than `cap`.
331
334
  *
332
335
  * The cut is made at the cap, or before it when the cap would split a
333
336
  * surrogate pair (safeSlice's rule), and that is where the moves back are
@@ -395,9 +398,13 @@ export function cutStoredToolResult(text: string, cap: number, shown: ShownRange
395
398
  /** What a stored copy re-appends after its truncation notice: the kept
396
399
  * slots, each on its own line so a reference stub stays parseable, under a
397
400
  * note saying how many fell past the cut, and how many of those were left
398
- * out and aren't counted. Empty when none fell past the cut. */
401
+ * out and aren't counted, all of them where none fit the bound
402
+ * (cutStoredToolResultToFit). Empty when none fell past the cut. */
399
403
  export function formatKeptImageSlots(cut: StoredCut): string {
400
404
  if (cut.fell === 0) return '';
405
+ if (cut.kept.length === 0) {
406
+ return `\n[${cut.fell} image slot(s) fell past the cut and are left out, and save_recent_image doesn't count them.]`;
407
+ }
401
408
  const leftOut = cut.fell - cut.kept.length;
402
409
  const note = leftOut === 0
403
410
  ? `${cut.fell} image slot(s) fell past the cut; kept here in order so save_recent_image still counts them:`
@@ -406,14 +413,61 @@ export function formatKeptImageSlots(cut: StoredCut): string {
406
413
  return `\n[${note}]\n${cut.kept.join('\n')}`;
407
414
  }
408
415
 
416
+ /** The most a stored copy's truncation notice takes (spillOrTruncate,
417
+ * framework.ts): the spill notice names its file twice, once in its paging
418
+ * advice, and fits this on a path from a mount name of up to about seventy
419
+ * characters. */
420
+ export const TOOL_RESULT_NOTICE_ROOM = 640;
421
+
422
+ /**
423
+ * A result's cap and bound when its message holds `n` results. A reader's
424
+ * strategy spends its per-message bound across the message's blocks in
425
+ * order, so each result takes an equal share of the readers' smallest
426
+ * bound, fixed before any result is cut, since its siblings may still be
427
+ * running when save_recent_image counts it, and its cap is at most that
428
+ * share. `keptCap` is where a withheld stub's kept fields are cut, leaving
429
+ * the stub's notice its room (tool-result-guard.ts). With no bound, each
430
+ * keeps the cap.
431
+ */
432
+ export function toolResultShare(cap: number, bound: number | undefined, n: number): { cap: number; bound: number | undefined; keptCap: number } {
433
+ if (bound === undefined) return { cap, bound, keptCap: cap };
434
+ const share = Math.floor(bound / Math.max(1, n));
435
+ return { cap: Math.min(cap, share), bound: share, keptCap: Math.min(cap, Math.max(1, share - TOOL_RESULT_NOTICE_ROOM)) };
436
+ }
437
+
438
+ /**
439
+ * Cut a tool result's text at `cap` as cutStoredToolResult does, unless its
440
+ * stored copy would then pass `bound`, the most a reader's strategy keeps of
441
+ * it (toolResultShare): the head, room for the notice after it, and the kept
442
+ * slots after that (formatKeptImageSlots). A strategy cuts a longer message
443
+ * from its end, and with it the notice that holds the way to the rest, so
444
+ * there the head moves back until the copy fits. The kept slots grow as the
445
+ * head moves back, so the cut is found again until head and slots fit or the
446
+ * head is empty. Slots that alone pass the room keep the newest that fit,
447
+ * the rest left out and uncounted, as past MAX_KEPT_IMAGE_SLOTS. A bound
448
+ * under the notice's room can't hold the notice. With no bound, or where the
449
+ * cut at the cap already fits, the cut is cutStoredToolResult's.
450
+ */
451
+ export function cutStoredToolResultToFit(text: string, cap: number, shown: ShownRanges | null, bound: number | undefined): StoredCut {
452
+ let cut = cutStoredToolResult(text, cap, shown);
453
+ if (bound === undefined || text.length <= cap) return cut;
454
+ const room = bound - TOOL_RESULT_NOTICE_ROOM;
455
+ while (cut.head.length > 0 && cut.head.length + formatKeptImageSlots(cut).length > room) {
456
+ cut = cutStoredToolResult(text, Math.max(0, room - formatKeptImageSlots(cut).length), shown);
457
+ }
458
+ while (cut.kept.length > 0 && formatKeptImageSlots(cut).length > room) cut = { ...cut, kept: cut.kept.slice(1) };
459
+ return cut;
460
+ }
461
+
409
462
  /**
410
463
  * The image slots a tool result's stored copy holds: its text's, or, when the
411
464
  * text is longer than `cap`, those the cut keeps, in head and after the
412
- * notice (cutStoredToolResult). The notice itself holds none. For counting a
413
- * result before its stored copy is written, as it will count after.
465
+ * notice (cutStoredToolResultToFit, under the readers' `bound`). The notice
466
+ * itself holds none. For counting a result before its stored copy is
467
+ * written, as it will count after.
414
468
  */
415
- export function storedImageSlots(text: string, cap: number, shown: ShownRanges | null): ParsedImagePlaceholder[] {
416
- const cut = cutStoredToolResult(text, cap, shown);
469
+ export function storedImageSlots(text: string, cap: number, shown: ShownRanges | null, bound: number | undefined): ParsedImagePlaceholder[] {
470
+ const cut = cutStoredToolResultToFit(text, cap, shown, bound);
417
471
  return parseImagePlaceholders(cut.head + formatKeptImageSlots(cut));
418
472
  }
419
473