From 7707e3406ce1bb31e956235f21486ec351c6016b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Feb 2026 00:41:46 +0100 Subject: [PATCH] fix: await DiscordMessageListener handler for queued messages (#22396) Co-authored-by: Irene --- CHANGELOG.md | 1 + src/discord/monitor.test.ts | 34 ++++++++++++++++++++++++++------ src/discord/monitor/listeners.ts | 3 +-- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d182a29c7d..125711ecbd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,6 +106,7 @@ Docs: https://docs.openclaw.ai - Telegram/Streaming: restore 30-char first-preview debounce and scope `NO_REPLY` prefix suppression to partial sentinel fragments so normal `No...` text is not filtered. (#22613) thanks @obviyus. - Telegram/Status reactions: refresh stall timers on repeated phase updates and honor ack-reaction scope when lifecycle reactions are enabled, preventing false stall emojis and unwanted group reactions. Thanks @wolly-tundracube and @thewilloftheshadow. - Telegram/Status reactions: keep lifecycle reactions active when available-reactions lookup fails by falling back to unrestricted variant selection instead of suppressing reaction updates. (#22380) thanks @obviyus. +- Discord/Events: await `DiscordMessageListener` message handlers so regular `MESSAGE_CREATE` traffic is processed through queue ordering/timeout flow instead of fire-and-forget drops. (#22396) Thanks @sIlENtbuffER. - Discord/Streaming: apply `replyToMode: first` only to the first Discord chunk so block-streamed replies do not spam mention pings. (#20726) Thanks @thewilloftheshadow for the report. - Discord/Components: map DM channel targets back to user-scoped component sessions so button/select interactions stay in the main DM session. Thanks @thewilloftheshadow. - Discord/Allowlist: lazy-load guild lists when resolving Discord user allowlists so ID-only entries resolve even if guild fetch fails. (#20208) Thanks @zhangjunmengyang. diff --git a/src/discord/monitor.test.ts b/src/discord/monitor.test.ts index 4a0e95e5cd8..eda94190e3e 100644 --- a/src/discord/monitor.test.ts +++ b/src/discord/monitor.test.ts @@ -67,7 +67,7 @@ describe("registerDiscordListener", () => { }); describe("DiscordMessageListener", () => { - it("returns before the handler finishes", async () => { + it("awaits the handler before returning", async () => { let handlerResolved = false; let resolveHandler: (() => void) | null = null; const handlerPromise = new Promise((resolve) => { @@ -79,19 +79,30 @@ describe("DiscordMessageListener", () => { const handler = vi.fn(() => handlerPromise); const listener = new DiscordMessageListener(handler); - await listener.handle( + const handlePromise = listener.handle( {} as unknown as import("./monitor/listeners.js").DiscordMessageEvent, {} as unknown as import("@buape/carbon").Client, ); + let handleResolved = false; + void handlePromise.then(() => { + handleResolved = true; + }); + // Handler should be called but not yet resolved expect(handler).toHaveBeenCalledOnce(); expect(handlerResolved).toBe(false); + await Promise.resolve(); + expect(handleResolved).toBe(false); + // Release the handler const release = resolveHandler; if (typeof release === "function") { (release as () => void)(); } - await handlerPromise; + + // Now await handle() - it should complete only after handler resolves + await handlePromise; + expect(handlerResolved).toBe(true); }); it("logs handler failures", async () => { @@ -129,18 +140,29 @@ describe("DiscordMessageListener", () => { } as unknown as ReturnType; const listener = new DiscordMessageListener(handler, logger); - await listener.handle( + // Start handle() but don't await yet + const handlePromise = listener.handle( {} as unknown as import("./monitor/listeners.js").DiscordMessageEvent, {} as unknown as import("@buape/carbon").Client, ); + let handleResolved = false; + void handlePromise.then(() => { + handleResolved = true; + }); + await Promise.resolve(); + expect(handleResolved).toBe(false); + // Advance time past the slow listener threshold vi.setSystemTime(31_000); + + // Release the handler const release = resolveHandler; if (typeof release === "function") { (release as () => void)(); } - await handlerPromise; - await Promise.resolve(); + + // Now await handle() - it should complete and log the slow listener + await handlePromise; expect(logger.warn).toHaveBeenCalled(); const warnMock = logger.warn as unknown as { mock: { calls: unknown[][] } }; diff --git a/src/discord/monitor/listeners.ts b/src/discord/monitor/listeners.ts index 20cc76aa31e..e9516f84502 100644 --- a/src/discord/monitor/listeners.ts +++ b/src/discord/monitor/listeners.ts @@ -86,8 +86,7 @@ export class DiscordMessageListener extends MessageCreateListener { async handle(data: DiscordMessageEvent, client: Client) { const startedAt = Date.now(); - const task = Promise.resolve(this.handler(data, client)); - void task + await this.handler(data, client) .catch((err) => { const logger = this.logger ?? discordEventQueueLog; logger.error(danger(`discord handler failed: ${String(err)}`));