@poolzin/pool-bot 2026.2.21 → 2026.2.23

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 (378) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/agents/api-key-rotation.js +47 -0
  3. package/dist/agents/apply-patch-update.js +19 -9
  4. package/dist/agents/apply-patch.js +72 -47
  5. package/dist/agents/bash-tools.exec.js +141 -559
  6. package/dist/agents/cli-backends.js +49 -6
  7. package/dist/agents/cli-runner/helpers.js +69 -152
  8. package/dist/agents/cli-runner.js +70 -19
  9. package/dist/agents/identity.js +20 -1
  10. package/dist/agents/image-sanitization.js +9 -0
  11. package/dist/agents/live-auth-keys.js +123 -26
  12. package/dist/agents/live-model-filter.js +13 -4
  13. package/dist/agents/model-catalog.js +40 -9
  14. package/dist/agents/model-forward-compat.js +60 -23
  15. package/dist/agents/model-selection.js +134 -41
  16. package/dist/agents/pi-auth-json.js +2 -2
  17. package/dist/agents/pi-embedded-helpers/bootstrap.js +65 -15
  18. package/dist/agents/pi-embedded-helpers/errors.js +140 -15
  19. package/dist/agents/pi-embedded-helpers/images.js +22 -12
  20. package/dist/agents/pi-embedded-helpers.js +2 -2
  21. package/dist/agents/pi-embedded-runner/abort.js +10 -3
  22. package/dist/agents/pi-embedded-runner/compact.js +230 -32
  23. package/dist/agents/pi-embedded-runner/extra-params.js +203 -12
  24. package/dist/agents/pi-embedded-runner/google.js +109 -19
  25. package/dist/agents/pi-embedded-runner/history.js +35 -17
  26. package/dist/agents/pi-embedded-runner/run/attempt.js +386 -95
  27. package/dist/agents/pi-embedded-runner/run/images.js +81 -55
  28. package/dist/agents/pi-embedded-runner/run/payloads.js +89 -39
  29. package/dist/agents/pi-embedded-runner/run.js +193 -25
  30. package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +2 -2
  31. package/dist/agents/pi-embedded-runner/runs.js +17 -8
  32. package/dist/agents/pi-embedded-runner/tool-result-context-guard.js +262 -0
  33. package/dist/agents/pi-embedded-runner.js +1 -1
  34. package/dist/agents/pi-embedded-subscribe.handlers.tools.js +180 -10
  35. package/dist/agents/pi-embedded-subscribe.js +37 -0
  36. package/dist/agents/pi-embedded-subscribe.tools.js +127 -30
  37. package/dist/agents/pi-model-discovery.js +9 -2
  38. package/dist/agents/pi-tool-definition-adapter.js +60 -8
  39. package/dist/agents/pi-tools.before-tool-call.js +1 -1
  40. package/dist/agents/pi-tools.js +113 -94
  41. package/dist/agents/pi-tools.read.js +337 -38
  42. package/dist/agents/poolbot-tools.js +14 -5
  43. package/dist/agents/sandbox/docker.js +10 -5
  44. package/dist/agents/sandbox/registry.js +96 -46
  45. package/dist/agents/sandbox/sanitize-env-vars.js +82 -0
  46. package/dist/agents/sandbox-paths.js +43 -10
  47. package/dist/agents/session-tool-result-guard-wrapper.js +23 -11
  48. package/dist/agents/session-tool-result-guard.js +39 -39
  49. package/dist/agents/session-transcript-repair.js +36 -33
  50. package/dist/agents/session-write-lock.js +62 -44
  51. package/dist/agents/skills/frontmatter.js +49 -88
  52. package/dist/agents/skills/workspace.js +335 -28
  53. package/dist/agents/subagent-announce.js +508 -174
  54. package/dist/agents/subagent-registry.js +45 -4
  55. package/dist/agents/subagent-spawn.js +16 -33
  56. package/dist/agents/system-prompt-report.js +27 -10
  57. package/dist/agents/system-prompt.js +26 -32
  58. package/dist/agents/tool-call-id.js +69 -17
  59. package/dist/agents/tool-display-common.js +1 -1
  60. package/dist/agents/tool-images.js +64 -31
  61. package/dist/agents/tools/canvas-tool.js +17 -11
  62. package/dist/agents/tools/common.js +37 -19
  63. package/dist/agents/tools/cron-tool.js +40 -38
  64. package/dist/agents/tools/gateway.js +70 -2
  65. package/dist/agents/tools/message-tool.js +181 -40
  66. package/dist/agents/tools/nodes-tool.js +128 -36
  67. package/dist/agents/tools/nodes-utils.js +12 -38
  68. package/dist/agents/tools/session-status-tool.js +24 -71
  69. package/dist/agents/tools/sessions-helpers.js +38 -210
  70. package/dist/agents/tools/sessions-spawn-tool.js +28 -198
  71. package/dist/agents/tools/telegram-actions.js +58 -7
  72. package/dist/agents/tools/web-fetch-utils.js +112 -7
  73. package/dist/agents/tools/web-fetch.js +279 -175
  74. package/dist/agents/tools/web-shared.js +71 -8
  75. package/dist/agents/usage.js +25 -16
  76. package/dist/auto-reply/commands-registry.data.js +85 -11
  77. package/dist/auto-reply/dispatch.js +40 -21
  78. package/dist/auto-reply/reply/abort.js +102 -33
  79. package/dist/auto-reply/reply/commands-core.js +82 -33
  80. package/dist/auto-reply/reply/commands-export-session.js +1 -1
  81. package/dist/auto-reply/reply/commands-info.js +41 -12
  82. package/dist/auto-reply/reply/commands-subagents.js +352 -100
  83. package/dist/auto-reply/reply/commands-system-prompt.js +2 -2
  84. package/dist/auto-reply/reply/dispatch-from-config.js +100 -29
  85. package/dist/auto-reply/reply/elevated-unavailable.js +1 -1
  86. package/dist/auto-reply/reply/inbound-meta.js +12 -1
  87. package/dist/auto-reply/reply/mentions.js +18 -11
  88. package/dist/auto-reply/reply/normalize-reply.js +17 -8
  89. package/dist/auto-reply/reply/reply-dispatcher.js +62 -10
  90. package/dist/auto-reply/reply/session.js +102 -21
  91. package/dist/auto-reply/reply/streaming-directives.js +16 -5
  92. package/dist/auto-reply/status.js +73 -50
  93. package/dist/browser/extension-relay.js +3 -3
  94. package/dist/browser/http-auth.js +1 -1
  95. package/dist/browser/paths.js +2 -2
  96. package/dist/build-info.json +3 -3
  97. package/dist/channels/allowlist-match.js +20 -0
  98. package/dist/channels/allowlists/resolve-utils.js +65 -2
  99. package/dist/channels/chat-type.js +8 -4
  100. package/dist/channels/dock.js +127 -35
  101. package/dist/channels/draft-stream-loop.js +6 -2
  102. package/dist/channels/plugins/actions/telegram.js +42 -18
  103. package/dist/channels/plugins/allowlist-match.js +1 -1
  104. package/dist/channels/plugins/group-mentions.js +51 -41
  105. package/dist/channels/plugins/message-action-names.js +2 -0
  106. package/dist/channels/plugins/message-actions.js +24 -5
  107. package/dist/channels/plugins/normalize/discord.js +26 -4
  108. package/dist/channels/plugins/normalize/signal.js +35 -22
  109. package/dist/channels/plugins/onboarding/helpers.js +8 -26
  110. package/dist/channels/plugins/outbound/imessage.js +15 -14
  111. package/dist/channels/registry.js +20 -7
  112. package/dist/cli/acp-cli.js +7 -5
  113. package/dist/cli/browser-cli-extension.js +25 -12
  114. package/dist/cli/browser-cli-state.cookies-storage.js +25 -6
  115. package/dist/cli/browser-cli-state.js +101 -145
  116. package/dist/cli/command-options.js +28 -0
  117. package/dist/cli/completion-cli.js +6 -6
  118. package/dist/cli/cron-cli/register.cron-add.js +25 -1
  119. package/dist/cli/cron-cli/register.cron-edit.js +44 -0
  120. package/dist/cli/cron-cli/shared.js +7 -1
  121. package/dist/cli/daemon-cli/lifecycle-core.js +23 -21
  122. package/dist/cli/daemon-cli/lifecycle.js +23 -247
  123. package/dist/cli/daemon-cli/register-service-commands.js +25 -4
  124. package/dist/cli/daemon-cli.js +1 -0
  125. package/dist/cli/devices-cli.js +33 -20
  126. package/dist/cli/gateway-cli/register.js +37 -105
  127. package/dist/cli/gateway-cli/run.js +49 -11
  128. package/dist/cli/nodes-camera.js +59 -4
  129. package/dist/cli/nodes-cli/register.camera.js +27 -24
  130. package/dist/cli/nodes-cli/rpc.js +21 -38
  131. package/dist/cli/qr-cli.js +2 -2
  132. package/dist/cli/skills-cli.format.js +2 -2
  133. package/dist/cli/update-cli/progress.js +2 -2
  134. package/dist/cli/update-cli/restart-helper.js +28 -7
  135. package/dist/cli/update-cli/shared.js +7 -7
  136. package/dist/cli/update-cli/status.js +1 -1
  137. package/dist/cli/update-cli/update-command.js +14 -8
  138. package/dist/cli/update-cli/wizard.js +2 -2
  139. package/dist/cli/update-cli.js +21 -1027
  140. package/dist/commands/auth-choice.apply.anthropic.js +10 -2
  141. package/dist/commands/channels/add-mutators.js +3 -35
  142. package/dist/commands/channels/add.js +39 -51
  143. package/dist/commands/config-validation.js +1 -1
  144. package/dist/commands/configure.gateway-auth.js +52 -15
  145. package/dist/commands/configure.gateway.js +84 -40
  146. package/dist/commands/doctor-completion.js +3 -3
  147. package/dist/commands/doctor-config-flow.js +536 -16
  148. package/dist/commands/doctor-gateway-services.js +103 -79
  149. package/dist/commands/doctor-memory-search.js +9 -9
  150. package/dist/commands/doctor-platform-notes.js +57 -30
  151. package/dist/commands/doctor-prompter.js +26 -15
  152. package/dist/commands/doctor-session-locks.js +1 -1
  153. package/dist/commands/doctor.js +21 -9
  154. package/dist/commands/model-picker.js +120 -95
  155. package/dist/commands/models/set.js +2 -21
  156. package/dist/commands/models/shared.js +65 -37
  157. package/dist/commands/onboard-helpers.js +81 -39
  158. package/dist/commands/openai-codex-oauth.js +1 -1
  159. package/dist/commands/sessions.js +52 -53
  160. package/dist/commands/status.summary.js +52 -34
  161. package/dist/commands/test-wizard-helpers.js +2 -2
  162. package/dist/config/defaults.js +79 -42
  163. package/dist/config/group-policy.js +50 -18
  164. package/dist/config/includes.js +37 -10
  165. package/dist/config/schema.help.js +5 -4
  166. package/dist/config/schema.hints.js +2 -2
  167. package/dist/config/schema.labels.js +1 -0
  168. package/dist/config/sessions/group.js +12 -11
  169. package/dist/config/sessions/paths.js +137 -11
  170. package/dist/config/sessions/store.js +185 -65
  171. package/dist/config/sessions/types.js +15 -1
  172. package/dist/config/sessions.js +1 -0
  173. package/dist/config/telegram-custom-commands.js +3 -2
  174. package/dist/config/types.js +2 -0
  175. package/dist/config/zod-schema.agent-defaults.js +6 -27
  176. package/dist/config/zod-schema.agent-runtime.js +171 -79
  177. package/dist/config/zod-schema.providers-core.js +138 -65
  178. package/dist/config/zod-schema.session.js +49 -22
  179. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
  180. package/dist/cron/isolated-agent/run.js +224 -57
  181. package/dist/cron/normalize.js +48 -45
  182. package/dist/cron/run-log.js +14 -0
  183. package/dist/cron/service/jobs.js +190 -28
  184. package/dist/cron/service/normalize.js +29 -11
  185. package/dist/cron/service/store.js +30 -44
  186. package/dist/cron/service/timer.js +182 -96
  187. package/dist/cron/service.js +3 -0
  188. package/dist/cron/stagger.js +37 -0
  189. package/dist/daemon/inspect.js +132 -92
  190. package/dist/daemon/runtime-paths.js +25 -4
  191. package/dist/daemon/service-audit.js +47 -16
  192. package/dist/discord/accounts.js +23 -20
  193. package/dist/discord/monitor/agent-components.js +1115 -219
  194. package/dist/discord/monitor/allow-list.js +114 -34
  195. package/dist/discord/monitor/listeners.js +204 -97
  196. package/dist/discord/monitor/message-handler.js +21 -10
  197. package/dist/discord/monitor/message-handler.preflight.js +195 -101
  198. package/dist/discord/monitor/message-handler.process.js +384 -123
  199. package/dist/discord/monitor/message-utils.js +86 -23
  200. package/dist/discord/monitor/native-command.js +77 -57
  201. package/dist/discord/monitor/provider.js +122 -117
  202. package/dist/discord/monitor/reply-context.js +20 -16
  203. package/dist/discord/monitor/reply-delivery.js +40 -8
  204. package/dist/discord/monitor/rest-fetch.js +22 -0
  205. package/dist/discord/monitor/threading.js +117 -24
  206. package/dist/discord/send.js +2 -1
  207. package/dist/discord/send.outbound.js +124 -11
  208. package/dist/discord/send.shared.js +112 -72
  209. package/dist/discord/voice-message.js +3 -3
  210. package/dist/gateway/auth.js +119 -44
  211. package/dist/gateway/call.js +76 -34
  212. package/dist/gateway/channel-health-monitor.js +57 -50
  213. package/dist/gateway/client.js +63 -29
  214. package/dist/gateway/control-ui-contract.js +1 -1
  215. package/dist/gateway/gateway-config-prompts.shared.js +2 -2
  216. package/dist/gateway/net.js +109 -1
  217. package/dist/gateway/protocol/index.js +5 -8
  218. package/dist/gateway/protocol/schema/agent.js +19 -1
  219. package/dist/gateway/protocol/schema/channels.js +21 -0
  220. package/dist/gateway/protocol/schema/cron.js +43 -30
  221. package/dist/gateway/protocol/schema/protocol-schemas.js +6 -11
  222. package/dist/gateway/protocol/schema/sessions.js +5 -1
  223. package/dist/gateway/protocol/schema.js +0 -1
  224. package/dist/gateway/server/presence-events.js +12 -0
  225. package/dist/gateway/server/ws-connection/message-handler.js +203 -212
  226. package/dist/gateway/server/ws-connection.js +58 -21
  227. package/dist/gateway/server-broadcast.js +18 -13
  228. package/dist/gateway/server-cron.js +177 -10
  229. package/dist/gateway/server-methods/agent-job.js +131 -38
  230. package/dist/gateway/server-methods/send.js +60 -14
  231. package/dist/gateway/server-methods/sessions.js +160 -96
  232. package/dist/gateway/server-methods/system.js +5 -7
  233. package/dist/gateway/server-methods-list.js +8 -0
  234. package/dist/gateway/server-methods.js +24 -8
  235. package/dist/gateway/server-node-events.js +278 -68
  236. package/dist/gateway/session-utils.fs.js +316 -75
  237. package/dist/gateway/session-utils.js +224 -70
  238. package/dist/gateway/sessions-patch.js +63 -20
  239. package/dist/gateway/test-temp-config.js +1 -1
  240. package/dist/gateway/tools-invoke-http.js +118 -70
  241. package/dist/gateway/ws-log.js +135 -107
  242. package/dist/hooks/frontmatter.js +36 -82
  243. package/dist/hooks/install.js +149 -139
  244. package/dist/hooks/internal-hooks.js +29 -4
  245. package/dist/hooks/plugin-hooks.js +2 -1
  246. package/dist/imessage/monitor/deliver.js +10 -4
  247. package/dist/imessage/monitor/monitor-provider.js +138 -375
  248. package/dist/imessage/monitor/runtime.js +4 -8
  249. package/dist/imessage/send.js +65 -19
  250. package/dist/infra/exec-approvals-allowlist.js +7 -0
  251. package/dist/infra/exec-approvals.js +35 -920
  252. package/dist/infra/exec-safe-bin-trust.js +64 -0
  253. package/dist/infra/heartbeat-runner.js +207 -134
  254. package/dist/infra/heartbeat-wake.js +183 -22
  255. package/dist/infra/install-source-utils.js +47 -0
  256. package/dist/infra/net/ssrf.js +170 -36
  257. package/dist/infra/outbound/deliver.js +224 -58
  258. package/dist/infra/outbound/message-action-spec.js +12 -5
  259. package/dist/infra/outbound/outbound-session.js +27 -25
  260. package/dist/infra/poolbot-root.js +32 -22
  261. package/dist/infra/ports.js +14 -11
  262. package/dist/infra/skills-remote.js +48 -37
  263. package/dist/infra/system-events.js +25 -11
  264. package/dist/infra/system-presence.js +26 -33
  265. package/dist/infra/tmp-poolbot-dir.js +81 -2
  266. package/dist/infra/wsl.js +37 -1
  267. package/dist/line/bot-message-context.js +163 -191
  268. package/dist/logging/subsystem.js +59 -22
  269. package/dist/markdown/ir.js +124 -50
  270. package/dist/media/store.js +1 -1
  271. package/dist/media-understanding/runner.entries.js +42 -25
  272. package/dist/media-understanding/runner.js +53 -488
  273. package/dist/memory/embeddings-gemini.js +53 -38
  274. package/dist/memory/manager-embedding-ops.js +48 -69
  275. package/dist/pairing/pairing-store.js +178 -119
  276. package/dist/plugin-sdk/index.js +34 -6
  277. package/dist/plugins/hooks.js +135 -14
  278. package/dist/plugins/install.js +190 -152
  279. package/dist/polls.js +11 -0
  280. package/dist/routing/resolve-route.js +190 -56
  281. package/dist/routing/session-key.js +38 -22
  282. package/dist/runtime.js +35 -9
  283. package/dist/security/audit-channel.js +1 -1
  284. package/dist/sessions/session-key-utils.js +29 -11
  285. package/dist/shared/frontmatter.js +5 -5
  286. package/dist/shared/node-list-types.js +1 -0
  287. package/dist/shared/string-normalization.js +15 -0
  288. package/dist/signal/monitor/event-handler.js +68 -36
  289. package/dist/signal/send.js +29 -37
  290. package/dist/slack/monitor/allow-list.js +10 -11
  291. package/dist/slack/monitor/commands.js +14 -3
  292. package/dist/slack/monitor/events/interactions.js +4 -4
  293. package/dist/slack/monitor/media.js +224 -16
  294. package/dist/slack/monitor/message-handler/dispatch.js +247 -13
  295. package/dist/slack/monitor/message-handler/prepare.js +128 -45
  296. package/dist/slack/monitor/slash.js +357 -144
  297. package/dist/slack/streaming.js +77 -0
  298. package/dist/telegram/accounts.js +40 -13
  299. package/dist/telegram/allowed-updates.js +3 -0
  300. package/dist/telegram/bot/delivery.js +129 -66
  301. package/dist/telegram/bot/helpers.js +136 -122
  302. package/dist/telegram/bot-handlers.js +600 -339
  303. package/dist/telegram/bot-message-context.js +115 -73
  304. package/dist/telegram/bot-message-dispatch.js +235 -104
  305. package/dist/telegram/bot-native-command-menu.js +3 -1
  306. package/dist/telegram/bot-native-commands.js +213 -193
  307. package/dist/telegram/bot.js +24 -132
  308. package/dist/telegram/draft-stream.js +84 -75
  309. package/dist/telegram/format.js +150 -6
  310. package/dist/telegram/send.js +415 -255
  311. package/dist/telegram/targets.js +21 -2
  312. package/dist/telegram/update-offset-store.js +19 -3
  313. package/dist/terminal/restore.js +5 -2
  314. package/dist/test-utils/fetch-mock.js +5 -0
  315. package/dist/version.js +18 -5
  316. package/dist/web/auto-reply/monitor/broadcast.js +7 -3
  317. package/dist/web/auto-reply/monitor/on-message.js +6 -3
  318. package/dist/web/inbound/media.js +34 -8
  319. package/dist/web/inbound/monitor.js +34 -17
  320. package/dist/web/inbound/send-api.js +18 -17
  321. package/dist/web/outbound.js +12 -5
  322. package/dist/wizard/clack-prompter.js +40 -7
  323. package/extensions/bluebubbles/package.json +1 -1
  324. package/extensions/copilot-proxy/package.json +1 -1
  325. package/extensions/device-pair/index.ts +2 -2
  326. package/extensions/diagnostics-otel/package.json +1 -1
  327. package/extensions/discord/package.json +1 -1
  328. package/extensions/feishu/package.json +1 -1
  329. package/extensions/google-antigravity-auth/package.json +1 -1
  330. package/extensions/google-gemini-cli-auth/package.json +1 -1
  331. package/extensions/googlechat/package.json +1 -1
  332. package/extensions/imessage/package.json +1 -1
  333. package/extensions/irc/package.json +1 -1
  334. package/extensions/irc/src/accounts.ts +1 -1
  335. package/extensions/irc/src/onboarding.ts +4 -4
  336. package/extensions/line/package.json +1 -1
  337. package/extensions/llm-task/package.json +1 -1
  338. package/extensions/lobster/package.json +1 -1
  339. package/extensions/matrix/CHANGELOG.md +10 -0
  340. package/extensions/matrix/package.json +1 -1
  341. package/extensions/mattermost/package.json +1 -1
  342. package/extensions/memory-core/package.json +1 -1
  343. package/extensions/memory-lancedb/package.json +1 -1
  344. package/extensions/minimax-portal-auth/package.json +1 -1
  345. package/extensions/msteams/CHANGELOG.md +10 -0
  346. package/extensions/msteams/package.json +1 -1
  347. package/extensions/nextcloud-talk/package.json +1 -1
  348. package/extensions/nostr/CHANGELOG.md +10 -0
  349. package/extensions/nostr/package.json +1 -1
  350. package/extensions/open-prose/package.json +1 -1
  351. package/extensions/openai-codex-auth/package.json +1 -1
  352. package/extensions/signal/package.json +1 -1
  353. package/extensions/slack/package.json +1 -1
  354. package/extensions/telegram/package.json +1 -1
  355. package/extensions/tlon/package.json +1 -1
  356. package/extensions/twitch/CHANGELOG.md +10 -0
  357. package/extensions/twitch/package.json +1 -1
  358. package/extensions/voice-call/CHANGELOG.md +10 -0
  359. package/extensions/voice-call/package.json +1 -1
  360. package/extensions/whatsapp/package.json +1 -1
  361. package/extensions/zalo/CHANGELOG.md +10 -0
  362. package/extensions/zalo/package.json +1 -1
  363. package/extensions/zalouser/CHANGELOG.md +10 -0
  364. package/extensions/zalouser/package.json +1 -1
  365. package/package.json +1 -1
  366. package/skills/apple-reminders/SKILL.md +100 -49
  367. package/skills/coding-agent/SKILL.md +34 -28
  368. package/skills/github/SKILL.md +131 -16
  369. package/skills/imsg/SKILL.md +112 -15
  370. package/skills/openhue/SKILL.md +101 -19
  371. package/skills/tmux/SKILL.md +111 -79
  372. package/skills/weather/SKILL.md +88 -25
  373. package/dist/agents/openclaw-tools.js +0 -151
  374. package/dist/agents/tool-security.js +0 -96
  375. package/dist/gateway/url-validation.js +0 -94
  376. package/dist/infra/openclaw-root.js +0 -109
  377. package/dist/infra/tmp-openclaw-dir.js +0 -81
  378. package/dist/media/path-sanitization.js +0 -78
