@animalabs/agent-framework 0.22.2 → 0.23.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 (997) hide show
  1. package/README.md +108 -4
  2. package/dist/src/agent.d.ts +108 -17
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +207 -67
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/mcp-server-core.js +1 -1
  7. package/dist/src/api/mcp-server-core.js.map +1 -1
  8. package/dist/src/api/server.d.ts.map +1 -1
  9. package/dist/src/api/server.js +5 -1
  10. package/dist/src/api/server.js.map +1 -1
  11. package/dist/src/api/types.d.ts +1 -1
  12. package/dist/src/api/types.d.ts.map +1 -1
  13. package/dist/src/branch-names.d.ts +14 -0
  14. package/dist/src/branch-names.d.ts.map +1 -0
  15. package/dist/src/branch-names.js +22 -0
  16. package/dist/src/branch-names.js.map +1 -0
  17. package/dist/src/closest-names.d.ts +28 -0
  18. package/dist/src/closest-names.d.ts.map +1 -0
  19. package/dist/src/closest-names.js +98 -0
  20. package/dist/src/closest-names.js.map +1 -0
  21. package/dist/src/code-execution/index.d.ts +1 -1
  22. package/dist/src/code-execution/index.d.ts.map +1 -1
  23. package/dist/src/code-execution/index.js.map +1 -1
  24. package/dist/src/code-execution/py-runner.d.ts +50 -7
  25. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  26. package/dist/src/code-execution/py-runner.js +258 -90
  27. package/dist/src/code-execution/py-runner.js.map +1 -1
  28. package/dist/src/code-execution/runtime-py.d.ts +3 -2
  29. package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
  30. package/dist/src/code-execution/runtime-py.js +94 -8
  31. package/dist/src/code-execution/runtime-py.js.map +1 -1
  32. package/dist/src/code-execution/tool-definition.d.ts +17 -1
  33. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  34. package/dist/src/code-execution/tool-definition.js +72 -6
  35. package/dist/src/code-execution/tool-definition.js.map +1 -1
  36. package/dist/src/config-checks.d.ts +28 -0
  37. package/dist/src/config-checks.d.ts.map +1 -0
  38. package/dist/src/config-checks.js +56 -0
  39. package/dist/src/config-checks.js.map +1 -0
  40. package/dist/src/deadline-timer.d.ts +21 -0
  41. package/dist/src/deadline-timer.d.ts.map +1 -0
  42. package/dist/src/deadline-timer.js +43 -0
  43. package/dist/src/deadline-timer.js.map +1 -0
  44. package/dist/src/error-message.d.ts +30 -0
  45. package/dist/src/error-message.d.ts.map +1 -0
  46. package/dist/src/error-message.js +50 -0
  47. package/dist/src/error-message.js.map +1 -0
  48. package/dist/src/framework.d.ts +1529 -165
  49. package/dist/src/framework.d.ts.map +1 -1
  50. package/dist/src/framework.js +8264 -1939
  51. package/dist/src/framework.js.map +1 -1
  52. package/dist/src/gate/event-gate.d.ts +8 -0
  53. package/dist/src/gate/event-gate.d.ts.map +1 -1
  54. package/dist/src/gate/event-gate.js +36 -5
  55. package/dist/src/gate/event-gate.js.map +1 -1
  56. package/dist/src/held-writes.d.ts +16 -0
  57. package/dist/src/held-writes.d.ts.map +1 -0
  58. package/dist/src/held-writes.js +56 -0
  59. package/dist/src/held-writes.js.map +1 -0
  60. package/dist/src/index.d.ts +10 -4
  61. package/dist/src/index.d.ts.map +1 -1
  62. package/dist/src/index.js +11 -5
  63. package/dist/src/index.js.map +1 -1
  64. package/dist/src/mcpl/capability-grant.d.ts +4 -0
  65. package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
  66. package/dist/src/mcpl/capability-grant.js +1 -1
  67. package/dist/src/mcpl/capability-grant.js.map +1 -1
  68. package/dist/src/mcpl/channel-registry.d.ts +233 -18
  69. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  70. package/dist/src/mcpl/channel-registry.js +975 -386
  71. package/dist/src/mcpl/channel-registry.js.map +1 -1
  72. package/dist/src/mcpl/checkpoint-manager.d.ts +21 -1
  73. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  74. package/dist/src/mcpl/checkpoint-manager.js +91 -30
  75. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  76. package/dist/src/mcpl/conversation-router.d.ts +19 -3
  77. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  78. package/dist/src/mcpl/conversation-router.js +23 -8
  79. package/dist/src/mcpl/conversation-router.js.map +1 -1
  80. package/dist/src/mcpl/convert-block.d.ts +20 -0
  81. package/dist/src/mcpl/convert-block.d.ts.map +1 -0
  82. package/dist/src/mcpl/convert-block.js +88 -0
  83. package/dist/src/mcpl/convert-block.js.map +1 -0
  84. package/dist/src/mcpl/errors.d.ts +13 -0
  85. package/dist/src/mcpl/errors.d.ts.map +1 -1
  86. package/dist/src/mcpl/errors.js +28 -0
  87. package/dist/src/mcpl/errors.js.map +1 -1
  88. package/dist/src/mcpl/hook-orchestrator.d.ts +23 -0
  89. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  90. package/dist/src/mcpl/hook-orchestrator.js +83 -72
  91. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  92. package/dist/src/mcpl/inbound-source.d.ts +114 -0
  93. package/dist/src/mcpl/inbound-source.d.ts.map +1 -0
  94. package/dist/src/mcpl/inbound-source.js +83 -0
  95. package/dist/src/mcpl/inbound-source.js.map +1 -0
  96. package/dist/src/mcpl/index.d.ts +7 -1
  97. package/dist/src/mcpl/index.d.ts.map +1 -1
  98. package/dist/src/mcpl/index.js +5 -1
  99. package/dist/src/mcpl/index.js.map +1 -1
  100. package/dist/src/mcpl/inference-router.d.ts +19 -13
  101. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  102. package/dist/src/mcpl/inference-router.js +155 -87
  103. package/dist/src/mcpl/inference-router.js.map +1 -1
  104. package/dist/src/mcpl/log-value.d.ts +7 -0
  105. package/dist/src/mcpl/log-value.d.ts.map +1 -1
  106. package/dist/src/mcpl/log-value.js +9 -0
  107. package/dist/src/mcpl/log-value.js.map +1 -1
  108. package/dist/src/mcpl/modern-connection.d.ts +227 -0
  109. package/dist/src/mcpl/modern-connection.d.ts.map +1 -0
  110. package/dist/src/mcpl/modern-connection.js +1088 -0
  111. package/dist/src/mcpl/modern-connection.js.map +1 -0
  112. package/dist/src/mcpl/prefix-rule.d.ts +18 -0
  113. package/dist/src/mcpl/prefix-rule.d.ts.map +1 -0
  114. package/dist/src/mcpl/prefix-rule.js +25 -0
  115. package/dist/src/mcpl/prefix-rule.js.map +1 -0
  116. package/dist/src/mcpl/protocol-family.d.ts +179 -0
  117. package/dist/src/mcpl/protocol-family.d.ts.map +1 -0
  118. package/dist/src/mcpl/protocol-family.js +697 -0
  119. package/dist/src/mcpl/protocol-family.js.map +1 -0
  120. package/dist/src/mcpl/push-coalescer.d.ts +58 -17
  121. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  122. package/dist/src/mcpl/push-coalescer.js +93 -32
  123. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  124. package/dist/src/mcpl/push-handler.d.ts +16 -9
  125. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  126. package/dist/src/mcpl/push-handler.js +18 -83
  127. package/dist/src/mcpl/push-handler.js.map +1 -1
  128. package/dist/src/mcpl/reference-fetcher.d.ts +76 -13
  129. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  130. package/dist/src/mcpl/reference-fetcher.js +275 -66
  131. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  132. package/dist/src/mcpl/references.d.ts +98 -18
  133. package/dist/src/mcpl/references.d.ts.map +1 -1
  134. package/dist/src/mcpl/references.js +170 -32
  135. package/dist/src/mcpl/references.js.map +1 -1
  136. package/dist/src/mcpl/server-connection.d.ts +406 -31
  137. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  138. package/dist/src/mcpl/server-connection.js +1331 -272
  139. package/dist/src/mcpl/server-connection.js.map +1 -1
  140. package/dist/src/mcpl/server-registry.d.ts +35 -6
  141. package/dist/src/mcpl/server-registry.d.ts.map +1 -1
  142. package/dist/src/mcpl/server-registry.js +63 -13
  143. package/dist/src/mcpl/server-registry.js.map +1 -1
  144. package/dist/src/mcpl/skip-reply-wake.d.ts +36 -0
  145. package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -0
  146. package/dist/src/mcpl/skip-reply-wake.js +55 -0
  147. package/dist/src/mcpl/skip-reply-wake.js.map +1 -0
  148. package/dist/src/mcpl/tool-result-normalize.d.ts +54 -0
  149. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -0
  150. package/dist/src/mcpl/tool-result-normalize.js +232 -0
  151. package/dist/src/mcpl/tool-result-normalize.js.map +1 -0
  152. package/dist/src/mcpl/transport.d.ts +104 -12
  153. package/dist/src/mcpl/transport.d.ts.map +1 -1
  154. package/dist/src/mcpl/transport.js +315 -85
  155. package/dist/src/mcpl/transport.js.map +1 -1
  156. package/dist/src/mcpl/types.d.ts +76 -19
  157. package/dist/src/mcpl/types.d.ts.map +1 -1
  158. package/dist/src/mcpl/types.js.map +1 -1
  159. package/dist/src/mcpl/visible-content.d.ts +16 -0
  160. package/dist/src/mcpl/visible-content.d.ts.map +1 -1
  161. package/dist/src/mcpl/visible-content.js +26 -0
  162. package/dist/src/mcpl/visible-content.js.map +1 -1
  163. package/dist/src/module-registry.d.ts +34 -6
  164. package/dist/src/module-registry.d.ts.map +1 -1
  165. package/dist/src/module-registry.js +119 -31
  166. package/dist/src/module-registry.js.map +1 -1
  167. package/dist/src/modules/api/index.d.ts +1 -1
  168. package/dist/src/modules/api/index.d.ts.map +1 -1
  169. package/dist/src/modules/api/index.js +3 -2
  170. package/dist/src/modules/api/index.js.map +1 -1
  171. package/dist/src/modules/discord/discord-js-client.d.ts +43 -2
  172. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
  173. package/dist/src/modules/discord/discord-js-client.js +286 -67
  174. package/dist/src/modules/discord/discord-js-client.js.map +1 -1
  175. package/dist/src/modules/discord/index.d.ts +53 -5
  176. package/dist/src/modules/discord/index.d.ts.map +1 -1
  177. package/dist/src/modules/discord/index.js +712 -186
  178. package/dist/src/modules/discord/index.js.map +1 -1
  179. package/dist/src/modules/discord/refusal.d.ts +14 -0
  180. package/dist/src/modules/discord/refusal.d.ts.map +1 -0
  181. package/dist/src/modules/discord/refusal.js +14 -0
  182. package/dist/src/modules/discord/refusal.js.map +1 -0
  183. package/dist/src/modules/discord/types.d.ts +27 -4
  184. package/dist/src/modules/discord/types.d.ts.map +1 -1
  185. package/dist/src/modules/health/index.d.ts.map +1 -1
  186. package/dist/src/modules/health/index.js +8 -3
  187. package/dist/src/modules/health/index.js.map +1 -1
  188. package/dist/src/modules/history/index.d.ts +3 -1
  189. package/dist/src/modules/history/index.d.ts.map +1 -1
  190. package/dist/src/modules/history/index.js +359 -127
  191. package/dist/src/modules/history/index.js.map +1 -1
  192. package/dist/src/modules/history/semantic.d.ts +28 -1
  193. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  194. package/dist/src/modules/history/semantic.js +177 -32
  195. package/dist/src/modules/history/semantic.js.map +1 -1
  196. package/dist/src/modules/index.d.ts +1 -1
  197. package/dist/src/modules/index.d.ts.map +1 -1
  198. package/dist/src/modules/index.js +1 -1
  199. package/dist/src/modules/index.js.map +1 -1
  200. package/dist/src/modules/unknown-tool.d.ts +10 -0
  201. package/dist/src/modules/unknown-tool.d.ts.map +1 -0
  202. package/dist/src/modules/unknown-tool.js +17 -0
  203. package/dist/src/modules/unknown-tool.js.map +1 -0
  204. package/dist/src/modules/workspace/disk-agreement.d.ts +57 -4
  205. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  206. package/dist/src/modules/workspace/disk-agreement.js +75 -7
  207. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  208. package/dist/src/modules/workspace/effects.d.ts +66 -7
  209. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  210. package/dist/src/modules/workspace/effects.js +303 -46
  211. package/dist/src/modules/workspace/effects.js.map +1 -1
  212. package/dist/src/modules/workspace/index.d.ts +313 -45
  213. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  214. package/dist/src/modules/workspace/index.js +1404 -304
  215. package/dist/src/modules/workspace/index.js.map +1 -1
  216. package/dist/src/modules/workspace/observe.d.ts +315 -16
  217. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  218. package/dist/src/modules/workspace/observe.js +727 -37
  219. package/dist/src/modules/workspace/observe.js.map +1 -1
  220. package/dist/src/modules/workspace/reconcile.d.ts +58 -7
  221. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  222. package/dist/src/modules/workspace/reconcile.js +521 -56
  223. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  224. package/dist/src/modules/workspace/types.d.ts +28 -5
  225. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  226. package/dist/src/modules/workspace/types.js.map +1 -1
  227. package/dist/src/modules/workspace/watcher.d.ts +49 -2
  228. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  229. package/dist/src/modules/workspace/watcher.js +158 -29
  230. package/dist/src/modules/workspace/watcher.js.map +1 -1
  231. package/dist/src/prose-drafts.d.ts +240 -0
  232. package/dist/src/prose-drafts.d.ts.map +1 -0
  233. package/dist/src/prose-drafts.js +362 -0
  234. package/dist/src/prose-drafts.js.map +1 -0
  235. package/dist/src/prose-segments.d.ts +21 -2
  236. package/dist/src/prose-segments.d.ts.map +1 -1
  237. package/dist/src/prose-segments.js +46 -9
  238. package/dist/src/prose-segments.js.map +1 -1
  239. package/dist/src/recovery/offline-branch.d.ts +1 -0
  240. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  241. package/dist/src/recovery/offline-branch.js +3 -1
  242. package/dist/src/recovery/offline-branch.js.map +1 -1
  243. package/dist/src/safe-slice.d.ts +5 -1
  244. package/dist/src/safe-slice.d.ts.map +1 -1
  245. package/dist/src/safe-slice.js +9 -1
  246. package/dist/src/safe-slice.js.map +1 -1
  247. package/dist/src/silent-heartbeat.d.ts +112 -39
  248. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  249. package/dist/src/silent-heartbeat.js +100 -29
  250. package/dist/src/silent-heartbeat.js.map +1 -1
  251. package/dist/src/tool-args.d.ts +40 -0
  252. package/dist/src/tool-args.d.ts.map +1 -0
  253. package/dist/src/tool-args.js +207 -0
  254. package/dist/src/tool-args.js.map +1 -0
  255. package/dist/src/tool-image-ledger.d.ts +87 -19
  256. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  257. package/dist/src/tool-image-ledger.js +135 -32
  258. package/dist/src/tool-image-ledger.js.map +1 -1
  259. package/dist/src/tool-presentation.d.ts +9 -0
  260. package/dist/src/tool-presentation.d.ts.map +1 -1
  261. package/dist/src/tool-presentation.js +266 -62
  262. package/dist/src/tool-presentation.js.map +1 -1
  263. package/dist/src/tool-result-guard.d.ts.map +1 -1
  264. package/dist/src/tool-result-guard.js +7 -10
  265. package/dist/src/tool-result-guard.js.map +1 -1
  266. package/dist/src/tool-result-history.d.ts +32 -3
  267. package/dist/src/tool-result-history.d.ts.map +1 -1
  268. package/dist/src/tool-result-history.js +106 -19
  269. package/dist/src/tool-result-history.js.map +1 -1
  270. package/dist/src/tune-out/coordinator.d.ts +25 -1
  271. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  272. package/dist/src/tune-out/coordinator.js +260 -65
  273. package/dist/src/tune-out/coordinator.js.map +1 -1
  274. package/dist/src/types/agent.d.ts +44 -22
  275. package/dist/src/types/agent.d.ts.map +1 -1
  276. package/dist/src/types/events.d.ts +25 -0
  277. package/dist/src/types/events.d.ts.map +1 -1
  278. package/dist/src/types/framework.d.ts +38 -7
  279. package/dist/src/types/framework.d.ts.map +1 -1
  280. package/dist/src/types/inference-log.d.ts +1 -1
  281. package/dist/src/types/inference-log.d.ts.map +1 -1
  282. package/dist/src/types/module.d.ts +77 -14
  283. package/dist/src/types/module.d.ts.map +1 -1
  284. package/dist/src/types/trace.d.ts +97 -8
  285. package/dist/src/types/trace.d.ts.map +1 -1
  286. package/dist/src/usage/usage-tracker.d.ts +5 -0
  287. package/dist/src/usage/usage-tracker.d.ts.map +1 -1
  288. package/dist/src/usage/usage-tracker.js +35 -8
  289. package/dist/src/usage/usage-tracker.js.map +1 -1
  290. package/package.json +5 -4
  291. package/src/agent.ts +257 -72
  292. package/src/api/mcp-server-core.ts +1 -1
  293. package/src/api/server.ts +4 -1
  294. package/src/api/types.ts +1 -1
  295. package/src/branch-names.ts +19 -0
  296. package/src/closest-names.ts +94 -0
  297. package/src/code-execution/index.ts +1 -0
  298. package/src/code-execution/py-runner.ts +304 -93
  299. package/src/code-execution/runtime-py.ts +94 -8
  300. package/src/code-execution/tool-definition.ts +85 -7
  301. package/src/config-checks.ts +50 -0
  302. package/src/deadline-timer.ts +44 -0
  303. package/src/error-message.ts +48 -0
  304. package/src/framework.ts +8625 -1984
  305. package/src/gate/event-gate.ts +38 -5
  306. package/src/held-writes.ts +59 -0
  307. package/src/index.ts +29 -5
  308. package/src/mcpl/capability-grant.ts +1 -1
  309. package/src/mcpl/channel-registry.ts +1095 -411
  310. package/src/mcpl/checkpoint-manager.ts +104 -29
  311. package/src/mcpl/conversation-router.ts +28 -8
  312. package/src/mcpl/convert-block.ts +105 -0
  313. package/src/mcpl/errors.ts +28 -0
  314. package/src/mcpl/hook-orchestrator.ts +84 -77
  315. package/src/mcpl/inbound-source.ts +156 -0
  316. package/src/mcpl/index.ts +23 -1
  317. package/src/mcpl/inference-router.ts +179 -103
  318. package/src/mcpl/log-value.ts +10 -0
  319. package/src/mcpl/modern-connection.ts +1182 -0
  320. package/src/mcpl/prefix-rule.ts +32 -0
  321. package/src/mcpl/protocol-family.ts +691 -0
  322. package/src/mcpl/push-coalescer.ts +110 -36
  323. package/src/mcpl/push-handler.ts +20 -90
  324. package/src/mcpl/reference-fetcher.ts +318 -67
  325. package/src/mcpl/references.ts +183 -34
  326. package/src/mcpl/server-connection.ts +1404 -280
  327. package/src/mcpl/server-registry.ts +63 -14
  328. package/src/mcpl/skip-reply-wake.ts +73 -0
  329. package/src/mcpl/tool-result-normalize.ts +294 -0
  330. package/src/mcpl/transport.ts +320 -76
  331. package/src/mcpl/types.ts +77 -19
  332. package/src/mcpl/visible-content.ts +28 -0
  333. package/src/module-registry.ts +126 -36
  334. package/src/modules/api/index.ts +3 -2
  335. package/src/modules/discord/discord-js-client.ts +306 -74
  336. package/src/modules/discord/index.ts +737 -201
  337. package/src/modules/discord/refusal.ts +13 -0
  338. package/src/modules/discord/types.ts +27 -6
  339. package/src/modules/health/index.ts +8 -3
  340. package/src/modules/history/index.ts +410 -120
  341. package/src/modules/history/semantic.ts +189 -32
  342. package/src/modules/index.ts +1 -1
  343. package/src/modules/unknown-tool.ts +16 -0
  344. package/src/modules/workspace/disk-agreement.ts +97 -8
  345. package/src/modules/workspace/effects.ts +291 -36
  346. package/src/modules/workspace/index.ts +1469 -321
  347. package/src/modules/workspace/observe.ts +784 -41
  348. package/src/modules/workspace/reconcile.ts +619 -64
  349. package/src/modules/workspace/types.ts +28 -5
  350. package/src/modules/workspace/watcher.ts +154 -30
  351. package/src/prose-drafts.ts +481 -0
  352. package/src/prose-segments.ts +51 -8
  353. package/src/recovery/offline-branch.ts +4 -1
  354. package/src/safe-slice.ts +9 -1
  355. package/src/silent-heartbeat.ts +184 -59
  356. package/src/tool-args.ts +213 -0
  357. package/src/tool-image-ledger.ts +153 -34
  358. package/src/tool-presentation.ts +233 -42
  359. package/src/tool-result-guard.ts +7 -8
  360. package/src/tool-result-history.ts +124 -20
  361. package/src/tune-out/coordinator.ts +259 -65
  362. package/src/types/agent.ts +44 -24
  363. package/src/types/events.ts +25 -0
  364. package/src/types/framework.ts +38 -7
  365. package/src/types/inference-log.ts +1 -1
  366. package/src/types/module.ts +79 -14
  367. package/src/types/trace.ts +103 -8
  368. package/src/usage/usage-tracker.ts +32 -8
  369. package/dist/test/activation-request-sanitize.test.d.ts +0 -2
  370. package/dist/test/activation-request-sanitize.test.d.ts.map +0 -1
  371. package/dist/test/activation-request-sanitize.test.js +0 -89
  372. package/dist/test/activation-request-sanitize.test.js.map +0 -1
  373. package/dist/test/agent-cache-ttl.test.d.ts +0 -2
  374. package/dist/test/agent-cache-ttl.test.d.ts.map +0 -1
  375. package/dist/test/agent-cache-ttl.test.js +0 -41
  376. package/dist/test/agent-cache-ttl.test.js.map +0 -1
  377. package/dist/test/agent-runtime-settings.test.d.ts +0 -2
  378. package/dist/test/agent-runtime-settings.test.d.ts.map +0 -1
  379. package/dist/test/agent-runtime-settings.test.js +0 -1598
  380. package/dist/test/agent-runtime-settings.test.js.map +0 -1
  381. package/dist/test/api-admin-gate.test.d.ts +0 -2
  382. package/dist/test/api-admin-gate.test.d.ts.map +0 -1
  383. package/dist/test/api-admin-gate.test.js +0 -116
  384. package/dist/test/api-admin-gate.test.js.map +0 -1
  385. package/dist/test/assistant-persist-refusal.test.d.ts +0 -2
  386. package/dist/test/assistant-persist-refusal.test.d.ts.map +0 -1
  387. package/dist/test/assistant-persist-refusal.test.js +0 -63
  388. package/dist/test/assistant-persist-refusal.test.js.map +0 -1
  389. package/dist/test/batched-wake-locus.test.d.ts +0 -2
  390. package/dist/test/batched-wake-locus.test.d.ts.map +0 -1
  391. package/dist/test/batched-wake-locus.test.js +0 -166
  392. package/dist/test/batched-wake-locus.test.js.map +0 -1
  393. package/dist/test/busy-agent-scheduler.test.d.ts +0 -2
  394. package/dist/test/busy-agent-scheduler.test.d.ts.map +0 -1
  395. package/dist/test/busy-agent-scheduler.test.js +0 -92
  396. package/dist/test/busy-agent-scheduler.test.js.map +0 -1
  397. package/dist/test/channel-incoming-targeting.test.d.ts +0 -2
  398. package/dist/test/channel-incoming-targeting.test.d.ts.map +0 -1
  399. package/dist/test/channel-incoming-targeting.test.js +0 -81
  400. package/dist/test/channel-incoming-targeting.test.js.map +0 -1
  401. package/dist/test/channel-label-history.test.d.ts +0 -2
  402. package/dist/test/channel-label-history.test.d.ts.map +0 -1
  403. package/dist/test/channel-label-history.test.js +0 -424
  404. package/dist/test/channel-label-history.test.js.map +0 -1
  405. package/dist/test/channel-lifecycle-provenance.test.d.ts +0 -2
  406. package/dist/test/channel-lifecycle-provenance.test.d.ts.map +0 -1
  407. package/dist/test/channel-lifecycle-provenance.test.js +0 -47
  408. package/dist/test/channel-lifecycle-provenance.test.js.map +0 -1
  409. package/dist/test/channel-registry-routing.test.d.ts +0 -2
  410. package/dist/test/channel-registry-routing.test.d.ts.map +0 -1
  411. package/dist/test/channel-registry-routing.test.js +0 -446
  412. package/dist/test/channel-registry-routing.test.js.map +0 -1
  413. package/dist/test/channel-registry-typing.test.d.ts +0 -2
  414. package/dist/test/channel-registry-typing.test.d.ts.map +0 -1
  415. package/dist/test/channel-registry-typing.test.js +0 -91
  416. package/dist/test/channel-registry-typing.test.js.map +0 -1
  417. package/dist/test/channels-changed-respond-first.test.d.ts +0 -2
  418. package/dist/test/channels-changed-respond-first.test.d.ts.map +0 -1
  419. package/dist/test/channels-changed-respond-first.test.js +0 -74
  420. package/dist/test/channels-changed-respond-first.test.js.map +0 -1
  421. package/dist/test/checkpoint-attribution.test.d.ts +0 -2
  422. package/dist/test/checkpoint-attribution.test.d.ts.map +0 -1
  423. package/dist/test/checkpoint-attribution.test.js +0 -200
  424. package/dist/test/checkpoint-attribution.test.js.map +0 -1
  425. package/dist/test/code-execution.test.d.ts +0 -11
  426. package/dist/test/code-execution.test.d.ts.map +0 -1
  427. package/dist/test/code-execution.test.js +0 -1272
  428. package/dist/test/code-execution.test.js.map +0 -1
  429. package/dist/test/component-defaults.test.d.ts +0 -2
  430. package/dist/test/component-defaults.test.d.ts.map +0 -1
  431. package/dist/test/component-defaults.test.js +0 -54
  432. package/dist/test/component-defaults.test.js.map +0 -1
  433. package/dist/test/conversation-fork-injection-scoping.test.d.ts +0 -2
  434. package/dist/test/conversation-fork-injection-scoping.test.d.ts.map +0 -1
  435. package/dist/test/conversation-fork-injection-scoping.test.js +0 -200
  436. package/dist/test/conversation-fork-injection-scoping.test.js.map +0 -1
  437. package/dist/test/conversation-router.test.d.ts +0 -2
  438. package/dist/test/conversation-router.test.d.ts.map +0 -1
  439. package/dist/test/conversation-router.test.js +0 -174
  440. package/dist/test/conversation-router.test.js.map +0 -1
  441. package/dist/test/conversation-routing.test.d.ts +0 -7
  442. package/dist/test/conversation-routing.test.d.ts.map +0 -1
  443. package/dist/test/conversation-routing.test.js +0 -621
  444. package/dist/test/conversation-routing.test.js.map +0 -1
  445. package/dist/test/deferred-wake-integrity.test.d.ts +0 -29
  446. package/dist/test/deferred-wake-integrity.test.d.ts.map +0 -1
  447. package/dist/test/deferred-wake-integrity.test.js +0 -320
  448. package/dist/test/deferred-wake-integrity.test.js.map +0 -1
  449. package/dist/test/discord-awareness-outbox.test.d.ts +0 -2
  450. package/dist/test/discord-awareness-outbox.test.d.ts.map +0 -1
  451. package/dist/test/discord-awareness-outbox.test.js +0 -371
  452. package/dist/test/discord-awareness-outbox.test.js.map +0 -1
  453. package/dist/test/discord-history-delete-window.test.d.ts +0 -2
  454. package/dist/test/discord-history-delete-window.test.d.ts.map +0 -1
  455. package/dist/test/discord-history-delete-window.test.js +0 -81
  456. package/dist/test/discord-history-delete-window.test.js.map +0 -1
  457. package/dist/test/discord-history-sync.test.d.ts +0 -2
  458. package/dist/test/discord-history-sync.test.d.ts.map +0 -1
  459. package/dist/test/discord-history-sync.test.js +0 -56
  460. package/dist/test/discord-history-sync.test.js.map +0 -1
  461. package/dist/test/discord-typing-keepalive.test.d.ts +0 -2
  462. package/dist/test/discord-typing-keepalive.test.d.ts.map +0 -1
  463. package/dist/test/discord-typing-keepalive.test.js +0 -77
  464. package/dist/test/discord-typing-keepalive.test.js.map +0 -1
  465. package/dist/test/ephemeral-settle.test.d.ts +0 -2
  466. package/dist/test/ephemeral-settle.test.d.ts.map +0 -1
  467. package/dist/test/ephemeral-settle.test.js +0 -354
  468. package/dist/test/ephemeral-settle.test.js.map +0 -1
  469. package/dist/test/event-gate-passthrough.test.d.ts +0 -15
  470. package/dist/test/event-gate-passthrough.test.d.ts.map +0 -1
  471. package/dist/test/event-gate-passthrough.test.js +0 -289
  472. package/dist/test/event-gate-passthrough.test.js.map +0 -1
  473. package/dist/test/event-gate-policy-mutation.test.d.ts +0 -2
  474. package/dist/test/event-gate-policy-mutation.test.d.ts.map +0 -1
  475. package/dist/test/event-gate-policy-mutation.test.js +0 -137
  476. package/dist/test/event-gate-policy-mutation.test.js.map +0 -1
  477. package/dist/test/event-gate-rate-limit.test.d.ts +0 -2
  478. package/dist/test/event-gate-rate-limit.test.d.ts.map +0 -1
  479. package/dist/test/event-gate-rate-limit.test.js +0 -627
  480. package/dist/test/event-gate-rate-limit.test.js.map +0 -1
  481. package/dist/test/event-gate-script.test.d.ts +0 -2
  482. package/dist/test/event-gate-script.test.d.ts.map +0 -1
  483. package/dist/test/event-gate-script.test.js +0 -95
  484. package/dist/test/event-gate-script.test.js.map +0 -1
  485. package/dist/test/event-gate-selfwake.test.d.ts +0 -15
  486. package/dist/test/event-gate-selfwake.test.d.ts.map +0 -1
  487. package/dist/test/event-gate-selfwake.test.js +0 -125
  488. package/dist/test/event-gate-selfwake.test.js.map +0 -1
  489. package/dist/test/event-gate-tags.test.d.ts +0 -2
  490. package/dist/test/event-gate-tags.test.d.ts.map +0 -1
  491. package/dist/test/event-gate-tags.test.js +0 -76
  492. package/dist/test/event-gate-tags.test.js.map +0 -1
  493. package/dist/test/event-gate.test.d.ts +0 -2
  494. package/dist/test/event-gate.test.d.ts.map +0 -1
  495. package/dist/test/event-gate.test.js +0 -919
  496. package/dist/test/event-gate.test.js.map +0 -1
  497. package/dist/test/explicit-prose-routing.test.d.ts +0 -11
  498. package/dist/test/explicit-prose-routing.test.d.ts.map +0 -1
  499. package/dist/test/explicit-prose-routing.test.js +0 -1715
  500. package/dist/test/explicit-prose-routing.test.js.map +0 -1
  501. package/dist/test/framework.test.d.ts +0 -5
  502. package/dist/test/framework.test.d.ts.map +0 -1
  503. package/dist/test/framework.test.js +0 -1048
  504. package/dist/test/framework.test.js.map +0 -1
  505. package/dist/test/gather-context-timeout.test.d.ts +0 -2
  506. package/dist/test/gather-context-timeout.test.d.ts.map +0 -1
  507. package/dist/test/gather-context-timeout.test.js +0 -120
  508. package/dist/test/gather-context-timeout.test.js.map +0 -1
  509. package/dist/test/health-snapshot.test.d.ts +0 -2
  510. package/dist/test/health-snapshot.test.d.ts.map +0 -1
  511. package/dist/test/health-snapshot.test.js +0 -74
  512. package/dist/test/health-snapshot.test.js.map +0 -1
  513. package/dist/test/helpers/coalescing-fixture.d.ts +0 -74
  514. package/dist/test/helpers/coalescing-fixture.d.ts.map +0 -1
  515. package/dist/test/helpers/coalescing-fixture.js +0 -152
  516. package/dist/test/helpers/coalescing-fixture.js.map +0 -1
  517. package/dist/test/helpers/coalescing-kill-child.d.ts +0 -2
  518. package/dist/test/helpers/coalescing-kill-child.d.ts.map +0 -1
  519. package/dist/test/helpers/coalescing-kill-child.js +0 -24
  520. package/dist/test/helpers/coalescing-kill-child.js.map +0 -1
  521. package/dist/test/helpers/mock-membrane.d.ts +0 -53
  522. package/dist/test/helpers/mock-membrane.d.ts.map +0 -1
  523. package/dist/test/helpers/mock-membrane.js +0 -168
  524. package/dist/test/helpers/mock-membrane.js.map +0 -1
  525. package/dist/test/helpers/pytc-stop-rows.d.ts +0 -11
  526. package/dist/test/helpers/pytc-stop-rows.d.ts.map +0 -1
  527. package/dist/test/helpers/pytc-stop-rows.js +0 -143
  528. package/dist/test/helpers/pytc-stop-rows.js.map +0 -1
  529. package/dist/test/helpers/quiesce-crash-child.d.ts +0 -2
  530. package/dist/test/helpers/quiesce-crash-child.d.ts.map +0 -1
  531. package/dist/test/helpers/quiesce-crash-child.js +0 -156
  532. package/dist/test/helpers/quiesce-crash-child.js.map +0 -1
  533. package/dist/test/hide-then-turn.test.d.ts +0 -2
  534. package/dist/test/hide-then-turn.test.d.ts.map +0 -1
  535. package/dist/test/hide-then-turn.test.js +0 -101
  536. package/dist/test/hide-then-turn.test.js.map +0 -1
  537. package/dist/test/history-module-overview.test.d.ts +0 -2
  538. package/dist/test/history-module-overview.test.d.ts.map +0 -1
  539. package/dist/test/history-module-overview.test.js +0 -711
  540. package/dist/test/history-module-overview.test.js.map +0 -1
  541. package/dist/test/history-module-realstore.test.d.ts +0 -2
  542. package/dist/test/history-module-realstore.test.d.ts.map +0 -1
  543. package/dist/test/history-module-realstore.test.js +0 -363
  544. package/dist/test/history-module-realstore.test.js.map +0 -1
  545. package/dist/test/history-module-semantic.test.d.ts +0 -2
  546. package/dist/test/history-module-semantic.test.d.ts.map +0 -1
  547. package/dist/test/history-module-semantic.test.js +0 -213
  548. package/dist/test/history-module-semantic.test.js.map +0 -1
  549. package/dist/test/history-module-ux.test.d.ts +0 -2
  550. package/dist/test/history-module-ux.test.d.ts.map +0 -1
  551. package/dist/test/history-module-ux.test.js +0 -304
  552. package/dist/test/history-module-ux.test.js.map +0 -1
  553. package/dist/test/history-module.test.d.ts +0 -2
  554. package/dist/test/history-module.test.d.ts.map +0 -1
  555. package/dist/test/history-module.test.js +0 -619
  556. package/dist/test/history-module.test.js.map +0 -1
  557. package/dist/test/history-semantic-review-findings.test.d.ts +0 -2
  558. package/dist/test/history-semantic-review-findings.test.d.ts.map +0 -1
  559. package/dist/test/history-semantic-review-findings.test.js +0 -338
  560. package/dist/test/history-semantic-review-findings.test.js.map +0 -1
  561. package/dist/test/history-semantic-sync-converges.test.d.ts +0 -2
  562. package/dist/test/history-semantic-sync-converges.test.d.ts.map +0 -1
  563. package/dist/test/history-semantic-sync-converges.test.js +0 -109
  564. package/dist/test/history-semantic-sync-converges.test.js.map +0 -1
  565. package/dist/test/host-quiesce.test.d.ts +0 -2
  566. package/dist/test/host-quiesce.test.d.ts.map +0 -1
  567. package/dist/test/host-quiesce.test.js +0 -988
  568. package/dist/test/host-quiesce.test.js.map +0 -1
  569. package/dist/test/inference-exhaustion-rewind.test.d.ts +0 -2
  570. package/dist/test/inference-exhaustion-rewind.test.d.ts.map +0 -1
  571. package/dist/test/inference-exhaustion-rewind.test.js +0 -387
  572. package/dist/test/inference-exhaustion-rewind.test.js.map +0 -1
  573. package/dist/test/inference-failure-bounds.test.d.ts +0 -2
  574. package/dist/test/inference-failure-bounds.test.d.ts.map +0 -1
  575. package/dist/test/inference-failure-bounds.test.js +0 -1202
  576. package/dist/test/inference-failure-bounds.test.js.map +0 -1
  577. package/dist/test/inference-failure-observability.test.d.ts +0 -2
  578. package/dist/test/inference-failure-observability.test.d.ts.map +0 -1
  579. package/dist/test/inference-failure-observability.test.js +0 -84
  580. package/dist/test/inference-failure-observability.test.js.map +0 -1
  581. package/dist/test/journal-tool.test.d.ts +0 -2
  582. package/dist/test/journal-tool.test.d.ts.map +0 -1
  583. package/dist/test/journal-tool.test.js +0 -33
  584. package/dist/test/journal-tool.test.js.map +0 -1
  585. package/dist/test/kv-unified-open-flight-retry.test.d.ts +0 -2
  586. package/dist/test/kv-unified-open-flight-retry.test.d.ts.map +0 -1
  587. package/dist/test/kv-unified-open-flight-retry.test.js +0 -189
  588. package/dist/test/kv-unified-open-flight-retry.test.js.map +0 -1
  589. package/dist/test/kv-unified-wiring.test.d.ts +0 -2
  590. package/dist/test/kv-unified-wiring.test.d.ts.map +0 -1
  591. package/dist/test/kv-unified-wiring.test.js +0 -95
  592. package/dist/test/kv-unified-wiring.test.js.map +0 -1
  593. package/dist/test/late-events-after-stop.test.d.ts +0 -2
  594. package/dist/test/late-events-after-stop.test.d.ts.map +0 -1
  595. package/dist/test/late-events-after-stop.test.js +0 -122
  596. package/dist/test/late-events-after-stop.test.js.map +0 -1
  597. package/dist/test/liveness-watchdog.test.d.ts +0 -2
  598. package/dist/test/liveness-watchdog.test.d.ts.map +0 -1
  599. package/dist/test/liveness-watchdog.test.js +0 -94
  600. package/dist/test/liveness-watchdog.test.js.map +0 -1
  601. package/dist/test/machine-close-provenance.test.d.ts +0 -2
  602. package/dist/test/machine-close-provenance.test.d.ts.map +0 -1
  603. package/dist/test/machine-close-provenance.test.js +0 -116
  604. package/dist/test/machine-close-provenance.test.js.map +0 -1
  605. package/dist/test/maintenance-timer.test.d.ts +0 -2
  606. package/dist/test/maintenance-timer.test.d.ts.map +0 -1
  607. package/dist/test/maintenance-timer.test.js +0 -1827
  608. package/dist/test/maintenance-timer.test.js.map +0 -1
  609. package/dist/test/mcp-server-bin.test.d.ts +0 -2
  610. package/dist/test/mcp-server-bin.test.d.ts.map +0 -1
  611. package/dist/test/mcp-server-bin.test.js +0 -142
  612. package/dist/test/mcp-server-bin.test.js.map +0 -1
  613. package/dist/test/mcpl-access-provider.test.d.ts +0 -2
  614. package/dist/test/mcpl-access-provider.test.d.ts.map +0 -1
  615. package/dist/test/mcpl-access-provider.test.js +0 -20
  616. package/dist/test/mcpl-access-provider.test.js.map +0 -1
  617. package/dist/test/mcpl-awareness-barrier.test.d.ts +0 -2
  618. package/dist/test/mcpl-awareness-barrier.test.d.ts.map +0 -1
  619. package/dist/test/mcpl-awareness-barrier.test.js +0 -769
  620. package/dist/test/mcpl-awareness-barrier.test.js.map +0 -1
  621. package/dist/test/mcpl-capability-grant.test.d.ts +0 -10
  622. package/dist/test/mcpl-capability-grant.test.d.ts.map +0 -1
  623. package/dist/test/mcpl-capability-grant.test.js +0 -111
  624. package/dist/test/mcpl-capability-grant.test.js.map +0 -1
  625. package/dist/test/mcpl-capability-scoping.test.d.ts +0 -2
  626. package/dist/test/mcpl-capability-scoping.test.d.ts.map +0 -1
  627. package/dist/test/mcpl-capability-scoping.test.js +0 -266
  628. package/dist/test/mcpl-capability-scoping.test.js.map +0 -1
  629. package/dist/test/mcpl-child-env.test.d.ts +0 -2
  630. package/dist/test/mcpl-child-env.test.d.ts.map +0 -1
  631. package/dist/test/mcpl-child-env.test.js +0 -158
  632. package/dist/test/mcpl-child-env.test.js.map +0 -1
  633. package/dist/test/mcpl-coalesced-content-parity.test.d.ts +0 -2
  634. package/dist/test/mcpl-coalesced-content-parity.test.d.ts.map +0 -1
  635. package/dist/test/mcpl-coalesced-content-parity.test.js +0 -532
  636. package/dist/test/mcpl-coalesced-content-parity.test.js.map +0 -1
  637. package/dist/test/mcpl-coalescing.test.d.ts +0 -2
  638. package/dist/test/mcpl-coalescing.test.d.ts.map +0 -1
  639. package/dist/test/mcpl-coalescing.test.js +0 -1125
  640. package/dist/test/mcpl-coalescing.test.js.map +0 -1
  641. package/dist/test/mcpl-config-diagnostics.test.d.ts +0 -2
  642. package/dist/test/mcpl-config-diagnostics.test.d.ts.map +0 -1
  643. package/dist/test/mcpl-config-diagnostics.test.js +0 -452
  644. package/dist/test/mcpl-config-diagnostics.test.js.map +0 -1
  645. package/dist/test/mcpl-empty-content.test.d.ts +0 -2
  646. package/dist/test/mcpl-empty-content.test.d.ts.map +0 -1
  647. package/dist/test/mcpl-empty-content.test.js +0 -368
  648. package/dist/test/mcpl-empty-content.test.js.map +0 -1
  649. package/dist/test/mcpl-gate-roundtrip.test.d.ts +0 -2
  650. package/dist/test/mcpl-gate-roundtrip.test.d.ts.map +0 -1
  651. package/dist/test/mcpl-gate-roundtrip.test.js +0 -285
  652. package/dist/test/mcpl-gate-roundtrip.test.js.map +0 -1
  653. package/dist/test/mcpl-list-status.test.d.ts +0 -2
  654. package/dist/test/mcpl-list-status.test.d.ts.map +0 -1
  655. package/dist/test/mcpl-list-status.test.js +0 -82
  656. package/dist/test/mcpl-list-status.test.js.map +0 -1
  657. package/dist/test/mcpl-manifest-changed.test.d.ts +0 -2
  658. package/dist/test/mcpl-manifest-changed.test.d.ts.map +0 -1
  659. package/dist/test/mcpl-manifest-changed.test.js +0 -178
  660. package/dist/test/mcpl-manifest-changed.test.js.map +0 -1
  661. package/dist/test/mcpl-push-convert.test.d.ts +0 -2
  662. package/dist/test/mcpl-push-convert.test.d.ts.map +0 -1
  663. package/dist/test/mcpl-push-convert.test.js +0 -42
  664. package/dist/test/mcpl-push-convert.test.js.map +0 -1
  665. package/dist/test/mcpl-reconnect-events.test.d.ts +0 -2
  666. package/dist/test/mcpl-reconnect-events.test.d.ts.map +0 -1
  667. package/dist/test/mcpl-reconnect-events.test.js +0 -144
  668. package/dist/test/mcpl-reconnect-events.test.js.map +0 -1
  669. package/dist/test/mcpl-reconnect-reregistration.test.d.ts +0 -2
  670. package/dist/test/mcpl-reconnect-reregistration.test.d.ts.map +0 -1
  671. package/dist/test/mcpl-reconnect-reregistration.test.js +0 -238
  672. package/dist/test/mcpl-reconnect-reregistration.test.js.map +0 -1
  673. package/dist/test/mcpl-reference-fetcher.test.d.ts +0 -2
  674. package/dist/test/mcpl-reference-fetcher.test.d.ts.map +0 -1
  675. package/dist/test/mcpl-reference-fetcher.test.js +0 -427
  676. package/dist/test/mcpl-reference-fetcher.test.js.map +0 -1
  677. package/dist/test/mcpl-reference-fields.test.d.ts +0 -2
  678. package/dist/test/mcpl-reference-fields.test.d.ts.map +0 -1
  679. package/dist/test/mcpl-reference-fields.test.js +0 -149
  680. package/dist/test/mcpl-reference-fields.test.js.map +0 -1
  681. package/dist/test/mcpl-rejection-log-lines.test.d.ts +0 -2
  682. package/dist/test/mcpl-rejection-log-lines.test.d.ts.map +0 -1
  683. package/dist/test/mcpl-rejection-log-lines.test.js +0 -349
  684. package/dist/test/mcpl-rejection-log-lines.test.js.map +0 -1
  685. package/dist/test/mcpl-request-timeout.test.d.ts +0 -2
  686. package/dist/test/mcpl-request-timeout.test.d.ts.map +0 -1
  687. package/dist/test/mcpl-request-timeout.test.js +0 -138
  688. package/dist/test/mcpl-request-timeout.test.js.map +0 -1
  689. package/dist/test/mcpl-runtime-lifecycle.test.d.ts +0 -7
  690. package/dist/test/mcpl-runtime-lifecycle.test.d.ts.map +0 -1
  691. package/dist/test/mcpl-runtime-lifecycle.test.js +0 -142
  692. package/dist/test/mcpl-runtime-lifecycle.test.js.map +0 -1
  693. package/dist/test/mcpl-stderr-tail.test.d.ts +0 -2
  694. package/dist/test/mcpl-stderr-tail.test.d.ts.map +0 -1
  695. package/dist/test/mcpl-stderr-tail.test.js +0 -210
  696. package/dist/test/mcpl-stderr-tail.test.js.map +0 -1
  697. package/dist/test/mcpl-tool-class-listing.test.d.ts +0 -2
  698. package/dist/test/mcpl-tool-class-listing.test.d.ts.map +0 -1
  699. package/dist/test/mcpl-tool-class-listing.test.js +0 -147
  700. package/dist/test/mcpl-tool-class-listing.test.js.map +0 -1
  701. package/dist/test/mcpl-tool-classes.test.d.ts +0 -7
  702. package/dist/test/mcpl-tool-classes.test.d.ts.map +0 -1
  703. package/dist/test/mcpl-tool-classes.test.js +0 -80
  704. package/dist/test/mcpl-tool-classes.test.js.map +0 -1
  705. package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts +0 -2
  706. package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts.map +0 -1
  707. package/dist/test/mcpl-tool-lifecycle-e2e.test.js +0 -237
  708. package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +0 -1
  709. package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts +0 -11
  710. package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts.map +0 -1
  711. package/dist/test/mcpl-tool-lifecycle-emitter.test.js +0 -412
  712. package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +0 -1
  713. package/dist/test/mcpl-tool-lifecycle-script.test.d.ts +0 -2
  714. package/dist/test/mcpl-tool-lifecycle-script.test.d.ts.map +0 -1
  715. package/dist/test/mcpl-tool-lifecycle-script.test.js +0 -288
  716. package/dist/test/mcpl-tool-lifecycle-script.test.js.map +0 -1
  717. package/dist/test/mcpl-tool-lifecycle.test.d.ts +0 -11
  718. package/dist/test/mcpl-tool-lifecycle.test.d.ts.map +0 -1
  719. package/dist/test/mcpl-tool-lifecycle.test.js +0 -270
  720. package/dist/test/mcpl-tool-lifecycle.test.js.map +0 -1
  721. package/dist/test/mcpl-transport-close.test.d.ts +0 -2
  722. package/dist/test/mcpl-transport-close.test.d.ts.map +0 -1
  723. package/dist/test/mcpl-transport-close.test.js +0 -373
  724. package/dist/test/mcpl-transport-close.test.js.map +0 -1
  725. package/dist/test/mcpl-transport-spawn.test.d.ts +0 -2
  726. package/dist/test/mcpl-transport-spawn.test.d.ts.map +0 -1
  727. package/dist/test/mcpl-transport-spawn.test.js +0 -58
  728. package/dist/test/mcpl-transport-spawn.test.js.map +0 -1
  729. package/dist/test/mcpl-ws-transport.test.d.ts +0 -2
  730. package/dist/test/mcpl-ws-transport.test.d.ts.map +0 -1
  731. package/dist/test/mcpl-ws-transport.test.js +0 -259
  732. package/dist/test/mcpl-ws-transport.test.js.map +0 -1
  733. package/dist/test/module-context-integration.test.d.ts +0 -18
  734. package/dist/test/module-context-integration.test.d.ts.map +0 -1
  735. package/dist/test/module-context-integration.test.js +0 -181
  736. package/dist/test/module-context-integration.test.js.map +0 -1
  737. package/dist/test/offline-recovery-branch.test.d.ts +0 -2
  738. package/dist/test/offline-recovery-branch.test.d.ts.map +0 -1
  739. package/dist/test/offline-recovery-branch.test.js +0 -570
  740. package/dist/test/offline-recovery-branch.test.js.map +0 -1
  741. package/dist/test/operator-surgery.test.d.ts +0 -2
  742. package/dist/test/operator-surgery.test.d.ts.map +0 -1
  743. package/dist/test/operator-surgery.test.js +0 -595
  744. package/dist/test/operator-surgery.test.js.map +0 -1
  745. package/dist/test/ops-alert.test.d.ts +0 -2
  746. package/dist/test/ops-alert.test.d.ts.map +0 -1
  747. package/dist/test/ops-alert.test.js +0 -153
  748. package/dist/test/ops-alert.test.js.map +0 -1
  749. package/dist/test/overbudget-drain-breaker.test.d.ts +0 -2
  750. package/dist/test/overbudget-drain-breaker.test.d.ts.map +0 -1
  751. package/dist/test/overbudget-drain-breaker.test.js +0 -198
  752. package/dist/test/overbudget-drain-breaker.test.js.map +0 -1
  753. package/dist/test/pack-installs-alone.test.d.ts +0 -2
  754. package/dist/test/pack-installs-alone.test.d.ts.map +0 -1
  755. package/dist/test/pack-installs-alone.test.js +0 -284
  756. package/dist/test/pack-installs-alone.test.js.map +0 -1
  757. package/dist/test/per-agent-delivery.test.d.ts +0 -2
  758. package/dist/test/per-agent-delivery.test.d.ts.map +0 -1
  759. package/dist/test/per-agent-delivery.test.js +0 -108
  760. package/dist/test/per-agent-delivery.test.js.map +0 -1
  761. package/dist/test/physical-window-restart.test.d.ts +0 -16
  762. package/dist/test/physical-window-restart.test.d.ts.map +0 -1
  763. package/dist/test/physical-window-restart.test.js +0 -487
  764. package/dist/test/physical-window-restart.test.js.map +0 -1
  765. package/dist/test/placed-reaction-baseline.test.d.ts +0 -2
  766. package/dist/test/placed-reaction-baseline.test.d.ts.map +0 -1
  767. package/dist/test/placed-reaction-baseline.test.js +0 -312
  768. package/dist/test/placed-reaction-baseline.test.js.map +0 -1
  769. package/dist/test/present-while-acting.test.d.ts +0 -19
  770. package/dist/test/present-while-acting.test.d.ts.map +0 -1
  771. package/dist/test/present-while-acting.test.js +0 -1649
  772. package/dist/test/present-while-acting.test.js.map +0 -1
  773. package/dist/test/prose-segments.test.d.ts +0 -2
  774. package/dist/test/prose-segments.test.d.ts.map +0 -1
  775. package/dist/test/prose-segments.test.js +0 -52
  776. package/dist/test/prose-segments.test.js.map +0 -1
  777. package/dist/test/prose-stream-router.test.d.ts +0 -2
  778. package/dist/test/prose-stream-router.test.d.ts.map +0 -1
  779. package/dist/test/prose-stream-router.test.js +0 -171
  780. package/dist/test/prose-stream-router.test.js.map +0 -1
  781. package/dist/test/provider-acceleration-cooldown.test.d.ts +0 -2
  782. package/dist/test/provider-acceleration-cooldown.test.d.ts.map +0 -1
  783. package/dist/test/provider-acceleration-cooldown.test.js +0 -435
  784. package/dist/test/provider-acceleration-cooldown.test.js.map +0 -1
  785. package/dist/test/provider-waits.test.d.ts +0 -2
  786. package/dist/test/provider-waits.test.d.ts.map +0 -1
  787. package/dist/test/provider-waits.test.js +0 -2259
  788. package/dist/test/provider-waits.test.js.map +0 -1
  789. package/dist/test/puppet-tool-call.test.d.ts +0 -2
  790. package/dist/test/puppet-tool-call.test.d.ts.map +0 -1
  791. package/dist/test/puppet-tool-call.test.js +0 -226
  792. package/dist/test/puppet-tool-call.test.js.map +0 -1
  793. package/dist/test/puppet-turn-reservation.test.d.ts +0 -2
  794. package/dist/test/puppet-turn-reservation.test.d.ts.map +0 -1
  795. package/dist/test/puppet-turn-reservation.test.js +0 -139
  796. package/dist/test/puppet-turn-reservation.test.js.map +0 -1
  797. package/dist/test/reaction-baseline-platform.test.d.ts +0 -2
  798. package/dist/test/reaction-baseline-platform.test.d.ts.map +0 -1
  799. package/dist/test/reaction-baseline-platform.test.js +0 -124
  800. package/dist/test/reaction-baseline-platform.test.js.map +0 -1
  801. package/dist/test/record-journal.test.d.ts +0 -6
  802. package/dist/test/record-journal.test.d.ts.map +0 -1
  803. package/dist/test/record-journal.test.js +0 -269
  804. package/dist/test/record-journal.test.js.map +0 -1
  805. package/dist/test/refusal-reaction-baseline.test.d.ts +0 -2
  806. package/dist/test/refusal-reaction-baseline.test.d.ts.map +0 -1
  807. package/dist/test/refusal-reaction-baseline.test.js +0 -81
  808. package/dist/test/refusal-reaction-baseline.test.js.map +0 -1
  809. package/dist/test/refusal-rewind.test.d.ts +0 -2
  810. package/dist/test/refusal-rewind.test.d.ts.map +0 -1
  811. package/dist/test/refusal-rewind.test.js +0 -117
  812. package/dist/test/refusal-rewind.test.js.map +0 -1
  813. package/dist/test/refusal-single-call.test.d.ts +0 -2
  814. package/dist/test/refusal-single-call.test.d.ts.map +0 -1
  815. package/dist/test/refusal-single-call.test.js +0 -177
  816. package/dist/test/refusal-single-call.test.js.map +0 -1
  817. package/dist/test/release-bump.test.d.ts +0 -2
  818. package/dist/test/release-bump.test.d.ts.map +0 -1
  819. package/dist/test/release-bump.test.js +0 -248
  820. package/dist/test/release-bump.test.js.map +0 -1
  821. package/dist/test/release-changelog.test.d.ts +0 -2
  822. package/dist/test/release-changelog.test.d.ts.map +0 -1
  823. package/dist/test/release-changelog.test.js +0 -159
  824. package/dist/test/release-changelog.test.js.map +0 -1
  825. package/dist/test/release-holds.test.d.ts +0 -2
  826. package/dist/test/release-holds.test.d.ts.map +0 -1
  827. package/dist/test/release-holds.test.js +0 -456
  828. package/dist/test/release-holds.test.js.map +0 -1
  829. package/dist/test/same-round-think-policy.test.d.ts +0 -2
  830. package/dist/test/same-round-think-policy.test.d.ts.map +0 -1
  831. package/dist/test/same-round-think-policy.test.js +0 -853
  832. package/dist/test/same-round-think-policy.test.js.map +0 -1
  833. package/dist/test/save-recent-image.test.d.ts +0 -26
  834. package/dist/test/save-recent-image.test.d.ts.map +0 -1
  835. package/dist/test/save-recent-image.test.js +0 -974
  836. package/dist/test/save-recent-image.test.js.map +0 -1
  837. package/dist/test/shutdown-errors.test.d.ts +0 -2
  838. package/dist/test/shutdown-errors.test.d.ts.map +0 -1
  839. package/dist/test/shutdown-errors.test.js +0 -662
  840. package/dist/test/shutdown-errors.test.js.map +0 -1
  841. package/dist/test/silent-heartbeat-rows.test.d.ts +0 -2
  842. package/dist/test/silent-heartbeat-rows.test.d.ts.map +0 -1
  843. package/dist/test/silent-heartbeat-rows.test.js +0 -451
  844. package/dist/test/silent-heartbeat-rows.test.js.map +0 -1
  845. package/dist/test/silent-heartbeat.test.d.ts +0 -2
  846. package/dist/test/silent-heartbeat.test.d.ts.map +0 -1
  847. package/dist/test/silent-heartbeat.test.js +0 -165
  848. package/dist/test/silent-heartbeat.test.js.map +0 -1
  849. package/dist/test/state-scaling-e2e.test.d.ts +0 -22
  850. package/dist/test/state-scaling-e2e.test.d.ts.map +0 -1
  851. package/dist/test/state-scaling-e2e.test.js +0 -206
  852. package/dist/test/state-scaling-e2e.test.js.map +0 -1
  853. package/dist/test/state-scaling.test.d.ts +0 -12
  854. package/dist/test/state-scaling.test.d.ts.map +0 -1
  855. package/dist/test/state-scaling.test.js +0 -251
  856. package/dist/test/state-scaling.test.js.map +0 -1
  857. package/dist/test/stop-with-host-operations.test.d.ts +0 -20
  858. package/dist/test/stop-with-host-operations.test.d.ts.map +0 -1
  859. package/dist/test/stop-with-host-operations.test.js +0 -1174
  860. package/dist/test/stop-with-host-operations.test.js.map +0 -1
  861. package/dist/test/stop-with-work-in-flight.test.d.ts +0 -23
  862. package/dist/test/stop-with-work-in-flight.test.d.ts.map +0 -1
  863. package/dist/test/stop-with-work-in-flight.test.js +0 -784
  864. package/dist/test/stop-with-work-in-flight.test.js.map +0 -1
  865. package/dist/test/surgery-marks-receipt.test.d.ts +0 -2
  866. package/dist/test/surgery-marks-receipt.test.d.ts.map +0 -1
  867. package/dist/test/surgery-marks-receipt.test.js +0 -540
  868. package/dist/test/surgery-marks-receipt.test.js.map +0 -1
  869. package/dist/test/thinking-token-stamp.test.d.ts +0 -12
  870. package/dist/test/thinking-token-stamp.test.d.ts.map +0 -1
  871. package/dist/test/thinking-token-stamp.test.js +0 -129
  872. package/dist/test/thinking-token-stamp.test.js.map +0 -1
  873. package/dist/test/timezone.test.d.ts +0 -2
  874. package/dist/test/timezone.test.d.ts.map +0 -1
  875. package/dist/test/timezone.test.js +0 -18
  876. package/dist/test/timezone.test.js.map +0 -1
  877. package/dist/test/tool-batch-hook.test.d.ts +0 -8
  878. package/dist/test/tool-batch-hook.test.d.ts.map +0 -1
  879. package/dist/test/tool-batch-hook.test.js +0 -275
  880. package/dist/test/tool-batch-hook.test.js.map +0 -1
  881. package/dist/test/tool-policy.test.d.ts +0 -2
  882. package/dist/test/tool-policy.test.d.ts.map +0 -1
  883. package/dist/test/tool-policy.test.js +0 -51
  884. package/dist/test/tool-policy.test.js.map +0 -1
  885. package/dist/test/tool-presentation.test.d.ts +0 -2
  886. package/dist/test/tool-presentation.test.d.ts.map +0 -1
  887. package/dist/test/tool-presentation.test.js +0 -268
  888. package/dist/test/tool-presentation.test.js.map +0 -1
  889. package/dist/test/tool-result-content.test.d.ts +0 -2
  890. package/dist/test/tool-result-content.test.d.ts.map +0 -1
  891. package/dist/test/tool-result-content.test.js +0 -37
  892. package/dist/test/tool-result-content.test.js.map +0 -1
  893. package/dist/test/tool-result-guard.test.d.ts +0 -2
  894. package/dist/test/tool-result-guard.test.d.ts.map +0 -1
  895. package/dist/test/tool-result-guard.test.js +0 -3729
  896. package/dist/test/tool-result-guard.test.js.map +0 -1
  897. package/dist/test/tool-result-history.test.d.ts +0 -7
  898. package/dist/test/tool-result-history.test.d.ts.map +0 -1
  899. package/dist/test/tool-result-history.test.js +0 -156
  900. package/dist/test/tool-result-history.test.js.map +0 -1
  901. package/dist/test/tool-result-spill.test.d.ts +0 -13
  902. package/dist/test/tool-result-spill.test.d.ts.map +0 -1
  903. package/dist/test/tool-result-spill.test.js +0 -586
  904. package/dist/test/tool-result-spill.test.js.map +0 -1
  905. package/dist/test/tool-wrapper-prose-guard-integration.test.d.ts +0 -2
  906. package/dist/test/tool-wrapper-prose-guard-integration.test.d.ts.map +0 -1
  907. package/dist/test/tool-wrapper-prose-guard-integration.test.js +0 -485
  908. package/dist/test/tool-wrapper-prose-guard-integration.test.js.map +0 -1
  909. package/dist/test/tool-wrapper-prose-guard.test.d.ts +0 -2
  910. package/dist/test/tool-wrapper-prose-guard.test.d.ts.map +0 -1
  911. package/dist/test/tool-wrapper-prose-guard.test.js +0 -34
  912. package/dist/test/tool-wrapper-prose-guard.test.js.map +0 -1
  913. package/dist/test/tools-observe-parser-conformance.test.d.ts +0 -2
  914. package/dist/test/tools-observe-parser-conformance.test.d.ts.map +0 -1
  915. package/dist/test/tools-observe-parser-conformance.test.js +0 -129
  916. package/dist/test/tools-observe-parser-conformance.test.js.map +0 -1
  917. package/dist/test/trunk-channel-routing.test.d.ts +0 -19
  918. package/dist/test/trunk-channel-routing.test.d.ts.map +0 -1
  919. package/dist/test/trunk-channel-routing.test.js +0 -652
  920. package/dist/test/trunk-channel-routing.test.js.map +0 -1
  921. package/dist/test/tune-out-e2e.test.d.ts +0 -2
  922. package/dist/test/tune-out-e2e.test.d.ts.map +0 -1
  923. package/dist/test/tune-out-e2e.test.js +0 -142
  924. package/dist/test/tune-out-e2e.test.js.map +0 -1
  925. package/dist/test/tune-out-gate-composition.test.d.ts +0 -2
  926. package/dist/test/tune-out-gate-composition.test.d.ts.map +0 -1
  927. package/dist/test/tune-out-gate-composition.test.js +0 -221
  928. package/dist/test/tune-out-gate-composition.test.js.map +0 -1
  929. package/dist/test/tune-out-lifecycle.test.d.ts +0 -2
  930. package/dist/test/tune-out-lifecycle.test.d.ts.map +0 -1
  931. package/dist/test/tune-out-lifecycle.test.js +0 -121
  932. package/dist/test/tune-out-lifecycle.test.js.map +0 -1
  933. package/dist/test/turn-trigger-provenance.test.d.ts +0 -2
  934. package/dist/test/turn-trigger-provenance.test.d.ts.map +0 -1
  935. package/dist/test/turn-trigger-provenance.test.js +0 -137
  936. package/dist/test/turn-trigger-provenance.test.js.map +0 -1
  937. package/dist/test/unconvertible-tool-result.test.d.ts +0 -2
  938. package/dist/test/unconvertible-tool-result.test.d.ts.map +0 -1
  939. package/dist/test/unconvertible-tool-result.test.js +0 -358
  940. package/dist/test/unconvertible-tool-result.test.js.map +0 -1
  941. package/dist/test/unsent-compiles.test.d.ts +0 -2
  942. package/dist/test/unsent-compiles.test.d.ts.map +0 -1
  943. package/dist/test/unsent-compiles.test.js +0 -139
  944. package/dist/test/unsent-compiles.test.js.map +0 -1
  945. package/dist/test/usage-accounting.test.d.ts +0 -2
  946. package/dist/test/usage-accounting.test.d.ts.map +0 -1
  947. package/dist/test/usage-accounting.test.js +0 -13
  948. package/dist/test/usage-accounting.test.js.map +0 -1
  949. package/dist/test/usage-tracker.test.d.ts +0 -2
  950. package/dist/test/usage-tracker.test.d.ts.map +0 -1
  951. package/dist/test/usage-tracker.test.js +0 -129
  952. package/dist/test/usage-tracker.test.js.map +0 -1
  953. package/dist/test/utils-surface.test.d.ts +0 -2
  954. package/dist/test/utils-surface.test.d.ts.map +0 -1
  955. package/dist/test/utils-surface.test.js +0 -145
  956. package/dist/test/utils-surface.test.js.map +0 -1
  957. package/dist/test/workspace-branch-continuation.test.d.ts +0 -15
  958. package/dist/test/workspace-branch-continuation.test.d.ts.map +0 -1
  959. package/dist/test/workspace-branch-continuation.test.js +0 -183
  960. package/dist/test/workspace-branch-continuation.test.js.map +0 -1
  961. package/dist/test/workspace-materialize-freshness.test.d.ts +0 -16
  962. package/dist/test/workspace-materialize-freshness.test.d.ts.map +0 -1
  963. package/dist/test/workspace-materialize-freshness.test.js +0 -252
  964. package/dist/test/workspace-materialize-freshness.test.js.map +0 -1
  965. package/dist/test/workspace-path-containment.test.d.ts +0 -19
  966. package/dist/test/workspace-path-containment.test.d.ts.map +0 -1
  967. package/dist/test/workspace-path-containment.test.js +0 -91
  968. package/dist/test/workspace-path-containment.test.js.map +0 -1
  969. package/dist/test/workspace-read-file-from-disk.test.d.ts +0 -17
  970. package/dist/test/workspace-read-file-from-disk.test.d.ts.map +0 -1
  971. package/dist/test/workspace-read-file-from-disk.test.js +0 -174
  972. package/dist/test/workspace-read-file-from-disk.test.js.map +0 -1
  973. package/dist/test/workspace-read-image.test.d.ts +0 -2
  974. package/dist/test/workspace-read-image.test.d.ts.map +0 -1
  975. package/dist/test/workspace-read-image.test.js +0 -597
  976. package/dist/test/workspace-read-image.test.js.map +0 -1
  977. package/dist/test/workspace-read-text.test.d.ts +0 -2
  978. package/dist/test/workspace-read-text.test.d.ts.map +0 -1
  979. package/dist/test/workspace-read-text.test.js +0 -101
  980. package/dist/test/workspace-read-text.test.js.map +0 -1
  981. package/dist/test/workspace-restore-order.test.d.ts +0 -16
  982. package/dist/test/workspace-restore-order.test.d.ts.map +0 -1
  983. package/dist/test/workspace-restore-order.test.js +0 -140
  984. package/dist/test/workspace-restore-order.test.js.map +0 -1
  985. package/dist/test/workspace-three-way.test.d.ts +0 -12
  986. package/dist/test/workspace-three-way.test.d.ts.map +0 -1
  987. package/dist/test/workspace-three-way.test.js +0 -4094
  988. package/dist/test/workspace-three-way.test.js.map +0 -1
  989. package/dist/test/workspace-watcher.test.d.ts +0 -2
  990. package/dist/test/workspace-watcher.test.d.ts.map +0 -1
  991. package/dist/test/workspace-watcher.test.js +0 -281
  992. package/dist/test/workspace-watcher.test.js.map +0 -1
  993. package/dist/test/xml-round-persist.test.d.ts +0 -19
  994. package/dist/test/xml-round-persist.test.d.ts.map +0 -1
  995. package/dist/test/xml-round-persist.test.js +0 -235
  996. package/dist/test/xml-round-persist.test.js.map +0 -1
  997. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { ContentBlock } from '@animalabs/membrane';
