@openclaw/discord 2026.3.13 → 2026.5.1-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 (498) 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 +3282 -1
  14. package/package.json +67 -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 +185 -0
  34. package/src/actions/handle-action.ts +332 -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 +244 -0
  40. package/src/actions/runtime.messaging.shared.ts +92 -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 +1056 -0
  49. package/src/actions/runtime.ts +81 -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 +236 -0
  65. package/src/channel-actions.ts +198 -0
  66. package/src/channel-api.ts +28 -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 +539 -12
  72. package/src/channel.ts +596 -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 +139 -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 +297 -0
  123. package/src/internal/client.ts +246 -0
  124. package/src/internal/command-deploy.ts +202 -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 +475 -0
  140. package/src/internal/gateway.ts +437 -0
  141. package/src/internal/interaction-dispatch.test.ts +148 -0
  142. package/src/internal/interaction-dispatch.ts +130 -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 +253 -0
  146. package/src/internal/interactions.ts +337 -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 +412 -0
  155. package/src/internal/rest.test.ts +437 -0
  156. package/src/internal/rest.ts +213 -0
  157. package/src/internal/schemas.ts +36 -0
  158. package/src/internal/structures.ts +278 -0
  159. package/src/internal/test-builders.test-support.ts +163 -0
  160. package/src/internal/voice.ts +49 -0
  161. package/src/media-detection.ts +28 -0
  162. package/src/mentions.test.ts +111 -0
  163. package/src/mentions.ts +147 -0
  164. package/src/monitor/access-groups.ts +55 -0
  165. package/src/monitor/ack-reactions.ts +70 -0
  166. package/src/monitor/acp-bind-here.integration.test.ts +211 -0
  167. package/src/monitor/agent-components-auth.ts +7 -0
  168. package/src/monitor/agent-components-context.ts +144 -0
  169. package/src/monitor/agent-components-data.ts +224 -0
  170. package/src/monitor/agent-components-dm-auth.ts +221 -0
  171. package/src/monitor/agent-components-guild-auth.ts +322 -0
  172. package/src/monitor/agent-components-helpers.runtime.ts +5 -0
  173. package/src/monitor/agent-components-helpers.ts +34 -0
  174. package/src/monitor/agent-components-reply.ts +10 -0
  175. package/src/monitor/agent-components.deps.runtime.ts +2 -0
  176. package/src/monitor/agent-components.dispatch.ts +366 -0
  177. package/src/monitor/agent-components.handlers.ts +303 -0
  178. package/src/monitor/agent-components.modal.ts +160 -0
  179. package/src/monitor/agent-components.plugin-interactive.ts +187 -0
  180. package/src/monitor/agent-components.runtime.ts +14 -0
  181. package/src/monitor/agent-components.system-controls.ts +211 -0
  182. package/src/monitor/agent-components.ts +70 -0
  183. package/src/monitor/agent-components.types.ts +57 -0
  184. package/src/monitor/agent-components.wildcard-controls.ts +168 -0
  185. package/src/monitor/agent-components.wildcard.test.ts +71 -0
  186. package/src/monitor/allow-list.ts +623 -0
  187. package/src/monitor/auto-presence.test.ts +156 -0
  188. package/src/monitor/auto-presence.ts +356 -0
  189. package/src/monitor/channel-access.ts +70 -0
  190. package/src/monitor/commands.test.ts +24 -0
  191. package/src/monitor/commands.ts +9 -0
  192. package/src/monitor/dm-command-auth.test.ts +197 -0
  193. package/src/monitor/dm-command-auth.ts +158 -0
  194. package/src/monitor/dm-command-decision.test.ts +113 -0
  195. package/src/monitor/dm-command-decision.ts +49 -0
  196. package/src/monitor/exec-approvals.test.ts +226 -0
  197. package/src/monitor/exec-approvals.ts +158 -0
  198. package/src/monitor/format.ts +45 -0
  199. package/src/monitor/gateway-handle.ts +34 -0
  200. package/src/monitor/gateway-metadata.test.ts +29 -0
  201. package/src/monitor/gateway-metadata.ts +298 -0
  202. package/src/monitor/gateway-plugin.test.ts +297 -0
  203. package/src/monitor/gateway-plugin.ts +294 -0
  204. package/src/monitor/gateway-registry.ts +37 -0
  205. package/src/monitor/gateway-supervisor.test.ts +150 -0
  206. package/src/monitor/gateway-supervisor.ts +206 -0
  207. package/src/monitor/inbound-context.test-helpers.ts +37 -0
  208. package/src/monitor/inbound-context.test.ts +106 -0
  209. package/src/monitor/inbound-context.ts +103 -0
  210. package/src/monitor/inbound-dedupe.ts +79 -0
  211. package/src/monitor/inbound-job.test.ts +203 -0
  212. package/src/monitor/inbound-job.ts +118 -0
  213. package/src/monitor/listeners.queue.ts +91 -0
  214. package/src/monitor/listeners.reactions.ts +610 -0
  215. package/src/monitor/listeners.test.ts +200 -0
  216. package/src/monitor/listeners.ts +150 -0
  217. package/src/monitor/message-channel-info.ts +96 -0
  218. package/src/monitor/message-forwarded.ts +107 -0
  219. package/src/monitor/message-handler.batch-gate.test.ts +22 -0
  220. package/src/monitor/message-handler.batch-gate.ts +19 -0
  221. package/src/monitor/message-handler.bot-self-filter.test.ts +68 -0
  222. package/src/monitor/message-handler.context.ts +393 -0
  223. package/src/monitor/message-handler.dm-preflight.ts +123 -0
  224. package/src/monitor/message-handler.draft-preview.ts +246 -0
  225. package/src/monitor/message-handler.hydration.test.ts +80 -0
  226. package/src/monitor/message-handler.hydration.ts +198 -0
  227. package/src/monitor/message-handler.inbound-context.test.ts +59 -0
  228. package/src/monitor/message-handler.module-test-helpers.ts +31 -0
  229. package/src/monitor/message-handler.preflight-channel-access.ts +86 -0
  230. package/src/monitor/message-handler.preflight-channel-context.ts +55 -0
  231. package/src/monitor/message-handler.preflight-context.ts +54 -0
  232. package/src/monitor/message-handler.preflight-helpers.ts +164 -0
  233. package/src/monitor/message-handler.preflight-history.ts +23 -0
  234. package/src/monitor/message-handler.preflight-logging.ts +36 -0
  235. package/src/monitor/message-handler.preflight-pluralkit.ts +27 -0
  236. package/src/monitor/message-handler.preflight-runtime.ts +28 -0
  237. package/src/monitor/message-handler.preflight-thread.ts +49 -0
  238. package/src/monitor/message-handler.preflight.acp-bindings.test.ts +369 -0
  239. package/src/monitor/message-handler.preflight.test-helpers.ts +111 -0
  240. package/src/monitor/message-handler.preflight.test.ts +1544 -0
  241. package/src/monitor/message-handler.preflight.ts +680 -0
  242. package/src/monitor/message-handler.preflight.types.ts +109 -0
  243. package/src/monitor/message-handler.process.test.ts +1301 -0
  244. package/src/monitor/message-handler.process.ts +684 -0
  245. package/src/monitor/message-handler.queue.test.ts +496 -0
  246. package/src/monitor/message-handler.routing-preflight.ts +112 -0
  247. package/src/monitor/message-handler.test-harness.ts +99 -0
  248. package/src/monitor/message-handler.test-helpers.ts +75 -0
  249. package/src/monitor/message-handler.ts +274 -0
  250. package/src/monitor/message-media.ts +507 -0
  251. package/src/monitor/message-run-queue.ts +101 -0
  252. package/src/monitor/message-text.ts +171 -0
  253. package/src/monitor/message-utils.test.ts +1151 -0
  254. package/src/monitor/message-utils.ts +32 -0
  255. package/src/monitor/model-picker-preferences.test.ts +67 -0
  256. package/src/monitor/model-picker-preferences.ts +184 -0
  257. package/src/monitor/model-picker.state.ts +364 -0
  258. package/src/monitor/model-picker.test-utils.ts +26 -0
  259. package/src/monitor/model-picker.test.ts +794 -0
  260. package/src/monitor/model-picker.ts +38 -0
  261. package/src/monitor/model-picker.view.ts +695 -0
  262. package/src/monitor/monitor.agent-components.test.ts +375 -0
  263. package/src/monitor/monitor.test.ts +849 -0
  264. package/src/monitor/monitor.threading-utils.test.ts +598 -0
  265. package/src/monitor/native-command-agent-reply.ts +123 -0
  266. package/src/monitor/native-command-arg-ui.ts +233 -0
  267. package/src/monitor/native-command-auth.ts +308 -0
  268. package/src/monitor/native-command-bypass.ts +13 -0
  269. package/src/monitor/native-command-context.test.ts +98 -0
  270. package/src/monitor/native-command-context.ts +103 -0
  271. package/src/monitor/native-command-dispatch.ts +35 -0
  272. package/src/monitor/native-command-model-picker-apply.ts +177 -0
  273. package/src/monitor/native-command-model-picker-interaction.ts +461 -0
  274. package/src/monitor/native-command-model-picker-ui.ts +368 -0
  275. package/src/monitor/native-command-reply.test.ts +68 -0
  276. package/src/monitor/native-command-reply.ts +183 -0
  277. package/src/monitor/native-command-route.ts +91 -0
  278. package/src/monitor/native-command-status.ts +76 -0
  279. package/src/monitor/native-command-ui.ts +26 -0
  280. package/src/monitor/native-command-ui.types.ts +20 -0
  281. package/src/monitor/native-command.args.ts +45 -0
  282. package/src/monitor/native-command.command-arg.test.ts +99 -0
  283. package/src/monitor/native-command.commands-allowfrom.test.ts +490 -0
  284. package/src/monitor/native-command.model-picker.test.ts +767 -0
  285. package/src/monitor/native-command.options.test.ts +369 -0
  286. package/src/monitor/native-command.options.ts +153 -0
  287. package/src/monitor/native-command.plugin-dispatch.test.ts +879 -0
  288. package/src/monitor/native-command.runtime.ts +50 -0
  289. package/src/monitor/native-command.status-direct.test.ts +272 -0
  290. package/src/monitor/native-command.test-helpers.ts +64 -0
  291. package/src/monitor/native-command.think-autocomplete.test.ts +416 -0
  292. package/src/monitor/native-command.ts +699 -0
  293. package/src/monitor/native-command.types.ts +9 -0
  294. package/src/monitor/native-interaction-channel-context.ts +50 -0
  295. package/src/monitor/preflight-audio.runtime.ts +9 -0
  296. package/src/monitor/preflight-audio.test.ts +157 -0
  297. package/src/monitor/preflight-audio.ts +130 -0
  298. package/src/monitor/presence-cache.ts +61 -0
  299. package/src/monitor/presence.test.ts +44 -0
  300. package/src/monitor/presence.ts +50 -0
  301. package/src/monitor/provider-session.runtime.ts +12 -0
  302. package/src/monitor/provider.acp.ts +89 -0
  303. package/src/monitor/provider.allowlist.test.ts +149 -0
  304. package/src/monitor/provider.allowlist.ts +394 -0
  305. package/src/monitor/provider.cleanup.ts +41 -0
  306. package/src/monitor/provider.commands.ts +129 -0
  307. package/src/monitor/provider.config-log.ts +45 -0
  308. package/src/monitor/provider.deploy-errors.ts +362 -0
  309. package/src/monitor/provider.deploy.ts +221 -0
  310. package/src/monitor/provider.interactions.ts +160 -0
  311. package/src/monitor/provider.lifecycle.test.ts +658 -0
  312. package/src/monitor/provider.lifecycle.ts +545 -0
  313. package/src/monitor/provider.proxy.test.ts +745 -0
  314. package/src/monitor/provider.rest-proxy.test.ts +121 -0
  315. package/src/monitor/provider.runtime.ts +1 -0
  316. package/src/monitor/provider.skill-dedupe.test.ts +42 -0
  317. package/src/monitor/provider.startup-log.ts +32 -0
  318. package/src/monitor/provider.startup.test.ts +426 -0
  319. package/src/monitor/provider.startup.ts +323 -0
  320. package/src/monitor/provider.test.ts +1111 -0
  321. package/src/monitor/provider.ts +713 -0
  322. package/src/monitor/reply-context.ts +64 -0
  323. package/src/monitor/reply-delivery.test.ts +244 -0
  324. package/src/monitor/reply-delivery.ts +203 -0
  325. package/src/monitor/rest-fetch.ts +43 -0
  326. package/src/monitor/route-resolution.test.ts +204 -0
  327. package/src/monitor/route-resolution.ts +140 -0
  328. package/src/monitor/sender-identity.ts +81 -0
  329. package/src/monitor/startup-status.test.ts +30 -0
  330. package/src/monitor/startup-status.ts +10 -0
  331. package/src/monitor/status.ts +22 -0
  332. package/src/monitor/system-events.ts +55 -0
  333. package/src/monitor/thread-bindings.config.ts +35 -0
  334. package/src/monitor/thread-bindings.discord-api.test.ts +229 -0
  335. package/src/monitor/thread-bindings.discord-api.ts +318 -0
  336. package/src/monitor/thread-bindings.lifecycle.test.ts +1871 -0
  337. package/src/monitor/thread-bindings.lifecycle.ts +354 -0
  338. package/src/monitor/thread-bindings.manager.ts +553 -0
  339. package/src/monitor/thread-bindings.messages.ts +6 -0
  340. package/src/monitor/thread-bindings.persona.test.ts +34 -0
  341. package/src/monitor/thread-bindings.persona.ts +25 -0
  342. package/src/monitor/thread-bindings.session-adapter.ts +229 -0
  343. package/src/monitor/thread-bindings.session-shared.ts +59 -0
  344. package/src/monitor/thread-bindings.session-updates.ts +35 -0
  345. package/src/monitor/thread-bindings.shared-state.test.ts +36 -0
  346. package/src/monitor/thread-bindings.state.ts +540 -0
  347. package/src/monitor/thread-bindings.ts +48 -0
  348. package/src/monitor/thread-bindings.types.ts +83 -0
  349. package/src/monitor/thread-channel-context.ts +112 -0
  350. package/src/monitor/thread-session-close.test.ts +180 -0
  351. package/src/monitor/thread-session-close.ts +63 -0
  352. package/src/monitor/thread-title.generate.test.ts +197 -0
  353. package/src/monitor/thread-title.test.ts +31 -0
  354. package/src/monitor/thread-title.ts +181 -0
  355. package/src/monitor/threading.auto-thread.test.ts +327 -0
  356. package/src/monitor/threading.auto-thread.ts +287 -0
  357. package/src/monitor/threading.cache.ts +45 -0
  358. package/src/monitor/threading.parent-info.test.ts +156 -0
  359. package/src/monitor/threading.starter.test.ts +260 -0
  360. package/src/monitor/threading.starter.ts +287 -0
  361. package/src/monitor/threading.ts +20 -0
  362. package/src/monitor/threading.types.ts +102 -0
  363. package/src/monitor/timeouts.ts +84 -0
  364. package/src/monitor/typing.test.ts +42 -0
  365. package/src/monitor/typing.ts +17 -0
  366. package/src/monitor.gateway.test.ts +187 -0
  367. package/src/monitor.gateway.ts +75 -0
  368. package/src/monitor.test.ts +1397 -0
  369. package/src/monitor.ts +28 -0
  370. package/src/normalize.test.ts +56 -0
  371. package/src/normalize.ts +86 -0
  372. package/src/outbound-adapter.interactive-order.test.ts +64 -0
  373. package/src/outbound-adapter.test-harness.ts +207 -0
  374. package/src/outbound-adapter.test.ts +696 -0
  375. package/src/outbound-adapter.ts +291 -0
  376. package/src/outbound-approval.ts +29 -0
  377. package/src/outbound-components.ts +81 -0
  378. package/src/outbound-payload.contract.test.ts +38 -0
  379. package/src/outbound-payload.ts +134 -0
  380. package/src/outbound-send-context.ts +92 -0
  381. package/src/outbound-session-route.test.ts +34 -0
  382. package/src/outbound-session-route.ts +72 -0
  383. package/src/pluralkit.test.ts +67 -0
  384. package/src/pluralkit.ts +58 -0
  385. package/src/preview-streaming.ts +32 -0
  386. package/src/probe.intents.test.ts +94 -0
  387. package/src/probe.parse-token.test.ts +43 -0
  388. package/src/probe.runtime.ts +1 -0
  389. package/src/probe.ts +237 -0
  390. package/src/proxy-fetch.ts +92 -0
  391. package/src/proxy-request-client.test.ts +78 -0
  392. package/src/proxy-request-client.ts +54 -0
  393. package/src/recipient-resolution.ts +39 -0
  394. package/src/resolve-allowlist-common.test.ts +36 -0
  395. package/src/resolve-allowlist-common.ts +39 -0
  396. package/src/resolve-channels.test.ts +340 -0
  397. package/src/resolve-channels.ts +369 -0
  398. package/src/resolve-users.test.ts +222 -0
  399. package/src/resolve-users.ts +184 -0
  400. package/src/retry.test.ts +83 -0
  401. package/src/retry.ts +98 -0
  402. package/src/runtime-api.ts +64 -0
  403. package/src/runtime.ts +22 -5
  404. package/src/secret-config-contract.ts +140 -0
  405. package/src/security-audit.runtime.ts +1 -0
  406. package/src/security-audit.test.ts +246 -0
  407. package/src/security-audit.ts +208 -0
  408. package/src/security-contract.ts +47 -0
  409. package/src/security-doctor.test.ts +25 -0
  410. package/src/security-doctor.ts +20 -0
  411. package/src/security.ts +60 -0
  412. package/src/send-target-parsing.ts +14 -0
  413. package/src/send.channels.ts +139 -0
  414. package/src/send.components.test.ts +275 -0
  415. package/src/send.components.ts +383 -0
  416. package/src/send.creates-thread.test.ts +643 -0
  417. package/src/send.emojis-stickers.ts +57 -0
  418. package/src/send.guild.ts +170 -0
  419. package/src/send.message-request.ts +97 -0
  420. package/src/send.messages.test.ts +53 -0
  421. package/src/send.messages.ts +225 -0
  422. package/src/send.outbound.ts +414 -0
  423. package/src/send.permissions.authz.test.ts +188 -0
  424. package/src/send.permissions.ts +283 -0
  425. package/src/send.reactions.ts +155 -0
  426. package/src/send.sends-basic-channel-messages.test.ts +919 -0
  427. package/src/send.shared.ts +445 -0
  428. package/src/send.test-harness.ts +56 -0
  429. package/src/send.ts +82 -0
  430. package/src/send.types.ts +188 -0
  431. package/src/send.typing.test.ts +41 -0
  432. package/src/send.typing.ts +9 -0
  433. package/src/send.voice.ts +134 -0
  434. package/src/send.webhook-activity.test.ts +105 -0
  435. package/src/send.webhook.proxy.test.ts +191 -0
  436. package/src/send.webhook.ts +133 -0
  437. package/src/session-contract.ts +3 -0
  438. package/src/session-key-normalization.test.ts +44 -0
  439. package/src/session-key-normalization.ts +47 -0
  440. package/src/setup-account-state.test.ts +91 -0
  441. package/src/setup-account-state.ts +144 -0
  442. package/src/setup-adapter.ts +12 -0
  443. package/src/setup-core.ts +180 -0
  444. package/src/setup-runtime-helpers.ts +10 -0
  445. package/src/setup-surface.test.ts +96 -0
  446. package/src/setup-surface.ts +129 -0
  447. package/src/shared-interactive.test.ts +153 -0
  448. package/src/shared-interactive.ts +124 -0
  449. package/src/shared.test.ts +159 -0
  450. package/src/shared.ts +190 -0
  451. package/src/status-issues.test.ts +70 -0
  452. package/src/status-issues.ts +169 -0
  453. package/src/subagent-hooks.test.ts +40 -44
  454. package/src/subagent-hooks.ts +185 -122
  455. package/src/target-parsing.ts +53 -0
  456. package/src/target-resolver.ts +129 -0
  457. package/src/targets.test.ts +367 -0
  458. package/src/targets.ts +12 -0
  459. package/src/test-http-helpers.ts +10 -0
  460. package/src/test-support/component-runtime.ts +190 -0
  461. package/src/test-support/config.ts +7 -0
  462. package/src/test-support/configured-binding-runtime.ts +29 -0
  463. package/src/test-support/partial-channel.ts +26 -0
  464. package/src/test-support/provider.test-support.ts +545 -0
  465. package/src/token.test.ts +107 -0
  466. package/src/token.ts +60 -0
  467. package/src/ui-colors.ts +27 -0
  468. package/src/ui.ts +20 -0
  469. package/src/voice/access.test.ts +217 -0
  470. package/src/voice/access.ts +124 -0
  471. package/src/voice/audio.ts +173 -0
  472. package/src/voice/capture-state.test.ts +48 -0
  473. package/src/voice/capture-state.ts +120 -0
  474. package/src/voice/command.test.ts +164 -0
  475. package/src/voice/command.ts +283 -0
  476. package/src/voice/config.ts +8 -0
  477. package/src/voice/manager.e2e.test.ts +928 -0
  478. package/src/voice/manager.ready-listener.test.ts +37 -0
  479. package/src/voice/manager.runtime.ts +11 -0
  480. package/src/voice/manager.ts +691 -0
  481. package/src/voice/prompt.test.ts +16 -0
  482. package/src/voice/prompt.ts +17 -0
  483. package/src/voice/receive-recovery.test.ts +79 -0
  484. package/src/voice/receive-recovery.ts +159 -0
  485. package/src/voice/sanitize.test.ts +34 -0
  486. package/src/voice/sanitize.ts +32 -0
  487. package/src/voice/sdk-runtime.ts +14 -0
  488. package/src/voice/segment.ts +156 -0
  489. package/src/voice/session.ts +50 -0
  490. package/src/voice/speaker-context.ts +127 -0
  491. package/src/voice/tts.ts +125 -0
  492. package/src/voice-message.test.ts +234 -0
  493. package/src/voice-message.ts +444 -0
  494. package/subagent-hooks-api.ts +27 -0
  495. package/test-api.ts +4 -0
  496. package/thread-binding-api.ts +1 -0
  497. package/timeouts.ts +6 -0
  498. package/tsconfig.json +16 -0
