@openclaw/discord 2026.3.13 → 2026.5.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (502) hide show
  1. package/account-inspect-api.ts +6 -0
  2. package/action-runtime-api.ts +1 -0
  3. package/api.ts +132 -0
  4. package/channel-config-api.ts +1 -0
  5. package/channel-plugin-api.ts +3 -0
  6. package/config-api.ts +4 -0
  7. package/configured-state.ts +6 -0
  8. package/contract-api.ts +21 -0
  9. package/directory-contract-api.ts +4 -0
  10. package/doctor-contract-api.ts +1 -0
  11. package/index.test.ts +13 -0
  12. package/index.ts +18 -13
  13. package/openclaw.plugin.json +3326 -1
  14. package/package.json +68 -2
  15. package/runtime-api.actions.ts +15 -0
  16. package/runtime-api.lookup.ts +22 -0
  17. package/runtime-api.monitor.ts +50 -0
  18. package/runtime-api.send.ts +79 -0
  19. package/runtime-api.threads.ts +30 -0
  20. package/runtime-api.ts +180 -0
  21. package/runtime-setter-api.ts +3 -0
  22. package/secret-contract-api.ts +4 -0
  23. package/security-audit-contract-api.ts +1 -0
  24. package/security-contract-api.ts +4 -0
  25. package/session-key-api.ts +1 -0
  26. package/setup-entry.ts +9 -0
  27. package/setup-plugin-api.ts +3 -0
  28. package/src/account-inspect.test.ts +126 -0
  29. package/src/account-inspect.ts +132 -0
  30. package/src/accounts.test.ts +247 -0
  31. package/src/accounts.ts +196 -0
  32. package/src/actions/handle-action.guild-admin.ts +411 -0
  33. package/src/actions/handle-action.test.ts +306 -0
  34. package/src/actions/handle-action.ts +372 -0
  35. package/src/actions/runtime.guild.ts +446 -0
  36. package/src/actions/runtime.messaging.messages.ts +205 -0
  37. package/src/actions/runtime.messaging.reactions.ts +67 -0
  38. package/src/actions/runtime.messaging.runtime.ts +69 -0
  39. package/src/actions/runtime.messaging.send.ts +248 -0
  40. package/src/actions/runtime.messaging.shared.ts +97 -0
  41. package/src/actions/runtime.messaging.ts +37 -0
  42. package/src/actions/runtime.moderation-shared.ts +48 -0
  43. package/src/actions/runtime.moderation.authz.test.ts +151 -0
  44. package/src/actions/runtime.moderation.ts +116 -0
  45. package/src/actions/runtime.presence.test.ts +160 -0
  46. package/src/actions/runtime.presence.ts +117 -0
  47. package/src/actions/runtime.shared.ts +83 -0
  48. package/src/actions/runtime.test.ts +1087 -0
  49. package/src/actions/runtime.ts +87 -0
  50. package/src/api-barrel.test.ts +80 -0
  51. package/src/api.test.ts +130 -0
  52. package/src/api.ts +169 -0
  53. package/src/approval-handler.runtime.test.ts +41 -0
  54. package/src/approval-handler.runtime.ts +632 -0
  55. package/src/approval-native.test.ts +330 -0
  56. package/src/approval-native.ts +219 -0
  57. package/src/approval-runtime.ts +14 -0
  58. package/src/approval-shared.ts +53 -0
  59. package/src/audit-core.ts +141 -0
  60. package/src/audit.test.ts +145 -0
  61. package/src/audit.ts +32 -0
  62. package/src/channel-actions.contract.test.ts +45 -0
  63. package/src/channel-actions.runtime.ts +1 -0
  64. package/src/channel-actions.test.ts +275 -0
  65. package/src/channel-actions.ts +203 -0
  66. package/src/channel-api.ts +29 -0
  67. package/src/channel.conversation.ts +159 -0
  68. package/src/channel.loaders.ts +47 -0
  69. package/src/channel.runtime.ts +1 -0
  70. package/src/channel.setup.ts +12 -0
  71. package/src/channel.test.ts +547 -12
  72. package/src/channel.ts +597 -430
  73. package/src/chunk.test.ts +157 -0
  74. package/src/chunk.ts +321 -0
  75. package/src/client.proxy.test.ts +176 -0
  76. package/src/client.test.ts +76 -0
  77. package/src/client.ts +132 -0
  78. package/src/component-custom-id.ts +72 -0
  79. package/src/components-registry.ts +356 -0
  80. package/src/components.builders.ts +409 -0
  81. package/src/components.modal.ts +124 -0
  82. package/src/components.parse.ts +407 -0
  83. package/src/components.test.ts +312 -0
  84. package/src/components.ts +54 -0
  85. package/src/components.types.ts +187 -0
  86. package/src/config-schema.test.ts +325 -0
  87. package/src/config-schema.ts +6 -0
  88. package/src/config-ui-hints.ts +249 -0
  89. package/src/conversation-identity.ts +58 -0
  90. package/src/delivery-retry.ts +56 -0
  91. package/src/directory-cache.ts +116 -0
  92. package/src/directory-config.ts +58 -0
  93. package/src/directory-contract.test.ts +129 -0
  94. package/src/directory-live.test.ts +126 -0
  95. package/src/directory-live.ts +135 -0
  96. package/src/doctor-contract.ts +477 -0
  97. package/src/doctor-shared.ts +5 -0
  98. package/src/doctor.test.ts +405 -0
  99. package/src/doctor.ts +340 -0
  100. package/src/draft-chunking.test.ts +64 -0
  101. package/src/draft-chunking.ts +43 -0
  102. package/src/draft-stream.test.ts +159 -0
  103. package/src/draft-stream.ts +154 -0
  104. package/src/error-body.ts +38 -0
  105. package/src/exec-approvals.test.ts +88 -0
  106. package/src/exec-approvals.ts +110 -0
  107. package/src/gateway-logging.test.ts +98 -0
  108. package/src/gateway-logging.ts +67 -0
  109. package/src/group-policy.ts +113 -0
  110. package/src/guilds.ts +29 -0
  111. package/src/inbound-context.contract.test.ts +11 -0
  112. package/src/interactive-dispatch.ts +104 -0
  113. package/src/internal/api.commands.ts +51 -0
  114. package/src/internal/api.guild.ts +164 -0
  115. package/src/internal/api.interactions.ts +53 -0
  116. package/src/internal/api.messages.ts +113 -0
  117. package/src/internal/api.reactions.ts +38 -0
  118. package/src/internal/api.test.ts +262 -0
  119. package/src/internal/api.ts +61 -0
  120. package/src/internal/api.users.ts +19 -0
  121. package/src/internal/api.webhooks.ts +13 -0
  122. package/src/internal/client.test.ts +408 -0
  123. package/src/internal/client.ts +308 -0
  124. package/src/internal/command-deploy.ts +237 -0
  125. package/src/internal/commands.ts +188 -0
  126. package/src/internal/components.base.ts +65 -0
  127. package/src/internal/components.message.ts +279 -0
  128. package/src/internal/components.modal.ts +95 -0
  129. package/src/internal/components.ts +31 -0
  130. package/src/internal/discord.ts +11 -0
  131. package/src/internal/embeds.ts +35 -0
  132. package/src/internal/entity-cache.ts +98 -0
  133. package/src/internal/event-queue.ts +162 -0
  134. package/src/internal/gateway-close-codes.ts +25 -0
  135. package/src/internal/gateway-dispatch.ts +96 -0
  136. package/src/internal/gateway-identify-limiter.ts +26 -0
  137. package/src/internal/gateway-lifecycle.ts +61 -0
  138. package/src/internal/gateway-rate-limit.ts +104 -0
  139. package/src/internal/gateway.test.ts +603 -0
  140. package/src/internal/gateway.ts +476 -0
  141. package/src/internal/interaction-dispatch.test.ts +148 -0
  142. package/src/internal/interaction-dispatch.ts +162 -0
  143. package/src/internal/interaction-options.ts +98 -0
  144. package/src/internal/interaction-response.ts +53 -0
  145. package/src/internal/interactions.test.ts +325 -0
  146. package/src/internal/interactions.ts +378 -0
  147. package/src/internal/listeners.ts +85 -0
  148. package/src/internal/live-smoke.live.test.ts +26 -0
  149. package/src/internal/modal-fields.ts +95 -0
  150. package/src/internal/payload.ts +69 -0
  151. package/src/internal/rest-body.ts +115 -0
  152. package/src/internal/rest-errors.ts +88 -0
  153. package/src/internal/rest-routes.ts +50 -0
  154. package/src/internal/rest-scheduler.ts +557 -0
  155. package/src/internal/rest.test.ts +673 -0
  156. package/src/internal/rest.ts +322 -0
  157. package/src/internal/schemas.ts +36 -0
  158. package/src/internal/structures.test.ts +43 -0
  159. package/src/internal/structures.ts +280 -0
  160. package/src/internal/test-builders.test-support.ts +163 -0
  161. package/src/internal/voice.ts +49 -0
  162. package/src/media-detection.ts +28 -0
  163. package/src/mentions.test.ts +111 -0
  164. package/src/mentions.ts +147 -0
  165. package/src/monitor/access-groups.ts +55 -0
  166. package/src/monitor/ack-reactions.ts +70 -0
  167. package/src/monitor/acp-bind-here.integration.test.ts +211 -0
  168. package/src/monitor/agent-components-auth.ts +7 -0
  169. package/src/monitor/agent-components-context.ts +154 -0
  170. package/src/monitor/agent-components-data.ts +224 -0
  171. package/src/monitor/agent-components-dm-auth.ts +221 -0
  172. package/src/monitor/agent-components-guild-auth.ts +322 -0
  173. package/src/monitor/agent-components-helpers.runtime.ts +5 -0
  174. package/src/monitor/agent-components-helpers.ts +34 -0
  175. package/src/monitor/agent-components-reply.ts +10 -0
  176. package/src/monitor/agent-components.deps.runtime.ts +2 -0
  177. package/src/monitor/agent-components.dispatch.ts +366 -0
  178. package/src/monitor/agent-components.handlers.ts +303 -0
  179. package/src/monitor/agent-components.modal.ts +160 -0
  180. package/src/monitor/agent-components.plugin-interactive.ts +187 -0
  181. package/src/monitor/agent-components.runtime.ts +14 -0
  182. package/src/monitor/agent-components.system-controls.ts +211 -0
  183. package/src/monitor/agent-components.ts +70 -0
  184. package/src/monitor/agent-components.types.ts +58 -0
  185. package/src/monitor/agent-components.wildcard-controls.ts +168 -0
  186. package/src/monitor/agent-components.wildcard.test.ts +71 -0
  187. package/src/monitor/allow-list.test.ts +14 -0
  188. package/src/monitor/allow-list.ts +633 -0
  189. package/src/monitor/auto-presence.test.ts +156 -0
  190. package/src/monitor/auto-presence.ts +356 -0
  191. package/src/monitor/channel-access.test.ts +99 -0
  192. package/src/monitor/channel-access.ts +102 -0
  193. package/src/monitor/commands.test.ts +24 -0
  194. package/src/monitor/commands.ts +9 -0
  195. package/src/monitor/dm-command-auth.test.ts +197 -0
  196. package/src/monitor/dm-command-auth.ts +158 -0
  197. package/src/monitor/dm-command-decision.test.ts +113 -0
  198. package/src/monitor/dm-command-decision.ts +49 -0
  199. package/src/monitor/exec-approvals.test.ts +226 -0
  200. package/src/monitor/exec-approvals.ts +158 -0
  201. package/src/monitor/format.ts +45 -0
  202. package/src/monitor/gateway-handle.ts +34 -0
  203. package/src/monitor/gateway-metadata.test.ts +29 -0
  204. package/src/monitor/gateway-metadata.ts +298 -0
  205. package/src/monitor/gateway-plugin.test.ts +297 -0
  206. package/src/monitor/gateway-plugin.ts +294 -0
  207. package/src/monitor/gateway-registry.ts +37 -0
  208. package/src/monitor/gateway-supervisor.test.ts +150 -0
  209. package/src/monitor/gateway-supervisor.ts +206 -0
  210. package/src/monitor/inbound-context.test-helpers.ts +37 -0
  211. package/src/monitor/inbound-context.test.ts +106 -0
  212. package/src/monitor/inbound-context.ts +103 -0
  213. package/src/monitor/inbound-dedupe.ts +79 -0
  214. package/src/monitor/inbound-job.test.ts +203 -0
  215. package/src/monitor/inbound-job.ts +118 -0
  216. package/src/monitor/listeners.queue.ts +91 -0
  217. package/src/monitor/listeners.reactions.ts +610 -0
  218. package/src/monitor/listeners.test.ts +200 -0
  219. package/src/monitor/listeners.ts +150 -0
  220. package/src/monitor/message-channel-info.ts +96 -0
  221. package/src/monitor/message-forwarded.ts +107 -0
  222. package/src/monitor/message-handler.batch-gate.test.ts +22 -0
  223. package/src/monitor/message-handler.batch-gate.ts +19 -0
  224. package/src/monitor/message-handler.bot-self-filter.test.ts +68 -0
  225. package/src/monitor/message-handler.context.ts +406 -0
  226. package/src/monitor/message-handler.dm-preflight.ts +123 -0
  227. package/src/monitor/message-handler.draft-preview.ts +246 -0
  228. package/src/monitor/message-handler.hydration.test.ts +80 -0
  229. package/src/monitor/message-handler.hydration.ts +198 -0
  230. package/src/monitor/message-handler.inbound-context.test.ts +59 -0
  231. package/src/monitor/message-handler.module-test-helpers.ts +31 -0
  232. package/src/monitor/message-handler.preflight-channel-access.ts +86 -0
  233. package/src/monitor/message-handler.preflight-channel-context.test.ts +18 -0
  234. package/src/monitor/message-handler.preflight-channel-context.ts +58 -0
  235. package/src/monitor/message-handler.preflight-context.ts +54 -0
  236. package/src/monitor/message-handler.preflight-helpers.ts +164 -0
  237. package/src/monitor/message-handler.preflight-history.ts +23 -0
  238. package/src/monitor/message-handler.preflight-logging.ts +36 -0
  239. package/src/monitor/message-handler.preflight-pluralkit.ts +26 -0
  240. package/src/monitor/message-handler.preflight-runtime.ts +28 -0
  241. package/src/monitor/message-handler.preflight-thread.ts +49 -0
  242. package/src/monitor/message-handler.preflight.acp-bindings.test.ts +369 -0
  243. package/src/monitor/message-handler.preflight.test-helpers.ts +111 -0
  244. package/src/monitor/message-handler.preflight.test.ts +1623 -0
  245. package/src/monitor/message-handler.preflight.ts +679 -0
  246. package/src/monitor/message-handler.preflight.types.ts +110 -0
  247. package/src/monitor/message-handler.process.test.ts +1369 -0
  248. package/src/monitor/message-handler.process.ts +686 -0
  249. package/src/monitor/message-handler.queue.test.ts +496 -0
  250. package/src/monitor/message-handler.routing-preflight.ts +112 -0
  251. package/src/monitor/message-handler.test-harness.ts +99 -0
  252. package/src/monitor/message-handler.test-helpers.ts +75 -0
  253. package/src/monitor/message-handler.ts +274 -0
  254. package/src/monitor/message-media.ts +509 -0
  255. package/src/monitor/message-run-queue.ts +101 -0
  256. package/src/monitor/message-text.ts +171 -0
  257. package/src/monitor/message-utils.test.ts +1157 -0
  258. package/src/monitor/message-utils.ts +32 -0
  259. package/src/monitor/model-picker-preferences.test.ts +67 -0
  260. package/src/monitor/model-picker-preferences.ts +184 -0
  261. package/src/monitor/model-picker.state.ts +364 -0
  262. package/src/monitor/model-picker.test-utils.ts +26 -0
  263. package/src/monitor/model-picker.test.ts +794 -0
  264. package/src/monitor/model-picker.ts +38 -0
  265. package/src/monitor/model-picker.view.ts +695 -0
  266. package/src/monitor/monitor.agent-components.test.ts +375 -0
  267. package/src/monitor/monitor.test.ts +849 -0
  268. package/src/monitor/monitor.threading-utils.test.ts +598 -0
  269. package/src/monitor/native-command-agent-reply.ts +125 -0
  270. package/src/monitor/native-command-arg-ui.ts +233 -0
  271. package/src/monitor/native-command-auth.ts +308 -0
  272. package/src/monitor/native-command-bypass.ts +13 -0
  273. package/src/monitor/native-command-context.test.ts +98 -0
  274. package/src/monitor/native-command-context.ts +103 -0
  275. package/src/monitor/native-command-dispatch.ts +35 -0
  276. package/src/monitor/native-command-model-picker-apply.ts +177 -0
  277. package/src/monitor/native-command-model-picker-interaction.ts +461 -0
  278. package/src/monitor/native-command-model-picker-ui.ts +368 -0
  279. package/src/monitor/native-command-reply.test.ts +68 -0
  280. package/src/monitor/native-command-reply.ts +185 -0
  281. package/src/monitor/native-command-route.ts +91 -0
  282. package/src/monitor/native-command-status.ts +76 -0
  283. package/src/monitor/native-command-ui.ts +26 -0
  284. package/src/monitor/native-command-ui.types.ts +20 -0
  285. package/src/monitor/native-command.args.ts +45 -0
  286. package/src/monitor/native-command.command-arg.test.ts +99 -0
  287. package/src/monitor/native-command.commands-allowfrom.test.ts +490 -0
  288. package/src/monitor/native-command.model-picker.test.ts +767 -0
  289. package/src/monitor/native-command.options.test.ts +369 -0
  290. package/src/monitor/native-command.options.ts +153 -0
  291. package/src/monitor/native-command.plugin-dispatch.test.ts +961 -0
  292. package/src/monitor/native-command.runtime.ts +50 -0
  293. package/src/monitor/native-command.status-direct.test.ts +272 -0
  294. package/src/monitor/native-command.test-helpers.ts +64 -0
  295. package/src/monitor/native-command.think-autocomplete.test.ts +416 -0
  296. package/src/monitor/native-command.ts +700 -0
  297. package/src/monitor/native-command.types.ts +9 -0
  298. package/src/monitor/native-interaction-channel-context.ts +50 -0
  299. package/src/monitor/preflight-audio.runtime.ts +9 -0
  300. package/src/monitor/preflight-audio.test.ts +157 -0
  301. package/src/monitor/preflight-audio.ts +130 -0
  302. package/src/monitor/presence-cache.ts +61 -0
  303. package/src/monitor/presence.test.ts +44 -0
  304. package/src/monitor/presence.ts +50 -0
  305. package/src/monitor/provider-session.runtime.ts +12 -0
  306. package/src/monitor/provider.acp.ts +89 -0
  307. package/src/monitor/provider.allowlist.test.ts +149 -0
  308. package/src/monitor/provider.allowlist.ts +394 -0
  309. package/src/monitor/provider.cleanup.ts +41 -0
  310. package/src/monitor/provider.commands.ts +129 -0
  311. package/src/monitor/provider.config-log.ts +45 -0
  312. package/src/monitor/provider.deploy-errors.ts +362 -0
  313. package/src/monitor/provider.deploy.ts +221 -0
  314. package/src/monitor/provider.interactions.ts +160 -0
  315. package/src/monitor/provider.lifecycle.test.ts +713 -0
  316. package/src/monitor/provider.lifecycle.ts +552 -0
  317. package/src/monitor/provider.proxy.test.ts +745 -0
  318. package/src/monitor/provider.rest-proxy.test.ts +121 -0
  319. package/src/monitor/provider.runtime.ts +1 -0
  320. package/src/monitor/provider.skill-dedupe.test.ts +42 -0
  321. package/src/monitor/provider.startup-log.ts +32 -0
  322. package/src/monitor/provider.startup.test.ts +426 -0
  323. package/src/monitor/provider.startup.ts +323 -0
  324. package/src/monitor/provider.test.ts +1111 -0
  325. package/src/monitor/provider.ts +713 -0
  326. package/src/monitor/reply-context.ts +64 -0
  327. package/src/monitor/reply-delivery.test.ts +244 -0
  328. package/src/monitor/reply-delivery.ts +203 -0
  329. package/src/monitor/rest-fetch.ts +43 -0
  330. package/src/monitor/route-resolution.test.ts +204 -0
  331. package/src/monitor/route-resolution.ts +140 -0
  332. package/src/monitor/sender-identity.ts +81 -0
  333. package/src/monitor/startup-status.test.ts +30 -0
  334. package/src/monitor/startup-status.ts +10 -0
  335. package/src/monitor/status.ts +22 -0
  336. package/src/monitor/system-events.ts +55 -0
  337. package/src/monitor/thread-bindings.config.ts +35 -0
  338. package/src/monitor/thread-bindings.discord-api.test.ts +229 -0
  339. package/src/monitor/thread-bindings.discord-api.ts +310 -0
  340. package/src/monitor/thread-bindings.lifecycle.test.ts +1871 -0
  341. package/src/monitor/thread-bindings.lifecycle.ts +354 -0
  342. package/src/monitor/thread-bindings.manager.ts +553 -0
  343. package/src/monitor/thread-bindings.messages.ts +6 -0
  344. package/src/monitor/thread-bindings.persona.test.ts +34 -0
  345. package/src/monitor/thread-bindings.persona.ts +25 -0
  346. package/src/monitor/thread-bindings.session-adapter.ts +229 -0
  347. package/src/monitor/thread-bindings.session-shared.ts +59 -0
  348. package/src/monitor/thread-bindings.session-updates.ts +35 -0
  349. package/src/monitor/thread-bindings.shared-state.test.ts +36 -0
  350. package/src/monitor/thread-bindings.state.ts +540 -0
  351. package/src/monitor/thread-bindings.ts +48 -0
  352. package/src/monitor/thread-bindings.types.ts +83 -0
  353. package/src/monitor/thread-channel-context.ts +112 -0
  354. package/src/monitor/thread-session-close.test.ts +180 -0
  355. package/src/monitor/thread-session-close.ts +63 -0
  356. package/src/monitor/thread-title.generate.test.ts +197 -0
  357. package/src/monitor/thread-title.test.ts +31 -0
  358. package/src/monitor/thread-title.ts +181 -0
  359. package/src/monitor/threading.auto-thread.test.ts +327 -0
  360. package/src/monitor/threading.auto-thread.ts +287 -0
  361. package/src/monitor/threading.cache.ts +45 -0
  362. package/src/monitor/threading.parent-info.test.ts +156 -0
  363. package/src/monitor/threading.starter.test.ts +260 -0
  364. package/src/monitor/threading.starter.ts +287 -0
  365. package/src/monitor/threading.ts +20 -0
  366. package/src/monitor/threading.types.ts +102 -0
  367. package/src/monitor/timeouts.ts +84 -0
  368. package/src/monitor/typing.test.ts +42 -0
  369. package/src/monitor/typing.ts +17 -0
  370. package/src/monitor.gateway.test.ts +187 -0
  371. package/src/monitor.gateway.ts +75 -0
  372. package/src/monitor.test.ts +1397 -0
  373. package/src/monitor.ts +28 -0
  374. package/src/normalize.test.ts +56 -0
  375. package/src/normalize.ts +86 -0
  376. package/src/outbound-adapter.interactive-order.test.ts +64 -0
  377. package/src/outbound-adapter.test-harness.ts +207 -0
  378. package/src/outbound-adapter.test.ts +696 -0
  379. package/src/outbound-adapter.ts +291 -0
  380. package/src/outbound-approval.ts +29 -0
  381. package/src/outbound-components.ts +81 -0
  382. package/src/outbound-payload.contract.test.ts +38 -0
  383. package/src/outbound-payload.ts +134 -0
  384. package/src/outbound-send-context.ts +92 -0
  385. package/src/outbound-session-route.test.ts +34 -0
  386. package/src/outbound-session-route.ts +72 -0
  387. package/src/pluralkit.test.ts +67 -0
  388. package/src/pluralkit.ts +58 -0
  389. package/src/preview-streaming.ts +32 -0
  390. package/src/probe.intents.test.ts +94 -0
  391. package/src/probe.parse-token.test.ts +43 -0
  392. package/src/probe.runtime.ts +1 -0
  393. package/src/probe.ts +237 -0
  394. package/src/proxy-fetch.ts +92 -0
  395. package/src/proxy-request-client.test.ts +78 -0
  396. package/src/proxy-request-client.ts +21 -0
  397. package/src/recipient-resolution.ts +39 -0
  398. package/src/resolve-allowlist-common.test.ts +36 -0
  399. package/src/resolve-allowlist-common.ts +39 -0
  400. package/src/resolve-channels.test.ts +340 -0
  401. package/src/resolve-channels.ts +369 -0
  402. package/src/resolve-users.test.ts +222 -0
  403. package/src/resolve-users.ts +184 -0
  404. package/src/retry.test.ts +83 -0
  405. package/src/retry.ts +98 -0
  406. package/src/runtime-api.ts +64 -0
  407. package/src/runtime.ts +22 -5
  408. package/src/secret-config-contract.ts +140 -0
  409. package/src/security-audit.runtime.ts +1 -0
  410. package/src/security-audit.test.ts +246 -0
  411. package/src/security-audit.ts +208 -0
  412. package/src/security-contract.ts +47 -0
  413. package/src/security-doctor.test.ts +25 -0
  414. package/src/security-doctor.ts +20 -0
  415. package/src/security.ts +60 -0
  416. package/src/send-target-parsing.ts +14 -0
  417. package/src/send.channels.ts +139 -0
  418. package/src/send.components.test.ts +275 -0
  419. package/src/send.components.ts +381 -0
  420. package/src/send.creates-thread.test.ts +643 -0
  421. package/src/send.emojis-stickers.ts +57 -0
  422. package/src/send.guild.ts +170 -0
  423. package/src/send.message-request.ts +97 -0
  424. package/src/send.messages.test.ts +53 -0
  425. package/src/send.messages.ts +225 -0
  426. package/src/send.outbound.ts +413 -0
  427. package/src/send.permissions.authz.test.ts +188 -0
  428. package/src/send.permissions.ts +283 -0
  429. package/src/send.reactions.ts +155 -0
  430. package/src/send.sends-basic-channel-messages.test.ts +941 -0
  431. package/src/send.shared.ts +447 -0
  432. package/src/send.test-harness.ts +56 -0
  433. package/src/send.ts +82 -0
  434. package/src/send.types.ts +188 -0
  435. package/src/send.typing.test.ts +41 -0
  436. package/src/send.typing.ts +9 -0
  437. package/src/send.voice.ts +134 -0
  438. package/src/send.webhook-activity.test.ts +105 -0
  439. package/src/send.webhook.proxy.test.ts +191 -0
  440. package/src/send.webhook.ts +133 -0
  441. package/src/session-contract.ts +3 -0
  442. package/src/session-key-normalization.test.ts +44 -0
  443. package/src/session-key-normalization.ts +47 -0
  444. package/src/setup-account-state.test.ts +91 -0
  445. package/src/setup-account-state.ts +144 -0
  446. package/src/setup-adapter.ts +12 -0
  447. package/src/setup-core.ts +212 -0
  448. package/src/setup-runtime-helpers.ts +10 -0
  449. package/src/setup-surface.test.ts +137 -0
  450. package/src/setup-surface.ts +129 -0
  451. package/src/shared-interactive.test.ts +153 -0
  452. package/src/shared-interactive.ts +124 -0
  453. package/src/shared.test.ts +165 -0
  454. package/src/shared.ts +190 -0
  455. package/src/status-issues.test.ts +70 -0
  456. package/src/status-issues.ts +169 -0
  457. package/src/subagent-hooks.test.ts +130 -81
  458. package/src/subagent-hooks.ts +184 -122
  459. package/src/target-parsing.ts +53 -0
  460. package/src/target-resolver.ts +129 -0
  461. package/src/targets.test.ts +367 -0
  462. package/src/targets.ts +12 -0
  463. package/src/test-http-helpers.ts +10 -0
  464. package/src/test-support/component-runtime.ts +190 -0
  465. package/src/test-support/config.ts +7 -0
  466. package/src/test-support/configured-binding-runtime.ts +29 -0
  467. package/src/test-support/partial-channel.ts +26 -0
  468. package/src/test-support/provider.test-support.ts +545 -0
  469. package/src/token.test.ts +107 -0
  470. package/src/token.ts +60 -0
  471. package/src/ui-colors.ts +27 -0
  472. package/src/ui.ts +20 -0
  473. package/src/voice/access.test.ts +217 -0
  474. package/src/voice/access.ts +124 -0
  475. package/src/voice/audio.ts +173 -0
  476. package/src/voice/capture-state.test.ts +48 -0
  477. package/src/voice/capture-state.ts +120 -0
  478. package/src/voice/command.test.ts +164 -0
  479. package/src/voice/command.ts +283 -0
  480. package/src/voice/config.ts +8 -0
  481. package/src/voice/manager.e2e.test.ts +928 -0
  482. package/src/voice/manager.ready-listener.test.ts +37 -0
  483. package/src/voice/manager.runtime.ts +11 -0
  484. package/src/voice/manager.ts +691 -0
  485. package/src/voice/prompt.test.ts +16 -0
  486. package/src/voice/prompt.ts +17 -0
  487. package/src/voice/receive-recovery.test.ts +79 -0
  488. package/src/voice/receive-recovery.ts +159 -0
  489. package/src/voice/sanitize.test.ts +34 -0
  490. package/src/voice/sanitize.ts +32 -0
  491. package/src/voice/sdk-runtime.ts +14 -0
  492. package/src/voice/segment.ts +156 -0
  493. package/src/voice/session.ts +50 -0
  494. package/src/voice/speaker-context.ts +127 -0
  495. package/src/voice/tts.ts +125 -0
  496. package/src/voice-message.test.ts +234 -0
  497. package/src/voice-message.ts +444 -0
  498. package/subagent-hooks-api.ts +27 -0
  499. package/test-api.ts +4 -0
  500. package/thread-binding-api.ts +1 -0
  501. package/timeouts.ts +6 -0
  502. package/tsconfig.json +16 -0
