@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
@@ -4,19 +4,53 @@ import { createReplyReferencePlanner } from "../../auto-reply/reply/reply-refere
4
4
  import { logVerbose } from "../../globals.js";
5
5
  import { buildAgentSessionKey } from "../../routing/resolve-route.js";
6
6
  import { truncateUtf16Safe } from "../../utils.js";
7
- import { resolveDiscordChannelInfo } from "./message-utils.js";
7
+ import { resolveDiscordChannelInfo, resolveDiscordMessageChannelId } from "./message-utils.js";
8
+ // Cache configuration: 5 minute TTL (thread starters rarely change), max 500 entries
9
+ const DISCORD_THREAD_STARTER_CACHE_TTL_MS = 5 * 60 * 1000;
10
+ const DISCORD_THREAD_STARTER_CACHE_MAX = 500;
8
11
  const DISCORD_THREAD_STARTER_CACHE = new Map();
9
12
  export function __resetDiscordThreadStarterCacheForTest() {
10
13
  DISCORD_THREAD_STARTER_CACHE.clear();
11
14
  }
15
+ // Get cached entry with TTL check, refresh LRU position on hit
16
+ function getCachedThreadStarter(key, now) {
17
+ const entry = DISCORD_THREAD_STARTER_CACHE.get(key);
18
+ if (!entry) {
19
+ return undefined;
20
+ }
21
+ // Check TTL expiry
22
+ if (now - entry.updatedAt > DISCORD_THREAD_STARTER_CACHE_TTL_MS) {
23
+ DISCORD_THREAD_STARTER_CACHE.delete(key);
24
+ return undefined;
25
+ }
26
+ // Refresh LRU position by re-inserting (Map maintains insertion order)
27
+ DISCORD_THREAD_STARTER_CACHE.delete(key);
28
+ DISCORD_THREAD_STARTER_CACHE.set(key, { ...entry, updatedAt: now });
29
+ return entry.value;
30
+ }
31
+ // Set cached entry with LRU eviction when max size exceeded
32
+ function setCachedThreadStarter(key, value, now) {
33
+ // Remove existing entry first (to update LRU position)
34
+ DISCORD_THREAD_STARTER_CACHE.delete(key);
35
+ DISCORD_THREAD_STARTER_CACHE.set(key, { value, updatedAt: now });
36
+ // Evict oldest entries (first in Map) when over max size
37
+ while (DISCORD_THREAD_STARTER_CACHE.size > DISCORD_THREAD_STARTER_CACHE_MAX) {
38
+ const iter = DISCORD_THREAD_STARTER_CACHE.keys().next();
39
+ if (iter.done) {
40
+ break;
41
+ }
42
+ DISCORD_THREAD_STARTER_CACHE.delete(iter.value);
43
+ }
44
+ }
12
45
  function isDiscordThreadType(type) {
13
46
  return (type === ChannelType.PublicThread ||
14
47
  type === ChannelType.PrivateThread ||
15
48
  type === ChannelType.AnnouncementThread);
16
49
  }
