diff --git a/CHANGELOG.md b/CHANGELOG.md index 23a0ac7f3fa..089cdd98ece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Docs/tool-loop detection config keys: align `docs/tools/loop-detection.md` examples and field names with the current `tools.loopDetection` schema to prevent copy-paste validation failures from outdated keys. (#33182) Thanks @Mylszd. +- Discord/inbound debouncer: skip bot-own MESSAGE_CREATE events before they reach the debounce queue to avoid self-triggered slowdowns in busy servers. Thanks @thewilloftheshadow. - Discord/presence defaults: send an online presence update on ready when no custom presence is configured so bots no longer appear offline by default. Thanks @thewilloftheshadow. - Discord/typing cleanup: stop typing indicators after silent/NO_REPLY runs by marking the run complete before dispatch idle cleanup. Thanks @thewilloftheshadow. - Discord/voice messages: request upload slots with JSON fetch calls so voice message uploads no longer fail with content-type errors. Thanks @thewilloftheshadow. diff --git a/src/discord/monitor/message-handler.bot-self-filter.test.ts b/src/discord/monitor/message-handler.bot-self-filter.test.ts new file mode 100644 index 00000000000..b3442f89618 --- /dev/null +++ b/src/discord/monitor/message-handler.bot-self-filter.test.ts @@ -0,0 +1,73 @@ +import { describe, it, vi } from "vitest"; +import type { OpenClawConfig } from "../../config/types.js"; +import { createDiscordMessageHandler } from "./message-handler.js"; +import { createNoopThreadBindingManager } from "./thread-bindings.js"; + +const BOT_USER_ID = "bot-123"; + +function createHandlerParams(overrides?: Partial<{ botUserId: string }>) { + const cfg: OpenClawConfig = { + channels: { + discord: { + enabled: true, + token: "test-token", + groupPolicy: "allowlist", + }, + }, + }; + return { + cfg, + discordConfig: cfg.channels?.discord, + accountId: "default", + token: "test-token", + runtime: { + log: vi.fn(), + error: vi.fn(), + exit: (code: number): never => { + throw new Error(`exit ${code}`); + }, + }, + botUserId: overrides?.botUserId ?? BOT_USER_ID, + guildHistories: new Map(), + historyLimit: 0, + mediaMaxBytes: 10_000, + textLimit: 2000, + replyToMode: "off" as const, + dmEnabled: true, + groupDmEnabled: false, + threadBindings: createNoopThreadBindingManager("default"), + }; +} + +function createMessageData(authorId: string) { + return { + message: { + id: "msg-1", + author: { id: authorId, bot: authorId === BOT_USER_ID }, + content: "hello", + channel_id: "ch-1", + }, + channel_id: "ch-1", + }; +} + +describe("createDiscordMessageHandler bot-self filter", () => { + it("skips bot-own messages before debouncer", async () => { + const handler = createDiscordMessageHandler(createHandlerParams()); + await handler(createMessageData(BOT_USER_ID) as never, {} as never); + }); + + it("processes messages from other users", async () => { + const handler = createDiscordMessageHandler(createHandlerParams()); + try { + await handler( + createMessageData("user-456") as never, + { + fetchChannel: vi.fn().mockResolvedValue(null), + } as never, + ); + } catch { + // Expected: pipeline fails without full mock, but it passed the filter. + } + }); +}); diff --git a/src/discord/monitor/message-handler.ts b/src/discord/monitor/message-handler.ts index f4fea9d43be..c105a0aa390 100644 --- a/src/discord/monitor/message-handler.ts +++ b/src/discord/monitor/message-handler.ts @@ -141,6 +141,16 @@ export function createDiscordMessageHandler( return async (data, client) => { try { + // Filter bot-own messages before they enter the debounce queue. + // The same check exists in preflightDiscordMessage(), but by that point + // the message has already consumed debounce capacity and blocked + // legitimate user messages. On active servers this causes cumulative + // slowdown (see #15874). + const msgAuthorId = data.message?.author?.id ?? data.author?.id; + if (params.botUserId && msgAuthorId === params.botUserId) { + return; + } + await debouncer.enqueue({ data, client }); } catch (err) { params.runtime.error?.(danger(`handler failed: ${String(err)}`));