@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,294 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import * as httpsProxyAgent from "https-proxy-agent";
3
+ import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-types";
4
+ import {
5
+ captureWsEvent,
6
+ resolveEffectiveDebugProxyUrl,
7
+ resolveDebugProxySettings,
8
+ } from "openclaw/plugin-sdk/proxy-capture";
9
+ import { danger } from "openclaw/plugin-sdk/runtime-env";
10
+ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
11
+ import * as ws from "ws";
12
+ import * as discordGateway from "../internal/gateway.js";
13
+ import { validateDiscordProxyUrl } from "../proxy-fetch.js";
14
+ import { resolveDiscordVoiceEnabled } from "../voice/config.js";
15
+ import { DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT } from "./gateway-handle.js";
16
+ import {
17
+ fetchDiscordGatewayInfoWithTimeout,
18
+ fetchDiscordGatewayMetadataDirect,
19
+ resolveDiscordGatewayInfoTimeoutMs,
20
+ resolveGatewayInfoWithFallback,
21
+ type DiscordGatewayFetch,
22
+ type DiscordGatewayFetchInit,
23
+ } from "./gateway-metadata.js";
24
+
25
+ export {
26
+ parseDiscordGatewayInfoBody,
27
+ resolveDiscordGatewayInfoTimeoutMs,
28
+ } from "./gateway-metadata.js";
29
+
30
+ const DISCORD_GATEWAY_HANDSHAKE_TIMEOUT_MS = 30_000;
31
+
32
+ type DiscordGatewayWebSocketCtor = new (
33
+ url: string,
34
+ options?: { agent?: unknown; handshakeTimeout?: number },
35
+ ) => ws.WebSocket;
36
+ const registrationPromises = new WeakMap<discordGateway.GatewayPlugin, Promise<void>>();
37
+ type DiscordGatewayClient = Parameters<discordGateway.GatewayPlugin["registerClient"]>[0];
38
+ type GatewayPluginTestingOptions = {
39
+ registerClient?: (
40
+ plugin: discordGateway.GatewayPlugin,
41
+ client: DiscordGatewayClient,
42
+ ) => Promise<void>;
43
+ webSocketCtor?: DiscordGatewayWebSocketCtor;
44
+ };
45
+ type CreateDiscordGatewayPluginTestingOptions = GatewayPluginTestingOptions & {
46
+ HttpsProxyAgentCtor?: typeof httpsProxyAgent.HttpsProxyAgent;
47
+ };
48
+ type DiscordGatewayRegistrationState = {
49
+ client?: DiscordGatewayClient;
50
+ ws?: unknown;
51
+ isConnecting?: boolean;
52
+ };
53
+
54
+ function assignGatewayClient(
55
+ plugin: discordGateway.GatewayPlugin,
56
+ client: DiscordGatewayClient,
57
+ ): void {
58
+ (plugin as unknown as DiscordGatewayRegistrationState).client = client;
59
+ }
60
+
61
+ function hasGatewaySocketStarted(plugin: discordGateway.GatewayPlugin): boolean {
62
+ const state = plugin as unknown as DiscordGatewayRegistrationState;
63
+ return state.ws != null || state.isConnecting === true;
64
+ }
65
+
66
+ type ResolveDiscordGatewayIntentsParams = {
67
+ intentsConfig?: import("openclaw/plugin-sdk/config-types").DiscordIntentsConfig;
68
+ voiceEnabled?: boolean;
69
+ };
70
+
71
+ export function resolveDiscordGatewayIntents(params?: ResolveDiscordGatewayIntentsParams): number {
72
+ const intentsConfig = params?.intentsConfig;
73
+ const voiceEnabled = params?.voiceEnabled;
74
+ const voiceStatesEnabled = intentsConfig?.voiceStates ?? voiceEnabled ?? false;
75
+ let intents =
76
+ discordGateway.GatewayIntents.Guilds |
77
+ discordGateway.GatewayIntents.GuildMessages |
78
+ discordGateway.GatewayIntents.MessageContent |
79
+ discordGateway.GatewayIntents.DirectMessages |
80
+ discordGateway.GatewayIntents.GuildMessageReactions |
81
+ discordGateway.GatewayIntents.DirectMessageReactions;
82
+ if (voiceStatesEnabled) {
83
+ intents |= discordGateway.GatewayIntents.GuildVoiceStates;
84
+ }
85
+ if (intentsConfig?.presence) {
86
+ intents |= discordGateway.GatewayIntents.GuildPresences;
87
+ }
88
+ if (intentsConfig?.guildMembers) {
89
+ intents |= discordGateway.GatewayIntents.GuildMembers;
90
+ }
91
+ return intents;
92
+ }
93
+
94
+ function createGatewayPlugin(params: {
95
+ options: {
96
+ reconnect: { maxAttempts: number };
97
+ intents: number;
98
+ autoInteractions: boolean;
99
+ };
100
+ gatewayInfoTimeoutMs: number;
101
+ fetchImpl: DiscordGatewayFetch;
102
+ fetchInit?: DiscordGatewayFetchInit;
103
+ wsAgent?: InstanceType<typeof httpsProxyAgent.HttpsProxyAgent<string>>;
104
+ runtime?: RuntimeEnv;
105
+ testing?: GatewayPluginTestingOptions;
106
+ }): discordGateway.GatewayPlugin {
107
+ class OpenClawGatewayPlugin extends discordGateway.GatewayPlugin {
108
+ private gatewayInfoUsedFallback = false;
109
+
110
+ constructor() {
111
+ super(params.options);
112
+ }
113
+
114
+ override registerClient(client: DiscordGatewayClient) {
115
+ const registration = this.registerClientInternal(client);
116
+ // Client construction starts plugin hooks without awaiting them. Mark the
117
+ // promise handled immediately, then let startup await the original promise.
118
+ registration.catch(() => {});
119
+ registrationPromises.set(this, registration);
120
+ return registration;
121
+ }
122
+
123
+ private async registerClientInternal(client: DiscordGatewayClient) {
124
+ // Publish the client reference before the metadata fetch can yield, so an external
125
+ // connect()->identify() cannot silently drop IDENTIFY (#52372).
126
+ assignGatewayClient(this, client);
127
+
128
+ if (!this.gatewayInfo || this.gatewayInfoUsedFallback) {
129
+ const resolved = await fetchDiscordGatewayInfoWithTimeout({
130
+ token: client.options.token,
131
+ fetchImpl: params.fetchImpl,
132
+ fetchInit: params.fetchInit,
133
+ timeoutMs: params.gatewayInfoTimeoutMs,
134
+ })
135
+ .then((info) => ({
136
+ info,
137
+ usedFallback: false,
138
+ }))
139
+ .catch((error) => resolveGatewayInfoWithFallback({ runtime: params.runtime, error }));
140
+ this.gatewayInfo = resolved.info;
141
+ this.gatewayInfoUsedFallback = resolved.usedFallback;
142
+ }
143
+ if (params.testing?.registerClient) {
144
+ await params.testing.registerClient(this, client);
145
+ return;
146
+ }
147
+ // If the lifecycle timeout already started a socket while metadata was
148
+ // loading, do not register again; it would close that socket and open another one.
149
+ if (hasGatewaySocketStarted(this)) {
150
+ return;
151
+ }
152
+ return super.registerClient(client);
153
+ }
154
+
155
+ override createWebSocket(url: string) {
156
+ if (!url) {
157
+ throw new Error("Gateway URL is required");
158
+ }
159
+ const wsFlowId = randomUUID();
160
+ // Avoid Node's undici-backed global WebSocket here. We have seen late
161
+ // close-path crashes during Discord gateway teardown; the ws transport is
162
+ // already our proxy path and behaves predictably for lifecycle cleanup.
163
+ const WebSocketCtor = params.testing?.webSocketCtor ?? ws.default;
164
+ const socket = new WebSocketCtor(url, {
165
+ handshakeTimeout: DISCORD_GATEWAY_HANDSHAKE_TIMEOUT_MS,
166
+ ...(params.wsAgent ? { agent: params.wsAgent } : {}),
167
+ });
168
+ const emitTransportActivity = () => {
169
+ if ((this as unknown as { ws?: unknown }).ws !== socket) {
170
+ return;
171
+ }
172
+ this.emitter.emit(DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT, { at: Date.now() });
173
+ };
174
+ captureWsEvent({
175
+ url,
176
+ direction: "local",
177
+ kind: "ws-open",
178
+ flowId: wsFlowId,
179
+ meta: { subsystem: "discord-gateway" },
180
+ });
181
+ socket.on?.("message", (data: unknown) => {
182
+ emitTransportActivity();
183
+ captureWsEvent({
184
+ url,
185
+ direction: "inbound",
186
+ kind: "ws-frame",
187
+ flowId: wsFlowId,
188
+ payload: Buffer.isBuffer(data) ? data : Buffer.from(String(data)),
189
+ meta: { subsystem: "discord-gateway" },
190
+ });
191
+ });
192
+ socket.on?.("close", (code: number, reason: Buffer) => {
193
+ captureWsEvent({
194
+ url,
195
+ direction: "local",
196
+ kind: "ws-close",
197
+ flowId: wsFlowId,
198
+ closeCode: code,
199
+ payload: reason,
200
+ meta: { subsystem: "discord-gateway" },
201
+ });
202
+ });
203
+ socket.on?.("error", (error: Error) => {
204
+ captureWsEvent({
205
+ url,
206
+ direction: "local",
207
+ kind: "error",
208
+ flowId: wsFlowId,
209
+ errorText: error.message,
210
+ meta: { subsystem: "discord-gateway" },
211
+ });
212
+ });
213
+ if ("binaryType" in socket) {
214
+ try {
215
+ socket.binaryType = "arraybuffer";
216
+ } catch {
217
+ // Ignore runtimes that expose a readonly binaryType.
218
+ }
219
+ }
220
+ return socket;
221
+ }
222
+ }
223
+
224
+ return new OpenClawGatewayPlugin();
225
+ }
226
+
227
+ function createDiscordGatewayMetadataFetch(debugCaptureEnabled: boolean): DiscordGatewayFetch {
228
+ return (input, init) =>
229
+ fetchDiscordGatewayMetadataDirect(
230
+ input,
231
+ init,
232
+ debugCaptureEnabled
233
+ ? false
234
+ : {
235
+ flowId: randomUUID(),
236
+ meta: { subsystem: "discord-gateway-metadata" },
237
+ },
238
+ );
239
+ }
240
+
241
+ export function waitForDiscordGatewayPluginRegistration(
242
+ plugin: unknown,
243
+ ): Promise<void> | undefined {
244
+ if (typeof plugin !== "object" || plugin === null) {
245
+ return undefined;
246
+ }
247
+ return registrationPromises.get(plugin as discordGateway.GatewayPlugin);
248
+ }
249
+
250
+ export function createDiscordGatewayPlugin(params: {
251
+ discordConfig: DiscordAccountConfig;
252
+ runtime: RuntimeEnv;
253
+ __testing?: CreateDiscordGatewayPluginTestingOptions;
254
+ }): discordGateway.GatewayPlugin {
255
+ const intents = resolveDiscordGatewayIntents({
256
+ intentsConfig: params.discordConfig?.intents,
257
+ voiceEnabled: resolveDiscordVoiceEnabled(params.discordConfig?.voice),
258
+ });
259
+ const proxy = resolveEffectiveDebugProxyUrl(params.discordConfig?.proxy);
260
+ const debugProxySettings = resolveDebugProxySettings();
261
+ const gatewayInfoTimeoutMs = resolveDiscordGatewayInfoTimeoutMs({
262
+ configuredTimeoutMs: params.discordConfig?.gatewayInfoTimeoutMs,
263
+ env: process.env,
264
+ });
265
+ let fetchImpl = createDiscordGatewayMetadataFetch(debugProxySettings.enabled);
266
+ let wsAgent: InstanceType<typeof httpsProxyAgent.HttpsProxyAgent<string>> | undefined;
267
+
268
+ if (proxy) {
269
+ try {
270
+ validateDiscordProxyUrl(proxy);
271
+ const HttpsProxyAgentCtor =
272
+ params.__testing?.HttpsProxyAgentCtor ?? httpsProxyAgent.HttpsProxyAgent;
273
+ wsAgent = new HttpsProxyAgentCtor<string>(proxy);
274
+ params.runtime.log?.("discord: gateway proxy enabled");
275
+ } catch (err) {
276
+ params.runtime.error?.(danger(`discord: invalid gateway proxy: ${String(err)}`));
277
+ fetchImpl = (input, init) => fetchDiscordGatewayMetadataDirect(input, init, false);
278
+ }
279
+ }
280
+
281
+ return createGatewayPlugin({
282
+ options: {
283
+ reconnect: { maxAttempts: 50 },
284
+ intents,
285
+ // OpenClaw registers its own async interaction listener.
286
+ autoInteractions: false,
287
+ },
288
+ gatewayInfoTimeoutMs,
289
+ fetchImpl,
290
+ runtime: params.runtime,
291
+ testing: params.__testing,
292
+ ...(wsAgent ? { wsAgent } : {}),
293
+ });
294
+ }
@@ -0,0 +1,37 @@
1
+ import type { GatewayPlugin } from "../internal/gateway.js";
2
+
3
+ /**
4
+ * Module-level registry of active Discord GatewayPlugin instances.
5
+ * Bridges the gap between agent tool handlers (which only have REST access)
6
+ * and the gateway WebSocket (needed for operations like updatePresence).
7
+ * Follows the same pattern as presence-cache.ts.
8
+ */
9
+ const gatewayRegistry = new Map<string, GatewayPlugin>();
10
+
11
+ // Sentinel key for the default (unnamed) account. Uses a prefix that cannot
12
+ // collide with user-configured account IDs.
13
+ const DEFAULT_ACCOUNT_KEY = "\0__default__";
14
+
15
+ function resolveAccountKey(accountId?: string): string {
16
+ return accountId ?? DEFAULT_ACCOUNT_KEY;
17
+ }
18
+
19
+ /** Register a GatewayPlugin instance for an account. */
20
+ export function registerGateway(accountId: string | undefined, gateway: GatewayPlugin): void {
21
+ gatewayRegistry.set(resolveAccountKey(accountId), gateway);
22
+ }
23
+
24
+ /** Unregister a GatewayPlugin instance for an account. */
25
+ export function unregisterGateway(accountId?: string): void {
26
+ gatewayRegistry.delete(resolveAccountKey(accountId));
27
+ }
28
+
29
+ /** Get the GatewayPlugin for an account. Returns undefined if not registered. */
30
+ export function getGateway(accountId?: string): GatewayPlugin | undefined {
31
+ return gatewayRegistry.get(resolveAccountKey(accountId));
32
+ }
33
+
34
+ /** Clear all registered gateways (for testing). */
35
+ export function clearGateways(): void {
36
+ gatewayRegistry.clear();
37
+ }
@@ -0,0 +1,150 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import {
4
+ classifyDiscordGatewayEvent,
5
+ DiscordGatewayLifecycleError,
6
+ createDiscordGatewaySupervisor,
7
+ } from "./gateway-supervisor.js";
8
+
9
+ describe("classifyDiscordGatewayEvent", () => {
10
+ it("maps current gateway errors onto domain events", () => {
11
+ const transientTypeError = new TypeError();
12
+ transientTypeError.stack = "TypeError\n at gatewayCrash (discord-gateway.js:12:34)";
13
+ const reconnectEvent = classifyDiscordGatewayEvent({
14
+ err: new Error("Max reconnect attempts (0) reached after close code 1006"),
15
+ isDisallowedIntentsError: () => false,
16
+ });
17
+ const fatalEvent = classifyDiscordGatewayEvent({
18
+ err: new Error("Fatal gateway close code: 4000"),
19
+ isDisallowedIntentsError: () => false,
20
+ });
21
+ const disallowedEvent = classifyDiscordGatewayEvent({
22
+ err: new Error("Fatal gateway close code: 4014"),
23
+ isDisallowedIntentsError: (err) => String(err).includes("4014"),
24
+ });
25
+ const transientEvent = classifyDiscordGatewayEvent({
26
+ err: transientTypeError,
27
+ isDisallowedIntentsError: () => false,
28
+ });
29
+
30
+ expect(reconnectEvent.type).toBe("reconnect-exhausted");
31
+ expect(reconnectEvent.shouldStopLifecycle).toBe(true);
32
+ expect(fatalEvent.type).toBe("fatal");
33
+ expect(disallowedEvent.type).toBe("disallowed-intents");
34
+ expect(transientEvent.type).toBe("fatal");
35
+ expect(transientEvent.message).toBe("TypeError @ gatewayCrash (discord-gateway.js:12:34)");
36
+ expect(transientEvent.shouldStopLifecycle).toBe(true);
37
+ });
38
+
39
+ it("wraps fatal lifecycle stops with discord-specific context", () => {
40
+ const transientTypeError = new TypeError();
41
+ transientTypeError.stack = "TypeError\n at gatewayCrash (discord-gateway.js:12:34)";
42
+ const event = classifyDiscordGatewayEvent({
43
+ err: transientTypeError,
44
+ isDisallowedIntentsError: () => false,
45
+ });
46
+
47
+ const wrapped = new DiscordGatewayLifecycleError(event);
48
+
49
+ expect(wrapped.name).toBe("DiscordGatewayLifecycleError");
50
+ expect(wrapped.message).toBe(
51
+ "discord gateway fatal: TypeError @ gatewayCrash (discord-gateway.js:12:34)",
52
+ );
53
+ expect(wrapped.eventType).toBe("fatal");
54
+ expect(wrapped.cause).toBeInstanceOf(TypeError);
55
+ });
56
+ });
57
+
58
+ describe("createDiscordGatewaySupervisor", () => {
59
+ it("buffers early errors, routes active ones, and logs late teardown errors", () => {
60
+ const emitter = new EventEmitter();
61
+ const runtime = {
62
+ error: vi.fn(),
63
+ };
64
+ const supervisor = createDiscordGatewaySupervisor({
65
+ gateway: { emitter },
66
+ isDisallowedIntentsError: (err) => String(err).includes("4014"),
67
+ runtime: runtime as never,
68
+ });
69
+ const seen: string[] = [];
70
+
71
+ emitter.emit("error", new Error("Fatal gateway close code: 4014"));
72
+ expect(
73
+ supervisor.drainPending((event) => {
74
+ seen.push(event.type);
75
+ return "continue";
76
+ }),
77
+ ).toBe("continue");
78
+
79
+ supervisor.attachLifecycle((event) => {
80
+ seen.push(event.type);
81
+ });
82
+ emitter.emit("error", new Error("Fatal gateway close code: 4000"));
83
+
84
+ supervisor.detachLifecycle();
85
+ emitter.emit("error", new Error("Max reconnect attempts (0) reached after close code 1006"));
86
+
87
+ expect(seen).toEqual(["disallowed-intents", "fatal"]);
88
+ expect(runtime.error).toHaveBeenCalledWith(
89
+ expect.stringContaining("suppressed late gateway reconnect-exhausted error during teardown"),
90
+ );
91
+ });
92
+
93
+ it("is idempotent on dispose and noops without an emitter", () => {
94
+ const supervisor = createDiscordGatewaySupervisor({
95
+ gateway: undefined,
96
+ isDisallowedIntentsError: () => false,
97
+ runtime: { error: vi.fn() } as never,
98
+ });
99
+
100
+ expect(supervisor.drainPending(() => "continue")).toBe("continue");
101
+ expect(() => supervisor.attachLifecycle(() => {})).not.toThrow();
102
+ expect(() => supervisor.detachLifecycle()).not.toThrow();
103
+ expect(() => supervisor.dispose()).not.toThrow();
104
+ expect(() => supervisor.dispose()).not.toThrow();
105
+ });
106
+
107
+ it("keeps suppressing late gateway errors after dispose", () => {
108
+ const emitter = new EventEmitter();
109
+ const runtime = { error: vi.fn() };
110
+ const supervisor = createDiscordGatewaySupervisor({
111
+ gateway: { emitter },
112
+ isDisallowedIntentsError: () => false,
113
+ runtime: runtime as never,
114
+ });
115
+
116
+ supervisor.dispose();
117
+
118
+ expect(() =>
119
+ emitter.emit("error", new Error("Max reconnect attempts (0) reached after close code 1005")),
120
+ ).not.toThrow();
121
+ expect(runtime.error).toHaveBeenCalledWith(
122
+ expect.stringContaining("suppressed late gateway reconnect-exhausted error after dispose"),
123
+ );
124
+ });
125
+
126
+ it("dedupes identical late gateway errors after dispose", () => {
127
+ const emitter = new EventEmitter();
128
+ const runtime = { error: vi.fn() };
129
+ const supervisor = createDiscordGatewaySupervisor({
130
+ gateway: { emitter },
131
+ isDisallowedIntentsError: () => false,
132
+ runtime: runtime as never,
133
+ });
134
+
135
+ supervisor.dispose();
136
+ const first = new TypeError();
137
+ first.stack = "TypeError\n at gatewayCrash (discord-gateway.js:12:34)";
138
+ const second = new TypeError();
139
+ second.stack = "TypeError\n at gatewayCrash (discord-gateway.js:12:34)";
140
+ emitter.emit("error", first);
141
+ emitter.emit("error", second);
142
+
143
+ expect(runtime.error).toHaveBeenCalledTimes(1);
144
+ expect(runtime.error).toHaveBeenCalledWith(
145
+ expect.stringContaining(
146
+ "suppressed late gateway fatal error after dispose: TypeError @ gatewayCrash (discord-gateway.js:12:34)",
147
+ ),
148
+ );
149
+ });
150
+ });
@@ -0,0 +1,206 @@
1
+ import type { EventEmitter } from "node:events";
2
+ import { danger } from "openclaw/plugin-sdk/runtime-env";
3
+ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
4
+ import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
5
+
6
+ type DiscordGatewayEventType = "disallowed-intents" | "fatal" | "other" | "reconnect-exhausted";
7
+
8
+ export type DiscordGatewayEvent = {
9
+ type: DiscordGatewayEventType;
10
+ err: unknown;
11
+ message: string;
12
+ shouldStopLifecycle: boolean;
13
+ };
14
+
15
+ export class DiscordGatewayLifecycleError extends Error {
16
+ readonly eventType: DiscordGatewayEventType;
17
+
18
+ constructor(event: Pick<DiscordGatewayEvent, "type" | "message" | "err">) {
19
+ super(`discord gateway ${event.type}: ${event.message}`, {
20
+ cause: event.err instanceof Error ? event.err : undefined,
21
+ });
22
+ this.name = "DiscordGatewayLifecycleError";
23
+ this.eventType = event.type;
24
+ }
25
+ }
26
+
27
+ export function getDiscordGatewayEmitter(gateway?: unknown): EventEmitter | undefined {
28
+ return (gateway as { emitter?: EventEmitter } | undefined)?.emitter;
29
+ }
30
+
31
+ export type DiscordGatewaySupervisor = {
32
+ emitter?: EventEmitter;
33
+ attachLifecycle: (handler: (event: DiscordGatewayEvent) => void) => void;
34
+ detachLifecycle: () => void;
35
+ drainPending: (
36
+ handler: (event: DiscordGatewayEvent) => "continue" | "stop",
37
+ ) => "continue" | "stop";
38
+ dispose: () => void;
39
+ };
40
+
41
+ type GatewaySupervisorPhase = "active" | "buffering" | "disposed" | "teardown";
42
+
43
+ function readFirstStackFrame(err: Error): string | undefined {
44
+ const stack = err.stack;
45
+ if (!stack) {
46
+ return undefined;
47
+ }
48
+ const frame = stack
49
+ .split("\n")
50
+ .slice(1)
51
+ .map((line) => line.trim())
52
+ .find(Boolean);
53
+ return frame ? frame.replace(/^at\s+/, "") : undefined;
54
+ }
55
+
56
+ function formatDiscordGatewayErrorMessage(err: unknown): string {
57
+ if (!(err instanceof Error)) {
58
+ return formatErrorMessage(err);
59
+ }
60
+ if (err.message) {
61
+ const detail = formatErrorMessage(err);
62
+ return err.name ? `${err.name}: ${detail}` : detail;
63
+ }
64
+ const detail = formatErrorMessage(err);
65
+ const firstFrame = readFirstStackFrame(err);
66
+ if (firstFrame && detail === (err.name || "Error")) {
67
+ return `${detail} @ ${firstFrame}`;
68
+ }
69
+ return detail;
70
+ }
71
+
72
+ export function classifyDiscordGatewayEvent(params: {
73
+ err: unknown;
74
+ isDisallowedIntentsError: (err: unknown) => boolean;
75
+ }): DiscordGatewayEvent {
76
+ const message = formatDiscordGatewayErrorMessage(params.err);
77
+ if (params.isDisallowedIntentsError(params.err)) {
78
+ return {
79
+ type: "disallowed-intents",
80
+ err: params.err,
81
+ message,
82
+ shouldStopLifecycle: true,
83
+ };
84
+ }
85
+ if (message.includes("Max reconnect attempts")) {
86
+ return {
87
+ type: "reconnect-exhausted",
88
+ err: params.err,
89
+ message,
90
+ shouldStopLifecycle: true,
91
+ };
92
+ }
93
+ if (
94
+ params.err instanceof TypeError ||
95
+ message.includes("Fatal Gateway error") ||
96
+ message.includes("Fatal gateway close code") ||
97
+ message.includes("Gateway HELLO missing heartbeat") ||
98
+ message.includes("Invalid gateway payload") ||
99
+ message.includes("Gateway socket emitted an unknown error")
100
+ ) {
101
+ return {
102
+ type: "fatal",
103
+ err: params.err,
104
+ message,
105
+ shouldStopLifecycle: true,
106
+ };
107
+ }
108
+ return {
109
+ type: "other",
110
+ err: params.err,
111
+ message,
112
+ shouldStopLifecycle: false,
113
+ };
114
+ }
115
+
116
+ export function createDiscordGatewaySupervisor(params: {
117
+ gateway?: unknown;
118
+ isDisallowedIntentsError: (err: unknown) => boolean;
119
+ runtime: RuntimeEnv;
120
+ }): DiscordGatewaySupervisor {
121
+ const emitter = getDiscordGatewayEmitter(params.gateway);
122
+ const pending: DiscordGatewayEvent[] = [];
123
+ if (!emitter) {
124
+ return {
125
+ attachLifecycle: () => {},
126
+ detachLifecycle: () => {},
127
+ drainPending: () => "continue",
128
+ dispose: () => {},
129
+ emitter,
130
+ };
131
+ }
132
+
133
+ let lifecycleHandler: ((event: DiscordGatewayEvent) => void) | undefined;
134
+ let phase: GatewaySupervisorPhase = "buffering";
135
+ const seenLateEventKeys = new Set<string>();
136
+ const logLateEvent =
137
+ (state: Extract<GatewaySupervisorPhase, "disposed" | "teardown">) =>
138
+ (event: DiscordGatewayEvent) => {
139
+ const key = `${state}:${event.type}:${event.message}`;
140
+ if (seenLateEventKeys.has(key)) {
141
+ return;
142
+ }
143
+ seenLateEventKeys.add(key);
144
+ params.runtime.error?.(
145
+ danger(
146
+ `discord: suppressed late gateway ${event.type} error ${
147
+ state === "disposed" ? "after dispose" : "during teardown"
148
+ }: ${event.message}`,
149
+ ),
150
+ );
151
+ };
152
+ const onGatewayError = (err: unknown) => {
153
+ const event = classifyDiscordGatewayEvent({
154
+ err,
155
+ isDisallowedIntentsError: params.isDisallowedIntentsError,
156
+ });
157
+ switch (phase) {
158
+ case "disposed":
159
+ logLateEvent("disposed")(event);
160
+ return;
161
+ case "active":
162
+ lifecycleHandler?.(event);
163
+ return;
164
+ case "teardown":
165
+ logLateEvent("teardown")(event);
166
+ return;
167
+ case "buffering":
168
+ pending.push(event);
169
+ return;
170
+ }
171
+ };
172
+ emitter.on("error", onGatewayError);
173
+
174
+ return {
175
+ emitter,
176
+ attachLifecycle: (handler) => {
177
+ lifecycleHandler = handler;
178
+ phase = "active";
179
+ },
180
+ detachLifecycle: () => {
181
+ lifecycleHandler = undefined;
182
+ phase = "teardown";
183
+ },
184
+ drainPending: (handler) => {
185
+ if (pending.length === 0) {
186
+ return "continue";
187
+ }
188
+ const queued = [...pending];
189
+ pending.length = 0;
190
+ for (const event of queued) {
191
+ if (handler(event) === "stop") {
192
+ return "stop";
193
+ }
194
+ }
195
+ return "continue";
196
+ },
197
+ dispose: () => {
198
+ if (phase === "disposed") {
199
+ return;
200
+ }
201
+ lifecycleHandler = undefined;
202
+ phase = "disposed";
203
+ pending.length = 0;
204
+ },
205
+ };
206
+ }