11
+ import type { StoredMessage } from '@animalabs/context-manager';
11
12
  import type {
12
13
  Module,
13
14
  ModuleContext,
@@ -24,11 +25,13 @@ import type {
24
25
  DiscordModuleConfig,
25
26
  DiscordModuleState,
26
27
  ConversationContext,
28
+ DiscordAttachment,
27
29
  DiscordClientInterface,
28
30
  DiscordMessageData,
29
31
  DiscordMessageEvent,
30
32
  DiscordEditEvent,
31
33
  DiscordDeleteEvent,
34
+ HistoryMessage,
32
35
  SendInput,
33
36
  SendDMInput,
34
37
  ReplyInput,
@@ -42,12 +45,16 @@ import type {
42
45
  FetchHistoryInput,
43
46
  } from './types.js';
44
47
  import { formatZonedDateTime, resolveTimeZone } from '../../timezone.js';
48
+ import { logValue } from '../../mcpl/log-value.js';
49
+ import { unknownToolMessage } from '../unknown-tool.js';
50
+ import { DiscordRefusal } from './refusal.js';
45
51
 
46
52
  // Re-export event types for consumers who want to handle them
47
53
  export type { DiscordMessageEvent, DiscordEditEvent, DiscordDeleteEvent } from './types.js';
48
54
 
49
55
  export * from './types.js';
50
56
  export { DiscordJsClient, type DiscordJsClientConfig } from './discord-js-client.js';
57
+ export { DiscordRefusal } from './refusal.js';
51
58
 
52
59
  const DEFAULT_CONFIG: Partial<DiscordModuleConfig> = {
53
60
  handleDMs: true,
@@ -67,6 +74,14 @@ const TYPING_REFRESH_MS = 8_000;
67
74
  // stuck on forever.
68
75
  const TYPING_MAX_MS = 5 * 60_000;
69
76
 
77
+ // The most agents whose latest post the module keeps for their turn's
78
+ // thoughts (lastPosts). Past it, the entry recorded longest ago goes, which
79
+ // bounds the entries of agents that are gone, such as closed conversations'.
80
+ // An entry is wanted only until its turn's speech arrives, so one goes while
81
+ // still wanted only if this many other agents post in that time, and its
82
+ // turn's thoughts then go into no message.
83
+ const MAX_LAST_POSTS = 1_024;
84
+
70
85
  // Trace event types that end an agent's turn (onTrace's contract) — any of
71
86
  // these stops the typing keepalive. A turn's tool-call cycles stay inside its
72
87
  // stream, so they come after them. inference:completed is the normal end.
@@ -95,6 +110,248 @@ const TEXT_FILE_EXTENSIONS = [
95
110
 
96
111
  const CONVERSATION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
97
112
 
113
+ /** A text attachment this many bytes or larger isn't read into the agent's context. */
114
+ const TEXT_FILE_LIMIT_BYTES = 100_000;
115
+
116
+ /** activate's and deactivate's answer before the module has started, or once it has stopped. */
117
+ const NOT_RUNNING = "Speech handling wasn't changed: the Discord module hasn't started on this host, or has stopped. Calling it again won't change that until it runs; ask whoever runs this host if you need Discord.";
118
+
119
+ /** What the agent reads for a text attachment whose contents couldn't be
120
+ * fetched: by an error, or by an answer that wasn't OK. */
121
+ function attachmentNotFetched(filename: string): { type: 'text'; text: string } {
122
+ return {
123
+ type: 'text',
124
+ text: `[Attachment: ${filename} couldn't be fetched, so its text isn't in your context. If you need it, ask the sender in the channel to paste it.]`,
125
+ };
126
+ }
127
+
128
+ /**
129
+ * What the agent reads when a tool stops on an error that isn't a refusal
130
+ * (DiscordRefusal), such as a network failure: its outcome is unknown. A send
131
+ * repeated blindly can post twice, so those say how to check; the other tools
132
+ * are safe to repeat, since a second call changes nothing more.
133
+ */
134
+ function unexpectedFailure(call: ToolCall, error: unknown): string {
135
+ const stopped = `discord--${call.name} stopped on an unexpected error: ${errorText(error)}.`;
136
+ const channelId = (call.input as { channelId?: unknown } | null | undefined)?.channelId;
137
+ switch (call.name) {
138
+ case 'send':
139
+ return `${stopped} Whether the message was sent is unknown: discord--fetch_history with channelId ${String(channelId)} shows it, so check before you send it again.`;
140
+ case 'dm':
141
+ return `${stopped} Whether the message was sent is unknown. If an earlier discord--dm to this user gave you its channelId, discord--fetch_history with it shows whether this one went; without it, sending it again may send it twice.`;
142
+ case 'edit':
143
+ case 'react':
144
+ case 'delete':
145
+ case 'create_thread':
146
+ return `${stopped} Whether it took effect is unknown. Calling it again is safe: if it did, the second call changes nothing more.`;
147
+ case 'list_guilds':
148
+ case 'list_channels':
149
+ case 'fetch_history':
150
+ return `${stopped} It changes nothing on Discord, so you can call it again.`;
151
+ default:
152
+ return `${stopped} Calling it again is safe.`;
153
+ }
154
+ }
155
+
156
+ /** An error's own words, for a sentence about it. */
157
+ function errorText(error: unknown): string {
158
+ return error instanceof Error ? error.message : String(error);
159
+ }
160
+
161
+ /** A count of seconds, for the agent's eyes. */
162
+ function seconds(n: number): string {
163
+ return `${n} second${n === 1 ? '' : 's'}`;
164
+ }
165
+
166
+ /** What a stored message reads when it has no text of its own and nothing the
167
+ * host added, so the agent still sees that a message was there. */
168
+ const EMPTY_MESSAGE = '[Empty message]';
169
+
170
+ /**
171
+ * Metadata on a stored Discord message whose first block is its own text,
172
+ * '' when it has none, and every block after it the host's: images, inlined
173
+ * file text, attachment notes and EMPTY_MESSAGE. An edit replaces content and
174
+ * can't touch metadata, so the mark holds for the row's life, and it tells an
175
+ * edit or a sync which block is Discord's to replace. For the same reason a
176
+ * row stored before the mark never gets it, even once a history sync
177
+ * rebuilds it in this layout. The request builder drops an empty text block,
178
+ * as it does wherever one occurs.
179
+ */
180
+ const TEXT_FIRST = 'discordTextFirst';
181
+
182
+ /** A message's own text as a stored row holds it: '' when it's blank. */
183
+ function ownText(content: string): string {
184
+ return content.trim() ? content : '';
185
+ }
186
+
187
+ const isEmptyMarker = (block: ContentBlock): boolean => block.type === 'text' && block.text === EMPTY_MESSAGE;
188
+ const isBlankText = (block: ContentBlock): boolean => block.type === 'text' && !block.text.trim();
189
+
190
+ /**
191
+ * A text-first row's content: the message's own text, '' when it has none,
192
+ * then `after`, what the host added; EMPTY_MESSAGE when there's neither.
193
+ */
194
+ function textFirst(text: string, after: ContentBlock[] = []): ContentBlock[] {
195
+ return [{ type: 'text', text }, ...(text || after.length > 0 ? after : [{ type: 'text' as const, text: EMPTY_MESSAGE }])];
196
+ }
197
+
198
+ /** A text-first row's content with `text` as the message's own text, in
199
+ * place of its first block, keeping what the host added after it. */
200
+ function withOwnText(content: ContentBlock[], text: string): ContentBlock[] {
201
+ return textFirst(ownText(text), content.slice(1).filter((block) => !isEmptyMarker(block)));
202
+ }
203
+
204
+ /**
205
+ * The content a stored Discord message should hold once Discord's text for it
206
+ * is `discordText`, or null when it holds that already.
207
+ *
208
+ * A text-first row keeps every block the host added and replaces only its own
209
+ * text; EMPTY_MESSAGE goes once there is text, and comes back when nothing is
210
+ * left. A row stored before that layout held its own text first when it had
211
+ * any, but a message without text began with what the host added, which may
212
+ * be text, a file's contents or a note, so its first block can't be told
213
+ * apart. Such a row is left alone unless Discord's text is there and differs
214
+ * from its first block, and then that text takes the first block's place
215
+ * when it's text, and goes before it when it isn't, and EMPTY_MESSAGE goes.
216
+ * So a message without text, stored beginning with a file's contents or a
217
+ * note, loses that block. The history sync, which has the message's
218
+ * attachments, rebuilds such a row instead (needsRebuild).
219
+ */
220
+ function contentForText(stored: StoredMessage, discordText: string): ContentBlock[] | null {
221
+ const text = ownText(discordText);
222
+ const [first, ...rest] = stored.content;
223
+ if (stored.metadata?.[TEXT_FIRST] === true) {
224
+ if (first?.type === 'text' && first.text === text) return null;
225
+ return withOwnText(stored.content, text);
226
+ }
227
+ if (!text || (first?.type === 'text' && first.text === discordText)) return null;
228
+ return [{ type: 'text', text: discordText }, ...(first?.type === 'text' ? rest : stored.content).filter((block) => !isEmptyMarker(block))];
229
+ }
230
+
231
+ /**
232
+ * Whether a stored row lacks or holds more than the live conversion gives
233
+ * Discord's `message` now, its own text aside, so the history sync stores it
234
+ * through that conversion again. The conversion gives each attachment one
235
+ * block: an image, a file's text, or a note when it isn't fetched or isn't
236
+ * read. So a text-first row holds one block per attachment after its text,
237
+ * EMPTY_MESSAGE aside, which the text's rule keeps. A row stored before that
238
+ * layout held its text, when it had any, then those blocks, or EMPTY_MESSAGE
239
+ * alone; one that doesn't was blanked or stripped by an earlier edit or sync,
240
+ * or its message gained or lost text, or lost an attachment. Its first block,
241
+ * when it is the message's own text, counts as that text, even when the text
242
+ * reads as EMPTY_MESSAGE. For a message without text of its own, a first
243
+ * block that's text may be what the host added or a caption the message has
244
+ * since lost, which no count tells apart, so that row is stored again; it
245
+ * then begins with its own '' text. One whose text alone changed is left to
246
+ * the text's rule, which keeps the blocks after it. History without
247
+ * attachments can't be compared, so it rebuilds nothing.
248
+ */
249
+ function needsRebuild(stored: StoredMessage, message: HistoryMessage): boolean {
250
+ if (message.attachments === undefined) return false;
251
+ const attachments = message.attachments.length;
252
+ const held = (blocks: ContentBlock[]) => blocks.filter((block) => !isBlankText(block) && !isEmptyMarker(block)).length;
253
+ if (stored.metadata?.[TEXT_FIRST] === true) return held(stored.content.slice(1)) !== attachments;
254
+ const own = ownText(message.content);
255
+ const [first, ...rest] = stored.content;
256
+ if (!own && first?.type === 'text' && !isBlankText(first) && !isEmptyMarker(first)) return true;
257
+ const expected = (own ? 1 : 0) + attachments;
258
+ const counted = own && first?.type === 'text' && first.text === own ? 1 + held(rest) : held(stored.content);
259
+ return counted !== expected || (expected === 0 && !stored.content.some(isEmptyMarker));
260
+ }
261
+
262
+ /**
263
+ * Whether a row holds non-blank text besides its message's own text now,
264
+ * which may be what a file gave. A marked row whose message's text changed
265
+ * counts its old text here too, which can delay its rebuild until a fetch
266
+ * succeeds but never loses text.
267
+ */
268
+ function holdsOtherText(stored: StoredMessage, message: HistoryMessage): boolean {
269
+ const own = ownText(message.content);
270
+ const [first, ...rest] = stored.content;
271
+ const others = own && first?.type === 'text' && first.text === own ? rest : stored.content;
272
+ return others.some((block) => block.type === 'text' && !isBlankText(block) && !isEmptyMarker(block));
273
+ }
274
+
275
+ /** Whether a message's conversion holds, after the message's own text, the
276
+ * note for an attachment of its whose text wasn't fetched. */
277
+ function holdsNotFetched(content: ContentBlock[], message: HistoryMessage): boolean {
278
+ const notes = new Set((message.attachments ?? []).map((attachment) => attachmentNotFetched(attachment.filename).text));
279
+ return content.slice(1).some((block) => block.type === 'text' && notes.has(block.text));
280
+ }
281
+
282
+ /** A URL without its query or fragment: Discord's CDN signs an attachment's
283
+ * URL afresh in each payload, so only its path names the attachment. */
284
+ function urlPath(url: string): string {
285
+ return url.split(/[?#]/)[0] ?? url;
286
+ }
287
+
288
+ /**
289
+ * Whether a block the host added for an attachment stands for none of
290
+ * `attachments`, matched as the live conversion writes it: an image by its
291
+ * URL, a file's text by its header, and a note by the filename it opens
292
+ * with, as every note for an attachment does, whatever its words after it.
293
+ * A block that stands for no attachment, such as EMPTY_MESSAGE, never does.
294
+ */
295
+ function forGoneAttachment(block: ContentBlock, attachments: readonly DiscordAttachment[]): boolean {
296
+ if (block.type === 'image') {
297
+ const source = (block as { source?: { type?: string; url?: unknown } }).source;
298
+ if (source?.type !== 'url' || typeof source.url !== 'string') return false;
299
+ const path = urlPath(source.url);
300
+ return !attachments.some((attachment) => urlPath(attachment.url) === path);
301
+ }
302
+ if (block.type !== 'text') return false;
303
+ const { text } = block;
304
+ const opening = text.startsWith('\n--- File: ') ? (filename: string) => `\n--- File: ${filename} ---\n`
305
+ : text.startsWith('[Attachment: ') ? (filename: string) => `[Attachment: ${filename} `
306
+ : null;
307
+ return opening !== null && !attachments.some((attachment) => text.startsWith(opening(attachment.filename)));
308
+ }
309
+
310
+ /**
311
+ * A text-first row's content without the blocks the host added for
312
+ * attachments its message no longer has, or `content` itself when none is
313
+ * gone. A block stays while an attachment it may stand for remains, so of two
314
+ * files of one name, removing one keeps both blocks, and a block that names
315
+ * no attachment stays. EMPTY_MESSAGE comes back once nothing is left.
316
+ */
317
+ function withoutGoneAttachments(content: ContentBlock[], attachments: readonly DiscordAttachment[]): ContentBlock[] {
318
+ const [first, ...rest] = content;
319
+ if (first?.type !== 'text') return content;
320
+ const kept = rest.filter((block) => !forGoneAttachment(block, attachments));
321
+ if (kept.length === rest.length) return content;
322
+ return textFirst(first.text, kept);
323
+ }
324
+
325
+ /** The ids to remove for a message's rows: each row once, and a body stored
326
+ * in shards once, since removing one shard takes out its whole body. */
327
+ function oncePerBody(rows: readonly StoredMessage[]): string[] {
328
+ const bodies = new Set<string>();
329
+ const ids: string[] = [];
330
+ for (const row of rows) {
331
+ if (row.bodyGroupId) {
332
+ if (bodies.has(row.bodyGroupId)) continue;
333
+ bodies.add(row.bodyGroupId);
334
+ }
335
+ ids.push(row.id);
336
+ }
337
+ return ids;
338
+ }
339
+
340
+ /** A live create on its way to the store (DiscordModule.creating). */
341
+ interface LiveCreate {
342
+ /** converting: the live path fetches its attachments; queued: its event
343
+ * waits in the queue; writing: onProcess answered it, and each Discord
344
+ * event after it looks for the message's row, and ends the entry once it
345
+ * shows. */
346
+ phase: 'converting' | 'queued' | 'writing';
347
+ /** A delete for the message came before its create was handled, so no row
348
+ * comes, and the entry waits for that delete to be handled. */
349
+ deleted: boolean;
350
+ /** The latest edit's text and attachments, from while it converted. */
351
+ text?: string;
352
+ attachments?: DiscordAttachment[];
353
+ }
354
+
98
355
  /**
99
356
  * Discord module for the agent framework.
100
357
  */
@@ -115,8 +372,41 @@ export class DiscordModule implements Module {
115
372
  // Track current reply context per agent
116
373
  private replyContexts: Map<string, ConversationContext> = new Map();
117
374
 
118
- // Last message sent by a tool (for thought-editing)
119
- private lastSentMessage: { channelId: string; messageId: string } | null = null;
375
+ // Where a tool turn's thoughts go (onAgentSpeech edits them in as a
376
+ // spoiler): each agent's latest message posted in a turn, with that turn's
377
+ // id (notePost). A `send` or `reply` records the message its client call
378
+ // returns, and so does the module's own speech. Thoughts go into it only
379
+ // when their speech ends the same turn, and taking them removes it. So
380
+ // neither another agent's message nor one from an earlier turn takes them,
381
+ // and an attempt in the turn that is refused or throws leaves the turn's own
382
+ // post in place. Turn ids increase, so an earlier turn's post that finishes
383
+ // after a later turn's leaves the later one in place. A DM records nothing,
384
+ // since its channel isn't known here, and neither does a post whose call or
385
+ // speech carries no turn. An entry goes when its thoughts are taken, when
386
+ // the agent's next post replaces it, or past MAX_LAST_POSTS agents.
387
+ private lastPosts: Map<string, { turnId: number; channelId: string; messageId: string }> = new Map();
388
+
389
+ // One set per running history sync: the Discord messages the live path
390
+ // stored, edited or was told were deleted since that sync began. What the
391
+ // live path saw is newer than what the sync fetched, so the sync leaves
392
+ // those messages alone.
393
+ private readonly liveDuringSync = new Set<Set<string>>();
394
+
395
+ // The Discord messages whose live create is on its way to the store: from
396
+ // the moment the live path takes one up, through its conversion and the
397
+ // event queue, until a query shows the row onProcess answered with, stored
398
+ // or held. The framework holds a module's writes while the agent's turn is
399
+ // alive or the host is quiesced, and queryMessages shows a held write's
400
+ // row, marked held, so the next Discord event finds it either way and ends
401
+ // its entry. A history sync stores none of these messages, so no message is
402
+ // stored twice. A delete before the create is handled drops it: nothing is
403
+ // stored and nobody is woken. Any message's entry ends when its delete is
404
+ // handled, since a sync running then skips the message (noteLive) and a
405
+ // later one doesn't fetch it. An edit while it converts is folded into it.
406
+ // A create whose answer the framework fails to write or hold shows no row,
407
+ // and keeps its entry, and the sync off its message, until its delete is
408
+ // handled or the module stops.
409
+ private readonly creating = new Map<string, LiveCreate>();
120
410
 
121
411
  // Typing keepalive: agentName -> { interval, deadline }. Started on
122
412
  // inference:started, stopped on the turn's terminal trace event. Discord's
@@ -165,7 +455,7 @@ export class DiscordModule implements Module {
165
455
 
166
456
  // Set up Discord event handlers
167
457
  this.client.onMessage((message) => this.handleDiscordMessage(message));
168
- this.client.onMessageEdit((id, content) => this.handleDiscordMessageEdit(id, content));
458
+ this.client.onMessageEdit((id, content, attachments) => this.handleDiscordMessageEdit(id, content, attachments));
169
459
  this.client.onMessageDelete((id) => this.handleDiscordMessageDelete(id));
170
460
 
171
461
  // Set up ready handler for history sync on (re)connect
@@ -204,8 +494,14 @@ export class DiscordModule implements Module {
204
494
  }
205
495
  this.typingLoops.clear();
206
496
 
207
- await this.client.disconnect();
497
+ // From here the module writes nothing, though the framework may dispatch
498
+ // to it until stop() resolves: the live path takes nothing up, a create
499
+ // still converting finds the context gone, and onProcess answers nothing.
500
+ // The record of creates on their way goes with the context.
208
501
  this.ctx = null;
502
+ this.creating.clear();
503
+
504
+ await this.client.disconnect();
209
505
  }
210
506
 
211
507
  /**
@@ -395,7 +691,7 @@ export class DiscordModule implements Module {
395
691
  },
396
692
  {
397
693
  name: 'list_channels',
398
- description: 'List all channels in a Discord server',
694
+ description: 'List the channels in a Discord server, with their ids. Threads aren\'t listed.',
399
695
  inputSchema: {
400
696
  type: 'object',
401
697
  properties: {
@@ -406,7 +702,7 @@ export class DiscordModule implements Module {
406
702
  },
407
703
  {
408
704
  name: 'fetch_history',
409
- description: 'Fetch recent message history from a channel',
705
+ description: 'Fetch recent message history from a channel. A message with attachments lists each one\'s filename, type and size; their contents aren\'t included.',
410
706
  inputSchema: {
411
707
  type: 'object',
412
708
  properties: {
@@ -424,11 +720,11 @@ export class DiscordModule implements Module {
424
720
  try {
425
721
  switch (call.name) {
426
722
  case 'send':
427
- return await this.handleSend(call.input as SendInput);
723
+ return await this.handleSend(call.input as SendInput, call);
428
724
  case 'dm':
429
725
  return await this.handleDM(call.input as SendDMInput);
430
726
  case 'reply':
431
- return await this.handleReply(call.input as ReplyInput);
727
+ return await this.handleReply(call.input as ReplyInput, call);
432
728
  case 'react':
433
729
  return await this.handleReact(call.input as ReactInput);
434
730
  case 'edit':
@@ -440,7 +736,8 @@ export class DiscordModule implements Module {
440
736
  case 'set_reply_context':
441
737
  return await this.handleSetReplyContext(call.input as SetReplyContextInput);
442
738
  case 'activate':
443
- return await this.handleActivate(call.input as { additive?: boolean });
739
+ // A call with no input at all activates with the defaults.
740
+ return await this.handleActivate((call.input ?? {}) as { additive?: boolean });
444
741
  case 'deactivate':
445
742
  return await this.handleDeactivate();
446
743
  case 'list_guilds':
@@ -450,21 +747,36 @@ export class DiscordModule implements Module {
450
747
  case 'fetch_history':
451
748
  return await this.handleFetchHistory(call.input as FetchHistoryInput);
452
749
  default:
453
- return { success: false, error: `Unknown tool: ${call.name}`, isError: true };
750
+ return { success: false, error: unknownToolMessage(this.name, call.name, this.getTools()), isError: true };
454
751
  }
455
752
  } catch (error) {
753
+ // A refusal says what was refused and what to do instead; anything else
754
+ // is unexpected, and its outcome is worded by the tool.
456
755
  return {
457
756
  success: false,
458
- error: error instanceof Error ? error.message : String(error),
757
+ error: error instanceof DiscordRefusal ? error.message : unexpectedFailure(call, error),
459
758
  isError: true,
460
759
  };
461
760
  }
462
761
  }
463
762
 
464
763
  async onProcess(event: ProcessEvent, state: ProcessState): Promise<EventResponse> {
764
+ // A stopped module answers nothing (stop()).
765
+ if (!this.ctx) return {};
766
+
767
+ // Each Discord event ends the entries whose row a query shows now, stored
768
+ // or held.
769
+ if (event.type.startsWith('discord:')) {
770
+ for (const [id, live] of this.creating) if (live.phase === 'writing' && this.storedRow(id)) this.creating.delete(id);
771
+ }
772
+
465
773
  // Handle new Discord message
466
774
  if (event.type === 'discord:message') {
467
775
  const msg = event as DiscordMessageEvent;
776
+ const live = this.creating.get(msg.discordMessageId);
777
+ // Deleted while its create waited in the queue: nothing is stored, and
778
+ // nobody is woken by a message its author took back.
779
+ if (live?.deleted) return {};
468
780
  const currentState = state.getState<DiscordModuleState>() ?? {
469
781
  connectedGuilds: [],
470
782
  conversations: {},
@@ -509,22 +821,34 @@ export class DiscordModule implements Module {
509
821
  });
510
822
  }
511
823
 
512
- // Build content blocks from message and attachments
513
- const contentBlocks = msg.contentBlocks ?? [{ type: 'text' as const, text: msg.content }];
824
+ // Built from the message and its attachments, or from its text alone
825
+ // when an event carries no blocks; text first, as TEXT_FIRST marks.
826
+ const contentBlocks = msg.contentBlocks ?? textFirst(ownText(msg.content));
827
+ this.noteLive(msg.discordMessageId);
828
+
829
+ // A message a history sync wrote first, stored or held, keeps that row:
830
+ // the create adds none, and still sets the reply context and asks for
831
+ // inference, since the message arrived live. Either way the entry waits
832
+ // until a Discord event finds the message's row.
833
+ const stored = this.storedRow(msg.discordMessageId);
834
+ if (live) live.phase = 'writing';
514
835
 
515
836
  // Return declarative response - framework applies all writes atomically
516
837
  return {
517
- addMessages: [
518
- {
519
- participant: msg.authorName,
520
- content: contentBlocks,
521
- metadata: {
522
- external: { source: 'discord', id: msg.discordMessageId },
523
- channelId: msg.channelId,
524
- timestamp: msg.timestamp,
838
+ ...(stored ? {} : {
839
+ addMessages: [
840
+ {
841
+ participant: msg.authorName,
842
+ content: contentBlocks,
843
+ metadata: {
844
+ external: { source: 'discord', id: msg.discordMessageId },
845
+ channelId: msg.channelId,
846
+ timestamp: msg.timestamp,
847
+ [TEXT_FIRST]: true,
848
+ },
525
849
  },
526
- },
527
- ],
850
+ ],
851
+ }),
528
852
  stateUpdate: newState,
529
853
  requestInference: msg.shouldTriggerInference ?? true,
530
854
  };
@@ -533,29 +857,49 @@ export class DiscordModule implements Module {
533
857
  // Handle Discord message edit
534
858
  if (event.type === 'discord:edit') {
535
859
  const edit = event as DiscordEditEvent;
536
- const internalId = state.findMessageByExternalId('discord', edit.discordMessageId);
537
- if (!internalId) {
860
+ // The message's row, not only its id: an edit changes the message's own
861
+ // text and keeps what the host added beside it, but for the blocks of
862
+ // attachments the edit took away, when the client passed the edit's
863
+ // attachments and the row is text-first.
864
+ // An edit that comes while the live path converts the message's create
865
+ // is folded into that create (handleDiscordMessageEdit). Any other edit
866
+ // to a message with no row, stored or held, is dropped: a history sync
867
+ // running now stores the text it fetched, and the next sync's text rule
868
+ // changes it. A row stored in shards can't be edited one shard at a
869
+ // time, so it keeps its text, and the host's log says so. Only an edit
870
+ // that writes marks the message live, so an update that changes nothing
871
+ // leaves a running sync free to rewrite the row.
872
+ const stored = this.storedRow(edit.discordMessageId);
873
+ if (!stored) return {};
874
+ if (stored.bodyGroupId) {
875
+ console.warn(`Discord: An update to message ${edit.discordMessageId} wasn't applied: it's stored in shards, which can't be edited, so if its text changed, the agent still has the earlier text`);
538
876
  return {};
539
877
  }
540
- return {
541
- editMessages: [
542
- {
543
- messageId: internalId,
544
- content: [{ type: 'text', text: edit.newContent }],
545
- },
546
- ],
547
- };
878
+ const texted = contentForText(stored, edit.newContent);
879
+ const content = edit.attachments && stored.metadata?.[TEXT_FIRST] === true
880
+ ? withoutGoneAttachments(texted ?? stored.content, edit.attachments)
881
+ : texted;
882
+ if (!content || content === stored.content) return {};
883
+ this.noteLive(edit.discordMessageId);
884
+ return { editMessages: [{ messageId: stored.id, content }] };
548
885
  }
549
886
 
550
887
  // Handle Discord message delete
551
888
  if (event.type === 'discord:delete') {
552
889
  const del = event as DiscordDeleteEvent;
553
- const internalId = state.findMessageByExternalId('discord', del.discordMessageId);
554
- if (!internalId) {
890
+ // Every row of the message goes, so one stored twice doesn't outlive
891
+ // its delete as a copy. A row stored in shards is removed whole: the
892
+ // framework takes out the body a shard belongs to. The message is the
893
+ // live path's even when no row is found, so a running sync doesn't
894
+ // store what its fetch may still hold, and that ends its entry.
895
+ this.noteLive(del.discordMessageId);
896
+ this.creating.delete(del.discordMessageId);
897
+ const rows = this.storedRows(del.discordMessageId);
898
+ if (rows.length === 0) {
555
899
  return {};
556
900
  }
557
901
  return {
558
- removeMessages: [internalId],
902
+ removeMessages: oncePerBody(rows),
559
903
  };
560
904
  }
561
905
 
@@ -570,13 +914,15 @@ export class DiscordModule implements Module {
570
914
  content: ContentBlock[],
571
915
  context: SpeechContext
572
916
  ): Promise<void> {
573
- // If thoughts are present and we have a recently sent message, edit it
574
- // to prepend the thoughts as spoiler text.
575
- if (context.thoughts && context.thoughts.length > 0 && this.lastSentMessage) {
917
+ // If thoughts are present and this agent posted a message in the turn
918
+ // they end (lastPosts), edit it to prepend the thoughts as spoiler text.
919
+ const post = this.lastPosts.get(agentName);
920
+ if (context.thoughts && context.thoughts.length > 0 && post && post.turnId === context.turnId) {
921
+ this.lastPosts.delete(agentName);
576
922
  const thoughtText = this.extractText(context.thoughts);
577
923
  if (thoughtText) {
578
924
  try {
579
- const { channelId, messageId } = this.lastSentMessage;
925
+ const { channelId, messageId } = post;
580
926
  // Fetch the current message content via the client isn't available,
581
927
  // but we know the tool just sent it — reconstruct by editing with
582
928
  // spoiler-wrapped thoughts prepended.
@@ -588,7 +934,6 @@ export class DiscordModule implements Module {
588
934
  console.warn('Discord: Failed to edit thoughts into message:', err);
589
935
  }
590
936
  }
591
- this.lastSentMessage = null;
592
937
  }
593
938
 
594
939
  // Get reply context for this agent
@@ -606,7 +951,7 @@ export class DiscordModule implements Module {
606
951
  }
607
952
 
608
953
  // Check rate limit
609
- if (!this.checkRateLimit(replyCtx.channelId)) {
954
+ if (this.rateLimitWait(replyCtx.channelId) > 0) {
610
955
  console.warn(`Discord: Rate limited for channel ${replyCtx.channelId}`);
611
956
  return;
612
957
  }
@@ -617,8 +962,8 @@ export class DiscordModule implements Module {
617
962
  replyTo: replyCtx.replyToMessageId,
618
963
  });
619
964
 
620
- // Track for thought-editing (in case of chained speech)
621
- this.lastSentMessage = { channelId: targetChannel, messageId: result.messageId };
965
+ // Track for thought-editing (in case of chained speech in the same turn)
966
+ this.notePost(agentName, context.turnId, targetChannel, result.messageId);
622
967
 
623
968
  // Clear reply-to after sending (don't keep replying to same message)
624
969
  replyCtx.replyToMessageId = undefined;
@@ -723,136 +1068,201 @@ export class DiscordModule implements Module {
723
1068
 
724
1069
  /**
725
1070
  * Sync history for a specific channel - compare with Context Manager and update.
726
- * Uses queryMessages() to enumerate stored messages for proper edit/delete detection.
1071
+ * Uses queryMessages() to enumerate the channel's rows, a held write's among
1072
+ * them, for proper edit/delete detection.
1073
+ *
1074
+ * The live path can store, edit or remove a row whenever the sync awaits.
1075
+ * So the sync fetches history and runs the live conversion it needs first,
1076
+ * then reads the store again and writes with no await between that read
1077
+ * and its writes. It leaves alone any message the live path stored,
1078
+ * changed or was told was deleted while it ran (liveDuringSync), an update
1079
+ * that changed nothing aside. A live edit to a message nobody had stored
1080
+ * yet is dropped, so a message the sync stores holds the text it fetched
1081
+ * until the next sync's text rule changes it. The framework writes an
1082
+ * event's response only once every module has processed the event, so an
1083
+ * event processed before the sync began can still be written after the
1084
+ * sync's own writes, which this doesn't cover.
727
1085
  */
728
1086
  private async syncChannelHistory(channelId: string): Promise<{
729
1087
  newMessages: number;
730
1088
  editedMessages: number;
731
1089
  deletedMessages: number;
732
1090
  }> {
733
- if (!this.ctx) {
734
- return { newMessages: 0, editedMessages: 0, deletedMessages: 0 };
1091
+ const unchanged = { newMessages: 0, editedMessages: 0, deletedMessages: 0 };
1092
+ const ctx = this.ctx;
1093
+ if (!ctx) {
1094
+ return unchanged;
735
1095
  }
736
-
737
- const scrollback = this.config.historyScrollback ?? 50;
738
-
739
- // Fetch recent history from Discord
740
- const discordMessages = await this.client.fetchHistory(channelId, { limit: scrollback });
741
-
742
- // Build a map of Discord message ID -> content for comparison
743
- const discordMessageMap = new Map<string, { content: string; authorName: string; timestamp: Date }>();
744
- for (const msg of discordMessages) {
745
- discordMessageMap.set(msg.id, {
746
- content: msg.content,
747
- authorName: msg.authorName,
748
- timestamp: msg.timestamp,
749
- });
750
- }
751
-
752
- // Query Context Manager for all Discord messages from this channel
753
- const { messages: storedMessages } = this.ctx.queryMessages({
754
- source: 'discord',
755
- metadata: { channelId },
756
- });
757
-
758
- // Build a map of stored message external IDs -> internal data
759
- const storedMessageMap = new Map<string, { internalId: string; content: string; sourceTimestamp?: number }>();
760
- for (const msg of storedMessages) {
761
- const external = msg.metadata?.external as { id?: string } | undefined;
762
- if (external?.id) {
763
- // Extract text content for comparison
764
- const textContent = msg.content
765
- .filter((block): block is { type: 'text'; text: string } => block.type === 'text')
766
- .map((block) => block.text)
767
- .join('\n');
768
- storedMessageMap.set(external.id, {
769
- internalId: msg.id,
770
- content: textContent,
771
- sourceTimestamp: typeof msg.metadata?.timestamp === 'number'
772
- ? msg.metadata.timestamp
773
- : undefined,
774
- });
1096
+ const live = new Set<string>();
1097
+ this.liveDuringSync.add(live);
1098
+ try {
1099
+ const scrollback = this.config.historyScrollback ?? 50;
1100
+ // The rows stored as the sync begins. One that is gone by the time it
1101
+ // writes was removed meanwhile, and isn't the sync's to bring back.
1102
+ const before = this.storedByDiscordId(ctx, channelId);
1103
+
1104
+ // Fetch recent history from Discord. If the module stopped or restarted
1105
+ // meanwhile, this sync's context is gone and it writes nothing.
1106
+ const discordMessages = await this.client.fetchHistory(channelId, { limit: scrollback });
1107
+ if (this.ctx !== ctx) return unchanged;
1108
+
1109
+ // Discord's ids, for delete detection inside the fetched window.
1110
+ const discordIds = new Set(discordMessages.map((msg) => msg.id));
1111
+
1112
+ // Discord history is newest-first, while addMessage appends to the stored
1113
+ // conversation. Process oldest-first so missed messages retain chronology.
1114
+ const chronologicalMessages = [...discordMessages].reverse();
1115
+
1116
+ // The live conversion, which fetches attachments, for a message the
1117
+ // store lacks and for a row that doesn't hold what the conversion gives
1118
+ // the message now (needsRebuild). A row stored in shards can't be
1119
+ // edited one shard at a time, so it's left as it is.
1120
+ const converted = new Map<string, ContentBlock[]>();
1121
+ for (const discordMsg of chronologicalMessages) {
1122
+ const stored = before.get(discordMsg.id)?.[0];
1123
+ if (stored && (stored.bodyGroupId || !needsRebuild(stored, discordMsg))) continue;
1124
+ const content = await this.convertMessageToContent(discordMsg);
1125
+ if (this.ctx !== ctx) return unchanged;
1126
+ // A failed fetch never costs a row the text it holds: when a file
1127
+ // wasn't fetched, a row holding other text, which may be that file's,
1128
+ // keeps what it holds, and the next sync tries again.
1129
+ if (stored && holdsNotFetched(content, discordMsg) && holdsOtherText(stored, discordMsg)) continue;
1130
+ converted.set(discordMsg.id, content);
775
1131
  }
776
- }
777
1132
 
778
- let newMessages = 0;
779
- let editedMessages = 0;
780
- let deletedMessages = 0;
781
-
782
- // Discord history is newest-first, while addMessage appends to the stored
783
- // conversation. Process oldest-first so missed messages retain chronology.
784
- const chronologicalMessages = [...discordMessages].reverse();
785
- for (const discordMsg of chronologicalMessages) {
786
- const stored = storedMessageMap.get(discordMsg.id);
787
-
788
- if (!stored) {
789
- // New message we haven't seen
790
- // For bot's own messages (from previous sessions), use the agent name
791
- // This ensures continuity of conversation history
792
- const isBotMessage = discordMsg.authorId === this.state.botUserId;
793
- const agentName = this.ctx.getAgents()[0]?.name;
794
- const participantName = isBotMessage
795
- ? (agentName ?? discordMsg.authorName)
796
- : discordMsg.authorName;
797
-
798
- this.ctx.addMessage(
799
- participantName,
800
- [{ type: 'text', text: discordMsg.content }],
801
- {
802
- external: { source: 'discord', id: discordMsg.id },
803
- channelId,
804
- timestamp: discordMsg.timestamp.getTime(),
805
- isOwnMessage: isBotMessage, // Mark as our own for reference
1133
+ // From here on nothing awaits, so the rows read now are the rows written.
1134
+ const storedMessageMap = this.storedByDiscordId(ctx, channelId);
1135
+ let newMessages = 0;
1136
+ let editedMessages = 0;
1137
+ let deletedMessages = 0;
1138
+ for (const discordMsg of chronologicalMessages) {
1139
+ if (live.has(discordMsg.id)) continue;
1140
+ const stored = storedMessageMap.get(discordMsg.id)?.[0];
1141
+
1142
+ if (!stored) {
1143
+ // A message we haven't stored. One that was stored when the sync
1144
+ // began, and isn't now, was removed meanwhile: not the sync's to
1145
+ // bring back. One the live path is storing (creating) is the live
1146
+ // path's, so it isn't stored twice.
1147
+ const content = converted.get(discordMsg.id);
1148
+ if (!content || before.has(discordMsg.id) || this.creating.has(discordMsg.id)) continue;
1149
+ // For bot's own messages (from previous sessions), use the agent name
1150
+ // This ensures continuity of conversation history
1151
+ const isBotMessage = discordMsg.authorId === this.state.botUserId;
1152
+ const agentName = ctx.getAgents()[0]?.name;
1153
+ const participantName = isBotMessage
1154
+ ? (agentName ?? discordMsg.authorName)
1155
+ : discordMsg.authorName;
1156
+ ctx.addMessage(
1157
+ participantName,
1158
+ content,
1159
+ {
1160
+ external: { source: 'discord', id: discordMsg.id },
1161
+ channelId,
1162
+ timestamp: discordMsg.timestamp.getTime(),
1163
+ isOwnMessage: isBotMessage, // Mark as our own for reference
1164
+ [TEXT_FIRST]: true,
1165
+ }
1166
+ );
1167
+ newMessages++;
1168
+ } else if (!stored.bodyGroupId) {
1169
+ // We have this message - check whether it changed (offline edit detection)
1170
+ const content = needsRebuild(stored, discordMsg)
1171
+ ? converted.get(discordMsg.id)
1172
+ : contentForText(stored, discordMsg.content);
1173
+ if (content) {
1174
+ ctx.editMessage(stored.id, content);
1175
+ editedMessages++;
806
1176
  }
1177
+ }
1178
+ }
1179
+
1180
+ // Check for deletes only inside the history we actually fetched. A short
1181
+ // response covers complete available history; a full page has an older
1182
+ // boundary whose tied millisecond must be preserved.
1183
+ const fetchedCompleteHistory = discordMessages.length < scrollback;
1184
+ const oldestFetchedTimestamp = discordMessages.length > 0
1185
+ ? Math.min(...discordMessages.map((msg) => msg.timestamp.getTime()))
1186
+ : undefined;
1187
+ for (const [externalId, rows] of storedMessageMap) {
1188
+ const stored = rows[0]!;
1189
+ const sourceTimestamp = typeof stored.metadata?.timestamp === 'number'
1190
+ ? stored.metadata.timestamp
1191
+ : undefined;
1192
+ const isInsideFetchedWindow = fetchedCompleteHistory || (
1193
+ sourceTimestamp !== undefined &&
1194
+ oldestFetchedTimestamp !== undefined &&
1195
+ // Discord timestamps are millisecond-precise, so an older message just
1196
+ // beyond the page can tie the oldest fetched timestamp. Preserve ties
1197
+ // rather than guessing that an unseen boundary message was deleted.
1198
+ sourceTimestamp > oldestFetchedTimestamp
807
1199
  );
808
- newMessages++;
809
- } else {
810
- // We have this message - check if content changed (offline edit detection)
811
- if (stored.content !== discordMsg.content) {
812
- this.ctx.editMessage(stored.internalId, [
813
- { type: 'text', text: discordMsg.content },
814
- ]);
815
- editedMessages++;
1200
+ if (isInsideFetchedWindow && !discordIds.has(externalId) && !live.has(externalId)) {
1201
+ // Message exists in our store but not in Discord - it was deleted
1202
+ // Note: This only catches deletes within the scrollback window.
1203
+ // A message the live path wrote while the sync ran is newer than
1204
+ // the fetch, so it stays. Every row of the message goes, and one
1205
+ // shard's id takes out a body whole.
1206
+ for (const id of oncePerBody(rows)) ctx.removeMessage(id);
1207
+ deletedMessages++;
816
1208
  }
817
1209
  }
818
- }
819
1210
 
820
- // Check for deletes only inside the history we actually fetched. A short
821
- // response covers complete available history; a full page has an older
822
- // boundary whose tied millisecond must be preserved.
823
- const fetchedCompleteHistory = discordMessages.length < scrollback;
824
- const oldestFetchedTimestamp = discordMessages.length > 0
825
- ? Math.min(...discordMessages.map((msg) => msg.timestamp.getTime()))
826
- : undefined;
827
- for (const [externalId, stored] of storedMessageMap) {
828
- const isInsideFetchedWindow = fetchedCompleteHistory || (
829
- stored.sourceTimestamp !== undefined &&
830
- oldestFetchedTimestamp !== undefined &&
831
- // Discord timestamps are millisecond-precise, so an older message just
832
- // beyond the page can tie the oldest fetched timestamp. Preserve ties
833
- // rather than guessing that an unseen boundary message was deleted.
834
- stored.sourceTimestamp > oldestFetchedTimestamp
835
- );
836
- if (isInsideFetchedWindow && !discordMessageMap.has(externalId)) {
837
- // Message exists in our store but not in Discord - it was deleted
838
- // Note: This only catches deletes within the scrollback window
839
- this.ctx.removeMessage(stored.internalId);
840
- deletedMessages++;
1211
+ // Update last read
1212
+ if (discordMessages.length > 0) {
1213
+ // Messages are typically newest first
1214
+ const newestId = discordMessages[0].id;
1215
+ this.state.lastReadMessageId[channelId] = newestId;
841
1216
  }
1217
+
1218
+ return { newMessages, editedMessages, deletedMessages };
1219
+ } finally {
1220
+ this.liveDuringSync.delete(live);
842
1221
  }
1222
+ }
843
1223
 
844
- // Update last read
845
- if (discordMessages.length > 0) {
846
- // Messages are typically newest first
847
- const newestId = discordMessages[0].id;
848
- this.state.lastReadMessageId[channelId] = newestId;
1224
+ /** A channel's stored Discord rows by their Discord message id, whole: an
1225
+ * edit replaces a row's own text and keeps what the host added beside it.
1226
+ * Each id's rows are in store order. A message stored twice holds two,
1227
+ * of which a sync acts on the first, as the live path does (storedRow),
1228
+ * and a delete takes both. A write the framework holds is a row too
1229
+ * (storedRows). */
1230
+ private storedByDiscordId(ctx: ModuleContext, channelId: string): Map<string, StoredMessage[]> {
1231
+ const rows = new Map<string, StoredMessage[]>();
1232
+ for (const msg of ctx.queryMessages({ source: 'discord', metadata: { channelId } }).messages) {
1233
+ const external = msg.metadata?.external as { id?: string } | undefined;
1234
+ if (!external?.id) continue;
1235
+ const held = rows.get(external.id);
1236
+ if (held) held.push(msg);
1237
+ else rows.set(external.id, [msg]);
849
1238
  }
1239
+ return rows;
1240
+ }
1241
+
1242
+ /** The stored rows of a Discord message, by its id in the row's metadata,
1243
+ * in store order. A write the framework holds comes after them as a held
1244
+ * row, which an edit or removal reaches as it does a stored one. The
1245
+ * module context's findMessageByExternalId holds only rows the module
1246
+ * stored through its context, not those a process response added, so it
1247
+ * misses what the live path stored. */
1248
+ private storedRows(discordMessageId: string): StoredMessage[] {
1249
+ return this.ctx?.queryMessages({ source: 'discord', externalIds: [discordMessageId] }).messages ?? [];
1250
+ }
850
1251
 
851
- return { newMessages, editedMessages, deletedMessages };
1252
+ /** The row an edit or a create acts on: a message's first row, a stored
1253
+ * one before a held one (storedRows). */
1254
+ private storedRow(discordMessageId: string): StoredMessage | undefined {
1255
+ return this.storedRows(discordMessageId)[0];
1256
+ }
1257
+
1258
+ /** Marks a Discord message as the live path's for every history sync running now. */
1259
+ private noteLive(discordMessageId: string): void {
1260
+ for (const live of this.liveDuringSync) live.add(discordMessageId);
852
1261
  }
853
1262
 
854
1263
  private async handleDiscordMessage(message: DiscordMessageData): Promise<void> {
855
- if (!this.ctx) return;
1264
+ const ctx = this.ctx;
1265
+ if (!ctx) return;
856
1266
 
857
1267
  // ALWAYS ignore our own messages to prevent feedback loops
858
1268
  if (this.state.botUserId && message.authorId === this.state.botUserId) {
@@ -887,39 +1297,65 @@ export class DiscordModule implements Module {
887
1297
  // Determine if this message should trigger inference
888
1298
  const shouldTrigger = this.shouldTriggerInference(message, isDM);
889
1299
 
1300
+ // From here the message is the live path's to store (creating). A second
1301
+ // delivery of a message whose create is already on its way is ignored.
1302
+ if (this.creating.has(message.id)) return;
1303
+ const live: LiveCreate = { phase: 'converting', deleted: false };
1304
+ this.creating.set(message.id, live);
1305
+ const drop = () => { if (this.creating.get(message.id) === live) this.creating.delete(message.id); };
1306
+
890
1307
  // Convert message content including attachments
891
- const contentBlocks = await this.convertMessageToContent(message);
1308
+ let contentBlocks: ContentBlock[];
1309
+ try {
1310
+ contentBlocks = await this.convertMessageToContent(message);
1311
+ } catch (error) {
1312
+ drop();
1313
+ throw error;
1314
+ }
1315
+ // A module stopped or started again meanwhile writes nothing.
1316
+ if (this.ctx !== ctx) {
1317
+ drop();
1318
+ return;
1319
+ }
1320
+ // A delete that came meanwhile drops the create: nothing is stored,
1321
+ // nobody is woken. Its entry stays until that delete is handled, so no
1322
+ // sync stores the message before then.
1323
+ if (live.deleted) return;
1324
+ // An edit that came meanwhile: its text, and the blocks of the
1325
+ // attachments it left.
1326
+ if (live.text !== undefined) contentBlocks = withOwnText(contentBlocks, live.text);
1327
+ if (live.attachments) contentBlocks = withoutGoneAttachments(contentBlocks, live.attachments);
1328
+ live.phase = 'queued';
892
1329
 
893
1330
  // Only queue the event - all state writes happen in onProcess()
894
- this.ctx.pushEvent({
1331
+ ctx.pushEvent({
895
1332
  type: 'discord:message',
896
1333
  discordMessageId: message.id,
897
1334
  channelId: message.channelId,
898
1335
  guildId: message.guildId,
899
1336
  authorId: message.authorId,
900
1337
  authorName: message.authorName,
901
- content: message.content,
1338
+ content: live.text ?? message.content,
902
1339
  contentBlocks,
903
1340
  timestamp: message.timestamp.getTime(),
904
- attachments: message.attachments,
1341
+ attachments: live.attachments ?? message.attachments,
905
1342
  shouldTriggerInference: shouldTrigger,
906
1343
  } as DiscordMessageEvent);
907
1344
  }
908
1345
 
909
1346
  /**
910
- * Convert Discord message to ContentBlock array.
911
- * Includes text content and any supported attachments (images, text files).
1347
+ * Convert a Discord message, live or from history, to ContentBlock array:
1348
+ * its own text first, '' when it has none, then any supported attachments
1349
+ * (images, text files) and notes for the rest, as TEXT_FIRST marks.
912
1350
  */
913
- private async convertMessageToContent(message: DiscordMessageData): Promise<ContentBlock[]> {
1351
+ private async convertMessageToContent(
1352
+ message: { id: string; content: string; attachments?: readonly DiscordAttachment[] },
1353
+ ): Promise<ContentBlock[]> {
1354
+ // What the host adds after the message's own text: one block per attachment.
914
1355
  const blocks: ContentBlock[] = [];
915
1356
 
916
- // Add text content if present
917
- if (message.content.trim()) {
918
- blocks.push({ type: 'text', text: message.content });
919
- }
920
-
921
1357
  // Process attachments
922
- for (const attachment of message.attachments) {
1358
+ for (const attachment of message.attachments ?? []) {
923
1359
  const contentType = attachment.contentType?.toLowerCase() ?? '';
924
1360
  const filename = attachment.filename.toLowerCase();
925
1361
 
@@ -937,7 +1373,15 @@ export class DiscordModule implements Module {
937
1373
 
938
1374
  // Check for text files - fetch and include content
939
1375
  const isTextFile = TEXT_FILE_EXTENSIONS.some(ext => filename.endsWith(ext));
940
- if (isTextFile && attachment.size && attachment.size < 100_000) { // < 100KB
1376
+ if (isTextFile && attachment.size >= TEXT_FILE_LIMIT_BYTES) {
1377
+ blocks.push({
1378
+ type: 'text',
1379
+ text: `[Attachment: ${attachment.filename} (${attachment.size.toLocaleString('en-US')} bytes): this host reads text files under ${TEXT_FILE_LIMIT_BYTES.toLocaleString('en-US')} bytes into your context, so its text isn't here. If you need it, ask the sender to paste the part that matters.]`,
1380
+ });
1381
+ continue;
1382
+ }
1383
+ // A text file of size 0 gets the note below, as before.
1384
+ if (isTextFile && attachment.size) {
941
1385
  try {
942
1386
  const response = await fetch(attachment.url);
943
1387
  if (response.ok) {
@@ -946,13 +1390,15 @@ export class DiscordModule implements Module {
946
1390
  type: 'text',
947
1391
  text: `\n--- File: ${attachment.filename} ---\n${textContent}\n--- End of ${attachment.filename} ---\n`,
948
1392
  });
1393
+ } else {
1394
+ // An answer whose body is never read holds its connection.
1395
+ await response.body?.cancel().catch(() => {});
1396
+ console.warn(`Discord: Attachment ${logValue(attachment.filename)} in message ${message.id} wasn't fetched: HTTP ${response.status}`);
1397
+ blocks.push(attachmentNotFetched(attachment.filename));
949
1398
  }
950
1399
  } catch (error) {
951
- // If fetch fails, just note the attachment
952
- blocks.push({
953
- type: 'text',
954
- text: `[Attachment: ${attachment.filename} (failed to fetch)]`,
955
- });
1400
+ console.warn(`Discord: Attachment ${logValue(attachment.filename)} in message ${message.id} wasn't fetched:`, error);
1401
+ blocks.push(attachmentNotFetched(attachment.filename));
956
1402
  }
957
1403
  continue;
958
1404
  }
@@ -960,16 +1406,11 @@ export class DiscordModule implements Module {
960
1406
  // For unsupported attachments, add a note
961
1407
  blocks.push({
962
1408
  type: 'text',
963
- text: `[Attachment: ${attachment.filename} (${contentType || 'unknown type'})]`,
1409
+ text: `[Attachment: ${attachment.filename} (${contentType || 'unknown type'}): this host didn't read its contents in, so they aren't in your context. If you need them, ask the sender for them in a form you can read, such as pasted text.]`,
964
1410
  });
965
1411
  }
966
1412
 
967
- // Ensure we have at least empty text if nothing else
968
- if (blocks.length === 0) {
969
- blocks.push({ type: 'text', text: '[Empty message]' });
970
- }
971
-
972
- return blocks;
1413
+ return textFirst(ownText(message.content), blocks);
973
1414
  }
974
1415
 
975
1416
  /**
@@ -1012,20 +1453,36 @@ export class DiscordModule implements Module {
1012
1453
  }
1013
1454
  }
1014
1455
 
1015
- private handleDiscordMessageEdit(messageId: string, newContent: string): void {
1456
+ private handleDiscordMessageEdit(messageId: string, newContent: string, attachments?: DiscordAttachment[]): void {
1016
1457
  if (!this.ctx) return;
1017
1458
 
1459
+ // An edit while the message's create converts is folded into that
1460
+ // create, which would otherwise store the text it was sent with; one that
1461
+ // comes after follows the create through the queue. Its event still goes,
1462
+ // for a row a history sync stored first.
1463
+ const live = this.creating.get(messageId);
1464
+ if (live) {
1465
+ live.text = newContent;
1466
+ if (attachments) live.attachments = attachments;
1467
+ }
1468
+
1018
1469
  // Only queue the event - state writes happen in onProcess()
1019
1470
  this.ctx.pushEvent({
1020
1471
  type: 'discord:edit',
1021
1472
  discordMessageId: messageId,
1022
1473
  newContent,
1474
+ ...(attachments ? { attachments } : {}),
1023
1475
  });
1024
1476
  }
1025
1477
 
1026
1478
  private handleDiscordMessageDelete(messageId: string): void {
1027
1479
  if (!this.ctx) return;
1028
1480
 
1481
+ // A delete before the message's create is handled drops that create.
1482
+ // Its event still goes, for a row a history sync stored first.
1483
+ const live = this.creating.get(messageId);
1484
+ if (live) live.deleted = true;
1485
+
1029
1486
  // Only queue the event - state writes happen in onProcess()
1030
1487
  this.ctx.pushEvent({
1031
1488
  type: 'discord:delete',
@@ -1037,9 +1494,30 @@ export class DiscordModule implements Module {
1037
1494
  // Tool Handlers
1038
1495
  // ==========================================================================
1039
1496
 
1040
- private async handleSend(input: SendInput): Promise<ToolResult> {
1041
- if (!this.checkRateLimit(input.channelId)) {
1042
- return { success: false, error: 'Rate limited', isError: true };
1497
+ /** Record a message the agent posted in a turn, for that turn's thoughts (lastPosts). */
1498
+ private notePost(agentName: string | undefined, turnId: number | undefined, channelId: string, messageId: string): void {
1499
+ if (agentName === undefined || turnId === undefined) return;
1500
+ // A later turn's post stays: this one's turn has ended, and its thoughts
1501
+ // have gone or will go nowhere.
1502
+ const current = this.lastPosts.get(agentName);
1503
+ if (current && current.turnId > turnId) return;
1504
+ // Set it last, so the entry recorded longest ago is the first to go.
1505
+ this.lastPosts.delete(agentName);
1506
+ this.lastPosts.set(agentName, { turnId, channelId, messageId });
1507
+ if (this.lastPosts.size > MAX_LAST_POSTS) {
1508
+ const oldest = this.lastPosts.keys().next().value;
1509
+ if (oldest !== undefined) this.lastPosts.delete(oldest);
1510
+ }
1511
+ }
1512
+
1513
+ private async handleSend(input: SendInput, call: ToolCall): Promise<ToolResult> {
1514
+ const wait = this.rateLimitWait(input.channelId);
1515
+ if (wait > 0) {
1516
+ return {
1517
+ success: false,
1518
+ error: `Nothing was sent: channel ${input.channelId} has reached ${this.rateLimitRule()}. Send it again in ${seconds(wait)}; other channels have their own limit.`,
1519
+ isError: true,
1520
+ };
1043
1521
  }
1044
1522
 
1045
1523
  const result = await this.client.sendMessage(input.channelId, input.content, {
@@ -1047,25 +1525,43 @@ export class DiscordModule implements Module {
1047
1525
  createThread: input.createThread,
1048
1526
  });
1049
1527
 
1050
- // Track for thought-editing
1051
- this.lastSentMessage = { channelId: input.channelId, messageId: result.messageId };
1528
+ // Track for thought-editing: the message is out, whether or not its
1529
+ // thread was started.
1530
+ this.notePost(call.callerAgentName, call.callerTurnId, input.channelId, result.messageId);
1052
1531
 
1053
- return { success: true, data: { messageId: result.messageId } };
1532
+ if (result.threadFailure !== undefined) {
1533
+ return { success: false, error: result.threadFailure, isError: true };
1534
+ }
1535
+ return { success: true, data: { messageId: result.messageId, ...(result.note !== undefined ? { note: result.note } : {}) } };
1054
1536
  }
1055
1537
 
1056
1538
  private async handleDM(input: SendDMInput): Promise<ToolResult> {
1057
1539
  const result = await this.client.sendDM(input.userId, input.content);
1058
- return { success: true, data: { messageId: result.messageId } };
1540
+ // The direct message channel's id is what fetch_history reads it by, should a later dm need checking.
1541
+ return { success: true, data: { messageId: result.messageId, ...(result.channelId !== undefined ? { channelId: result.channelId } : {}) } };
1059
1542
  }
1060
1543
 
1061
- private async handleReply(input: ReplyInput): Promise<ToolResult> {
1544
+ private async handleReply(input: ReplyInput, call: ToolCall): Promise<ToolResult> {
1062
1545
  const replyCtx = this.replyContexts.get('default');
1063
1546
  if (!replyCtx) {
1064
- return { success: false, error: 'No conversation context', isError: true };
1547
+ return {
1548
+ success: false,
1549
+ error: 'Nothing was sent: there\'s no conversation to reply in. Use discord--send with a channelId to post somewhere, or discord--set_reply_context with a channelId to choose where replies go, then reply again.',
1550
+ isError: true,
1551
+ };
1065
1552
  }
1066
1553
 
1067
- if (!this.checkRateLimit(replyCtx.channelId)) {
1068
- return { success: false, error: 'Rate limited', isError: true };
1554
+ // The reply goes to the context's thread when it names one, an empty id
1555
+ // as well, as it always has; the words name an empty id as what it is.
1556
+ const thread = replyCtx.threadId;
1557
+ const wait = this.rateLimitWait(replyCtx.channelId);
1558
+ if (wait > 0) {
1559
+ return {
1560
+ success: false,
1561
+ error: `Nothing was sent: this conversation's channel (${replyCtx.channelId}) has reached ${this.rateLimitRule()}`
1562
+ + `${thread ? `, which counts its replies in thread ${thread} too` : ''}. Reply again in ${seconds(wait)}.`,
1563
+ isError: true,
1564
+ };
1069
1565
  }
1070
1566
 
1071
1567
  let content = input.content;
@@ -1073,13 +1569,31 @@ export class DiscordModule implements Module {
1073
1569
  content = `<@${replyCtx.userId}> ${content}`;
1074
1570
  }
1075
1571
 
1076
- const targetChannel = replyCtx.threadId ?? replyCtx.channelId;
1077
- const result = await this.client.sendMessage(targetChannel, content, {
1078
- replyTo: replyCtx.replyToMessageId,
1079
- });
1572
+ // reply's call names no channel or message: its reply context does, as it
1573
+ // stands now, before the send, which an incoming message can change.
1574
+ const targetChannel = thread ?? replyCtx.channelId;
1575
+ const pointsAt = thread === undefined ? `channel ${targetChannel}` : thread === '' ? 'an empty thread id' : `thread ${targetChannel}`;
1576
+ const check = thread === ''
1577
+ ? `, and its reply context names an empty thread id: discord--set_reply_context with a threadId, or without one to reply in channel ${replyCtx.channelId}, puts that right`
1578
+ : `: discord--fetch_history with channelId ${targetChannel}, its reply context's ${thread !== undefined ? 'thread' : 'channel'}, shows it, so check before you reply again`;
1579
+ const replyTo = replyCtx.replyToMessageId;
1580
+ let result: { messageId: string };
1581
+ try {
1582
+ result = await this.client.sendMessage(targetChannel, content, { replyTo });
1583
+ } catch (error) {
1584
+ if (error instanceof DiscordRefusal) {
1585
+ throw new DiscordRefusal(`${error.message} A reply goes where its reply context points: ${pointsAt}`
1586
+ + `${replyTo ? `, answering message ${replyTo}` : ''}. discord--set_reply_context changes that.`, { cause: error });
1587
+ }
1588
+ return {
1589
+ success: false,
1590
+ error: `discord--reply stopped on an unexpected error: ${errorText(error)}. Whether the reply was sent is unknown${check}.`,
1591
+ isError: true,
1592
+ };
1593
+ }
1080
1594
 
1081
1595
  // Track for thought-editing
1082
- this.lastSentMessage = { channelId: targetChannel, messageId: result.messageId };
1596
+ this.notePost(call.callerAgentName, call.callerTurnId, targetChannel, result.messageId);
1083
1597
 
1084
1598
  // Clear reply-to after sending
1085
1599
  replyCtx.replyToMessageId = undefined;
@@ -1142,7 +1656,7 @@ export class DiscordModule implements Module {
1142
1656
 
1143
1657
  private async handleActivate(input: { additive?: boolean }): Promise<ToolResult> {
1144
1658
  if (!this.ctx) {
1145
- return { success: false, error: 'Module not started', isError: true };
1659
+ return { success: false, error: NOT_RUNNING, isError: true };
1146
1660
  }
1147
1661
 
1148
1662
  this.ctx.registerSpeechHandler('*', { additive: input.additive });
@@ -1151,7 +1665,7 @@ export class DiscordModule implements Module {
1151
1665
 
1152
1666
  private async handleDeactivate(): Promise<ToolResult> {
1153
1667
  if (!this.ctx) {
1154
- return { success: false, error: 'Module not started', isError: true };
1668
+ return { success: false, error: NOT_RUNNING, isError: true };
1155
1669
  }
1156
1670
 
1157
1671
  this.ctx.unregisterSpeechHandler();
@@ -1213,6 +1727,11 @@ export class DiscordModule implements Module {
1213
1727
  authorId: m.authorId,
1214
1728
  isBot: m.isBot,
1215
1729
  content: m.content,
1730
+ // Not their URLs: a link Discord signs works for anyone holding it
1731
+ // until it expires, and an agent could repeat it where others read.
1732
+ ...(m.attachments?.length
1733
+ ? { attachments: m.attachments.map(({ filename, contentType, size }) => ({ filename, contentType, size })) }
1734
+ : {}),
1216
1735
  timestamp: formatZonedDateTime(m.timestamp, this.timeZone),
1217
1736
  replyTo: m.replyTo,
1218
1737
  })),
@@ -1231,8 +1750,25 @@ export class DiscordModule implements Module {
1231
1750
  .join('\n');
1232
1751
  }
1233
1752
 
1234
- private checkRateLimit(channelId: string): boolean {
1235
- const limit = this.config.rateLimitPerMinute ?? 30;
1753
+ private rateLimitPerMinute(): number {
1754
+ return this.config.rateLimitPerMinute ?? 30;
1755
+ }
1756
+
1757
+ /** The limit, as the rate-limit refusals word it. */
1758
+ private rateLimitRule(): string {
1759
+ const limit = this.rateLimitPerMinute();
1760
+ return `this host's limit of ${limit} send${limit === 1 ? '' : 's'} a minute from the bot`;
1761
+ }
1762
+
1763
+ /**
1764
+ * Counts a send to `channelId` against its limit and returns 0, or, when
1765
+ * the channel has had its sends for this window, returns the whole seconds
1766
+ * until the window ends, at least 1. A window is the minute from the first
1767
+ * send this lets through, and each send it lets through counts, whether or
1768
+ * not Discord then takes it.
1769
+ */
1770
+ private rateLimitWait(channelId: string): number {
1771
+ const limit = this.rateLimitPerMinute();
1236
1772
  const now = Date.now();
1237
1773
  const windowMs = 60 * 1000;
1238
1774
 
@@ -1244,11 +1780,11 @@ export class DiscordModule implements Module {
1244
1780
  }
1245
1781
 
1246
1782
  if (info.count >= limit) {
1247
- return false;
1783
+ return Math.max(1, Math.ceil((info.windowStart + windowMs - now) / 1000));
1248
1784
  }
1249
1785
 
1250
1786
  info.count++;
1251
- return true;
1787
+ return 0;
1252
1788
  }
1253
1789
 
1254
1790
  private cleanupOldConversations(): void {