diff --git a/src/auto-reply/reply/agent-runner-payloads.test.ts b/src/auto-reply/reply/agent-runner-payloads.test.ts index e067b01a589..94088b2b5b8 100644 --- a/src/auto-reply/reply/agent-runner-payloads.test.ts +++ b/src/auto-reply/reply/agent-runner-payloads.test.ts @@ -58,6 +58,35 @@ describe("buildReplyPayloads media filter integration", () => { }); }); + it("drops only invalid media when reply media normalization fails", async () => { + const normalizeMediaPaths = async (payload: { mediaUrl?: string }) => { + if (payload.mediaUrl === "./bad.png") { + throw new Error("Path escapes sandbox root"); + } + return payload; + }; + + const { replyPayloads } = await buildReplyPayloads({ + ...baseParams, + payloads: [ + { text: "keep text", mediaUrl: "./bad.png", audioAsVoice: true }, + { text: "keep second" }, + ], + normalizeMediaPaths, + }); + + expect(replyPayloads).toHaveLength(2); + expect(replyPayloads[0]).toMatchObject({ + text: "keep text", + mediaUrl: undefined, + mediaUrls: undefined, + audioAsVoice: false, + }); + expect(replyPayloads[1]).toMatchObject({ + text: "keep second", + }); + }); + it("applies media filter after text filter", async () => { const { replyPayloads } = await buildReplyPayloads({ ...baseParams, diff --git a/src/auto-reply/reply/agent-runner-payloads.ts b/src/auto-reply/reply/agent-runner-payloads.ts index 8ed02bdf454..263dea9fd54 100644 --- a/src/auto-reply/reply/agent-runner-payloads.ts +++ b/src/auto-reply/reply/agent-runner-payloads.ts @@ -20,6 +20,31 @@ import { shouldSuppressMessagingToolReplies, } from "./reply-payloads.js"; +function hasPayloadMedia(payload: ReplyPayload): boolean { + return Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0; +} + +async function normalizeReplyPayloadMedia(params: { + payload: ReplyPayload; + normalizeMediaPaths?: (payload: ReplyPayload) => Promise; +}): Promise { + if (!params.normalizeMediaPaths || !hasPayloadMedia(params.payload)) { + return params.payload; + } + + try { + return await params.normalizeMediaPaths(params.payload); + } catch (err) { + logVerbose(`reply payload media normalization failed: ${String(err)}`); + return { + ...params.payload, + mediaUrl: undefined, + mediaUrls: undefined, + audioAsVoice: false, + }; + } +} + async function normalizeSentMediaUrlsForDedupe(params: { sentMediaUrls: string[]; normalizeMediaPaths?: (payload: ReplyPayload) => Promise; @@ -126,7 +151,10 @@ export async function buildReplyPayloads(params: { silentToken: SILENT_REPLY_TOKEN, parseMode: "always", }).payload; - return params.normalizeMediaPaths ? await params.normalizeMediaPaths(parsed) : parsed; + return await normalizeReplyPayloadMedia({ + payload: parsed, + normalizeMediaPaths: params.normalizeMediaPaths, + }); }), ) ).filter(isRenderablePayload);