From 923ff17ff3f19d53c246dca08c49984c17428708 Mon Sep 17 00:00:00 2001 From: OliYeet Date: Mon, 2 Mar 2026 22:58:41 +0100 Subject: [PATCH] fix(slack): filter inherited parent files from thread replies (#32203) Slack's Events API includes the parent message's files array in every thread reply event payload. This caused OpenClaw to re-download and attach the parent's files to every text-only thread reply, creating ghost media attachments. The fix filters out files that belong to the thread starter by comparing file IDs. The resolveSlackThreadStarter result is already cached, so this adds no extra API calls. Closes #32203 --- src/slack/monitor/message-handler/prepare.ts | 26 +++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/slack/monitor/message-handler/prepare.ts b/src/slack/monitor/message-handler/prepare.ts index 0e4dd7bea..b2616b639 100644 --- a/src/slack/monitor/message-handler/prepare.ts +++ b/src/slack/monitor/message-handler/prepare.ts @@ -515,8 +515,32 @@ export async function prepareSlackMessage(params: { return null; } + // When processing a thread reply, filter out files that belong to the thread + // starter (parent message). Slack's Events API includes the parent's `files` + // array in every thread reply payload, which causes ghost media attachments + // on text-only replies. We eagerly resolve the thread starter here (the result + // is cached) and exclude any file IDs that match the parent. (#32203) + let ownFiles = message.files; + if (isThreadReply && threadTs && message.files?.length) { + const starter = await resolveSlackThreadStarter({ + channelId: message.channel, + threadTs, + client: ctx.app.client, + }); + if (starter?.files?.length) { + const starterFileIds = new Set(starter.files.map((f) => f.id)); + const filtered = message.files.filter((f) => !f.id || !starterFileIds.has(f.id)); + if (filtered.length < message.files.length) { + logVerbose( + `slack: filtered ${message.files.length - filtered.length} inherited parent file(s) from thread reply`, + ); + } + ownFiles = filtered.length > 0 ? filtered : undefined; + } + } + const media = await resolveSlackMedia({ - files: message.files, + files: ownFiles, token: ctx.botToken, maxBytes: ctx.mediaMaxBytes, });