@@ -1,15 +1,17 @@
1
1
  import { buildChannelKeyCandidates, resolveChannelEntryMatchWithFallback, resolveChannelMatchConfig, } from "../../channels/channel-config.js";
2
2
  import { formatDiscordUserTag } from "./format.js";
3
3
  export function normalizeDiscordAllowList(raw, prefixes) {
4
- if (!raw || raw.length === 0)
4
+ if (!raw || raw.length === 0) {
5
5
  return null;
6
+ }
6
7
  const ids = new Set();
7
8
  const names = new Set();
8
9
  const allowAll = raw.some((entry) => String(entry).trim() === "*");
9
10
  for (const entry of raw) {
10
11
  const text = String(entry).trim();
11
- if (!text || text === "*")
12
+ if (!text || text === "*") {
12
13
  continue;
14
+ }
13
15
  const normalized = normalizeDiscordSlug(text);
14
16
  const maybeId = text.replace(/^<@!?/, "").replace(/>$/, "");
15
17
  if (/^\d+$/.test(maybeId)) {
@@ -19,8 +21,9 @@ export function normalizeDiscordAllowList(raw, prefixes) {
19
21
  const prefix = prefixes.find((entry) => text.startsWith(entry));
20
22
  if (prefix) {
21
23
  const candidate = text.slice(prefix.length);
22
- if (candidate)
24
+ if (candidate) {
23
25
  ids.add(candidate);
26
+ }
24
27
  continue;
25
28
  }
26
29
  if (normalized) {
@@ -38,15 +41,19 @@ export function normalizeDiscordSlug(value) {
38
41
  .replace(/^-+|-+$/g, "");
39
42
  }
40
43
  export function allowListMatches(list, candidate) {
41
- if (list.allowAll)
44
+ if (list.allowAll) {
42
45
  return true;
43
- if (candidate.id && list.ids.has(candidate.id))
46
+ }
47
+ if (candidate.id && list.ids.has(candidate.id)) {
44
48
  return true;
49
+ }
45
50
  const slug = candidate.name ? normalizeDiscordSlug(candidate.name) : "";
46
- if (slug && list.names.has(slug))
51
+ if (slug && list.names.has(slug)) {
47
52
  return true;
48
- if (candidate.tag && list.names.has(normalizeDiscordSlug(candidate.tag)))
53
+ }
54
+ if (candidate.tag && list.names.has(normalizeDiscordSlug(candidate.tag))) {
49
55
  return true;
56
+ }
50
57
  return false;
51
58
  }
52
59
  export function resolveDiscordAllowListMatch(params) {
@@ -69,25 +76,62 @@ export function resolveDiscordAllowListMatch(params) {
69
76
  }
70
77
  export function resolveDiscordUserAllowed(params) {
71
78
  const allowList = normalizeDiscordAllowList(params.allowList, ["discord:", "user:", "pk:"]);
72
- if (!allowList)
79
+ if (!allowList) {
73
80
  return true;
81
+ }
74
82
  return allowListMatches(allowList, {
75
83
  id: params.userId,
76
84
  name: params.userName,
77
85
  tag: params.userTag,
78
86
  });
79
87
  }
80
- export function resolveDiscordCommandAuthorized(params) {
81
- if (!params.isDirectMessage)
88
+ export function resolveDiscordRoleAllowed(params) {
89
+ // Role allowlists accept role IDs only. Names are ignored.
90
+ const allowList = normalizeDiscordAllowList(params.allowList, ["role:"]);
91
+ if (!allowList) {
82
92
  return true;
83
- const allowList = normalizeDiscordAllowList(params.allowFrom, ["discord:", "user:", "pk:"]);
84
- if (!allowList)
93
+ }
94
+ if (allowList.allowAll) {
85
95
  return true;
86
- return allowListMatches(allowList, {
87
- id: params.author.id,
88
- name: params.author.username,
89
- tag: formatDiscordUserTag(params.author),
96
+ }
97
+ return params.memberRoleIds.some((roleId) => allowList.ids.has(roleId));
98
+ }
99
+ export function resolveDiscordMemberAllowed(params) {
100
+ const hasUserRestriction = Array.isArray(params.userAllowList) && params.userAllowList.length > 0;
101
+ const hasRoleRestriction = Array.isArray(params.roleAllowList) && params.roleAllowList.length > 0;
102
+ if (!hasUserRestriction && !hasRoleRestriction) {
103
+ return true;
104
+ }
105
+ const userOk = hasUserRestriction
106
+ ? resolveDiscordUserAllowed({
107
+ allowList: params.userAllowList,
108
+ userId: params.userId,
109
+ userName: params.userName,
110
+ userTag: params.userTag,
111
+ })
112
+ : false;
113
+ const roleOk = hasRoleRestriction
114
+ ? resolveDiscordRoleAllowed({
115
+ allowList: params.roleAllowList,
116
+ memberRoleIds: params.memberRoleIds,
117
+ })
118
+ : false;
119
+ return userOk || roleOk;
120
+ }
121
+ export function resolveDiscordMemberAccessState(params) {
122
+ const channelUsers = params.channelConfig?.users ?? params.guildInfo?.users;
123
+ const channelRoles = params.channelConfig?.roles ?? params.guildInfo?.roles;
124
+ const hasAccessRestrictions = (Array.isArray(channelUsers) && channelUsers.length > 0) ||
125
+ (Array.isArray(channelRoles) && channelRoles.length > 0);
126
+ const memberAllowed = resolveDiscordMemberAllowed({
127
+ userAllowList: channelUsers,
128
+ roleAllowList: channelRoles,
129
+ memberRoleIds: params.memberRoleIds,
130
+ userId: params.sender.id,
131
+ userName: params.sender.name,
132
+ userTag: params.sender.tag,
90
133
  });
134
+ return { channelUsers, channelRoles, hasAccessRestrictions, memberAllowed };
91
135
  }
92
136
  export function resolveDiscordOwnerAllowFrom(params) {
93
137
  const rawAllowList = params.channelConfig?.users ?? params.guildInfo?.users;
@@ -111,21 +155,39 @@ export function resolveDiscordOwnerAllowFrom(params) {
111
155
  }
112
156
  return [match.matchKey];
113
157
  }
158
+ export function resolveDiscordCommandAuthorized(params) {
159
+ if (!params.isDirectMessage) {
160
+ return true;
161
+ }
162
+ const allowList = normalizeDiscordAllowList(params.allowFrom, ["discord:", "user:", "pk:"]);
163
+ if (!allowList) {
164
+ return true;
165
+ }
166
+ return allowListMatches(allowList, {
167
+ id: params.author.id,
168
+ name: params.author.username,
169
+ tag: formatDiscordUserTag(params.author),
170
+ });
171
+ }
114
172
  export function resolveDiscordGuildEntry(params) {
115
173
  const guild = params.guild;
116
174
  const entries = params.guildEntries;
117
- if (!guild || !entries)
175
+ if (!guild || !entries) {
118
176
  return null;
177
+ }
119
178
  const byId = entries[guild.id];
120
- if (byId)
179
+ if (byId) {
121
180
  return { ...byId, id: guild.id };
181
+ }
122
182
  const slug = normalizeDiscordSlug(guild.name ?? "");
123
183
  const bySlug = entries[slug];
124
- if (bySlug)
184
+ if (bySlug) {
125
185
  return { ...bySlug, id: guild.id, slug: slug || bySlug.slug };
186
+ }
126
187
  const wildcard = entries["*"];
127
- if (wildcard)
188
+ if (wildcard) {
128
189
  return { ...wildcard, id: guild.id, slug: slug || wildcard.slug };
190
+ }
129
191
  return null;
130
192
  }
131
193
  function buildDiscordChannelKeys(params) {
@@ -142,6 +204,9 @@ function resolveDiscordChannelEntryMatch(channels, params, parentParams) {
142
204
  wildcardKey: "*",
143
205
  });
144
206
  }
207
+ function hasConfiguredDiscordChannels(channels) {
208
+ return Boolean(channels && Object.keys(channels).length > 0);
209
+ }
145
210
  function resolveDiscordChannelConfigEntry(entry) {
146
211
  const resolved = {
147
212
  allowed: entry.allow !== false,
@@ -149,6 +214,7 @@ function resolveDiscordChannelConfigEntry(entry) {
149
214
  skills: entry.skills,
150
215
  enabled: entry.enabled,
151
216
  users: entry.users,
217
+ roles: entry.roles,
152
218
  systemPrompt: entry.systemPrompt,
153
219
  includeThreadStarter: entry.includeThreadStarter,
154
220
  autoThread: entry.autoThread,
@@ -158,8 +224,9 @@ function resolveDiscordChannelConfigEntry(entry) {
158
224
  export function resolveDiscordChannelConfig(params) {
159
225
  const { guildInfo, channelId, channelName, channelSlug } = params;
160
226
  const channels = guildInfo?.channels;
161
- if (!channels)
227
+ if (!hasConfiguredDiscordChannels(channels)) {
162
228
  return null;
229
+ }
163
230
  const match = resolveDiscordChannelEntryMatch(channels, {
164
231
  id: channelId,
165
232
  name: channelName,
@@ -171,8 +238,9 @@ export function resolveDiscordChannelConfig(params) {
171
238
  export function resolveDiscordChannelConfigWithFallback(params) {
172
239
  const { guildInfo, channelId, channelName, channelSlug, parentId, parentName, parentSlug, scope, } = params;
173
240
  const channels = guildInfo?.channels;
174
- if (!channels)
241
+ if (!hasConfiguredDiscordChannels(channels)) {
175
242
  return null;
243
+ }
176
244
  const resolvedParentSlug = parentSlug ?? (parentName ? normalizeDiscordSlug(parentName) : "");
177
245
  const match = resolveDiscordChannelEntryMatch(channels, {
178
246
  id: channelId,
@@ -189,39 +257,48 @@ export function resolveDiscordChannelConfigWithFallback(params) {
189
257
  return resolveChannelMatchConfig(match, resolveDiscordChannelConfigEntry) ?? { allowed: false };
190
258
  }
191
259
  export function resolveDiscordShouldRequireMention(params) {
192
- if (!params.isGuildMessage)
260
+ if (!params.isGuildMessage) {
193
261
  return false;
262
+ }
194
263
  // Only skip mention requirement in threads created by the bot (when autoThread is enabled).
195
264
  const isBotThread = params.isAutoThreadOwnedByBot ?? isDiscordAutoThreadOwnedByBot(params);
196
- if (isBotThread)
265
+ if (isBotThread) {
197
266
  return false;
267
+ }
198
268
  return params.channelConfig?.requireMention ?? params.guildInfo?.requireMention ?? true;
199
269
  }
200
270
  export function isDiscordAutoThreadOwnedByBot(params) {
201
- if (!params.isThread)
271
+ if (!params.isThread) {
202
272
  return false;
203
- if (!params.channelConfig?.autoThread)
273
+ }
274
+ if (!params.channelConfig?.autoThread) {
204
275
  return false;
276
+ }
205
277
  const botId = params.botId?.trim();
206
278
  const threadOwnerId = params.threadOwnerId?.trim();
207
279
  return Boolean(botId && threadOwnerId && botId === threadOwnerId);
208
280
  }
209
281
  export function isDiscordGroupAllowedByPolicy(params) {
210
282
  const { groupPolicy, guildAllowlisted, channelAllowlistConfigured, channelAllowed } = params;
211
- if (groupPolicy === "disabled")
283
+ if (groupPolicy === "disabled") {
212
284
  return false;
213
- if (groupPolicy === "open")
285
+ }
286
+ if (groupPolicy === "open") {
214
287
  return true;
215
- if (!guildAllowlisted)
288
+ }
289
+ if (!guildAllowlisted) {
216
290
  return false;
217
- if (!channelAllowlistConfigured)
291
+ }
292
+ if (!channelAllowlistConfigured) {
218
293
  return true;
294
+ }
219
295
  return channelAllowed;
220
296
  }
221
297
  export function resolveGroupDmAllow(params) {
222
298
  const { channels, channelId, channelName, channelSlug } = params;
223
- if (!channels || channels.length === 0)
299
+ if (!channels || channels.length === 0) {
224
300
  return true;
301
+ }
225
302
  const allowList = new Set(channels.map((entry) => normalizeDiscordSlug(String(entry))));
226
303
  const candidates = [
227
304
  normalizeDiscordSlug(channelId),
@@ -232,17 +309,20 @@ export function resolveGroupDmAllow(params) {
232
309
  }
233
310
  export function shouldEmitDiscordReactionNotification(params) {
234
311
  const mode = params.mode ?? "own";
235
- if (mode === "off")
312
+ if (mode === "off") {
236
313
  return false;
237
- if (mode === "all")
314
+ }
315
+ if (mode === "all") {
238
316
  return true;
317
+ }
239
318
  if (mode === "own") {
240
319
  return Boolean(params.botId && params.messageAuthorId === params.botId);
241
320
  }
242
321
  if (mode === "allowlist") {
243
322
  const list = normalizeDiscordAllowList(params.allowlist, ["discord:", "user:", "pk:"]);
244
- if (!list)
323
+ if (!list) {
245
324
  return false;
325
+ }
246
326
  return allowListMatches(list, {
247
327
  id: params.userId,
248
328
  name: params.userName,
@@ -1,18 +1,19 @@
1
1
  import { ChannelType, MessageCreateListener, MessageReactionAddListener, MessageReactionRemoveListener, PresenceUpdateListener, } from "@buape/carbon";
2
2
  import { danger } from "../../globals.js";
3
- import { formatDurationSeconds } from "../../infra/format-duration.js";
3
+ import { formatDurationSeconds } from "../../infra/format-time/format-duration.js";
4
4
  import { enqueueSystemEvent } from "../../infra/system-events.js";
5
- import { setPresence } from "./presence-cache.js";
6
5
  import { createSubsystemLogger } from "../../logging/subsystem.js";
7
6
  import { resolveAgentRoute } from "../../routing/resolve-route.js";
8
7
  import { normalizeDiscordSlug, resolveDiscordChannelConfigWithFallback, resolveDiscordGuildEntry, shouldEmitDiscordReactionNotification, } from "./allow-list.js";
9
8
  import { formatDiscordReactionEmoji, formatDiscordUserTag } from "./format.js";
10
9
  import { resolveDiscordChannelInfo } from "./message-utils.js";
10
+ import { setPresence } from "./presence-cache.js";
11
11
  const DISCORD_SLOW_LISTENER_THRESHOLD_MS = 30_000;
12
12
  const discordEventQueueLog = createSubsystemLogger("discord/event-queue");
13
13
  function logSlowDiscordListener(params) {
14
- if (params.durationMs < DISCORD_SLOW_LISTENER_THRESHOLD_MS)
14
+ if (params.durationMs < DISCORD_SLOW_LISTENER_THRESHOLD_MS) {
15
15
  return;
16
+ }
16
17
  const duration = formatDurationSeconds(params.durationMs, {
17
18
  decimals: 1,
18
19
  unit: "seconds",
@@ -67,27 +68,14 @@ export class DiscordReactionListener extends MessageReactionAddListener {
67
68
  this.params = params;
68
69
  }
69
70
  async handle(data, client) {
70
- const startedAt = Date.now();
71
- try {
72
- await handleDiscordReactionEvent({
73
- data,
74
- client,
75
- action: "added",
76
- cfg: this.params.cfg,
77
- accountId: this.params.accountId,
78
- botUserId: this.params.botUserId,
79
- guildEntries: this.params.guildEntries,
80
- logger: this.params.logger,
81
- });
82
- }
83
- finally {
84
- logSlowDiscordListener({
85
- logger: this.params.logger,
86
- listener: this.constructor.name,
87
- event: this.type,
88
- durationMs: Date.now() - startedAt,
89
- });
90
- }
71
+ await runDiscordReactionHandler({
72
+ data,
73
+ client,
74
+ action: "added",
75
+ handlerParams: this.params,
76
+ listener: this.constructor.name,
77
+ event: this.type,
78
+ });
91
79
  }
92
80
  }
93
81
  export class DiscordReactionRemoveListener extends MessageReactionRemoveListener {
@@ -97,69 +85,200 @@ export class DiscordReactionRemoveListener extends MessageReactionRemoveListener
97
85
  this.params = params;
98
86
  }
99
87
  async handle(data, client) {
100
- const startedAt = Date.now();
101
- try {
102
- await handleDiscordReactionEvent({
103
- data,
104
- client,
105
- action: "removed",
106
- cfg: this.params.cfg,
107
- accountId: this.params.accountId,
108
- botUserId: this.params.botUserId,
109
- guildEntries: this.params.guildEntries,
110
- logger: this.params.logger,
111
- });
112
- }
113
- finally {
114
- logSlowDiscordListener({
115
- logger: this.params.logger,
116
- listener: this.constructor.name,
117
- event: this.type,
118
- durationMs: Date.now() - startedAt,
119
- });
120
- }
88
+ await runDiscordReactionHandler({
89
+ data,
90
+ client,
91
+ action: "removed",
92
+ handlerParams: this.params,
93
+ listener: this.constructor.name,
94
+ event: this.type,
95
+ });
96
+ }
97
+ }
98
+ async function runDiscordReactionHandler(params) {
99
+ const startedAt = Date.now();
100
+ try {
101
+ await handleDiscordReactionEvent({
102
+ data: params.data,
103
+ client: params.client,
104
+ action: params.action,
105
+ cfg: params.handlerParams.cfg,
106
+ accountId: params.handlerParams.accountId,
107
+ botUserId: params.handlerParams.botUserId,
108
+ guildEntries: params.handlerParams.guildEntries,
109
+ logger: params.handlerParams.logger,
110
+ });
111
+ }
112
+ finally {
113
+ logSlowDiscordListener({
114
+ logger: params.handlerParams.logger,
115
+ listener: params.listener,
116
+ event: params.event,
117
+ durationMs: Date.now() - startedAt,
118
+ });
121
119
  }
122
120
  }
123
121
  async function handleDiscordReactionEvent(params) {
124
122
  try {
125
123
  const { data, client, action, botUserId, guildEntries } = params;
126
- if (!("user" in data))
124
+ if (!("user" in data)) {
127
125
  return;
126
+ }
128
127
  const user = data.user;
129
- if (!user || user.bot)
128
+ if (!user || user.bot) {
130
129
  return;
131
- if (!data.guild_id)
130
+ }
131
+ // Early exit: skip bot's own reactions before expensive network calls
132
+ if (botUserId && user.id === botUserId) {
132
133
  return;
133
- const guildInfo = resolveDiscordGuildEntry({
134
- guild: data.guild ?? undefined,
135
- guildEntries,
136
- });
137
- if (guildEntries && Object.keys(guildEntries).length > 0 && !guildInfo) {
134
+ }
135
+ const isGuildMessage = Boolean(data.guild_id);
136
+ const guildInfo = isGuildMessage
137
+ ? resolveDiscordGuildEntry({
138
+ guild: data.guild ?? undefined,
139
+ guildEntries,
140
+ })
141
+ : null;
142
+ if (isGuildMessage && guildEntries && Object.keys(guildEntries).length > 0 && !guildInfo) {
138
143
  return;
139
144
  }
140
145
  const channel = await client.fetchChannel(data.channel_id);
141
- if (!channel)
146
+ if (!channel) {
142
147
  return;
148
+ }
143
149
  const channelName = "name" in channel ? (channel.name ?? undefined) : undefined;
144
150
  const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
145
151
  const channelType = "type" in channel ? channel.type : undefined;
152
+ const isDirectMessage = channelType === ChannelType.DM;
153
+ const isGroupDm = channelType === ChannelType.GroupDM;
146
154
  const isThreadChannel = channelType === ChannelType.PublicThread ||
147
155
  channelType === ChannelType.PrivateThread ||
148
156
  channelType === ChannelType.AnnouncementThread;
149
157
  let parentId = "parentId" in channel ? (channel.parentId ?? undefined) : undefined;
150
158
  let parentName;
151
159
  let parentSlug = "";
152
- if (isThreadChannel) {
160
+ const memberRoleIds = Array.isArray(data.rawMember?.roles)
161
+ ? data.rawMember.roles.map((roleId) => String(roleId))
162
+ : [];
163
+ let reactionBase = null;
164
+ const resolveReactionBase = () => {
165
+ if (reactionBase) {
166
+ return reactionBase;
167
+ }
168
+ const emojiLabel = formatDiscordReactionEmoji(data.emoji);
169
+ const actorLabel = formatDiscordUserTag(user);
170
+ const guildSlug = guildInfo?.slug ||
171
+ (data.guild?.name
172
+ ? normalizeDiscordSlug(data.guild.name)
173
+ : (data.guild_id ?? (isGroupDm ? "group-dm" : "dm")));
174
+ const channelLabel = channelSlug
175
+ ? `#${channelSlug}`
176
+ : channelName
177
+ ? `#${normalizeDiscordSlug(channelName)}`
178
+ : `#${data.channel_id}`;
179
+ const baseText = `Discord reaction ${action}: ${emojiLabel} by ${actorLabel} on ${guildSlug} ${channelLabel} msg ${data.message_id}`;
180
+ const contextKey = `discord:reaction:${action}:${data.message_id}:${user.id}:${emojiLabel}`;
181
+ reactionBase = { baseText, contextKey };
182
+ return reactionBase;
183
+ };
184
+ const emitReaction = (text, parentPeerId) => {
185
+ const { contextKey } = resolveReactionBase();
186
+ const route = resolveAgentRoute({
187
+ cfg: params.cfg,
188
+ channel: "discord",
189
+ accountId: params.accountId,
190
+ guildId: data.guild_id ?? undefined,
191
+ memberRoleIds,
192
+ peer: {
193
+ kind: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
194
+ id: isDirectMessage ? user.id : data.channel_id,
195
+ },
196
+ parentPeer: parentPeerId ? { kind: "channel", id: parentPeerId } : undefined,
197
+ });
198
+ enqueueSystemEvent(text, {
199
+ sessionKey: route.sessionKey,
200
+ contextKey,
201
+ });
202
+ };
203
+ const shouldNotifyReaction = (options) => shouldEmitDiscordReactionNotification({
204
+ mode: options.mode,
205
+ botId: botUserId,
206
+ messageAuthorId: options.messageAuthorId,
207
+ userId: user.id,
208
+ userName: user.username,
209
+ userTag: formatDiscordUserTag(user),
210
+ allowlist: guildInfo?.users,
211
+ });
212
+ const emitReactionWithAuthor = (message) => {
213
+ const { baseText } = resolveReactionBase();
214
+ const authorLabel = message?.author ? formatDiscordUserTag(message.author) : undefined;
215
+ const text = authorLabel ? `${baseText} from ${authorLabel}` : baseText;
216
+ emitReaction(text, parentId);
217
+ };
218
+ const loadThreadParentInfo = async () => {
153
219
  if (!parentId) {
154
- const channelInfo = await resolveDiscordChannelInfo(client, data.channel_id);
220
+ return;
221
+ }
222
+ const parentInfo = await resolveDiscordChannelInfo(client, parentId);
223
+ parentName = parentInfo?.name;
224
+ parentSlug = parentName ? normalizeDiscordSlug(parentName) : "";
225
+ };
226
+ const resolveThreadChannelConfig = () => resolveDiscordChannelConfigWithFallback({
227
+ guildInfo,
228
+ channelId: data.channel_id,
229
+ channelName,
230
+ channelSlug,
231
+ parentId,
232
+ parentName,
233
+ parentSlug,
234
+ scope: "thread",
235
+ });
236
+ // Parallelize async operations for thread channels
237
+ if (isThreadChannel) {
238
+ const reactionMode = guildInfo?.reactionNotifications ?? "own";
239
+ // Early exit: skip fetching message if notifications are off
240
+ if (reactionMode === "off") {
241
+ return;
242
+ }
243
+ const channelInfoPromise = parentId
244
+ ? Promise.resolve({ parentId })
245
+ : resolveDiscordChannelInfo(client, data.channel_id);
246
+ // Fast path: for "all" and "allowlist" modes, we don't need to fetch the message
247
+ if (reactionMode === "all" || reactionMode === "allowlist") {
248
+ const channelInfo = await channelInfoPromise;
155
249
  parentId = channelInfo?.parentId;
250
+ await loadThreadParentInfo();
251
+ const channelConfig = resolveThreadChannelConfig();
252
+ if (channelConfig?.allowed === false) {
253
+ return;
254
+ }
255
+ // For allowlist mode, check if user is in allowlist first
256
+ if (reactionMode === "allowlist") {
257
+ if (!shouldNotifyReaction({ mode: reactionMode })) {
258
+ return;
259
+ }
260
+ }
261
+ const { baseText } = resolveReactionBase();
262
+ emitReaction(baseText, parentId);
263
+ return;
264
+ }
265
+ // For "own" mode, we need to fetch the message to check the author
266
+ const messagePromise = data.message.fetch().catch(() => null);
267
+ const [channelInfo, message] = await Promise.all([channelInfoPromise, messagePromise]);
268
+ parentId = channelInfo?.parentId;
269
+ await loadThreadParentInfo();
270
+ const channelConfig = resolveThreadChannelConfig();
271
+ if (channelConfig?.allowed === false) {
272
+ return;
156
273
  }
157
- if (parentId) {
158
- const parentInfo = await resolveDiscordChannelInfo(client, parentId);
159
- parentName = parentInfo?.name;
160
- parentSlug = parentName ? normalizeDiscordSlug(parentName) : "";
274
+ const messageAuthorId = message?.author?.id ?? undefined;
275
+ if (!shouldNotifyReaction({ mode: reactionMode, messageAuthorId })) {
276
+ return;
161
277
  }
278
+ emitReactionWithAuthor(message);
279
+ return;
162
280
  }
281
+ // Non-thread channel path
163
282
  const channelConfig = resolveDiscordChannelConfigWithFallback({
164
283
  guildInfo,
165
284
  channelId: data.channel_id,
@@ -168,48 +287,35 @@ async function handleDiscordReactionEvent(params) {
168
287
  parentId,
169
288
  parentName,
170
289
  parentSlug,
171
- scope: isThreadChannel ? "thread" : "channel",
290
+ scope: "channel",
172
291
  });
173
- if (channelConfig?.allowed === false)
174
- return;
175
- if (botUserId && user.id === botUserId)
292
+ if (channelConfig?.allowed === false) {
176
293
  return;
294
+ }
177
295
  const reactionMode = guildInfo?.reactionNotifications ?? "own";
296
+ // Early exit: skip fetching message if notifications are off
297
+ if (reactionMode === "off") {
298
+ return;
299
+ }
300
+ // Fast path: for "all" and "allowlist" modes, we don't need to fetch the message
301
+ if (reactionMode === "all" || reactionMode === "allowlist") {
302
+ // For allowlist mode, check if user is in allowlist first
303
+ if (reactionMode === "allowlist") {
304
+ if (!shouldNotifyReaction({ mode: reactionMode })) {
305
+ return;
306
+ }
307
+ }
308
+ const { baseText } = resolveReactionBase();
309
+ emitReaction(baseText, parentId);
310
+ return;
311
+ }
312
+ // For "own" mode, we need to fetch the message to check the author
178
313
  const message = await data.message.fetch().catch(() => null);
179
314
  const messageAuthorId = message?.author?.id ?? undefined;
180
- const shouldNotify = shouldEmitDiscordReactionNotification({
181
- mode: reactionMode,
182
- botId: botUserId,
183
- messageAuthorId,
184
- userId: user.id,
185
- userName: user.username,
186
- userTag: formatDiscordUserTag(user),
187
- allowlist: guildInfo?.users,
188
- });
189
- if (!shouldNotify)
315
+ if (!shouldNotifyReaction({ mode: reactionMode, messageAuthorId })) {
190
316
  return;
191
- const emojiLabel = formatDiscordReactionEmoji(data.emoji);
192
- const actorLabel = formatDiscordUserTag(user);
193
- const guildSlug = guildInfo?.slug || (data.guild?.name ? normalizeDiscordSlug(data.guild.name) : data.guild_id);
194
- const channelLabel = channelSlug
195
- ? `#${channelSlug}`
196
- : channelName
197
- ? `#${normalizeDiscordSlug(channelName)}`
198
- : `#${data.channel_id}`;
199
- const authorLabel = message?.author ? formatDiscordUserTag(message.author) : undefined;
200
- const baseText = `Discord reaction ${action}: ${emojiLabel} by ${actorLabel} on ${guildSlug} ${channelLabel} msg ${data.message_id}`;
201
- const text = authorLabel ? `${baseText} from ${authorLabel}` : baseText;
202
- const route = resolveAgentRoute({
203
- cfg: params.cfg,
204
- channel: "discord",
205
- accountId: params.accountId,
206
- guildId: data.guild_id ?? undefined,
207
- peer: { kind: "channel", id: data.channel_id },
208
- });
209
- enqueueSystemEvent(text, {
210
- sessionKey: route.sessionKey,
211
- contextKey: `discord:reaction:${action}:${data.message_id}:${user.id}:${emojiLabel}`,
212
- });
317
+ }
318
+ emitReactionWithAuthor(message);
213
319
  }
214
320
  catch (err) {
215
321
  params.logger.error(danger(`discord reaction handler failed: ${String(err)}`));
@@ -228,8 +334,9 @@ export class DiscordPresenceListener extends PresenceUpdateListener {
228
334
  const userId = "user" in data && data.user && typeof data.user === "object" && "id" in data.user
229
335
  ? String(data.user.id)
230
336
  : undefined;
231
- if (!userId)
337
+ if (!userId) {
232
338
  return;
339
+ }
233
340
  setPresence(this.accountId, userId, data);
234
341
  }
235
342
  catch (err) {