@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,767 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { ChannelType } from "discord-api-types/v10";
5
+ import * as commandRegistryModule from "openclaw/plugin-sdk/command-auth";
6
+ import type { ChatCommandDefinition, CommandArgsParsing } from "openclaw/plugin-sdk/command-auth";
7
+ import type { ModelsProviderData } from "openclaw/plugin-sdk/command-auth";
8
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
9
+ import * as globalsModule from "openclaw/plugin-sdk/runtime-env";
10
+ import {
11
+ loadSessionStore,
12
+ resolveStorePath,
13
+ saveSessionStore,
14
+ } from "openclaw/plugin-sdk/session-store-runtime";
15
+ import * as commandTextModule from "openclaw/plugin-sdk/text-runtime";
16
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
17
+ import { defineThrowingDiscordChannelGetter } from "../test-support/partial-channel.js";
18
+ import { resolveDiscordChannelContext } from "./agent-components-helpers.js";
19
+ import * as modelPickerPreferencesModule from "./model-picker-preferences.js";
20
+ import * as modelPickerModule from "./model-picker.js";
21
+ import { createModelsProviderData as createBaseModelsProviderData } from "./model-picker.test-utils.js";
22
+ import {
23
+ createDiscordModelPickerFallbackButton,
24
+ createDiscordModelPickerFallbackSelect,
25
+ replyWithDiscordModelPickerProviders,
26
+ type DispatchDiscordCommandInteraction,
27
+ } from "./native-command-ui.js";
28
+ import { createNoopThreadBindingManager, type ThreadBindingManager } from "./thread-bindings.js";
29
+
30
+ type ModelPickerContext = Parameters<typeof createDiscordModelPickerFallbackButton>[0]["ctx"];
31
+ type PickerButton = ReturnType<typeof createDiscordModelPickerFallbackButton>;
32
+ type PickerSelect = ReturnType<typeof createDiscordModelPickerFallbackSelect>;
33
+ type PickerButtonInteraction = Parameters<PickerButton["run"]>[0];
34
+ type PickerButtonData = Parameters<PickerButton["run"]>[1];
35
+ type PickerSelectInteraction = Parameters<PickerSelect["run"]>[0];
36
+ type PickerSelectData = Parameters<PickerSelect["run"]>[1];
37
+
38
+ type MockInteraction = {
39
+ user: { id: string; username: string; globalName: string };
40
+ channel: { type: ChannelType; id: string; name?: string; parentId?: string };
41
+ guild: { id: string } | null;
42
+ rawData: { id: string; member: { roles: string[] } };
43
+ values?: string[];
44
+ reply: ReturnType<typeof vi.fn>;
45
+ followUp: ReturnType<typeof vi.fn>;
46
+ update: ReturnType<typeof vi.fn>;
47
+ acknowledge: ReturnType<typeof vi.fn>;
48
+ client: object;
49
+ };
50
+
51
+ let tempDir: string;
52
+
53
+ function createModelsProviderData(entries: Record<string, string[]>): ModelsProviderData {
54
+ return createBaseModelsProviderData(entries, { defaultProviderOrder: "sorted" });
55
+ }
56
+
57
+ async function waitForCondition(
58
+ predicate: () => boolean,
59
+ opts?: { attempts?: number; delayMs?: number },
60
+ ): Promise<void> {
61
+ const attempts = opts?.attempts ?? 50;
62
+ const delayMs = opts?.delayMs ?? 0;
63
+ for (let index = 0; index < attempts; index += 1) {
64
+ if (predicate()) {
65
+ return;
66
+ }
67
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
68
+ }
69
+ throw new Error("condition not met");
70
+ }
71
+
72
+ function createModelPickerContext(): ModelPickerContext {
73
+ const cfg = {
74
+ session: {
75
+ store: path.join(tempDir, "sessions.json"),
76
+ },
77
+ channels: {
78
+ discord: {
79
+ dm: {
80
+ enabled: true,
81
+ policy: "open",
82
+ },
83
+ },
84
+ },
85
+ } as unknown as OpenClawConfig;
86
+
87
+ return {
88
+ cfg,
89
+ discordConfig: cfg.channels?.discord ?? {},
90
+ accountId: "default",
91
+ sessionPrefix: "discord:slash",
92
+ threadBindings: createNoopThreadBindingManager("default"),
93
+ postApplySettleMs: 0,
94
+ };
95
+ }
96
+
97
+ function createInteraction(params?: { userId?: string; values?: string[] }): MockInteraction {
98
+ const userId = params?.userId ?? "owner";
99
+ return {
100
+ user: {
101
+ id: userId,
102
+ username: "tester",
103
+ globalName: "Tester",
104
+ },
105
+ channel: {
106
+ type: ChannelType.DM,
107
+ id: "dm-1",
108
+ },
109
+ guild: null,
110
+ rawData: {
111
+ id: "interaction-1",
112
+ member: { roles: [] },
113
+ },
114
+ values: params?.values,
115
+ reply: vi.fn().mockResolvedValue({ ok: true }),
116
+ followUp: vi.fn().mockResolvedValue({ ok: true }),
117
+ update: vi.fn().mockResolvedValue({ ok: true }),
118
+ acknowledge: vi.fn().mockResolvedValue({ ok: true }),
119
+ client: {},
120
+ };
121
+ }
122
+
123
+ function createDefaultModelPickerData(): ModelsProviderData {
124
+ return createModelsProviderData({
125
+ openai: ["gpt-4.1", "gpt-4o"],
126
+ anthropic: ["claude-sonnet-4-5"],
127
+ });
128
+ }
129
+
130
+ function createModelCommandDefinition(): ChatCommandDefinition {
131
+ return {
132
+ key: "model",
133
+ nativeName: "model",
134
+ description: "Switch model",
135
+ textAliases: ["/model"],
136
+ acceptsArgs: true,
137
+ argsParsing: "none" as CommandArgsParsing,
138
+ scope: "native",
139
+ };
140
+ }
141
+
142
+ function mockModelCommandPipeline(modelCommand: ChatCommandDefinition) {
143
+ vi.spyOn(commandRegistryModule, "findCommandByNativeName").mockImplementation((name) =>
144
+ name === "model" ? modelCommand : undefined,
145
+ );
146
+ vi.spyOn(commandRegistryModule, "listChatCommands").mockReturnValue([modelCommand]);
147
+ vi.spyOn(commandRegistryModule, "resolveCommandArgMenu").mockReturnValue(null);
148
+ }
149
+
150
+ function createModelsViewSelectData(): PickerSelectData {
151
+ return {
152
+ cmd: "model",
153
+ act: "model",
154
+ view: "models",
155
+ u: "owner",
156
+ p: "openai",
157
+ pg: "1",
158
+ };
159
+ }
160
+
161
+ function createModelsViewSubmitData(): PickerButtonData {
162
+ return {
163
+ cmd: "model",
164
+ act: "submit",
165
+ view: "models",
166
+ u: "owner",
167
+ p: "openai",
168
+ pg: "1",
169
+ mi: "2",
170
+ };
171
+ }
172
+
173
+ async function safeInteractionCall<T>(_label: string, fn: () => Promise<T>): Promise<T | null> {
174
+ return await fn();
175
+ }
176
+
177
+ function createDispatchSpy() {
178
+ return vi.fn<DispatchDiscordCommandInteraction>().mockResolvedValue({ accepted: true });
179
+ }
180
+
181
+ function createModelPickerFallbackButton(
182
+ context: ModelPickerContext,
183
+ dispatchCommandInteraction: DispatchDiscordCommandInteraction = createDispatchSpy(),
184
+ ) {
185
+ return createDiscordModelPickerFallbackButton({
186
+ ctx: context,
187
+ safeInteractionCall,
188
+ dispatchCommandInteraction,
189
+ });
190
+ }
191
+
192
+ function createModelPickerFallbackSelect(
193
+ context: ModelPickerContext,
194
+ dispatchCommandInteraction: DispatchDiscordCommandInteraction = createDispatchSpy(),
195
+ ) {
196
+ return createDiscordModelPickerFallbackSelect({
197
+ ctx: context,
198
+ safeInteractionCall,
199
+ dispatchCommandInteraction,
200
+ });
201
+ }
202
+
203
+ async function runSubmitButton(params: {
204
+ context: ModelPickerContext;
205
+ data: PickerButtonData;
206
+ dispatchCommandInteraction?: DispatchDiscordCommandInteraction;
207
+ userId?: string;
208
+ }) {
209
+ const button = createModelPickerFallbackButton(params.context, params.dispatchCommandInteraction);
210
+ const submitInteraction = createInteraction({ userId: params.userId ?? "owner" });
211
+ await button.run(submitInteraction as unknown as PickerButtonInteraction, params.data);
212
+ return submitInteraction;
213
+ }
214
+
215
+ async function runModelSelect(params: {
216
+ context: ModelPickerContext;
217
+ data?: PickerSelectData;
218
+ dispatchCommandInteraction?: DispatchDiscordCommandInteraction;
219
+ userId?: string;
220
+ values?: string[];
221
+ }) {
222
+ const select = createModelPickerFallbackSelect(params.context, params.dispatchCommandInteraction);
223
+ const selectInteraction = createInteraction({
224
+ userId: params.userId ?? "owner",
225
+ values: params.values ?? ["gpt-4o"],
226
+ });
227
+ await select.run(
228
+ selectInteraction as unknown as PickerSelectInteraction,
229
+ params.data ?? createModelsViewSelectData(),
230
+ );
231
+ return selectInteraction;
232
+ }
233
+
234
+ function expectDispatchedModelSelection(params: {
235
+ dispatchSpy: ReturnType<typeof createDispatchSpy>;
236
+ model: string;
237
+ runtime?: string;
238
+ }) {
239
+ const dispatchCall = params.dispatchSpy.mock.calls[0]?.[0];
240
+ expect(dispatchCall?.prompt).toBe(
241
+ params.runtime
242
+ ? `/model ${params.model} --runtime ${params.runtime}`
243
+ : `/model ${params.model}`,
244
+ );
245
+ expect(dispatchCall?.commandArgs?.values?.model).toBe(params.model);
246
+ }
247
+
248
+ function createBoundThreadBindingManager(params: {
249
+ accountId: string;
250
+ threadId: string;
251
+ targetSessionKey: string;
252
+ agentId: string;
253
+ }): ThreadBindingManager {
254
+ const baseManager = createNoopThreadBindingManager(params.accountId);
255
+ const now = Date.now();
256
+ return {
257
+ ...baseManager,
258
+ getIdleTimeoutMs: () => 24 * 60 * 60 * 1000,
259
+ getMaxAgeMs: () => 0,
260
+ getByThreadId: (threadId: string) =>
261
+ threadId === params.threadId
262
+ ? {
263
+ accountId: params.accountId,
264
+ channelId: "parent-1",
265
+ threadId: params.threadId,
266
+ targetKind: "subagent",
267
+ targetSessionKey: params.targetSessionKey,
268
+ agentId: params.agentId,
269
+ boundBy: "system",
270
+ boundAt: now,
271
+ lastActivityAt: now,
272
+ idleTimeoutMs: 24 * 60 * 60 * 1000,
273
+ maxAgeMs: 0,
274
+ }
275
+ : baseManager.getByThreadId(threadId),
276
+ };
277
+ }
278
+
279
+ describe("Discord model picker interactions", () => {
280
+ beforeEach(async () => {
281
+ tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-discord-model-picker-"));
282
+ vi.useRealTimers();
283
+ vi.restoreAllMocks();
284
+ });
285
+
286
+ afterEach(async () => {
287
+ vi.useRealTimers();
288
+ await rm(tempDir, { recursive: true, force: true });
289
+ });
290
+
291
+ it("registers distinct fallback ids for button and select handlers", () => {
292
+ const context = createModelPickerContext();
293
+ const button = createModelPickerFallbackButton(context);
294
+ const select = createModelPickerFallbackSelect(context);
295
+
296
+ expect(button.customId).not.toBe(select.customId);
297
+ expect(button.customId.split(":")[0]).toBe(
298
+ modelPickerModule.DISCORD_MODEL_PICKER_CUSTOM_ID_KEY,
299
+ );
300
+ expect(select.customId.split(":")[0]).toBe(
301
+ modelPickerModule.DISCORD_MODEL_PICKER_CUSTOM_ID_KEY,
302
+ );
303
+ });
304
+
305
+ it("ignores interactions from users other than the picker owner", async () => {
306
+ const context = createModelPickerContext();
307
+ const loadSpy = vi.spyOn(modelPickerModule, "loadDiscordModelPickerData");
308
+ const button = createModelPickerFallbackButton(context);
309
+ const interaction = createInteraction({ userId: "intruder" });
310
+
311
+ const data: PickerButtonData = {
312
+ cmd: "model",
313
+ act: "back",
314
+ view: "providers",
315
+ u: "owner",
316
+ pg: "1",
317
+ };
318
+
319
+ await button.run(interaction as unknown as PickerButtonInteraction, data);
320
+
321
+ expect(interaction.acknowledge).toHaveBeenCalledTimes(1);
322
+ expect(interaction.update).not.toHaveBeenCalled();
323
+ expect(loadSpy).not.toHaveBeenCalled();
324
+ });
325
+
326
+ it("requires submit click before routing selected model through /model pipeline", async () => {
327
+ const context = createModelPickerContext();
328
+ const pickerData = createDefaultModelPickerData();
329
+ const modelCommand = createModelCommandDefinition();
330
+
331
+ vi.spyOn(modelPickerModule, "loadDiscordModelPickerData").mockResolvedValue(pickerData);
332
+ mockModelCommandPipeline(modelCommand);
333
+
334
+ const dispatchSpy = createDispatchSpy();
335
+
336
+ const selectInteraction = await runModelSelect({
337
+ context,
338
+ dispatchCommandInteraction: dispatchSpy,
339
+ });
340
+
341
+ expect(selectInteraction.update).toHaveBeenCalledTimes(1);
342
+ expect(dispatchSpy).not.toHaveBeenCalled();
343
+
344
+ const submitInteraction = await runSubmitButton({
345
+ context,
346
+ data: createModelsViewSubmitData(),
347
+ dispatchCommandInteraction: dispatchSpy,
348
+ });
349
+
350
+ expect(submitInteraction.update).toHaveBeenCalledTimes(1);
351
+ expect(dispatchSpy).toHaveBeenCalledTimes(1);
352
+ expectDispatchedModelSelection({
353
+ dispatchSpy,
354
+ model: "openai/gpt-4o",
355
+ });
356
+ });
357
+
358
+ it("applies the selected model even when component channel.name throws on a partial channel", async () => {
359
+ const context = createModelPickerContext();
360
+ const pickerData = createDefaultModelPickerData();
361
+ const modelCommand = createModelCommandDefinition();
362
+
363
+ vi.spyOn(modelPickerModule, "loadDiscordModelPickerData").mockResolvedValue(pickerData);
364
+ mockModelCommandPipeline(modelCommand);
365
+
366
+ const dispatchSpy = createDispatchSpy();
367
+ const submitInteraction = createInteraction({ userId: "owner" });
368
+ defineThrowingDiscordChannelGetter(submitInteraction.channel, "name");
369
+
370
+ const button = createModelPickerFallbackButton(context, dispatchSpy);
371
+ await button.run(
372
+ submitInteraction as unknown as PickerButtonInteraction,
373
+ createModelsViewSubmitData(),
374
+ );
375
+
376
+ expect(dispatchSpy).toHaveBeenCalledTimes(1);
377
+ expectDispatchedModelSelection({
378
+ dispatchSpy,
379
+ model: "openai/gpt-4o",
380
+ });
381
+ });
382
+
383
+ it("applies the selected model even when component thread parent.name throws on a partial channel", async () => {
384
+ const context = createModelPickerContext();
385
+ const pickerData = createDefaultModelPickerData();
386
+ const modelCommand = createModelCommandDefinition();
387
+
388
+ vi.spyOn(modelPickerModule, "loadDiscordModelPickerData").mockResolvedValue(pickerData);
389
+ mockModelCommandPipeline(modelCommand);
390
+
391
+ const dispatchSpy = createDispatchSpy();
392
+ const submitInteraction = createInteraction({ userId: "owner" });
393
+ submitInteraction.guild = { id: "guild-1" };
394
+ const threadChannel = {
395
+ type: ChannelType.PublicThread,
396
+ id: "thread-1",
397
+ parentId: "parent-1",
398
+ parent: { id: "parent-1", name: "parent-name" },
399
+ } as {
400
+ type: ChannelType;
401
+ id: string;
402
+ parentId: string;
403
+ parent?: { id?: string; name?: string };
404
+ };
405
+ submitInteraction.channel = threadChannel as MockInteraction["channel"];
406
+ defineThrowingDiscordChannelGetter(
407
+ threadChannel.parent as { id?: string; name?: string },
408
+ "name",
409
+ );
410
+
411
+ const button = createModelPickerFallbackButton(context, dispatchSpy);
412
+ await button.run(
413
+ submitInteraction as unknown as PickerButtonInteraction,
414
+ createModelsViewSubmitData(),
415
+ );
416
+
417
+ expect(dispatchSpy).toHaveBeenCalledTimes(1);
418
+ expectDispatchedModelSelection({
419
+ dispatchSpy,
420
+ model: "openai/gpt-4o",
421
+ });
422
+ });
423
+
424
+ it("routes selected runtime through the /model pipeline", async () => {
425
+ const context = createModelPickerContext();
426
+ const pickerData = createDefaultModelPickerData();
427
+ pickerData.runtimeChoicesByProvider = new Map([
428
+ [
429
+ "openai",
430
+ [
431
+ {
432
+ id: "pi",
433
+ label: "OpenClaw Pi Default",
434
+ description: "Use the built-in OpenClaw Pi runtime.",
435
+ },
436
+ {
437
+ id: "codex",
438
+ label: "codex",
439
+ description: "Run openai models through the codex harness.",
440
+ },
441
+ ],
442
+ ],
443
+ ]);
444
+ const modelCommand = createModelCommandDefinition();
445
+
446
+ vi.spyOn(modelPickerModule, "loadDiscordModelPickerData").mockResolvedValue(pickerData);
447
+ mockModelCommandPipeline(modelCommand);
448
+
449
+ const dispatchSpy = createDispatchSpy();
450
+ await runSubmitButton({
451
+ context,
452
+ data: {
453
+ ...createModelsViewSubmitData(),
454
+ r: "codex",
455
+ },
456
+ dispatchCommandInteraction: dispatchSpy,
457
+ });
458
+
459
+ expect(dispatchSpy).toHaveBeenCalledTimes(1);
460
+ expectDispatchedModelSelection({
461
+ dispatchSpy,
462
+ model: "openai/gpt-4o",
463
+ runtime: "codex",
464
+ });
465
+ });
466
+
467
+ it("ignores category parent metadata for non-thread component channels", () => {
468
+ const interaction = createInteraction({ userId: "owner" });
469
+ interaction.guild = { id: "guild-1" };
470
+ interaction.channel = {
471
+ type: ChannelType.GuildText,
472
+ id: "channel-1",
473
+ name: "general",
474
+ parentId: "category-1",
475
+ parent: { id: "category-1", name: "category-name" },
476
+ } as MockInteraction["channel"] & { parent?: { id?: string; name?: string } };
477
+
478
+ const channelCtx = resolveDiscordChannelContext(
479
+ interaction as unknown as Parameters<typeof resolveDiscordChannelContext>[0],
480
+ );
481
+
482
+ expect(channelCtx.isThread).toBe(false);
483
+ expect(channelCtx.parentId).toBeUndefined();
484
+ expect(channelCtx.parentName).toBeUndefined();
485
+ expect(channelCtx.parentSlug).toBe("");
486
+ });
487
+
488
+ it("shows timeout status and skips recents write when apply is still processing", async () => {
489
+ const context = createModelPickerContext();
490
+ const pickerData = createDefaultModelPickerData();
491
+ const modelCommand = createModelCommandDefinition();
492
+
493
+ vi.spyOn(modelPickerModule, "loadDiscordModelPickerData").mockResolvedValue(pickerData);
494
+ mockModelCommandPipeline(modelCommand);
495
+
496
+ const recordRecentSpy = vi
497
+ .spyOn(modelPickerPreferencesModule, "recordDiscordModelPickerRecentModel")
498
+ .mockResolvedValue();
499
+ const dispatchSpy = createDispatchSpy();
500
+ const withTimeoutSpy = vi
501
+ .spyOn(commandTextModule, "withTimeout")
502
+ .mockRejectedValue(new Error("timeout"));
503
+
504
+ await runModelSelect({ context, dispatchCommandInteraction: dispatchSpy });
505
+
506
+ const button = createModelPickerFallbackButton(context, dispatchSpy);
507
+ const submitInteraction = createInteraction({ userId: "owner" });
508
+ const submitData = createModelsViewSubmitData();
509
+
510
+ await button.run(submitInteraction as unknown as PickerButtonInteraction, submitData);
511
+
512
+ expect(withTimeoutSpy).toHaveBeenCalledTimes(1);
513
+ await waitForCondition(() => dispatchSpy.mock.calls.length === 1);
514
+ expect(submitInteraction.followUp).toHaveBeenCalledTimes(1);
515
+ const followUpPayload = submitInteraction.followUp.mock.calls[0]?.[0] as {
516
+ components?: Array<{ components?: Array<{ content?: string }> }>;
517
+ };
518
+ const followUpText = JSON.stringify(followUpPayload);
519
+ expect(followUpText).toContain("still processing");
520
+ expect(recordRecentSpy).not.toHaveBeenCalled();
521
+ });
522
+
523
+ it("clicking Recents button renders recents view", async () => {
524
+ const context = createModelPickerContext();
525
+ const pickerData = createModelsProviderData({
526
+ openai: ["gpt-4.1", "gpt-4o"],
527
+ anthropic: ["claude-sonnet-4-5"],
528
+ });
529
+
530
+ vi.spyOn(modelPickerModule, "loadDiscordModelPickerData").mockResolvedValue(pickerData);
531
+ vi.spyOn(modelPickerPreferencesModule, "readDiscordModelPickerRecentModels").mockResolvedValue([
532
+ "openai/gpt-4o",
533
+ "anthropic/claude-sonnet-4-5",
534
+ ]);
535
+
536
+ const button = createModelPickerFallbackButton(context);
537
+ const interaction = createInteraction({ userId: "owner" });
538
+
539
+ const data: PickerButtonData = {
540
+ cmd: "model",
541
+ act: "recents",
542
+ view: "recents",
543
+ u: "owner",
544
+ p: "openai",
545
+ pg: "1",
546
+ };
547
+
548
+ await button.run(interaction as unknown as PickerButtonInteraction, data);
549
+
550
+ expect(interaction.update).toHaveBeenCalledTimes(1);
551
+ const updatePayload = interaction.update.mock.calls[0]?.[0];
552
+ if (!updatePayload) {
553
+ throw new Error("recents button did not emit an update payload");
554
+ }
555
+ const updateText = JSON.stringify(updatePayload);
556
+ expect(updateText).toContain("gpt-4o");
557
+ expect(updateText).toContain("claude-sonnet-4-5");
558
+ });
559
+
560
+ it("clicking recents model button applies model through /model pipeline", async () => {
561
+ const context = createModelPickerContext();
562
+ const pickerData = createDefaultModelPickerData();
563
+ const modelCommand = createModelCommandDefinition();
564
+
565
+ vi.spyOn(modelPickerModule, "loadDiscordModelPickerData").mockResolvedValue(pickerData);
566
+ vi.spyOn(modelPickerPreferencesModule, "readDiscordModelPickerRecentModels").mockResolvedValue([
567
+ "openai/gpt-4o",
568
+ "anthropic/claude-sonnet-4-5",
569
+ ]);
570
+ mockModelCommandPipeline(modelCommand);
571
+
572
+ const dispatchSpy = createDispatchSpy();
573
+
574
+ // rs=2 -> first deduped recent (default is anthropic/claude-sonnet-4-5, so openai/gpt-4o remains)
575
+ const submitInteraction = await runSubmitButton({
576
+ context,
577
+ data: {
578
+ cmd: "model",
579
+ act: "submit",
580
+ view: "recents",
581
+ u: "owner",
582
+ pg: "1",
583
+ rs: "2",
584
+ },
585
+ dispatchCommandInteraction: dispatchSpy,
586
+ });
587
+
588
+ expect(submitInteraction.update).toHaveBeenCalledTimes(1);
589
+ expect(dispatchSpy).toHaveBeenCalledTimes(1);
590
+ expectDispatchedModelSelection({ dispatchSpy, model: "openai/gpt-4o" });
591
+ });
592
+
593
+ it("verifies model state against the bound thread session", async () => {
594
+ const context = createModelPickerContext();
595
+ context.threadBindings = createBoundThreadBindingManager({
596
+ accountId: "default",
597
+ threadId: "thread-bound",
598
+ targetSessionKey: "agent:worker:subagent:bound",
599
+ agentId: "worker",
600
+ });
601
+ const pickerData = createDefaultModelPickerData();
602
+ const modelCommand = createModelCommandDefinition();
603
+
604
+ vi.spyOn(modelPickerModule, "loadDiscordModelPickerData").mockResolvedValue(pickerData);
605
+ mockModelCommandPipeline(modelCommand);
606
+ const dispatchSpy = createDispatchSpy();
607
+ const verboseSpy = vi.spyOn(globalsModule, "logVerbose").mockImplementation(() => {});
608
+
609
+ const select = createModelPickerFallbackSelect(context, dispatchSpy);
610
+ const selectInteraction = createInteraction({
611
+ userId: "owner",
612
+ values: ["gpt-4o"],
613
+ });
614
+ selectInteraction.channel = {
615
+ type: ChannelType.PublicThread,
616
+ id: "thread-bound",
617
+ };
618
+ const selectData = createModelsViewSelectData();
619
+ await select.run(selectInteraction as unknown as PickerSelectInteraction, selectData);
620
+
621
+ const button = createModelPickerFallbackButton(context, dispatchSpy);
622
+ const submitInteraction = createInteraction({ userId: "owner" });
623
+ submitInteraction.channel = {
624
+ type: ChannelType.PublicThread,
625
+ id: "thread-bound",
626
+ };
627
+ const submitData = createModelsViewSubmitData();
628
+
629
+ await button.run(submitInteraction as unknown as PickerButtonInteraction, submitData);
630
+
631
+ const mismatchLog = verboseSpy.mock.calls.find(
632
+ (call) => typeof call[0] === "string" && call[0].includes("model picker override mismatch"),
633
+ )?.[0];
634
+ expect(mismatchLog).toContain("session key agent:worker:subagent:bound");
635
+ });
636
+
637
+ it("persists suffixed LM Studio model overrides when dispatch leaves the routed session stale", async () => {
638
+ const context = createModelPickerContext();
639
+ context.threadBindings = createBoundThreadBindingManager({
640
+ accountId: "default",
641
+ threadId: "thread-bound",
642
+ targetSessionKey: "agent:worker:subagent:bound",
643
+ agentId: "worker",
644
+ });
645
+ const pickerData = createModelsProviderData({
646
+ anthropic: ["claude-sonnet-4-5"],
647
+ lmstudio: ["unsloth/gemma-4-26b-a4b-it@iq4_xs"],
648
+ });
649
+ const modelCommand = createModelCommandDefinition();
650
+ const storePath = resolveStorePath(context.cfg.session?.store, { agentId: "worker" });
651
+ await saveSessionStore(storePath, {
652
+ "agent:worker:subagent:bound": {
653
+ updatedAt: Date.now(),
654
+ sessionId: "bound-session",
655
+ },
656
+ });
657
+
658
+ vi.spyOn(modelPickerModule, "loadDiscordModelPickerData").mockResolvedValue(pickerData);
659
+ mockModelCommandPipeline(modelCommand);
660
+
661
+ const dispatchSpy = createDispatchSpy();
662
+ const button = createModelPickerFallbackButton(context, dispatchSpy);
663
+ const submitInteraction = createInteraction({ userId: "owner" });
664
+ submitInteraction.channel = {
665
+ type: ChannelType.PublicThread,
666
+ id: "thread-bound",
667
+ };
668
+
669
+ await button.run(submitInteraction as unknown as PickerButtonInteraction, {
670
+ ...createModelsViewSubmitData(),
671
+ p: "lmstudio",
672
+ mi: "1",
673
+ });
674
+
675
+ const store = loadSessionStore(storePath, { skipCache: true });
676
+ expect(store["agent:worker:subagent:bound"]?.providerOverride).toBe("lmstudio");
677
+ expect(store["agent:worker:subagent:bound"]?.modelOverride).toBe(
678
+ "unsloth/gemma-4-26b-a4b-it@iq4_xs",
679
+ );
680
+ expect(store["agent:worker:subagent:bound"]?.liveModelSwitchPending).toBe(true);
681
+ expectDispatchedModelSelection({
682
+ dispatchSpy,
683
+ model: "lmstudio/unsloth/gemma-4-26b-a4b-it@iq4_xs",
684
+ });
685
+ expect(JSON.stringify(submitInteraction.followUp.mock.calls[0]?.[0])).toContain(
686
+ "✅ Model set to lmstudio/unsloth/gemma-4-26b-a4b-it@iq4_xs.",
687
+ );
688
+ });
689
+
690
+ it("does not write a fallback override when hidden /model dispatch is rejected", async () => {
691
+ const context = createModelPickerContext();
692
+ context.threadBindings = createBoundThreadBindingManager({
693
+ accountId: "default",
694
+ threadId: "thread-bound",
695
+ targetSessionKey: "agent:worker:subagent:bound",
696
+ agentId: "worker",
697
+ });
698
+ const pickerData = createDefaultModelPickerData();
699
+ const modelCommand = createModelCommandDefinition();
700
+ const storePath = resolveStorePath(context.cfg.session?.store, { agentId: "worker" });
701
+ await saveSessionStore(storePath, {
702
+ "agent:worker:subagent:bound": {
703
+ updatedAt: Date.now(),
704
+ sessionId: "bound-session",
705
+ },
706
+ });
707
+
708
+ vi.spyOn(modelPickerModule, "loadDiscordModelPickerData").mockResolvedValue(pickerData);
709
+ mockModelCommandPipeline(modelCommand);
710
+
711
+ const button = createModelPickerFallbackButton(
712
+ context,
713
+ vi.fn<DispatchDiscordCommandInteraction>().mockResolvedValue({ accepted: false }),
714
+ );
715
+ const submitInteraction = createInteraction({ userId: "owner" });
716
+ submitInteraction.channel = {
717
+ type: ChannelType.PublicThread,
718
+ id: "thread-bound",
719
+ };
720
+
721
+ await button.run(
722
+ submitInteraction as unknown as PickerButtonInteraction,
723
+ createModelsViewSubmitData(),
724
+ );
725
+
726
+ const store = loadSessionStore(storePath, { skipCache: true });
727
+ expect(store["agent:worker:subagent:bound"]?.providerOverride).toBeUndefined();
728
+ expect(store["agent:worker:subagent:bound"]?.modelOverride).toBeUndefined();
729
+ expect(JSON.stringify(submitInteraction.followUp.mock.calls[0]?.[0])).toContain(
730
+ "❌ Failed to apply openai/gpt-4o.",
731
+ );
732
+ });
733
+
734
+ it("loads model picker data from the effective bound route", async () => {
735
+ const context = createModelPickerContext();
736
+ context.threadBindings = createBoundThreadBindingManager({
737
+ accountId: "default",
738
+ threadId: "thread-bound",
739
+ targetSessionKey: "agent:worker:subagent:bound",
740
+ agentId: "worker",
741
+ });
742
+ const loadSpy = vi
743
+ .spyOn(modelPickerModule, "loadDiscordModelPickerData")
744
+ .mockResolvedValue(createDefaultModelPickerData());
745
+ const interaction = createInteraction({ userId: "owner" });
746
+ interaction.guild = { id: "guild-1" };
747
+ interaction.channel = {
748
+ type: ChannelType.PublicThread,
749
+ id: "thread-bound",
750
+ name: "bound-thread",
751
+ parentId: "parent-1",
752
+ };
753
+
754
+ await replyWithDiscordModelPickerProviders({
755
+ interaction: interaction as never,
756
+ cfg: context.cfg,
757
+ command: "model",
758
+ userId: "owner",
759
+ accountId: context.accountId,
760
+ threadBindings: context.threadBindings,
761
+ preferFollowUp: false,
762
+ safeInteractionCall: async (_label, fn) => await fn(),
763
+ });
764
+
765
+ expect(loadSpy).toHaveBeenCalledWith(context.cfg, "worker");
766
+ });
767
+ });