@@ -0,0 +1,1623 @@
1
+ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { ChannelType } from "../internal/discord.js";
3
+ import { createPartialDiscordChannelWithThrowingGetters } from "../test-support/partial-channel.js";
4
+
5
+ const transcribeFirstAudioMock = vi.hoisted(() => vi.fn());
6
+ const fetchPluralKitMessageInfoMock = vi.hoisted(() => vi.fn());
7
+ const resolveDiscordDmCommandAccessMock = vi.hoisted(() => vi.fn());
8
+ const handleDiscordDmCommandDecisionMock = vi.hoisted(() => vi.fn(async () => {}));
9
+
10
+ vi.mock("../pluralkit.js", () => ({
11
+ fetchPluralKitMessageInfo: (...args: unknown[]) => fetchPluralKitMessageInfoMock(...args),
12
+ }));
13
+ vi.mock("./preflight-audio.runtime.js", () => ({
14
+ transcribeFirstAudio: transcribeFirstAudioMock,
15
+ }));
16
+ vi.mock("./dm-command-auth.js", () => ({
17
+ resolveDiscordDmCommandAccess: resolveDiscordDmCommandAccessMock,
18
+ }));
19
+ vi.mock("./dm-command-decision.js", () => ({
20
+ handleDiscordDmCommandDecision: handleDiscordDmCommandDecisionMock,
21
+ }));
22
+ import {
23
+ __testing as sessionBindingTesting,
24
+ registerSessionBindingAdapter,
25
+ } from "openclaw/plugin-sdk/conversation-runtime";
26
+ import {
27
+ createDiscordMessage,
28
+ createDiscordPreflightArgs,
29
+ createGuildEvent,
30
+ createGuildTextClient,
31
+ DEFAULT_PREFLIGHT_CFG,
32
+ type DiscordClient,
33
+ type DiscordConfig,
34
+ type DiscordMessageEvent,
35
+ } from "./message-handler.preflight.test-helpers.js";
36
+ let preflightDiscordMessage: typeof import("./message-handler.preflight.js").preflightDiscordMessage;
37
+ let resolvePreflightMentionRequirement: typeof import("./message-handler.preflight.js").resolvePreflightMentionRequirement;
38
+ let shouldIgnoreBoundThreadWebhookMessage: typeof import("./message-handler.preflight.js").shouldIgnoreBoundThreadWebhookMessage;
39
+ let threadBindingTesting: typeof import("./thread-bindings.js").__testing;
40
+ let createThreadBindingManager: typeof import("./thread-bindings.js").createThreadBindingManager;
41
+
42
+ beforeAll(async () => {
43
+ ({
44
+ preflightDiscordMessage,
45
+ resolvePreflightMentionRequirement,
46
+ shouldIgnoreBoundThreadWebhookMessage,
47
+ } = await import("./message-handler.preflight.js"));
48
+ ({ __testing: threadBindingTesting, createThreadBindingManager } =
49
+ await import("./thread-bindings.js"));
50
+ });
51
+
52
+ beforeEach(() => {
53
+ fetchPluralKitMessageInfoMock.mockReset();
54
+ });
55
+
56
+ function createThreadBinding(
57
+ overrides?: Partial<import("openclaw/plugin-sdk/conversation-runtime").SessionBindingRecord>,
58
+ ) {
59
+ return {
60
+ bindingId: "default:thread-1",
61
+ targetSessionKey: "agent:main:subagent:child-1",
62
+ targetKind: "subagent",
63
+ conversation: {
64
+ channel: "discord",
65
+ accountId: "default",
66
+ conversationId: "thread-1",
67
+ parentConversationId: "parent-1",
68
+ },
69
+ status: "active",
70
+ boundAt: 1,
71
+ metadata: {
72
+ agentId: "main",
73
+ boundBy: "test",
74
+ webhookId: "wh-1",
75
+ webhookToken: "tok-1",
76
+ },
77
+ ...overrides,
78
+ } satisfies import("openclaw/plugin-sdk/conversation-runtime").SessionBindingRecord;
79
+ }
80
+
81
+ function createPreflightArgs(params: {
82
+ cfg: import("openclaw/plugin-sdk/config-types").OpenClawConfig;
83
+ discordConfig: DiscordConfig;
84
+ data: DiscordMessageEvent;
85
+ client: DiscordClient;
86
+ }): Parameters<typeof preflightDiscordMessage>[0] {
87
+ return createDiscordPreflightArgs(params);
88
+ }
89
+
90
+ function createThreadClient(params: { threadId: string; parentId: string }): DiscordClient {
91
+ return {
92
+ fetchChannel: async (channelId: string) => {
93
+ if (channelId === params.threadId) {
94
+ return {
95
+ id: params.threadId,
96
+ type: ChannelType.PublicThread,
97
+ name: "focus",
98
+ parentId: params.parentId,
99
+ ownerId: "owner-1",
100
+ };
101
+ }
102
+ if (channelId === params.parentId) {
103
+ return {
104
+ id: params.parentId,
105
+ type: ChannelType.GuildText,
106
+ name: "general",
107
+ };
108
+ }
109
+ return null;
110
+ },
111
+ } as unknown as DiscordClient;
112
+ }
113
+
114
+ function createDmClient(channelId: string): DiscordClient {
115
+ return {
116
+ fetchChannel: async (id: string) => {
117
+ if (id === channelId) {
118
+ return {
119
+ id: channelId,
120
+ type: ChannelType.DM,
121
+ };
122
+ }
123
+ return null;
124
+ },
125
+ } as unknown as DiscordClient;
126
+ }
127
+
128
+ function createMissingChannelClient(): DiscordClient {
129
+ return {
130
+ fetchChannel: async () => null,
131
+ } as unknown as DiscordClient;
132
+ }
133
+
134
+ async function runThreadBoundPreflight(params: {
135
+ threadId: string;
136
+ parentId: string;
137
+ message: import("../internal/discord.js").Message;
138
+ threadBinding: import("openclaw/plugin-sdk/conversation-runtime").SessionBindingRecord;
139
+ discordConfig: DiscordConfig;
140
+ registerBindingAdapter?: boolean;
141
+ }) {
142
+ if (params.registerBindingAdapter) {
143
+ registerSessionBindingAdapter({
144
+ channel: "discord",
145
+ accountId: "default",
146
+ listBySession: () => [],
147
+ resolveByConversation: (ref) =>
148
+ ref.conversationId === params.threadId ? params.threadBinding : null,
149
+ });
150
+ }
151
+
152
+ const client = createThreadClient({
153
+ threadId: params.threadId,
154
+ parentId: params.parentId,
155
+ });
156
+
157
+ return preflightDiscordMessage({
158
+ ...createPreflightArgs({
159
+ cfg: DEFAULT_PREFLIGHT_CFG,
160
+ discordConfig: params.discordConfig,
161
+ data: createGuildEvent({
162
+ channelId: params.threadId,
163
+ guildId: "guild-1",
164
+ author: params.message.author,
165
+ message: params.message,
166
+ }),
167
+ client,
168
+ }),
169
+ threadBindings: {
170
+ getByThreadId: (id: string) => (id === params.threadId ? params.threadBinding : undefined),
171
+ } as import("./thread-bindings.js").ThreadBindingManager,
172
+ });
173
+ }
174
+
175
+ async function runGuildPreflight(params: {
176
+ channelId: string;
177
+ guildId: string;
178
+ message: import("../internal/discord.js").Message;
179
+ discordConfig: DiscordConfig;
180
+ cfg?: import("openclaw/plugin-sdk/config-types").OpenClawConfig;
181
+ guildEntries?: Parameters<typeof preflightDiscordMessage>[0]["guildEntries"];
182
+ includeGuildObject?: boolean;
183
+ }) {
184
+ return preflightDiscordMessage({
185
+ ...createPreflightArgs({
186
+ cfg: params.cfg ?? DEFAULT_PREFLIGHT_CFG,
187
+ discordConfig: params.discordConfig,
188
+ data: createGuildEvent({
189
+ channelId: params.channelId,
190
+ guildId: params.guildId,
191
+ author: params.message.author,
192
+ message: params.message,
193
+ includeGuildObject: params.includeGuildObject,
194
+ }),
195
+ client: createGuildTextClient(params.channelId),
196
+ }),
197
+ guildEntries: params.guildEntries,
198
+ });
199
+ }
200
+
201
+ async function runDmPreflight(params: {
202
+ channelId: string;
203
+ message: import("../internal/discord.js").Message;
204
+ discordConfig: DiscordConfig;
205
+ }) {
206
+ return preflightDiscordMessage({
207
+ ...createPreflightArgs({
208
+ cfg: DEFAULT_PREFLIGHT_CFG,
209
+ discordConfig: params.discordConfig,
210
+ data: {
211
+ channel_id: params.channelId,
212
+ author: params.message.author,
213
+ message: params.message,
214
+ } as DiscordMessageEvent,
215
+ client: createDmClient(params.channelId),
216
+ }),
217
+ });
218
+ }
219
+
220
+ async function runUnresolvedDmPreflight(params: {
221
+ cfg?: import("openclaw/plugin-sdk/config-types").OpenClawConfig;
222
+ channelId: string;
223
+ message: import("../internal/discord.js").Message;
224
+ discordConfig: DiscordConfig;
225
+ }) {
226
+ return preflightDiscordMessage({
227
+ ...createPreflightArgs({
228
+ cfg: params.cfg ?? DEFAULT_PREFLIGHT_CFG,
229
+ discordConfig: params.discordConfig,
230
+ data: {
231
+ channel_id: params.channelId,
232
+ author: params.message.author,
233
+ message: params.message,
234
+ } as DiscordMessageEvent,
235
+ client: createMissingChannelClient(),
236
+ }),
237
+ });
238
+ }
239
+
240
+ async function runMentionOnlyBotPreflight(params: {
241
+ channelId: string;
242
+ guildId: string;
243
+ message: import("../internal/discord.js").Message;
244
+ }) {
245
+ return runGuildPreflight({
246
+ channelId: params.channelId,
247
+ guildId: params.guildId,
248
+ message: params.message,
249
+ discordConfig: {
250
+ allowBots: "mentions",
251
+ } as DiscordConfig,
252
+ });
253
+ }
254
+
255
+ async function runIgnoreOtherMentionsPreflight(params: {
256
+ channelId: string;
257
+ guildId: string;
258
+ message: import("../internal/discord.js").Message;
259
+ }) {
260
+ return runGuildPreflight({
261
+ channelId: params.channelId,
262
+ guildId: params.guildId,
263
+ message: params.message,
264
+ discordConfig: {} as DiscordConfig,
265
+ guildEntries: {
266
+ [params.guildId]: {
267
+ requireMention: false,
268
+ ignoreOtherMentions: true,
269
+ },
270
+ },
271
+ });
272
+ }
273
+
274
+ describe("resolvePreflightMentionRequirement", () => {
275
+ it("requires mention when config requires mention and thread is not bound", () => {
276
+ expect(
277
+ resolvePreflightMentionRequirement({
278
+ shouldRequireMention: true,
279
+ bypassMentionRequirement: false,
280
+ }),
281
+ ).toBe(true);
282
+ });
283
+
284
+ it("disables mention requirement when the route explicitly bypasses mentions", () => {
285
+ expect(
286
+ resolvePreflightMentionRequirement({
287
+ shouldRequireMention: true,
288
+ bypassMentionRequirement: true,
289
+ }),
290
+ ).toBe(false);
291
+ });
292
+
293
+ it("keeps mention requirement disabled when config already disables it", () => {
294
+ expect(
295
+ resolvePreflightMentionRequirement({
296
+ shouldRequireMention: false,
297
+ bypassMentionRequirement: false,
298
+ }),
299
+ ).toBe(false);
300
+ });
301
+ });
302
+
303
+ describe("preflightDiscordMessage", () => {
304
+ beforeEach(() => {
305
+ sessionBindingTesting.resetSessionBindingAdaptersForTests();
306
+ transcribeFirstAudioMock.mockReset();
307
+ resolveDiscordDmCommandAccessMock.mockReset();
308
+ resolveDiscordDmCommandAccessMock.mockResolvedValue({
309
+ commandAuthorized: true,
310
+ decision: "allow",
311
+ allowMatch: { allowed: true, matchedBy: "allowFrom", value: "123" },
312
+ });
313
+ handleDiscordDmCommandDecisionMock.mockReset();
314
+ handleDiscordDmCommandDecisionMock.mockResolvedValue(undefined);
315
+ });
316
+
317
+ it("drops bound-thread bot system messages to prevent ACP self-loop", async () => {
318
+ const threadBinding = createThreadBinding({
319
+ targetKind: "session",
320
+ targetSessionKey: "agent:main:acp:discord-thread-1",
321
+ });
322
+ const threadId = "thread-system-1";
323
+ const parentId = "channel-parent-1";
324
+ const message = createDiscordMessage({
325
+ id: "m-system-1",
326
+ channelId: threadId,
327
+ content:
328
+ "⚙️ codex-acp session active (auto-unfocus in 24h). Messages here go directly to this session.",
329
+ author: {
330
+ id: "relay-bot-1",
331
+ bot: true,
332
+ username: "OpenClaw",
333
+ },
334
+ });
335
+
336
+ const result = await runThreadBoundPreflight({
337
+ threadId,
338
+ parentId,
339
+ message,
340
+ threadBinding,
341
+ discordConfig: {
342
+ allowBots: true,
343
+ } as DiscordConfig,
344
+ });
345
+
346
+ expect(result).toBeNull();
347
+ });
348
+
349
+ it("restores direct-message bindings by user target instead of DM channel id", async () => {
350
+ registerSessionBindingAdapter({
351
+ channel: "discord",
352
+ accountId: "default",
353
+ listBySession: () => [],
354
+ resolveByConversation: (ref) =>
355
+ ref.conversationId === "user:user-1"
356
+ ? createThreadBinding({
357
+ conversation: {
358
+ channel: "discord",
359
+ accountId: "default",
360
+ conversationId: "user:user-1",
361
+ },
362
+ metadata: {
363
+ pluginBindingOwner: "plugin",
364
+ pluginId: "openclaw-codex-app-server",
365
+ pluginRoot: "/Users/huntharo/github/openclaw-app-server",
366
+ },
367
+ })
368
+ : null,
369
+ });
370
+
371
+ const result = await runDmPreflight({
372
+ channelId: "dm-channel-1",
373
+ message: createDiscordMessage({
374
+ id: "m-dm-1",
375
+ channelId: "dm-channel-1",
376
+ content: "who are you",
377
+ author: {
378
+ id: "user-1",
379
+ bot: false,
380
+ username: "alice",
381
+ },
382
+ }),
383
+ discordConfig: {
384
+ allowBots: true,
385
+ dmPolicy: "open",
386
+ } as DiscordConfig,
387
+ });
388
+
389
+ expect(result).not.toBeNull();
390
+ expect(result?.threadBinding).toMatchObject({
391
+ conversation: {
392
+ channel: "discord",
393
+ accountId: "default",
394
+ conversationId: "user:user-1",
395
+ },
396
+ metadata: {
397
+ pluginBindingOwner: "plugin",
398
+ pluginId: "openclaw-codex-app-server",
399
+ },
400
+ });
401
+ });
402
+
403
+ it("ignores stale route-shaped channel bindings when config now routes to another agent", async () => {
404
+ const channelId = "channel-stale-route";
405
+ registerSessionBindingAdapter({
406
+ channel: "discord",
407
+ accountId: "default",
408
+ listBySession: () => [],
409
+ resolveByConversation: (ref) =>
410
+ ref.conversationId === channelId
411
+ ? createThreadBinding({
412
+ bindingId: "default:channel-stale-route",
413
+ targetKind: "session",
414
+ targetSessionKey: `agent:oldagent:discord:channel:${channelId}`,
415
+ conversation: {
416
+ channel: "discord",
417
+ accountId: "default",
418
+ conversationId: channelId,
419
+ },
420
+ metadata: undefined,
421
+ })
422
+ : null,
423
+ });
424
+
425
+ const result = await runGuildPreflight({
426
+ channelId,
427
+ guildId: "guild-stale-route",
428
+ message: createDiscordMessage({
429
+ id: "m-stale-route",
430
+ channelId,
431
+ content: "which agent is this?",
432
+ author: {
433
+ id: "user-1",
434
+ bot: false,
435
+ username: "alice",
436
+ },
437
+ }),
438
+ cfg: {
439
+ agents: {
440
+ list: [{ id: "newagent" }],
441
+ },
442
+ bindings: [
443
+ {
444
+ agentId: "newagent",
445
+ match: {
446
+ channel: "discord",
447
+ accountId: "default",
448
+ peer: { kind: "channel", id: channelId },
449
+ },
450
+ },
451
+ ],
452
+ channels: {
453
+ discord: {},
454
+ },
455
+ },
456
+ discordConfig: {
457
+ allowBots: true,
458
+ } as DiscordConfig,
459
+ guildEntries: {
460
+ "guild-stale-route": {
461
+ channels: {
462
+ [channelId]: {
463
+ enabled: true,
464
+ requireMention: false,
465
+ },
466
+ },
467
+ },
468
+ },
469
+ });
470
+
471
+ expect(result).not.toBeNull();
472
+ expect(result?.route.agentId).toBe("newagent");
473
+ expect(result?.route.sessionKey).toBe(`agent:newagent:discord:channel:${channelId}`);
474
+ expect(result?.boundSessionKey).toBeUndefined();
475
+ expect(result?.threadBinding).toBeUndefined();
476
+ });
477
+
478
+ it("preflights direct-message voice notes without mention gating", async () => {
479
+ transcribeFirstAudioMock.mockResolvedValue("hello openclaw from dm audio");
480
+
481
+ const result = await runDmPreflight({
482
+ channelId: "dm-channel-audio-1",
483
+ message: createDiscordMessage({
484
+ id: "m-dm-audio-1",
485
+ channelId: "dm-channel-audio-1",
486
+ content: "",
487
+ attachments: [
488
+ {
489
+ id: "att-dm-audio-1",
490
+ url: "https://cdn.discordapp.com/attachments/voice.ogg",
491
+ content_type: "audio/ogg",
492
+ filename: "voice.ogg",
493
+ },
494
+ ],
495
+ author: {
496
+ id: "user-1",
497
+ bot: false,
498
+ username: "alice",
499
+ },
500
+ }),
501
+ discordConfig: {
502
+ dmPolicy: "open",
503
+ } as DiscordConfig,
504
+ });
505
+
506
+ expect(transcribeFirstAudioMock).toHaveBeenCalledTimes(1);
507
+ expect(transcribeFirstAudioMock).toHaveBeenCalledWith(
508
+ expect.objectContaining({
509
+ ctx: expect.objectContaining({
510
+ MediaUrls: ["https://cdn.discordapp.com/attachments/voice.ogg"],
511
+ MediaTypes: ["audio/ogg"],
512
+ }),
513
+ }),
514
+ );
515
+ expect(result).not.toBeNull();
516
+ expect(result?.isDirectMessage).toBe(true);
517
+ expect(result?.preflightAudioTranscript).toBe("hello openclaw from dm audio");
518
+ });
519
+
520
+ it("keeps no-guild messages direct when channel lookup is unavailable", async () => {
521
+ const result = await runUnresolvedDmPreflight({
522
+ cfg: {
523
+ ...DEFAULT_PREFLIGHT_CFG,
524
+ session: {
525
+ ...DEFAULT_PREFLIGHT_CFG.session,
526
+ dmScope: "per-channel-peer",
527
+ },
528
+ },
529
+ channelId: "dm-channel-unresolved-1",
530
+ message: createDiscordMessage({
531
+ id: "m-dm-unresolved-1",
532
+ channelId: "dm-channel-unresolved-1",
533
+ content: "hello from a degraded dm",
534
+ author: {
535
+ id: "user-1",
536
+ bot: false,
537
+ username: "alice",
538
+ },
539
+ }),
540
+ discordConfig: {
541
+ dmPolicy: "open",
542
+ } as DiscordConfig,
543
+ });
544
+
545
+ expect(result).not.toBeNull();
546
+ expect(result?.channelInfo).toBeNull();
547
+ expect(result?.isDirectMessage).toBe(true);
548
+ expect(result?.isGroupDm).toBe(false);
549
+ expect(result?.route.sessionKey).toBe("agent:main:discord:direct:user-1");
550
+ });
551
+
552
+ it("falls back to the default discord account for omitted-account dm authorization", async () => {
553
+ const message = createDiscordMessage({
554
+ id: "m-dm-default-account",
555
+ channelId: "dm-channel-default-account",
556
+ content: "who are you",
557
+ author: {
558
+ id: "user-1",
559
+ bot: false,
560
+ username: "alice",
561
+ },
562
+ });
563
+
564
+ await preflightDiscordMessage({
565
+ ...createPreflightArgs({
566
+ cfg: {
567
+ ...DEFAULT_PREFLIGHT_CFG,
568
+ channels: {
569
+ discord: {
570
+ defaultAccount: "work",
571
+ accounts: {
572
+ default: {
573
+ token: "token-default",
574
+ },
575
+ work: {
576
+ token: "token-work",
577
+ },
578
+ },
579
+ },
580
+ },
581
+ },
582
+ discordConfig: {
583
+ defaultAccount: "work",
584
+ dmPolicy: "allowlist",
585
+ } as DiscordConfig,
586
+ data: {
587
+ channel_id: "dm-channel-default-account",
588
+ author: message.author,
589
+ message,
590
+ } as DiscordMessageEvent,
591
+ client: createDmClient("dm-channel-default-account"),
592
+ }),
593
+ });
594
+
595
+ expect(resolveDiscordDmCommandAccessMock).toHaveBeenCalledWith(
596
+ expect.objectContaining({
597
+ accountId: "default",
598
+ }),
599
+ );
600
+ });
601
+
602
+ it("keeps bound-thread regular bot messages flowing when allowBots=true", async () => {
603
+ const threadBinding = createThreadBinding({
604
+ targetKind: "session",
605
+ targetSessionKey: "agent:main:acp:discord-thread-1",
606
+ });
607
+ const threadId = "thread-bot-regular-1";
608
+ const parentId = "channel-parent-regular-1";
609
+ const message = createDiscordMessage({
610
+ id: "m-bot-regular-1",
611
+ channelId: threadId,
612
+ content: "here is tool output chunk",
613
+ author: {
614
+ id: "relay-bot-1",
615
+ bot: true,
616
+ username: "Relay",
617
+ },
618
+ });
619
+
620
+ const result = await runThreadBoundPreflight({
621
+ threadId,
622
+ parentId,
623
+ message,
624
+ threadBinding,
625
+ discordConfig: {
626
+ allowBots: true,
627
+ } as DiscordConfig,
628
+ registerBindingAdapter: true,
629
+ });
630
+
631
+ expect(result).not.toBeNull();
632
+ expect(result?.boundSessionKey).toBe(threadBinding.targetSessionKey);
633
+ });
634
+
635
+ it("drops hydrated bound-thread webhook copies after fetching an empty payload", async () => {
636
+ const threadBinding = createThreadBinding({
637
+ targetKind: "session",
638
+ targetSessionKey: "agent:main:acp:discord-thread-1",
639
+ });
640
+ const threadId = "thread-webhook-hydrated-1";
641
+ const parentId = "channel-parent-webhook-hydrated-1";
642
+ const message = createDiscordMessage({
643
+ id: "m-webhook-hydrated-1",
644
+ channelId: threadId,
645
+ content: "",
646
+ author: {
647
+ id: "relay-bot-1",
648
+ bot: true,
649
+ username: "Relay",
650
+ },
651
+ });
652
+ const restGet = vi.fn(async () => ({
653
+ id: message.id,
654
+ content: "webhook relay",
655
+ webhook_id: "wh-1",
656
+ attachments: [],
657
+ embeds: [],
658
+ mentions: [],
659
+ mention_roles: [],
660
+ mention_everyone: false,
661
+ author: {
662
+ id: "relay-bot-1",
663
+ username: "Relay",
664
+ bot: true,
665
+ },
666
+ }));
667
+ const client = Object.assign(createThreadClient({ threadId, parentId }), {
668
+ rest: {
669
+ get: restGet,
670
+ },
671
+ }) as unknown as DiscordClient;
672
+
673
+ const result = await preflightDiscordMessage({
674
+ ...createPreflightArgs({
675
+ cfg: DEFAULT_PREFLIGHT_CFG,
676
+ discordConfig: {
677
+ allowBots: true,
678
+ } as DiscordConfig,
679
+ data: createGuildEvent({
680
+ channelId: threadId,
681
+ guildId: "guild-1",
682
+ author: message.author,
683
+ message,
684
+ }),
685
+ client,
686
+ }),
687
+ threadBindings: {
688
+ getByThreadId: (id: string) => (id === threadId ? threadBinding : undefined),
689
+ } as import("./thread-bindings.js").ThreadBindingManager,
690
+ });
691
+
692
+ expect(restGet).toHaveBeenCalledTimes(1);
693
+ expect(result).toBeNull();
694
+ });
695
+
696
+ it("drops bound-thread webhook copies from other webhook ids", async () => {
697
+ const threadBinding = createThreadBinding({
698
+ targetKind: "session",
699
+ targetSessionKey: "agent:main:acp:discord-thread-1",
700
+ });
701
+ const threadId = "thread-webhook-proxy-1";
702
+ const parentId = "channel-parent-webhook-proxy-1";
703
+ const message = createDiscordMessage({
704
+ id: "m-webhook-proxy-1",
705
+ channelId: threadId,
706
+ content: "proxied user message",
707
+ webhookId: "pluralkit-webhook-1",
708
+ author: {
709
+ id: "relay-bot-1",
710
+ bot: true,
711
+ username: "Proxy",
712
+ },
713
+ });
714
+
715
+ const result = await runThreadBoundPreflight({
716
+ threadId,
717
+ parentId,
718
+ message,
719
+ threadBinding,
720
+ discordConfig: {
721
+ allowBots: true,
722
+ } as DiscordConfig,
723
+ });
724
+
725
+ expect(result).toBeNull();
726
+ });
727
+
728
+ it("canonicalizes PluralKit webhook messages to the original Discord message id", async () => {
729
+ fetchPluralKitMessageInfoMock.mockResolvedValue({
730
+ id: "proxy-456",
731
+ original: "orig-123",
732
+ member: { id: "member-1", name: "Echo" },
733
+ system: { id: "system-1", name: "System" },
734
+ });
735
+
736
+ const result = await runGuildPreflight({
737
+ channelId: "c1",
738
+ guildId: "g1",
739
+ message: createDiscordMessage({
740
+ id: "proxy-456",
741
+ channelId: "c1",
742
+ content: "<@openclaw-bot> hello",
743
+ webhookId: "pluralkit-webhook-1",
744
+ author: {
745
+ id: "webhook-author",
746
+ bot: true,
747
+ username: "PluralKit",
748
+ },
749
+ mentionedUsers: [{ id: "openclaw-bot" }],
750
+ }),
751
+ discordConfig: {
752
+ pluralkit: { enabled: true },
753
+ } as DiscordConfig,
754
+ });
755
+
756
+ expect(fetchPluralKitMessageInfoMock).toHaveBeenCalledWith(
757
+ expect.objectContaining({
758
+ messageId: "proxy-456",
759
+ config: expect.objectContaining({ enabled: true }),
760
+ }),
761
+ );
762
+ expect(result).not.toBeNull();
763
+ expect(result?.sender.isPluralKit).toBe(true);
764
+ expect(result?.canonicalMessageId).toBe("orig-123");
765
+ });
766
+
767
+ it("skips PluralKit lookup for bound-thread webhook echoes", async () => {
768
+ const threadBinding = createThreadBinding({
769
+ targetKind: "session",
770
+ targetSessionKey: "agent:main:acp:discord-thread-1",
771
+ });
772
+ const threadId = "thread-webhook-pk-echo-1";
773
+ const parentId = "channel-parent-webhook-pk-echo-1";
774
+
775
+ const result = await runThreadBoundPreflight({
776
+ threadId,
777
+ parentId,
778
+ threadBinding,
779
+ message: createDiscordMessage({
780
+ id: "m-webhook-pk-echo-1",
781
+ channelId: threadId,
782
+ content: "proxied user message",
783
+ webhookId: "pluralkit-webhook-1",
784
+ author: {
785
+ id: "relay-bot-1",
786
+ bot: true,
787
+ username: "Proxy",
788
+ },
789
+ }),
790
+ discordConfig: {
791
+ pluralkit: { enabled: true },
792
+ } as DiscordConfig,
793
+ });
794
+
795
+ expect(result).toBeNull();
796
+ expect(fetchPluralKitMessageInfoMock).not.toHaveBeenCalled();
797
+ });
798
+
799
+ it("bypasses mention gating in bound threads for allowed bot senders", async () => {
800
+ const threadBinding = createThreadBinding();
801
+ const threadId = "thread-bot-focus";
802
+ const parentId = "channel-parent-focus";
803
+ const client = createThreadClient({ threadId, parentId });
804
+ const message = createDiscordMessage({
805
+ id: "m-bot-1",
806
+ channelId: threadId,
807
+ content: "relay message without mention",
808
+ author: {
809
+ id: "relay-bot-1",
810
+ bot: true,
811
+ username: "Relay",
812
+ },
813
+ });
814
+
815
+ registerSessionBindingAdapter({
816
+ channel: "discord",
817
+ accountId: "default",
818
+ listBySession: () => [],
819
+ resolveByConversation: (ref) => (ref.conversationId === threadId ? threadBinding : null),
820
+ });
821
+
822
+ const result = await preflightDiscordMessage(
823
+ createPreflightArgs({
824
+ cfg: {
825
+ ...DEFAULT_PREFLIGHT_CFG,
826
+ } as import("openclaw/plugin-sdk/config-types").OpenClawConfig,
827
+ discordConfig: {
828
+ allowBots: true,
829
+ } as DiscordConfig,
830
+ data: createGuildEvent({
831
+ channelId: threadId,
832
+ guildId: "guild-1",
833
+ author: message.author,
834
+ message,
835
+ }),
836
+ client,
837
+ }),
838
+ );
839
+
840
+ expect(result).not.toBeNull();
841
+ expect(result?.boundSessionKey).toBe(threadBinding.targetSessionKey);
842
+ expect(result?.shouldRequireMention).toBe(false);
843
+ });
844
+
845
+ it("drops bot messages without mention when allowBots=mentions", async () => {
846
+ const channelId = "channel-bot-mentions-off";
847
+ const guildId = "guild-bot-mentions-off";
848
+ const message = createDiscordMessage({
849
+ id: "m-bot-mentions-off",
850
+ channelId,
851
+ content: "relay chatter",
852
+ author: {
853
+ id: "relay-bot-1",
854
+ bot: true,
855
+ username: "Relay",
856
+ },
857
+ });
858
+
859
+ const result = await runMentionOnlyBotPreflight({ channelId, guildId, message });
860
+
861
+ expect(result).toBeNull();
862
+ });
863
+
864
+ it("allows bot messages with explicit mention when allowBots=mentions", async () => {
865
+ const channelId = "channel-bot-mentions-on";
866
+ const guildId = "guild-bot-mentions-on";
867
+ const message = createDiscordMessage({
868
+ id: "m-bot-mentions-on",
869
+ channelId,
870
+ content: "hi <@openclaw-bot>",
871
+ mentionedUsers: [{ id: "openclaw-bot" }],
872
+ author: {
873
+ id: "relay-bot-1",
874
+ bot: true,
875
+ username: "Relay",
876
+ },
877
+ });
878
+
879
+ const result = await runMentionOnlyBotPreflight({ channelId, guildId, message });
880
+
881
+ expect(result).not.toBeNull();
882
+ });
883
+
884
+ it("hydrates mention metadata from REST when bot mention syntax is present but mentions are missing", async () => {
885
+ const channelId = "channel-bot-mentions-hydrated";
886
+ const guildId = "guild-bot-mentions-hydrated";
887
+ const botId = "123456789012345678";
888
+ const message = createDiscordMessage({
889
+ id: "m-bot-mentions-hydrated",
890
+ channelId,
891
+ content: `hi <@${botId}>`,
892
+ author: {
893
+ id: "relay-bot-1",
894
+ bot: true,
895
+ username: "Relay",
896
+ },
897
+ mentionedUsers: [],
898
+ });
899
+ const client = createGuildTextClient(channelId);
900
+ client.rest = {
901
+ get: vi.fn(async () => ({
902
+ id: message.id,
903
+ content: message.content,
904
+ mentions: [{ id: botId, username: "OpenClaw", bot: true }],
905
+ mention_roles: [],
906
+ mention_everyone: false,
907
+ })),
908
+ } as unknown as DiscordClient["rest"];
909
+
910
+ const result = await preflightDiscordMessage({
911
+ ...createPreflightArgs({
912
+ cfg: DEFAULT_PREFLIGHT_CFG,
913
+ discordConfig: {
914
+ allowBots: "mentions",
915
+ } as DiscordConfig,
916
+ data: createGuildEvent({
917
+ channelId,
918
+ guildId,
919
+ author: message.author,
920
+ message,
921
+ }),
922
+ client,
923
+ }),
924
+ botUserId: botId,
925
+ });
926
+
927
+ expect(result).not.toBeNull();
928
+ });
929
+
930
+ it("still drops bot control commands without a real mention when allowBots=mentions", async () => {
931
+ const channelId = "channel-bot-command-no-mention";
932
+ const guildId = "guild-bot-command-no-mention";
933
+ const message = createDiscordMessage({
934
+ id: "m-bot-command-no-mention",
935
+ channelId,
936
+ content: "/new incident room",
937
+ author: {
938
+ id: "relay-bot-1",
939
+ bot: true,
940
+ username: "Relay",
941
+ },
942
+ });
943
+
944
+ const result = await runMentionOnlyBotPreflight({ channelId, guildId, message });
945
+
946
+ expect(result).toBeNull();
947
+ });
948
+
949
+ it("still allows bot control commands with an explicit mention when allowBots=mentions", async () => {
950
+ const channelId = "channel-bot-command-with-mention";
951
+ const guildId = "guild-bot-command-with-mention";
952
+ const message = createDiscordMessage({
953
+ id: "m-bot-command-with-mention",
954
+ channelId,
955
+ content: "<@openclaw-bot> /new incident room",
956
+ mentionedUsers: [{ id: "openclaw-bot" }],
957
+ author: {
958
+ id: "relay-bot-1",
959
+ bot: true,
960
+ username: "Relay",
961
+ },
962
+ });
963
+
964
+ const result = await runMentionOnlyBotPreflight({ channelId, guildId, message });
965
+
966
+ expect(result).not.toBeNull();
967
+ });
968
+
969
+ it("does not mask mention gating when bot id is missing but mention patterns can detect", async () => {
970
+ const channelId = "channel-missing-bot-id-mention-gate";
971
+ const guildId = "guild-missing-bot-id-mention-gate";
972
+ const message = createDiscordMessage({
973
+ id: "m-missing-bot-id-mention-gate",
974
+ channelId,
975
+ content: "general update without the configured mention",
976
+ author: {
977
+ id: "user-1",
978
+ bot: false,
979
+ username: "Alice",
980
+ },
981
+ });
982
+
983
+ const result = await preflightDiscordMessage({
984
+ ...createPreflightArgs({
985
+ cfg: {
986
+ ...DEFAULT_PREFLIGHT_CFG,
987
+ messages: {
988
+ groupChat: {
989
+ mentionPatterns: ["openclaw"],
990
+ },
991
+ },
992
+ } as import("openclaw/plugin-sdk/config-types").OpenClawConfig,
993
+ discordConfig: {} as DiscordConfig,
994
+ data: createGuildEvent({
995
+ channelId,
996
+ guildId,
997
+ author: message.author,
998
+ message,
999
+ }),
1000
+ client: createGuildTextClient(channelId),
1001
+ }),
1002
+ botUserId: undefined,
1003
+ guildEntries: {
1004
+ [guildId]: {
1005
+ channels: {
1006
+ [channelId]: {
1007
+ enabled: true,
1008
+ requireMention: true,
1009
+ },
1010
+ },
1011
+ },
1012
+ },
1013
+ });
1014
+
1015
+ expect(result).toBeNull();
1016
+ });
1017
+
1018
+ it("treats @everyone as a mention when requireMention is true", async () => {
1019
+ const channelId = "channel-everyone-mention";
1020
+ const guildId = "guild-everyone-mention";
1021
+ const message = createDiscordMessage({
1022
+ id: "m-everyone-mention",
1023
+ channelId,
1024
+ content: "@everyone standup time!",
1025
+ mentionedEveryone: true,
1026
+ author: {
1027
+ id: "user-1",
1028
+ bot: false,
1029
+ username: "Peter",
1030
+ },
1031
+ });
1032
+
1033
+ const result = await runGuildPreflight({
1034
+ channelId,
1035
+ guildId,
1036
+ message,
1037
+ discordConfig: {
1038
+ botId: "openclaw-bot",
1039
+ } as DiscordConfig,
1040
+ guildEntries: {
1041
+ [guildId]: {
1042
+ channels: {
1043
+ [channelId]: {
1044
+ enabled: true,
1045
+ requireMention: true,
1046
+ },
1047
+ },
1048
+ },
1049
+ },
1050
+ });
1051
+
1052
+ expect(result).not.toBeNull();
1053
+ expect(result?.shouldRequireMention).toBe(true);
1054
+ expect(result?.wasMentioned).toBe(true);
1055
+ });
1056
+
1057
+ it("accepts allowlisted guild messages when guild object is missing", async () => {
1058
+ const message = createDiscordMessage({
1059
+ id: "m-guild-id-only",
1060
+ channelId: "ch-1",
1061
+ content: "hello from maintainers",
1062
+ author: {
1063
+ id: "user-1",
1064
+ bot: false,
1065
+ username: "Peter",
1066
+ },
1067
+ });
1068
+
1069
+ const result = await runGuildPreflight({
1070
+ channelId: "ch-1",
1071
+ guildId: "guild-1",
1072
+ message,
1073
+ discordConfig: {} as DiscordConfig,
1074
+ guildEntries: {
1075
+ "guild-1": {
1076
+ channels: {
1077
+ "ch-1": {
1078
+ enabled: true,
1079
+ requireMention: false,
1080
+ },
1081
+ },
1082
+ },
1083
+ },
1084
+ includeGuildObject: false,
1085
+ });
1086
+
1087
+ expect(result).not.toBeNull();
1088
+ expect(result?.guildInfo?.id).toBe("guild-1");
1089
+ expect(result?.channelConfig?.allowed).toBe(true);
1090
+ expect(result?.shouldRequireMention).toBe(false);
1091
+ });
1092
+
1093
+ it("inherits parent thread allowlist when guild object is missing", async () => {
1094
+ const threadId = "thread-1";
1095
+ const parentId = "parent-1";
1096
+ const message = createDiscordMessage({
1097
+ id: "m-thread-id-only",
1098
+ channelId: threadId,
1099
+ content: "thread hello",
1100
+ author: {
1101
+ id: "user-1",
1102
+ bot: false,
1103
+ username: "Peter",
1104
+ },
1105
+ });
1106
+
1107
+ const result = await preflightDiscordMessage({
1108
+ ...createPreflightArgs({
1109
+ cfg: DEFAULT_PREFLIGHT_CFG,
1110
+ discordConfig: {} as DiscordConfig,
1111
+ data: createGuildEvent({
1112
+ channelId: threadId,
1113
+ guildId: "guild-1",
1114
+ author: message.author,
1115
+ message,
1116
+ includeGuildObject: false,
1117
+ }),
1118
+ client: createThreadClient({
1119
+ threadId,
1120
+ parentId,
1121
+ }),
1122
+ }),
1123
+ guildEntries: {
1124
+ "guild-1": {
1125
+ channels: {
1126
+ [parentId]: {
1127
+ enabled: true,
1128
+ requireMention: false,
1129
+ },
1130
+ },
1131
+ },
1132
+ },
1133
+ });
1134
+
1135
+ expect(result).not.toBeNull();
1136
+ expect(result?.guildInfo?.id).toBe("guild-1");
1137
+ expect(result?.threadParentId).toBe(parentId);
1138
+ expect(result?.channelConfig?.allowed).toBe(true);
1139
+ expect(result?.shouldRequireMention).toBe(false);
1140
+ });
1141
+
1142
+ it("handles partial thread channel owner getters during mention preflight", async () => {
1143
+ const threadId = "thread-partial-owner";
1144
+ const parentId = "parent-partial-owner";
1145
+ const message = createDiscordMessage({
1146
+ id: "m-thread-partial-owner",
1147
+ channelId: threadId,
1148
+ content: "thread hello",
1149
+ author: {
1150
+ id: "user-1",
1151
+ bot: false,
1152
+ username: "Peter",
1153
+ },
1154
+ });
1155
+ Object.defineProperty(message, "channel", {
1156
+ value: createPartialDiscordChannelWithThrowingGetters(
1157
+ {
1158
+ id: threadId,
1159
+ isThread: () => true,
1160
+ ownerId: "owner-1",
1161
+ parentId,
1162
+ parent: { id: parentId, name: "general" },
1163
+ },
1164
+ ["ownerId", "parentId", "parent"],
1165
+ ),
1166
+ configurable: true,
1167
+ enumerable: true,
1168
+ });
1169
+
1170
+ const result = await preflightDiscordMessage({
1171
+ ...createPreflightArgs({
1172
+ cfg: DEFAULT_PREFLIGHT_CFG,
1173
+ discordConfig: {} as DiscordConfig,
1174
+ data: createGuildEvent({
1175
+ channelId: threadId,
1176
+ guildId: "guild-1",
1177
+ author: message.author,
1178
+ message,
1179
+ includeGuildObject: false,
1180
+ }),
1181
+ client: createThreadClient({
1182
+ threadId,
1183
+ parentId,
1184
+ }),
1185
+ }),
1186
+ guildEntries: {
1187
+ "guild-1": {
1188
+ channels: {
1189
+ [parentId]: {
1190
+ enabled: true,
1191
+ requireMention: false,
1192
+ },
1193
+ },
1194
+ },
1195
+ },
1196
+ });
1197
+
1198
+ expect(result).not.toBeNull();
1199
+ expect(result?.threadParentId).toBe(parentId);
1200
+ expect(result?.shouldRequireMention).toBe(false);
1201
+ });
1202
+
1203
+ it("drops guild messages that mention another user when ignoreOtherMentions=true", async () => {
1204
+ const channelId = "channel-other-mention-1";
1205
+ const guildId = "guild-other-mention-1";
1206
+ const message = createDiscordMessage({
1207
+ id: "m-other-mention-1",
1208
+ channelId,
1209
+ content: "hello <@999>",
1210
+ mentionedUsers: [{ id: "999" }],
1211
+ author: {
1212
+ id: "user-1",
1213
+ bot: false,
1214
+ username: "Alice",
1215
+ },
1216
+ });
1217
+
1218
+ const result = await runIgnoreOtherMentionsPreflight({ channelId, guildId, message });
1219
+
1220
+ expect(result).toBeNull();
1221
+ });
1222
+
1223
+ it("does not drop @everyone messages when ignoreOtherMentions=true", async () => {
1224
+ const channelId = "channel-other-mention-everyone";
1225
+ const guildId = "guild-other-mention-everyone";
1226
+ const message = createDiscordMessage({
1227
+ id: "m-other-mention-everyone",
1228
+ channelId,
1229
+ content: "@everyone heads up",
1230
+ mentionedEveryone: true,
1231
+ author: {
1232
+ id: "user-1",
1233
+ bot: false,
1234
+ username: "Alice",
1235
+ },
1236
+ });
1237
+
1238
+ const result = await runIgnoreOtherMentionsPreflight({ channelId, guildId, message });
1239
+
1240
+ expect(result).not.toBeNull();
1241
+ expect(result?.hasAnyMention).toBe(true);
1242
+ });
1243
+
1244
+ it("ignores bot-sent @everyone mentions for detection", async () => {
1245
+ const channelId = "channel-everyone-1";
1246
+ const guildId = "guild-everyone-1";
1247
+ const client = createGuildTextClient(channelId);
1248
+ const message = createDiscordMessage({
1249
+ id: "m-everyone-1",
1250
+ channelId,
1251
+ content: "@everyone heads up",
1252
+ mentionedEveryone: true,
1253
+ author: {
1254
+ id: "relay-bot-1",
1255
+ bot: true,
1256
+ username: "Relay",
1257
+ },
1258
+ });
1259
+
1260
+ const result = await preflightDiscordMessage({
1261
+ ...createPreflightArgs({
1262
+ cfg: DEFAULT_PREFLIGHT_CFG,
1263
+ discordConfig: {
1264
+ allowBots: true,
1265
+ } as DiscordConfig,
1266
+ data: createGuildEvent({
1267
+ channelId,
1268
+ guildId,
1269
+ author: message.author,
1270
+ message,
1271
+ }),
1272
+ client,
1273
+ }),
1274
+ guildEntries: {
1275
+ [guildId]: {
1276
+ requireMention: false,
1277
+ },
1278
+ },
1279
+ });
1280
+
1281
+ expect(result).not.toBeNull();
1282
+ expect(result?.hasAnyMention).toBe(false);
1283
+ });
1284
+
1285
+ it("does not treat bot-sent @everyone as wasMentioned", async () => {
1286
+ const channelId = "channel-everyone-2";
1287
+ const guildId = "guild-everyone-2";
1288
+ const client = createGuildTextClient(channelId);
1289
+ const message = createDiscordMessage({
1290
+ id: "m-everyone-2",
1291
+ channelId,
1292
+ content: "@everyone relay message",
1293
+ mentionedEveryone: true,
1294
+ author: {
1295
+ id: "relay-bot-2",
1296
+ bot: true,
1297
+ username: "RelayBot",
1298
+ },
1299
+ });
1300
+
1301
+ const result = await preflightDiscordMessage({
1302
+ ...createPreflightArgs({
1303
+ cfg: DEFAULT_PREFLIGHT_CFG,
1304
+ discordConfig: {
1305
+ allowBots: true,
1306
+ } as DiscordConfig,
1307
+ data: createGuildEvent({
1308
+ channelId,
1309
+ guildId,
1310
+ author: message.author,
1311
+ message,
1312
+ }),
1313
+ client,
1314
+ }),
1315
+ guildEntries: {
1316
+ [guildId]: {
1317
+ requireMention: false,
1318
+ },
1319
+ },
1320
+ });
1321
+
1322
+ expect(result).not.toBeNull();
1323
+ expect(result?.wasMentioned).toBe(false);
1324
+ });
1325
+
1326
+ it("uses attachment content_type for guild audio preflight mention detection", async () => {
1327
+ transcribeFirstAudioMock.mockResolvedValue("hey openclaw");
1328
+
1329
+ const channelId = "channel-audio-1";
1330
+ const client = createGuildTextClient(channelId);
1331
+
1332
+ const message = createDiscordMessage({
1333
+ id: "m-audio-1",
1334
+ channelId,
1335
+ content: "",
1336
+ attachments: [
1337
+ {
1338
+ id: "att-1",
1339
+ url: "https://cdn.discordapp.com/attachments/voice.ogg",
1340
+ content_type: "audio/ogg",
1341
+ filename: "voice.ogg",
1342
+ },
1343
+ ],
1344
+ author: {
1345
+ id: "user-1",
1346
+ bot: false,
1347
+ username: "Alice",
1348
+ },
1349
+ });
1350
+
1351
+ const result = await preflightDiscordMessage({
1352
+ ...createPreflightArgs({
1353
+ cfg: {
1354
+ ...DEFAULT_PREFLIGHT_CFG,
1355
+ messages: {
1356
+ groupChat: {
1357
+ mentionPatterns: ["openclaw"],
1358
+ },
1359
+ },
1360
+ } as import("openclaw/plugin-sdk/config-types").OpenClawConfig,
1361
+ discordConfig: {} as DiscordConfig,
1362
+ data: createGuildEvent({
1363
+ channelId,
1364
+ guildId: "guild-1",
1365
+ author: message.author,
1366
+ message,
1367
+ }),
1368
+ client,
1369
+ }),
1370
+ guildEntries: {
1371
+ "guild-1": {
1372
+ channels: {
1373
+ [channelId]: {
1374
+ enabled: true,
1375
+ requireMention: true,
1376
+ },
1377
+ },
1378
+ },
1379
+ },
1380
+ });
1381
+
1382
+ expect(transcribeFirstAudioMock).toHaveBeenCalledTimes(1);
1383
+ expect(transcribeFirstAudioMock).toHaveBeenCalledWith(
1384
+ expect.objectContaining({
1385
+ ctx: expect.objectContaining({
1386
+ MediaUrls: ["https://cdn.discordapp.com/attachments/voice.ogg"],
1387
+ MediaTypes: ["audio/ogg"],
1388
+ }),
1389
+ }),
1390
+ );
1391
+ expect(result).not.toBeNull();
1392
+ expect(result?.wasMentioned).toBe(true);
1393
+ expect(result?.preflightAudioTranscript).toBe("hey openclaw");
1394
+ });
1395
+
1396
+ it("does not transcribe guild audio from unauthorized members", async () => {
1397
+ const channelId = "channel-audio-unauthorized-1";
1398
+ const guildId = "guild-audio-unauthorized-1";
1399
+ const client = createGuildTextClient(channelId);
1400
+
1401
+ const message = createDiscordMessage({
1402
+ id: "m-audio-unauthorized-1",
1403
+ channelId,
1404
+ content: "",
1405
+ attachments: [
1406
+ {
1407
+ id: "att-1",
1408
+ url: "https://cdn.discordapp.com/attachments/voice.ogg",
1409
+ content_type: "audio/ogg",
1410
+ filename: "voice.ogg",
1411
+ },
1412
+ ],
1413
+ author: {
1414
+ id: "user-2",
1415
+ bot: false,
1416
+ username: "Mallory",
1417
+ },
1418
+ });
1419
+
1420
+ const result = await preflightDiscordMessage({
1421
+ ...createPreflightArgs({
1422
+ cfg: {
1423
+ ...DEFAULT_PREFLIGHT_CFG,
1424
+ messages: {
1425
+ groupChat: {
1426
+ mentionPatterns: ["openclaw"],
1427
+ },
1428
+ },
1429
+ } as import("openclaw/plugin-sdk/config-types").OpenClawConfig,
1430
+ discordConfig: {} as DiscordConfig,
1431
+ data: createGuildEvent({
1432
+ channelId,
1433
+ guildId,
1434
+ author: message.author,
1435
+ message,
1436
+ }),
1437
+ client,
1438
+ }),
1439
+ guildEntries: {
1440
+ [guildId]: {
1441
+ channels: {
1442
+ [channelId]: {
1443
+ enabled: true,
1444
+ requireMention: true,
1445
+ users: ["user-1"],
1446
+ },
1447
+ },
1448
+ },
1449
+ },
1450
+ });
1451
+
1452
+ expect(transcribeFirstAudioMock).not.toHaveBeenCalled();
1453
+ expect(result).toBeNull();
1454
+ });
1455
+
1456
+ it("drops guild message without mention when channel has configuredBinding and requireMention: true", async () => {
1457
+ const conversationRuntime = await import("openclaw/plugin-sdk/conversation-runtime");
1458
+ const channelId = "ch-binding-1";
1459
+ const bindingRoute = {
1460
+ bindingResolution: {
1461
+ record: {
1462
+ targetSessionKey: "agent:main:acp:binding:discord:default:abc",
1463
+ targetKind: "session",
1464
+ },
1465
+ } as never,
1466
+ route: { agentId: "main", matchedBy: "binding.channel" } as never,
1467
+ boundSessionKey: "agent:main:acp:binding:discord:default:abc",
1468
+ boundAgentId: "main",
1469
+ };
1470
+ const routeSpy = vi
1471
+ .spyOn(conversationRuntime, "resolveConfiguredBindingRoute")
1472
+ .mockReturnValue(bindingRoute);
1473
+ const ensureSpy = vi
1474
+ .spyOn(conversationRuntime, "ensureConfiguredBindingRouteReady")
1475
+ .mockResolvedValue({ ok: true });
1476
+
1477
+ try {
1478
+ const result = await runGuildPreflight({
1479
+ channelId,
1480
+ guildId: "guild-1",
1481
+ message: createDiscordMessage({
1482
+ id: "m-binding-1",
1483
+ channelId,
1484
+ content: "hello without mention",
1485
+ author: { id: "user-1", bot: false, username: "alice" },
1486
+ }),
1487
+ discordConfig: {} as DiscordConfig,
1488
+ guildEntries: {
1489
+ "guild-1": { channels: { [channelId]: { enabled: true, requireMention: true } } },
1490
+ },
1491
+ });
1492
+ expect(result).toBeNull();
1493
+ } finally {
1494
+ routeSpy.mockRestore();
1495
+ ensureSpy.mockRestore();
1496
+ }
1497
+ });
1498
+
1499
+ it("allows guild message with mention when channel has configuredBinding and requireMention: true", async () => {
1500
+ const conversationRuntime = await import("openclaw/plugin-sdk/conversation-runtime");
1501
+ const channelId = "ch-binding-2";
1502
+ const bindingRoute = {
1503
+ bindingResolution: {
1504
+ record: {
1505
+ targetSessionKey: "agent:main:acp:binding:discord:default:def",
1506
+ targetKind: "session",
1507
+ },
1508
+ } as never,
1509
+ route: { agentId: "main", matchedBy: "binding.channel" } as never,
1510
+ boundSessionKey: "agent:main:acp:binding:discord:default:def",
1511
+ boundAgentId: "main",
1512
+ };
1513
+ const routeSpy = vi
1514
+ .spyOn(conversationRuntime, "resolveConfiguredBindingRoute")
1515
+ .mockReturnValue(bindingRoute);
1516
+ const ensureSpy = vi
1517
+ .spyOn(conversationRuntime, "ensureConfiguredBindingRouteReady")
1518
+ .mockResolvedValue({ ok: true });
1519
+
1520
+ try {
1521
+ const result = await runGuildPreflight({
1522
+ channelId,
1523
+ guildId: "guild-1",
1524
+ message: createDiscordMessage({
1525
+ id: "m-binding-2",
1526
+ channelId,
1527
+ content: "hello <@openclaw-bot>",
1528
+ author: { id: "user-1", bot: false, username: "alice" },
1529
+ mentionedUsers: [{ id: "openclaw-bot" }],
1530
+ }),
1531
+ discordConfig: {} as DiscordConfig,
1532
+ guildEntries: {
1533
+ "guild-1": { channels: { [channelId]: { enabled: true, requireMention: true } } },
1534
+ },
1535
+ });
1536
+ expect(result).not.toBeNull();
1537
+ } finally {
1538
+ routeSpy.mockRestore();
1539
+ ensureSpy.mockRestore();
1540
+ }
1541
+ });
1542
+ });
1543
+
1544
+ describe("shouldIgnoreBoundThreadWebhookMessage", () => {
1545
+ beforeEach(() => {
1546
+ sessionBindingTesting.resetSessionBindingAdaptersForTests();
1547
+ threadBindingTesting.resetThreadBindingsForTests();
1548
+ });
1549
+
1550
+ it("returns true when inbound webhook id matches the bound thread webhook", () => {
1551
+ expect(
1552
+ shouldIgnoreBoundThreadWebhookMessage({
1553
+ webhookId: "wh-1",
1554
+ threadBinding: createThreadBinding(),
1555
+ }),
1556
+ ).toBe(true);
1557
+ });
1558
+
1559
+ it("returns true when a bound thread receives a different webhook id", () => {
1560
+ expect(
1561
+ shouldIgnoreBoundThreadWebhookMessage({
1562
+ threadId: "thread-1",
1563
+ webhookId: "wh-other",
1564
+ threadBinding: createThreadBinding(),
1565
+ }),
1566
+ ).toBe(true);
1567
+ });
1568
+
1569
+ it("returns true when a bound thread receives a webhook without a recorded bound webhook id", () => {
1570
+ expect(
1571
+ shouldIgnoreBoundThreadWebhookMessage({
1572
+ threadId: "thread-1",
1573
+ webhookId: "wh-1",
1574
+ threadBinding: createThreadBinding({
1575
+ metadata: {
1576
+ webhookId: undefined,
1577
+ },
1578
+ }),
1579
+ }),
1580
+ ).toBe(true);
1581
+ });
1582
+
1583
+ it("returns false for differing webhook ids without a known thread id", () => {
1584
+ expect(
1585
+ shouldIgnoreBoundThreadWebhookMessage({
1586
+ webhookId: "wh-other",
1587
+ threadBinding: createThreadBinding(),
1588
+ }),
1589
+ ).toBe(false);
1590
+ });
1591
+
1592
+ it("returns true for recently unbound thread webhook echoes", async () => {
1593
+ const manager = createThreadBindingManager({
1594
+ cfg: DEFAULT_PREFLIGHT_CFG,
1595
+ accountId: "default",
1596
+ persist: false,
1597
+ enableSweeper: false,
1598
+ });
1599
+ const binding = await manager.bindTarget({
1600
+ threadId: "thread-1",
1601
+ channelId: "parent-1",
1602
+ targetKind: "subagent",
1603
+ targetSessionKey: "agent:main:subagent:child-1",
1604
+ agentId: "main",
1605
+ webhookId: "wh-1",
1606
+ webhookToken: "tok-1",
1607
+ });
1608
+ expect(binding).not.toBeNull();
1609
+
1610
+ manager.unbindThread({
1611
+ threadId: "thread-1",
1612
+ sendFarewell: false,
1613
+ });
1614
+
1615
+ expect(
1616
+ shouldIgnoreBoundThreadWebhookMessage({
1617
+ accountId: "default",
1618
+ threadId: "thread-1",
1619
+ webhookId: "wh-1",
1620
+ }),
1621
+ ).toBe(true);
1622
+ });
1623
+ });