fix(slack): deliver file-only messages when all media downloads fail

When a Slack message contains only files/audio (no text) and every file
download fails, `resolveSlackMedia` returns null and `rawBody` becomes
empty, causing `prepareSlackMessage` to silently drop the message.

Build a fallback placeholder from the original file names so the agent
still receives the message, matching the pattern already used in
`resolveSlackThreadHistory` for file-only thread entries.

Closes #25064
This commit is contained in:
justinhuangcode
2026-02-24 07:22:18 +00:00
committed by Peter Steinberger
parent 177386ed73
commit def28a87b2
2 changed files with 31 additions and 1 deletions

View File

@@ -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: {

View File

@@ -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) {