@@ -0,0 +1,540 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { loadJsonFile, saveJsonFile } from "openclaw/plugin-sdk/json-store";
4
+ import { normalizeAccountId, resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing";
5
+ import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
6
+ import {
7
+ normalizeLowercaseStringOrEmpty,
8
+ normalizeOptionalString,
9
+ normalizeOptionalStringifiedId,
10
+ } from "openclaw/plugin-sdk/text-runtime";
11
+ import {
12
+ DEFAULT_THREAD_BINDING_IDLE_TIMEOUT_MS,
13
+ DEFAULT_THREAD_BINDING_MAX_AGE_MS,
14
+ RECENT_UNBOUND_WEBHOOK_ECHO_WINDOW_MS,
15
+ THREAD_BINDINGS_VERSION,
16
+ type PersistedThreadBindingRecord,
17
+ type PersistedThreadBindingsPayload,
18
+ type ThreadBindingManager,
19
+ type ThreadBindingRecord,
20
+ type ThreadBindingTargetKind,
21
+ } from "./thread-bindings.types.js";
22
+
23
+ type ThreadBindingsGlobalState = {
24
+ managersByAccountId: Map<string, ThreadBindingManager>;
25
+ bindingsByThreadId: Map<string, ThreadBindingRecord>;
26
+ bindingsBySessionKey: Map<string, Set<string>>;
27
+ tokensByAccountId: Map<string, string>;
28
+ recentUnboundWebhookEchoesByBindingKey: Map<string, { webhookId: string; expiresAt: number }>;
29
+ reusableWebhooksByAccountChannel: Map<string, { webhookId: string; webhookToken: string }>;
30
+ persistByAccountId: Map<string, boolean>;
31
+ loadedBindings: boolean;
32
+ lastPersistedAtMs: number;
33
+ };
34
+
35
+ // Plugin hooks can load this module through a separate runtime path while core
36
+ // imports it via ESM. Store mutable state on globalThis so both paths share one
37
+ // registry.
38
+ const THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.discordThreadBindingsState");
39
+ let threadBindingsState: ThreadBindingsGlobalState | undefined;
40
+
41
+ function createThreadBindingsGlobalState(): ThreadBindingsGlobalState {
42
+ return {
43
+ managersByAccountId: new Map<string, ThreadBindingManager>(),
44
+ bindingsByThreadId: new Map<string, ThreadBindingRecord>(),
45
+ bindingsBySessionKey: new Map<string, Set<string>>(),
46
+ tokensByAccountId: new Map<string, string>(),
47
+ recentUnboundWebhookEchoesByBindingKey: new Map<
48
+ string,
49
+ { webhookId: string; expiresAt: number }
50
+ >(),
51
+ reusableWebhooksByAccountChannel: new Map<
52
+ string,
53
+ { webhookId: string; webhookToken: string }
54
+ >(),
55
+ persistByAccountId: new Map<string, boolean>(),
56
+ loadedBindings: false,
57
+ lastPersistedAtMs: 0,
58
+ };
59
+ }
60
+
61
+ function resolveThreadBindingsGlobalState(): ThreadBindingsGlobalState {
62
+ if (!threadBindingsState) {
63
+ const globalStore = globalThis as Record<PropertyKey, unknown>;
64
+ threadBindingsState =
65
+ (globalStore[THREAD_BINDINGS_STATE_KEY] as ThreadBindingsGlobalState | undefined) ??
66
+ createThreadBindingsGlobalState();
67
+ globalStore[THREAD_BINDINGS_STATE_KEY] = threadBindingsState;
68
+ }
69
+ return threadBindingsState;
70
+ }
71
+
72
+ const THREAD_BINDINGS_STATE = resolveThreadBindingsGlobalState();
73
+
74
+ export const MANAGERS_BY_ACCOUNT_ID = THREAD_BINDINGS_STATE.managersByAccountId;
75
+ export const BINDINGS_BY_THREAD_ID = THREAD_BINDINGS_STATE.bindingsByThreadId;
76
+ export const BINDINGS_BY_SESSION_KEY = THREAD_BINDINGS_STATE.bindingsBySessionKey;
77
+ export const TOKENS_BY_ACCOUNT_ID = THREAD_BINDINGS_STATE.tokensByAccountId;
78
+ export const RECENT_UNBOUND_WEBHOOK_ECHOES_BY_BINDING_KEY =
79
+ THREAD_BINDINGS_STATE.recentUnboundWebhookEchoesByBindingKey;
80
+ export const REUSABLE_WEBHOOKS_BY_ACCOUNT_CHANNEL =
81
+ THREAD_BINDINGS_STATE.reusableWebhooksByAccountChannel;
82
+ export const PERSIST_BY_ACCOUNT_ID = THREAD_BINDINGS_STATE.persistByAccountId;
83
+ export const THREAD_BINDING_TOUCH_PERSIST_MIN_INTERVAL_MS = 15_000;
84
+
85
+ export function rememberThreadBindingToken(params: { accountId?: string; token?: string }) {
86
+ const normalizedAccountId = normalizeAccountId(params.accountId);
87
+ const token = params.token?.trim();
88
+ if (!token) {
89
+ return;
90
+ }
91
+ TOKENS_BY_ACCOUNT_ID.set(normalizedAccountId, token);
92
+ }
93
+
94
+ export function forgetThreadBindingToken(accountId?: string) {
95
+ TOKENS_BY_ACCOUNT_ID.delete(normalizeAccountId(accountId));
96
+ }
97
+
98
+ export function getThreadBindingToken(accountId?: string): string | undefined {
99
+ return TOKENS_BY_ACCOUNT_ID.get(normalizeAccountId(accountId));
100
+ }
101
+
102
+ export function shouldDefaultPersist(): boolean {
103
+ return !(process.env.VITEST || process.env.NODE_ENV === "test");
104
+ }
105
+
106
+ export function resolveThreadBindingsPath(): string {
107
+ return path.join(resolveStateDir(process.env), "discord", "thread-bindings.json");
108
+ }
109
+
110
+ export function normalizeTargetKind(
111
+ raw: unknown,
112
+ targetSessionKey: string,
113
+ ): ThreadBindingTargetKind {
114
+ if (raw === "subagent" || raw === "acp") {
115
+ return raw;
116
+ }
117
+ return targetSessionKey.includes(":subagent:") ? "subagent" : "acp";
118
+ }
119
+
120
+ export function normalizeThreadId(raw: unknown): string | undefined {
121
+ return normalizeOptionalStringifiedId(raw);
122
+ }
123
+
124
+ export function toBindingRecordKey(params: { accountId: string; threadId: string }): string {
125
+ return `${normalizeAccountId(params.accountId)}:${params.threadId.trim()}`;
126
+ }
127
+
128
+ export function resolveBindingRecordKey(params: {
129
+ accountId?: string;
130
+ threadId: string;
131
+ }): string | undefined {
132
+ const threadId = normalizeThreadId(params.threadId);
133
+ if (!threadId) {
134
+ return undefined;
135
+ }
136
+ return toBindingRecordKey({
137
+ accountId: normalizeAccountId(params.accountId),
138
+ threadId,
139
+ });
140
+ }
141
+
142
+ function normalizePersistedBinding(threadIdKey: string, raw: unknown): ThreadBindingRecord | null {
143
+ if (!raw || typeof raw !== "object") {
144
+ return null;
145
+ }
146
+ const value = raw as Partial<PersistedThreadBindingRecord>;
147
+ const threadId = normalizeThreadId(value.threadId ?? threadIdKey);
148
+ const channelId = normalizeOptionalString(value.channelId) ?? "";
149
+ const targetSessionKey =
150
+ normalizeOptionalString(value.targetSessionKey) ??
151
+ normalizeOptionalString(value.sessionKey) ??
152
+ "";
153
+ if (!threadId || !channelId || !targetSessionKey) {
154
+ return null;
155
+ }
156
+ const accountId = normalizeAccountId(value.accountId);
157
+ const targetKind = normalizeTargetKind(value.targetKind, targetSessionKey);
158
+ const agentIdRaw = normalizeOptionalString(value.agentId) ?? "";
159
+ const agentId = agentIdRaw || resolveAgentIdFromSessionKey(targetSessionKey);
160
+ const label = normalizeOptionalString(value.label);
161
+ const webhookId = normalizeOptionalString(value.webhookId);
162
+ const webhookToken = normalizeOptionalString(value.webhookToken);
163
+ const boundBy = normalizeOptionalString(value.boundBy) ?? "system";
164
+ const boundAt =
165
+ typeof value.boundAt === "number" && Number.isFinite(value.boundAt)
166
+ ? Math.floor(value.boundAt)
167
+ : Date.now();
168
+ const lastActivityAt =
169
+ typeof value.lastActivityAt === "number" && Number.isFinite(value.lastActivityAt)
170
+ ? Math.max(0, Math.floor(value.lastActivityAt))
171
+ : boundAt;
172
+ const idleTimeoutMs =
173
+ typeof value.idleTimeoutMs === "number" && Number.isFinite(value.idleTimeoutMs)
174
+ ? Math.max(0, Math.floor(value.idleTimeoutMs))
175
+ : undefined;
176
+ const maxAgeMs =
177
+ typeof value.maxAgeMs === "number" && Number.isFinite(value.maxAgeMs)
178
+ ? Math.max(0, Math.floor(value.maxAgeMs))
179
+ : undefined;
180
+ const metadata =
181
+ value.metadata && typeof value.metadata === "object" ? { ...value.metadata } : undefined;
182
+ const legacyExpiresAt =
183
+ typeof (value as { expiresAt?: unknown }).expiresAt === "number" &&
184
+ Number.isFinite((value as { expiresAt?: unknown }).expiresAt)
185
+ ? Math.max(0, Math.floor((value as { expiresAt?: number }).expiresAt ?? 0))
186
+ : undefined;
187
+
188
+ let migratedIdleTimeoutMs = idleTimeoutMs;
189
+ let migratedMaxAgeMs = maxAgeMs;
190
+ if (
191
+ migratedIdleTimeoutMs === undefined &&
192
+ migratedMaxAgeMs === undefined &&
193
+ legacyExpiresAt != null
194
+ ) {
195
+ if (legacyExpiresAt <= 0) {
196
+ migratedIdleTimeoutMs = 0;
197
+ migratedMaxAgeMs = 0;
198
+ } else {
199
+ const baseBoundAt = boundAt > 0 ? boundAt : lastActivityAt;
200
+ // Legacy expiresAt represented an absolute timestamp; map it to max-age and disable idle timeout.
201
+ migratedIdleTimeoutMs = 0;
202
+ migratedMaxAgeMs = Math.max(1, legacyExpiresAt - Math.max(0, baseBoundAt));
203
+ }
204
+ }
205
+
206
+ return {
207
+ accountId,
208
+ channelId,
209
+ threadId,
210
+ targetKind,
211
+ targetSessionKey,
212
+ agentId,
213
+ label,
214
+ webhookId,
215
+ webhookToken,
216
+ boundBy,
217
+ boundAt,
218
+ lastActivityAt,
219
+ idleTimeoutMs: migratedIdleTimeoutMs,
220
+ maxAgeMs: migratedMaxAgeMs,
221
+ metadata,
222
+ };
223
+ }
224
+
225
+ export function normalizeThreadBindingDurationMs(raw: unknown, defaultsTo: number): number {
226
+ if (typeof raw !== "number" || !Number.isFinite(raw)) {
227
+ return defaultsTo;
228
+ }
229
+ const durationMs = Math.floor(raw);
230
+ if (durationMs < 0) {
231
+ return defaultsTo;
232
+ }
233
+ return durationMs;
234
+ }
235
+
236
+ export function resolveThreadBindingIdleTimeoutMs(params: {
237
+ record: Pick<ThreadBindingRecord, "idleTimeoutMs">;
238
+ defaultIdleTimeoutMs: number;
239
+ }): number {
240
+ const explicit = params.record.idleTimeoutMs;
241
+ if (typeof explicit === "number" && Number.isFinite(explicit)) {
242
+ return Math.max(0, Math.floor(explicit));
243
+ }
244
+ return Math.max(0, Math.floor(params.defaultIdleTimeoutMs));
245
+ }
246
+
247
+ export function resolveThreadBindingMaxAgeMs(params: {
248
+ record: Pick<ThreadBindingRecord, "maxAgeMs">;
249
+ defaultMaxAgeMs: number;
250
+ }): number {
251
+ const explicit = params.record.maxAgeMs;
252
+ if (typeof explicit === "number" && Number.isFinite(explicit)) {
253
+ return Math.max(0, Math.floor(explicit));
254
+ }
255
+ return Math.max(0, Math.floor(params.defaultMaxAgeMs));
256
+ }
257
+
258
+ export function resolveThreadBindingInactivityExpiresAt(params: {
259
+ record: Pick<ThreadBindingRecord, "lastActivityAt" | "idleTimeoutMs">;
260
+ defaultIdleTimeoutMs: number;
261
+ }): number | undefined {
262
+ const idleTimeoutMs = resolveThreadBindingIdleTimeoutMs({
263
+ record: params.record,
264
+ defaultIdleTimeoutMs: params.defaultIdleTimeoutMs,
265
+ });
266
+ if (idleTimeoutMs <= 0) {
267
+ return undefined;
268
+ }
269
+ const lastActivityAt = Math.floor(params.record.lastActivityAt);
270
+ if (!Number.isFinite(lastActivityAt) || lastActivityAt <= 0) {
271
+ return undefined;
272
+ }
273
+ return lastActivityAt + idleTimeoutMs;
274
+ }
275
+
276
+ export function resolveThreadBindingMaxAgeExpiresAt(params: {
277
+ record: Pick<ThreadBindingRecord, "boundAt" | "maxAgeMs">;
278
+ defaultMaxAgeMs: number;
279
+ }): number | undefined {
280
+ const maxAgeMs = resolveThreadBindingMaxAgeMs({
281
+ record: params.record,
282
+ defaultMaxAgeMs: params.defaultMaxAgeMs,
283
+ });
284
+ if (maxAgeMs <= 0) {
285
+ return undefined;
286
+ }
287
+ const boundAt = Math.floor(params.record.boundAt);
288
+ if (!Number.isFinite(boundAt) || boundAt <= 0) {
289
+ return undefined;
290
+ }
291
+ return boundAt + maxAgeMs;
292
+ }
293
+
294
+ function linkSessionBinding(targetSessionKey: string, bindingKey: string) {
295
+ const key = targetSessionKey.trim();
296
+ if (!key) {
297
+ return;
298
+ }
299
+ const threads = BINDINGS_BY_SESSION_KEY.get(key) ?? new Set<string>();
300
+ threads.add(bindingKey);
301
+ BINDINGS_BY_SESSION_KEY.set(key, threads);
302
+ }
303
+
304
+ function unlinkSessionBinding(targetSessionKey: string, bindingKey: string) {
305
+ const key = targetSessionKey.trim();
306
+ if (!key) {
307
+ return;
308
+ }
309
+ const threads = BINDINGS_BY_SESSION_KEY.get(key);
310
+ if (!threads) {
311
+ return;
312
+ }
313
+ threads.delete(bindingKey);
314
+ if (threads.size === 0) {
315
+ BINDINGS_BY_SESSION_KEY.delete(key);
316
+ }
317
+ }
318
+
319
+ export function toReusableWebhookKey(params: { accountId: string; channelId: string }): string {
320
+ return `${normalizeLowercaseStringOrEmpty(params.accountId)}:${params.channelId.trim()}`;
321
+ }
322
+
323
+ export function rememberReusableWebhook(record: ThreadBindingRecord) {
324
+ const webhookId = record.webhookId?.trim();
325
+ const webhookToken = record.webhookToken?.trim();
326
+ if (!webhookId || !webhookToken) {
327
+ return;
328
+ }
329
+ const key = toReusableWebhookKey({
330
+ accountId: record.accountId,
331
+ channelId: record.channelId,
332
+ });
333
+ REUSABLE_WEBHOOKS_BY_ACCOUNT_CHANNEL.set(key, { webhookId, webhookToken });
334
+ }
335
+
336
+ export function rememberRecentUnboundWebhookEcho(record: ThreadBindingRecord) {
337
+ const webhookId = record.webhookId?.trim();
338
+ if (!webhookId) {
339
+ return;
340
+ }
341
+ const bindingKey = resolveBindingRecordKey({
342
+ accountId: record.accountId,
343
+ threadId: record.threadId,
344
+ });
345
+ if (!bindingKey) {
346
+ return;
347
+ }
348
+ RECENT_UNBOUND_WEBHOOK_ECHOES_BY_BINDING_KEY.set(bindingKey, {
349
+ webhookId,
350
+ expiresAt: Date.now() + RECENT_UNBOUND_WEBHOOK_ECHO_WINDOW_MS,
351
+ });
352
+ }
353
+
354
+ function clearRecentUnboundWebhookEcho(bindingKeyRaw: string) {
355
+ const key = bindingKeyRaw.trim();
356
+ if (!key) {
357
+ return;
358
+ }
359
+ RECENT_UNBOUND_WEBHOOK_ECHOES_BY_BINDING_KEY.delete(key);
360
+ }
361
+
362
+ export function setBindingRecord(record: ThreadBindingRecord) {
363
+ const bindingKey = toBindingRecordKey({
364
+ accountId: record.accountId,
365
+ threadId: record.threadId,
366
+ });
367
+ const existing = BINDINGS_BY_THREAD_ID.get(bindingKey);
368
+ if (existing) {
369
+ unlinkSessionBinding(existing.targetSessionKey, bindingKey);
370
+ }
371
+ BINDINGS_BY_THREAD_ID.set(bindingKey, record);
372
+ linkSessionBinding(record.targetSessionKey, bindingKey);
373
+ clearRecentUnboundWebhookEcho(bindingKey);
374
+ rememberReusableWebhook(record);
375
+ }
376
+
377
+ export function removeBindingRecord(bindingKeyRaw: string): ThreadBindingRecord | null {
378
+ const key = bindingKeyRaw.trim();
379
+ if (!key) {
380
+ return null;
381
+ }
382
+ const existing = BINDINGS_BY_THREAD_ID.get(key);
383
+ if (!existing) {
384
+ return null;
385
+ }
386
+ BINDINGS_BY_THREAD_ID.delete(key);
387
+ unlinkSessionBinding(existing.targetSessionKey, key);
388
+ return existing;
389
+ }
390
+
391
+ export function isRecentlyUnboundThreadWebhookMessage(params: {
392
+ accountId?: string;
393
+ threadId: string;
394
+ webhookId?: string | null;
395
+ }): boolean {
396
+ const webhookId = normalizeOptionalString(params.webhookId) ?? "";
397
+ if (!webhookId) {
398
+ return false;
399
+ }
400
+ const bindingKey = resolveBindingRecordKey({
401
+ accountId: params.accountId,
402
+ threadId: params.threadId,
403
+ });
404
+ if (!bindingKey) {
405
+ return false;
406
+ }
407
+ const suppressed = RECENT_UNBOUND_WEBHOOK_ECHOES_BY_BINDING_KEY.get(bindingKey);
408
+ if (!suppressed) {
409
+ return false;
410
+ }
411
+ if (suppressed.expiresAt <= Date.now()) {
412
+ RECENT_UNBOUND_WEBHOOK_ECHOES_BY_BINDING_KEY.delete(bindingKey);
413
+ return false;
414
+ }
415
+ return suppressed.webhookId === webhookId;
416
+ }
417
+
418
+ function shouldPersistAnyBindingState(): boolean {
419
+ for (const value of PERSIST_BY_ACCOUNT_ID.values()) {
420
+ if (value) {
421
+ return true;
422
+ }
423
+ }
424
+ return false;
425
+ }
426
+
427
+ export function shouldPersistBindingMutations(): boolean {
428
+ if (shouldPersistAnyBindingState()) {
429
+ return true;
430
+ }
431
+ return fs.existsSync(resolveThreadBindingsPath());
432
+ }
433
+
434
+ export function saveBindingsToDisk(params: { force?: boolean; minIntervalMs?: number } = {}) {
435
+ if (!params.force && !shouldPersistAnyBindingState()) {
436
+ return;
437
+ }
438
+ const minIntervalMs =
439
+ typeof params.minIntervalMs === "number" && Number.isFinite(params.minIntervalMs)
440
+ ? Math.max(0, Math.floor(params.minIntervalMs))
441
+ : 0;
442
+ const now = Date.now();
443
+ if (
444
+ !params.force &&
445
+ minIntervalMs > 0 &&
446
+ THREAD_BINDINGS_STATE.lastPersistedAtMs > 0 &&
447
+ now - THREAD_BINDINGS_STATE.lastPersistedAtMs < minIntervalMs
448
+ ) {
449
+ return;
450
+ }
451
+ const bindings: Record<string, PersistedThreadBindingRecord> = {};
452
+ for (const [bindingKey, record] of BINDINGS_BY_THREAD_ID.entries()) {
453
+ bindings[bindingKey] = { ...record };
454
+ }
455
+ const payload: PersistedThreadBindingsPayload = {
456
+ version: THREAD_BINDINGS_VERSION,
457
+ bindings,
458
+ };
459
+ saveJsonFile(resolveThreadBindingsPath(), payload);
460
+ THREAD_BINDINGS_STATE.lastPersistedAtMs = now;
461
+ }
462
+
463
+ export function ensureBindingsLoaded() {
464
+ if (THREAD_BINDINGS_STATE.loadedBindings) {
465
+ return;
466
+ }
467
+ THREAD_BINDINGS_STATE.loadedBindings = true;
468
+ BINDINGS_BY_THREAD_ID.clear();
469
+ BINDINGS_BY_SESSION_KEY.clear();
470
+ REUSABLE_WEBHOOKS_BY_ACCOUNT_CHANNEL.clear();
471
+
472
+ const raw = loadJsonFile(resolveThreadBindingsPath());
473
+ if (!raw || typeof raw !== "object") {
474
+ return;
475
+ }
476
+ const payload = raw as Partial<PersistedThreadBindingsPayload>;
477
+ if (payload.version !== 1 || !payload.bindings || typeof payload.bindings !== "object") {
478
+ return;
479
+ }
480
+
481
+ for (const [threadId, entry] of Object.entries(payload.bindings)) {
482
+ const normalized = normalizePersistedBinding(threadId, entry);
483
+ if (!normalized) {
484
+ continue;
485
+ }
486
+ setBindingRecord(normalized);
487
+ }
488
+ }
489
+
490
+ export function resolveBindingIdsForSession(params: {
491
+ targetSessionKey: string;
492
+ accountId?: string;
493
+ targetKind?: ThreadBindingTargetKind;
494
+ }): string[] {
495
+ const key = params.targetSessionKey.trim();
496
+ if (!key) {
497
+ return [];
498
+ }
499
+ const ids = BINDINGS_BY_SESSION_KEY.get(key);
500
+ if (!ids) {
501
+ return [];
502
+ }
503
+ const out: string[] = [];
504
+ for (const bindingKey of ids.values()) {
505
+ const record = BINDINGS_BY_THREAD_ID.get(bindingKey);
506
+ if (!record) {
507
+ continue;
508
+ }
509
+ if (params.accountId && record.accountId !== params.accountId) {
510
+ continue;
511
+ }
512
+ if (params.targetKind && record.targetKind !== params.targetKind) {
513
+ continue;
514
+ }
515
+ out.push(bindingKey);
516
+ }
517
+ return out;
518
+ }
519
+
520
+ export function resolveDefaultThreadBindingDurations() {
521
+ return {
522
+ defaultIdleTimeoutMs: DEFAULT_THREAD_BINDING_IDLE_TIMEOUT_MS,
523
+ defaultMaxAgeMs: DEFAULT_THREAD_BINDING_MAX_AGE_MS,
524
+ };
525
+ }
526
+
527
+ export function resetThreadBindingsForTests() {
528
+ for (const manager of MANAGERS_BY_ACCOUNT_ID.values()) {
529
+ manager.stop();
530
+ }
531
+ MANAGERS_BY_ACCOUNT_ID.clear();
532
+ BINDINGS_BY_THREAD_ID.clear();
533
+ BINDINGS_BY_SESSION_KEY.clear();
534
+ RECENT_UNBOUND_WEBHOOK_ECHOES_BY_BINDING_KEY.clear();
535
+ REUSABLE_WEBHOOKS_BY_ACCOUNT_CHANNEL.clear();
536
+ TOKENS_BY_ACCOUNT_ID.clear();
537
+ PERSIST_BY_ACCOUNT_ID.clear();
538
+ THREAD_BINDINGS_STATE.loadedBindings = false;
539
+ THREAD_BINDINGS_STATE.lastPersistedAtMs = 0;
540
+ }
@@ -0,0 +1,48 @@
1
+ export type {
2
+ ThreadBindingManager,
3
+ ThreadBindingRecord,
4
+ ThreadBindingTargetKind,
5
+ } from "./thread-bindings.types.js";
6
+
7
+ export {
8
+ formatThreadBindingDurationLabel,
9
+ resolveThreadBindingIntroText,
10
+ resolveThreadBindingThreadName,
11
+ } from "./thread-bindings.messages.js";
12
+ export {
13
+ resolveThreadBindingPersona,
14
+ resolveThreadBindingPersonaFromRecord,
15
+ } from "./thread-bindings.persona.js";
16
+
17
+ export {
18
+ resolveDiscordThreadBindingIdleTimeoutMs,
19
+ resolveDiscordThreadBindingMaxAgeMs,
20
+ resolveThreadBindingsEnabled,
21
+ } from "./thread-bindings.config.js";
22
+
23
+ export {
24
+ isRecentlyUnboundThreadWebhookMessage,
25
+ resolveThreadBindingIdleTimeoutMs,
26
+ resolveThreadBindingInactivityExpiresAt,
27
+ resolveThreadBindingMaxAgeExpiresAt,
28
+ resolveThreadBindingMaxAgeMs,
29
+ } from "./thread-bindings.state.js";
30
+
31
+ export {
32
+ autoBindSpawnedDiscordSubagent,
33
+ listThreadBindingsBySessionKey,
34
+ listThreadBindingsForAccount,
35
+ reconcileAcpThreadBindingsOnStartup,
36
+ setThreadBindingIdleTimeoutBySessionKey,
37
+ setThreadBindingMaxAgeBySessionKey,
38
+ unbindThreadBindingsBySessionKey,
39
+ } from "./thread-bindings.lifecycle.js";
40
+
41
+ export type { AcpThreadBindingReconciliationResult } from "./thread-bindings.lifecycle.js";
42
+
43
+ export {
44
+ __testing,
45
+ createNoopThreadBindingManager,
46
+ createThreadBindingManager,
47
+ getThreadBindingManager,
48
+ } from "./thread-bindings.manager.js";
@@ -0,0 +1,83 @@
1
+ export type ThreadBindingTargetKind = "subagent" | "acp";
2
+
3
+ export type ThreadBindingRecord = {
4
+ accountId: string;
5
+ channelId: string;
6
+ threadId: string;
7
+ targetKind: ThreadBindingTargetKind;
8
+ targetSessionKey: string;
9
+ agentId: string;
10
+ label?: string;
11
+ webhookId?: string;
12
+ webhookToken?: string;
13
+ boundBy: string;
14
+ boundAt: number;
15
+ lastActivityAt: number;
16
+ /** Inactivity timeout window in milliseconds (0 disables inactivity auto-unfocus). */
17
+ idleTimeoutMs?: number;
18
+ /** Hard max-age window in milliseconds from bind time (0 disables hard cap). */
19
+ maxAgeMs?: number;
20
+ metadata?: Record<string, unknown>;
21
+ };
22
+
23
+ export type PersistedThreadBindingRecord = ThreadBindingRecord & {
24
+ sessionKey?: string;
25
+ /** @deprecated Legacy absolute expiry timestamp; migrated on load. */
26
+ expiresAt?: number;
27
+ };
28
+
29
+ export type PersistedThreadBindingsPayload = {
30
+ version: 1;
31
+ bindings: Record<string, PersistedThreadBindingRecord>;
32
+ };
33
+
34
+ export type ThreadBindingManager = {
35
+ accountId: string;
36
+ getIdleTimeoutMs: () => number;
37
+ getMaxAgeMs: () => number;
38
+ getByThreadId: (threadId: string) => ThreadBindingRecord | undefined;
39
+ getBySessionKey: (targetSessionKey: string) => ThreadBindingRecord | undefined;
40
+ listBySessionKey: (targetSessionKey: string) => ThreadBindingRecord[];
41
+ listBindings: () => ThreadBindingRecord[];
42
+ touchThread: (params: {
43
+ threadId: string;
44
+ at?: number;
45
+ persist?: boolean;
46
+ }) => ThreadBindingRecord | null;
47
+ bindTarget: (params: {
48
+ threadId?: string | number;
49
+ channelId?: string;
50
+ createThread?: boolean;
51
+ threadName?: string;
52
+ targetKind: ThreadBindingTargetKind;
53
+ targetSessionKey: string;
54
+ agentId?: string;
55
+ label?: string;
56
+ boundBy?: string;
57
+ introText?: string;
58
+ webhookId?: string;
59
+ webhookToken?: string;
60
+ metadata?: Record<string, unknown>;
61
+ }) => Promise<ThreadBindingRecord | null>;
62
+ unbindThread: (params: {
63
+ threadId: string;
64
+ reason?: string;
65
+ sendFarewell?: boolean;
66
+ farewellText?: string;
67
+ }) => ThreadBindingRecord | null;
68
+ unbindBySessionKey: (params: {
69
+ targetSessionKey: string;
70
+ targetKind?: ThreadBindingTargetKind;
71
+ reason?: string;
72
+ sendFarewell?: boolean;
73
+ farewellText?: string;
74
+ }) => ThreadBindingRecord[];
75
+ stop: () => void;
76
+ };
77
+
78
+ export const THREAD_BINDINGS_VERSION = 1 as const;
79
+ export const THREAD_BINDINGS_SWEEP_INTERVAL_MS = 120_000;
80
+ export const DEFAULT_THREAD_BINDING_IDLE_TIMEOUT_MS = 24 * 60 * 60 * 1000; // 24h
81
+ export const DEFAULT_THREAD_BINDING_MAX_AGE_MS = 0; // disabled
82
+ export const DISCORD_UNKNOWN_CHANNEL_ERROR_CODE = 10_003;
83
+ export const RECENT_UNBOUND_WEBHOOK_ECHO_WINDOW_MS = 30_000;