17
50
  export function resolveDiscordThreadChannel(params) {
18
- if (!params.isGuildMessage)
51
+ if (!params.isGuildMessage) {
19
52
  return null;
53
+ }
20
54
  const { message, channelInfo } = params;
21
55
  const channel = "channel" in message ? message.channel : undefined;
22
56
  const isThreadChannel = channel &&
@@ -24,12 +58,21 @@ export function resolveDiscordThreadChannel(params) {
24
58
  "isThread" in channel &&
25
59
  typeof channel.isThread === "function" &&
26
60
  channel.isThread();
27
- if (isThreadChannel)
61
+ if (isThreadChannel) {
28
62
  return channel;
29
- if (!isDiscordThreadType(channelInfo?.type))
63
+ }
64
+ if (!isDiscordThreadType(channelInfo?.type)) {
65
+ return null;
66
+ }
67
+ const messageChannelId = params.messageChannelId ||
68
+ resolveDiscordMessageChannelId({
69
+ message,
70
+ });
71
+ if (!messageChannelId) {
30
72
  return null;
73
+ }
31
74
  return {
32
- id: message.channelId,
75
+ id: messageChannelId,
33
76
  name: channelInfo?.name ?? undefined,
34
77
  parentId: channelInfo?.parentId ?? undefined,
35
78
  parent: undefined,
@@ -39,8 +82,9 @@ export function resolveDiscordThreadChannel(params) {
39
82
  export async function resolveDiscordThreadParentInfo(params) {
40
83
  const { threadChannel, channelInfo, client } = params;
41
84
  const parentId = threadChannel.parentId ?? threadChannel.parent?.id ?? channelInfo?.parentId ?? undefined;
42
- if (!parentId)
85
+ if (!parentId) {
43
86
  return {};
87
+ }
44
88
  let parentName = threadChannel.parent?.name;
45
89
  const parentInfo = await resolveDiscordChannelInfo(client, parentId);
46
90
  parentName = parentName ?? parentInfo?.name;
@@ -49,21 +93,26 @@ export async function resolveDiscordThreadParentInfo(params) {
49
93
  }
50
94
  export async function resolveDiscordThreadStarter(params) {
51
95
  const cacheKey = params.channel.id;
52
- const cached = DISCORD_THREAD_STARTER_CACHE.get(cacheKey);
53
- if (cached)
96
+ const now = Date.now();
97
+ const cached = getCachedThreadStarter(cacheKey, now);
98
+ if (cached) {
54
99
  return cached;
100
+ }
55
101
  try {
56
102
  const parentType = params.parentType;
57
103
  const isForumParent = parentType === ChannelType.GuildForum || parentType === ChannelType.GuildMedia;
58
104
  const messageChannelId = isForumParent ? params.channel.id : params.parentId;
59
- if (!messageChannelId)
105
+ if (!messageChannelId) {
60
106
  return null;
107
+ }
61
108
  const starter = (await params.client.rest.get(Routes.channelMessage(messageChannelId, params.channel.id)));
62
- if (!starter)
109
+ if (!starter) {
63
110
  return null;
111
+ }
64
112
  const text = starter.content?.trim() ?? starter.embeds?.[0]?.description?.trim() ?? "";
65
- if (!text)
113
+ if (!text) {
66
114
  return null;
115
+ }
67
116
  const author = starter.member?.nick ??
68
117
  starter.member?.displayName ??
69
118
  (starter.author
@@ -77,7 +126,7 @@ export async function resolveDiscordThreadStarter(params) {
77
126
  author,
78
127
  timestamp: timestamp ?? undefined,
79
128
  };
80
- DISCORD_THREAD_STARTER_CACHE.set(cacheKey, payload);
129
+ setCachedThreadStarter(cacheKey, payload, Date.now());
81
130
  return payload;
82
131
  }
83
132
  catch {
@@ -85,13 +134,16 @@ export async function resolveDiscordThreadStarter(params) {
85
134
  }
86
135
  }
87
136
  export function resolveDiscordReplyTarget(opts) {
88
- if (opts.replyToMode === "off")
137
+ if (opts.replyToMode === "off") {
89
138
  return undefined;
139
+ }
90
140
  const replyToId = opts.replyToId?.trim();
91
- if (!replyToId)
141
+ if (!replyToId) {
92
142
  return undefined;
93
- if (opts.replyToMode === "all")
143
+ }
144
+ if (opts.replyToMode === "all") {
94
145
  return replyToId;
146
+ }
95
147
  return opts.hasReplied ? undefined : replyToId;
96
148
  }
97
149
  export function sanitizeDiscordThreadName(rawName, fallbackId) {
@@ -107,11 +159,13 @@ export function sanitizeDiscordThreadName(rawName, fallbackId) {
107
159
  }
108
160
  export function resolveDiscordAutoThreadContext(params) {
109
161
  const createdThreadId = String(params.createdThreadId ?? "").trim();
110
- if (!createdThreadId)
162
+ if (!createdThreadId) {
111
163
  return null;
164
+ }
112
165
  const messageChannelId = params.messageChannelId.trim();
113
- if (!messageChannelId)
166
+ if (!messageChannelId) {
114
167
  return null;
168
+ }
115
169
  const threadSessionKey = buildAgentSessionKey({
116
170
  agentId: params.agentId,
117
171
  channel: params.channel,
@@ -132,13 +186,21 @@ export function resolveDiscordAutoThreadContext(params) {
132
186
  };
133
187
  }
134
188
  export async function resolveDiscordAutoThreadReplyPlan(params) {
135
- const originalReplyTarget = `channel:${params.message.channelId}`;
189
+ const messageChannelId = (params.messageChannelId ||
190
+ resolveDiscordMessageChannelId({
191
+ message: params.message,
192
+ })).trim();
193
+ // Prefer the resolved thread channel ID when available so replies stay in-thread.
194
+ const targetChannelId = params.threadChannel?.id ?? (messageChannelId || "unknown");
195
+ const originalReplyTarget = `channel:${targetChannelId}`;
136
196
  const createdThreadId = await maybeCreateDiscordAutoThread({
137
197
  client: params.client,
138
198
  message: params.message,
199
+ messageChannelId: messageChannelId || undefined,
139
200
  isGuildMessage: params.isGuildMessage,
140
201
  channelConfig: params.channelConfig,
141
202
  threadChannel: params.threadChannel,
203
+ channelType: params.channelType,
142
204
  baseText: params.baseText,
143
205
  combinedBody: params.combinedBody,
144
206
  });
@@ -153,22 +215,39 @@ export async function resolveDiscordAutoThreadReplyPlan(params) {
153
215
  ? resolveDiscordAutoThreadContext({
154
216
  agentId: params.agentId,
155
217
  channel: params.channel,
156
- messageChannelId: params.message.channelId,
218
+ messageChannelId,
157
219
  createdThreadId,
158
220
  })
159
221
  : null;
160
222
  return { ...deliveryPlan, createdThreadId, autoThreadContext };
161
223
  }
162
224
  export async function maybeCreateDiscordAutoThread(params) {
163
- if (!params.isGuildMessage)
225
+ if (!params.isGuildMessage) {
164
226
  return undefined;
165
- if (!params.channelConfig?.autoThread)
227
+ }
228
+ if (!params.channelConfig?.autoThread) {
166
229
  return undefined;
167
- if (params.threadChannel)
230
+ }
231
+ if (params.threadChannel) {
232
+ return undefined;
233
+ }
234
+ // Avoid creating threads in channels that don't support it or are already forums
235
+ if (params.channelType === ChannelType.GuildForum ||
236
+ params.channelType === ChannelType.GuildMedia ||
237
+ params.channelType === ChannelType.GuildVoice ||
238
+ params.channelType === ChannelType.GuildStageVoice) {
168
239
  return undefined;
240
+ }
241
+ const messageChannelId = (params.messageChannelId ||
242
+ resolveDiscordMessageChannelId({
243
+ message: params.message,
244
+ })).trim();
245
+ if (!messageChannelId) {
246
+ return undefined;
247
+ }
169
248
  try {
170
249
  const threadName = sanitizeDiscordThreadName(params.baseText || params.combinedBody || "Thread", params.message.id);
171
- const created = (await params.client.rest.post(`${Routes.channelMessage(params.message.channelId, params.message.id)}/threads`, {
250
+ const created = (await params.client.rest.post(`${Routes.channelMessage(messageChannelId, params.message.id)}/threads`, {
172
251
  body: {
173
252
  name: threadName,
174
253
  auto_archive_duration: 60,
@@ -178,7 +257,20 @@ export async function maybeCreateDiscordAutoThread(params) {
178
257
  return createdId || undefined;
179
258
  }
180
259
  catch (err) {
181
- logVerbose(`discord: autoThread failed for ${params.message.channelId}/${params.message.id}: ${String(err)}`);
260
+ logVerbose(`discord: autoThread creation failed for ${messageChannelId}/${params.message.id}: ${String(err)}`);
261
+ // Race condition: another agent may have already created a thread on this
262
+ // message. Re-fetch the message to check for an existing thread.
263
+ try {
264
+ const msg = (await params.client.rest.get(Routes.channelMessage(messageChannelId, params.message.id)));
265
+ const existingThreadId = msg?.thread?.id ? String(msg.thread.id) : "";
266
+ if (existingThreadId) {
267
+ logVerbose(`discord: autoThread reusing existing thread ${existingThreadId} on ${messageChannelId}/${params.message.id}`);
268
+ return existingThreadId;
269
+ }
270
+ }
271
+ catch {
272
+ // If the refetch also fails, fall through to return undefined.
273
+ }
182
274
  return undefined;
183
275
  }
184
276
  }
@@ -186,6 +278,7 @@ export function resolveDiscordReplyDeliveryPlan(params) {
186
278
  const originalReplyTarget = params.replyTarget;
187
279
  let deliverTarget = originalReplyTarget;
188
280
  let replyTarget = originalReplyTarget;
281
+ // When a new thread was created, route to the new thread.
189
282
  if (params.createdThreadId) {
190
283
  deliverTarget = `channel:${params.createdThreadId}`;
191
284
  replyTarget = deliverTarget;
@@ -2,6 +2,7 @@ export { createChannelDiscord, deleteChannelDiscord, editChannelDiscord, moveCha
2
2
  export { listGuildEmojisDiscord, uploadEmojiDiscord, uploadStickerDiscord, } from "./send.emojis-stickers.js";
3
3
  export { addRoleDiscord, banMemberDiscord, createScheduledEventDiscord, fetchChannelInfoDiscord, fetchMemberInfoDiscord, fetchRoleInfoDiscord, fetchVoiceStatusDiscord, kickMemberDiscord, listGuildChannelsDiscord, listScheduledEventsDiscord, removeRoleDiscord, timeoutMemberDiscord, } from "./send.guild.js";
4
4
  export { createThreadDiscord, deleteMessageDiscord, editMessageDiscord, fetchMessageDiscord, listPinsDiscord, listThreadsDiscord, pinMessageDiscord, readMessagesDiscord, searchMessagesDiscord, unpinMessageDiscord, } from "./send.messages.js";
5
- export { sendMessageDiscord, sendPollDiscord, sendStickerDiscord } from "./send.outbound.js";
5
+ export { sendMessageDiscord, sendPollDiscord, sendStickerDiscord, sendVoiceMessageDiscord, } from "./send.outbound.js";
6
+ export { sendDiscordComponentMessage } from "./send.components.js";
6
7
  export { fetchChannelPermissionsDiscord, fetchReactionsDiscord, reactMessageDiscord, removeOwnReactionsDiscord, removeReactionDiscord, } from "./send.reactions.js";
7
8
  export { DiscordSendError } from "./send.types.js";
@@ -1,11 +1,20 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { serializePayload } from "@buape/carbon";
1
5
  import { ChannelType, Routes } from "discord-api-types/v10";
2
6
  import { resolveChunkMode } from "../auto-reply/chunk.js";
3
7
  import { loadConfig } from "../config/config.js";
4
8
  import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
5
9
  import { recordChannelActivity } from "../infra/channel-activity.js";
10
+ import { resolvePreferredPoolbotTmpDir } from "../infra/tmp-poolbot-dir.js";
6
11
  import { convertMarkdownTables } from "../markdown/tables.js";
12
+ import { maxBytesForKind } from "../media/constants.js";
13
+ import { extensionForMime } from "../media/mime.js";
14
+ import { loadWebMediaRaw } from "../web/media.js";
7
15
  import { resolveDiscordAccount } from "./accounts.js";
8
- import { buildDiscordSendError, buildDiscordTextChunks, createDiscordClient, normalizeDiscordPollInput, normalizeStickerIds, parseAndResolveRecipient, resolveChannelId, sendDiscordMedia, sendDiscordText, } from "./send.shared.js";
16
+ import { buildDiscordMessagePayload, buildDiscordSendError, buildDiscordTextChunks, createDiscordClient, normalizeDiscordPollInput, normalizeStickerIds, parseAndResolveRecipient, resolveChannelId, resolveDiscordSendComponents, resolveDiscordSendEmbeds, sendDiscordMedia, sendDiscordText, stripUndefinedFields, SUPPRESS_NOTIFICATIONS_FLAG, } from "./send.shared.js";
17
+ import { ensureOggOpus, getVoiceMessageMetadata, sendDiscordVoiceMessage, } from "./voice-message.js";
9
18
  /** Discord thread names are capped at 100 characters. */
10
19
  const DISCORD_THREAD_NAME_LIMIT = 100;
11
20
  /** Derive a thread title from the first non-empty line of the message text. */
@@ -52,16 +61,25 @@ export async function sendMessageDiscord(to, text, opts = {}) {
52
61
  chunkMode,
53
62
  });
54
63
  const starterContent = chunks[0]?.trim() ? chunks[0] : threadName;
55
- const starterEmbeds = opts.embeds?.length ? opts.embeds : undefined;
64
+ const starterComponents = resolveDiscordSendComponents({
65
+ components: opts.components,
66
+ text: starterContent,
67
+ isFirst: true,
68
+ });
69
+ const starterEmbeds = resolveDiscordSendEmbeds({ embeds: opts.embeds, isFirst: true });
70
+ const silentFlags = opts.silent ? 1 << 12 : undefined;
71
+ const starterPayload = buildDiscordMessagePayload({
72
+ text: starterContent,
73
+ components: starterComponents,
74
+ embeds: starterEmbeds,
75
+ flags: silentFlags,
76
+ });
56
77
  let threadRes;
57
78
  try {
58
79
  threadRes = (await request(() => rest.post(Routes.threads(channelId), {
59
80
  body: {
60
81
  name: threadName,
61
- message: {
62
- content: starterContent,
63
- ...(starterEmbeds ? { embeds: starterEmbeds } : {}),
64
- },
82
+ message: stripUndefinedFields(serializePayload(starterPayload)),
65
83
  },
66
84
  }), "forum-thread"));
67
85
  }
@@ -80,14 +98,14 @@ export async function sendMessageDiscord(to, text, opts = {}) {
80
98
  try {
81
99
  if (opts.mediaUrl) {
82
100
  const [mediaCaption, ...afterMediaChunks] = remainingChunks;
83
- await sendDiscordMedia(rest, threadId, mediaCaption ?? "", opts.mediaUrl, undefined, request, accountInfo.config.maxLinesPerMessage, undefined, chunkMode);
101
+ await sendDiscordMedia(rest, threadId, mediaCaption ?? "", opts.mediaUrl, opts.mediaLocalRoots, undefined, request, accountInfo.config.maxLinesPerMessage, undefined, undefined, chunkMode, opts.silent);
84
102
  for (const chunk of afterMediaChunks) {
85
- await sendDiscordText(rest, threadId, chunk, undefined, request, accountInfo.config.maxLinesPerMessage, undefined, chunkMode);
103
+ await sendDiscordText(rest, threadId, chunk, undefined, request, accountInfo.config.maxLinesPerMessage, undefined, undefined, chunkMode, opts.silent);
86
104
  }
87
105
  }
88
106
  else {
89
107
  for (const chunk of remainingChunks) {
90
- await sendDiscordText(rest, threadId, chunk, undefined, request, accountInfo.config.maxLinesPerMessage, undefined, chunkMode);
108
+ await sendDiscordText(rest, threadId, chunk, undefined, request, accountInfo.config.maxLinesPerMessage, undefined, undefined, chunkMode, opts.silent);
91
109
  }
92
110
  }
93
111
  }
@@ -112,10 +130,10 @@ export async function sendMessageDiscord(to, text, opts = {}) {
112
130
  let result;
113
131
  try {
114
132
  if (opts.mediaUrl) {
115
- result = await sendDiscordMedia(rest, channelId, textWithTables, opts.mediaUrl, opts.replyTo, request, accountInfo.config.maxLinesPerMessage, opts.embeds, chunkMode);
133
+ result = await sendDiscordMedia(rest, channelId, textWithTables, opts.mediaUrl, opts.mediaLocalRoots, opts.replyTo, request, accountInfo.config.maxLinesPerMessage, opts.components, opts.embeds, chunkMode, opts.silent);
116
134
  }
117
135
  else {
118
- result = await sendDiscordText(rest, channelId, textWithTables, opts.replyTo, request, accountInfo.config.maxLinesPerMessage, opts.embeds, chunkMode);
136
+ result = await sendDiscordText(rest, channelId, textWithTables, opts.replyTo, request, accountInfo.config.maxLinesPerMessage, opts.components, opts.embeds, chunkMode, opts.silent);
119
137
  }
120
138
  }
121
139
  catch (err) {
@@ -160,11 +178,16 @@ export async function sendPollDiscord(to, poll, opts = {}) {
160
178
  const recipient = await parseAndResolveRecipient(to, opts.accountId);
161
179
  const { channelId } = await resolveChannelId(rest, recipient, request);
162
180
  const content = opts.content?.trim();
181
+ if (poll.durationSeconds !== undefined) {
182
+ throw new Error("Discord polls do not support durationSeconds; use durationHours");
183
+ }
163
184
  const payload = normalizeDiscordPollInput(poll);
185
+ const flags = opts.silent ? SUPPRESS_NOTIFICATIONS_FLAG : undefined;
164
186
  const res = (await request(() => rest.post(Routes.channelMessages(channelId), {
165
187
  body: {
166
188
  content: content || undefined,
167
189
  poll: payload,
190
+ ...(flags ? { flags } : {}),
168
191
  },
169
192
  }), "poll"));
170
193
  return {
@@ -172,3 +195,93 @@ export async function sendPollDiscord(to, poll, opts = {}) {
172
195
  channelId: String(res.channel_id ?? channelId),
173
196
  };
174
197
  }
198
+ async function materializeVoiceMessageInput(mediaUrl) {
199
+ // Security: reuse the standard media loader so we apply SSRF guards + allowed-local-root checks.
200
+ // Then write to a private temp file so ffmpeg/ffprobe never sees the original URL/path string.
201
+ const media = await loadWebMediaRaw(mediaUrl, maxBytesForKind("audio"));
202
+ const extFromName = media.fileName ? path.extname(media.fileName) : "";
203
+ const extFromMime = media.contentType ? extensionForMime(media.contentType) : "";
204
+ const ext = extFromName || extFromMime || ".bin";
205
+ const tempDir = resolvePreferredPoolbotTmpDir();
206
+ const filePath = path.join(tempDir, `voice-src-${crypto.randomUUID()}${ext}`);
207
+ await fs.writeFile(filePath, media.buffer, { mode: 0o600 });
208
+ return { filePath };
209
+ }
210
+ /**
211
+ * Send a voice message to Discord.
212
+ *
213
+ * Voice messages are a special Discord feature that displays audio with a waveform
214
+ * visualization. They require OGG/Opus format and cannot include text content.
215
+ *
216
+ * @param to - Recipient (user ID for DM or channel ID)
217
+ * @param audioPath - Path to local audio file (will be converted to OGG/Opus if needed)
218
+ * @param opts - Send options
219
+ */
220
+ export async function sendVoiceMessageDiscord(to, audioPath, opts = {}) {
221
+ const { filePath: localInputPath } = await materializeVoiceMessageInput(audioPath);
222
+ let oggPath = null;
223
+ let oggCleanup = false;
224
+ let token;
225
+ let rest;
226
+ let channelId;
227
+ try {
228
+ const cfg = loadConfig();
229
+ const accountInfo = resolveDiscordAccount({
230
+ cfg,
231
+ accountId: opts.accountId,
232
+ });
233
+ const client = createDiscordClient(opts, cfg);
234
+ token = client.token;
235
+ rest = client.rest;
236
+ const request = client.request;
237
+ const recipient = await parseAndResolveRecipient(to, opts.accountId);
238
+ channelId = (await resolveChannelId(rest, recipient, request)).channelId;
239
+ // Convert to OGG/Opus if needed
240
+ const ogg = await ensureOggOpus(localInputPath);
241
+ oggPath = ogg.path;
242
+ oggCleanup = ogg.cleanup;
243
+ // Get voice message metadata (duration and waveform)
244
+ const metadata = await getVoiceMessageMetadata(oggPath);
245
+ // Read the audio file
246
+ const audioBuffer = await fs.readFile(oggPath);
247
+ // Send the voice message
248
+ const result = await sendDiscordVoiceMessage(rest, channelId, audioBuffer, metadata, opts.replyTo, request, opts.silent);
249
+ recordChannelActivity({
250
+ channel: "discord",
251
+ accountId: accountInfo.accountId,
252
+ direction: "outbound",
253
+ });
254
+ return {
255
+ messageId: result.id ? String(result.id) : "unknown",
256
+ channelId: String(result.channel_id ?? channelId),
257
+ };
258
+ }
259
+ catch (err) {
260
+ if (channelId && rest && token) {
261
+ throw await buildDiscordSendError(err, {
262
+ channelId,
263
+ rest,
264
+ token,
265
+ hasMedia: true,
266
+ });
267
+ }
268
+ throw err;
269
+ }
270
+ finally {
271
+ // Clean up temporary OGG file if we created one
272
+ if (oggCleanup && oggPath) {
273
+ try {
274
+ await fs.unlink(oggPath);
275
+ }
276
+ catch {
277
+ // Ignore cleanup errors
278
+ }
279
+ }
280
+ try {
281
+ await fs.unlink(localInputPath);
282
+ }
283
+ catch {
284
+ // Ignore cleanup errors
285
+ }
286
+ }
287
+ }