From 95079949c3a2dab3380efe7593b34f02a416153f Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Sun, 5 Apr 2026 19:11:29 -0400 Subject: [PATCH] fix(discord): short-circuit bound thread self-loop drops --- .../src/monitor/message-handler.preflight.ts | 100 ++++++++++++++++-- 1 file changed, 93 insertions(+), 7 deletions(-) diff --git a/extensions/discord/src/monitor/message-handler.preflight.ts b/extensions/discord/src/monitor/message-handler.preflight.ts index a8b44a0d52b..879cf4bf994 100644 --- a/extensions/discord/src/monitor/message-handler.preflight.ts +++ b/extensions/discord/src/monitor/message-handler.preflight.ts @@ -122,6 +122,56 @@ function isBoundThreadBotSystemMessage(params: { return DISCORD_BOUND_THREAD_SYSTEM_PREFIXES.some((prefix) => text.startsWith(prefix)); } +type BoundThreadLookupRecordLike = { + webhookId?: string | null; + metadata?: { + webhookId?: string | null; + }; +}; + +function isDiscordThreadChannelType(type: ChannelType | undefined): boolean { + return ( + type === ChannelType.PublicThread || + type === ChannelType.PrivateThread || + type === ChannelType.AnnouncementThread + ); +} + +function isDiscordThreadChannelMessage(params: { + isGuildMessage: boolean; + message: Message; + channelInfo: import("./message-utils.js").DiscordChannelInfo | null; +}): boolean { + if (!params.isGuildMessage) { + return false; + } + const channel = + "channel" in params.message ? (params.message as { channel?: unknown }).channel : undefined; + return Boolean( + (channel && + typeof channel === "object" && + "isThread" in channel && + typeof (channel as { isThread?: unknown }).isThread === "function" && + (channel as { isThread: () => boolean }).isThread()) || + isDiscordThreadChannelType(params.channelInfo?.type), + ); +} + +function resolveInjectedBoundThreadLookupRecord(params: { + threadBindings: DiscordMessagePreflightParams["threadBindings"]; + threadId: string; +}): BoundThreadLookupRecordLike | undefined { + const getByThreadId = (params.threadBindings as { getByThreadId?: (threadId: string) => unknown }) + .getByThreadId; + if (typeof getByThreadId !== "function") { + return undefined; + } + const binding = getByThreadId(params.threadId); + return binding && typeof binding === "object" + ? (binding as BoundThreadLookupRecordLike) + : undefined; +} + function resolveDiscordMentionState(params: { authorIsBot: boolean; botId?: string; @@ -180,16 +230,18 @@ export function shouldIgnoreBoundThreadWebhookMessage(params: { accountId?: string; threadId?: string; webhookId?: string | null; - threadBinding?: SessionBindingRecord; + threadBinding?: BoundThreadLookupRecordLike; }): boolean { const webhookId = params.webhookId?.trim() || ""; if (!webhookId) { return false; } const boundWebhookId = - typeof params.threadBinding?.metadata?.webhookId === "string" - ? params.threadBinding.metadata.webhookId.trim() - : ""; + typeof params.threadBinding?.webhookId === "string" + ? params.threadBinding.webhookId.trim() + : typeof params.threadBinding?.metadata?.webhookId === "string" + ? params.threadBinding.metadata.webhookId.trim() + : ""; if (!boundWebhookId) { const threadId = params.threadId?.trim() || ""; if (!threadId) { @@ -371,6 +423,43 @@ export async function preflightDiscordMessage( } const isDirectMessage = channelInfo?.type === ChannelType.DM; const isGroupDm = channelInfo?.type === ChannelType.GroupDM; + const messageText = resolveDiscordMessageText(message, { + includeForwarded: true, + }); + const injectedBoundThreadBinding = + !isDirectMessage && !isGroupDm + ? resolveInjectedBoundThreadLookupRecord({ + threadBindings: params.threadBindings, + threadId: messageChannelId, + }) + : undefined; + if ( + shouldIgnoreBoundThreadWebhookMessage({ + accountId: params.accountId, + threadId: messageChannelId, + webhookId, + threadBinding: injectedBoundThreadBinding, + }) + ) { + logVerbose(`discord: drop bound-thread webhook echo message ${message.id}`); + return null; + } + if ( + isBoundThreadBotSystemMessage({ + isBoundThreadSession: + Boolean(injectedBoundThreadBinding) && + isDiscordThreadChannelMessage({ + isGuildMessage, + message, + channelInfo, + }), + isBotAuthor: Boolean(author.bot), + text: messageText, + }) + ) { + logVerbose(`discord: drop bound-thread bot system message ${message.id}`); + return null; + } const data = message === params.data.message ? params.data : { ...params.data, message }; logDebug( `[discord-preflight] channelId=${messageChannelId} guild_id=${params.data.guild_id} channelType=${channelInfo?.type} isGuild=${isGuildMessage} isDM=${isDirectMessage} isGroupDm=${isGroupDm}`, @@ -461,9 +550,6 @@ export async function preflightDiscordMessage( const baseText = resolveDiscordMessageText(message, { includeForwarded: false, }); - const messageText = resolveDiscordMessageText(message, { - includeForwarded: true, - }); // Intercept text-only slash commands (e.g. user typing "/reset" instead of using Discord's slash command picker) // These should not be forwarded to the agent; proper slash command interactions are handled elsewhere