diff --git a/src/slack/monitor/message-handler/prepare.test.ts b/src/slack/monitor/message-handler/prepare.test.ts index 3e8e8b2e8..08bee35b7 100644 --- a/src/slack/monitor/message-handler/prepare.test.ts +++ b/src/slack/monitor/message-handler/prepare.test.ts @@ -222,6 +222,23 @@ describe("slack prepareSlackMessage inbound contract", () => { expect(prepared).toBeNull(); }); + it("delivers file-only message with placeholder when media download fails", async () => { + // Files without url_private will fail to download, simulating a download + // failure. The message should still be delivered with a fallback + // placeholder instead of being silently dropped (#25064). + const prepared = await prepareWithDefaultCtx( + createSlackMessage({ + text: "", + files: [{ name: "voice.ogg" }, { name: "photo.jpg" }], + }), + ); + + expect(prepared).toBeTruthy(); + expect(prepared!.ctxPayload.RawBody).toContain("[Slack file:"); + expect(prepared!.ctxPayload.RawBody).toContain("voice.ogg"); + expect(prepared!.ctxPayload.RawBody).toContain("photo.jpg"); + }); + it("keeps channel metadata out of GroupSystemPrompt", async () => { const slackCtx = createInboundSlackCtx({ cfg: { diff --git a/src/slack/monitor/message-handler/prepare.ts b/src/slack/monitor/message-handler/prepare.ts index 39515ad62..0073de14d 100644 --- a/src/slack/monitor/message-handler/prepare.ts +++ b/src/slack/monitor/message-handler/prepare.ts @@ -362,8 +362,21 @@ export async function prepareSlackMessage(params: { const mediaPlaceholder = effectiveDirectMedia ? effectiveDirectMedia.map((m) => m.placeholder).join(" ") : undefined; + + // When files were attached but all downloads failed, create a fallback + // placeholder so the message is still delivered to the agent instead of + // being silently dropped (#25064). + const fileOnlyFallback = + !mediaPlaceholder && (message.files?.length ?? 0) > 0 + ? message + .files!.slice(0, 5) + .map((f) => f.name ?? "file") + .join(", ") + : undefined; + const fileOnlyPlaceholder = fileOnlyFallback ? `[Slack file: ${fileOnlyFallback}]` : undefined; + const rawBody = - [(message.text ?? "").trim(), attachmentContent?.text, mediaPlaceholder] + [(message.text ?? "").trim(), attachmentContent?.text, mediaPlaceholder, fileOnlyPlaceholder] .filter(Boolean) .join("\n") || ""; if (!rawBody) {