diff --git a/extensions/codex/src/app-server/dynamic-tools.test.ts b/extensions/codex/src/app-server/dynamic-tools.test.ts index 20d465dbf22..3abf0ef9e33 100644 --- a/extensions/codex/src/app-server/dynamic-tools.test.ts +++ b/extensions/codex/src/app-server/dynamic-tools.test.ts @@ -320,6 +320,45 @@ describe("createCodexDynamicToolBridge", () => { ]); }); + it("records message tool media attachment aliases as delivery evidence", async () => { + const toolResult = { + content: [{ type: "text", text: "Sent." }], + details: { messageId: "message-1" }, + } satisfies AgentToolResult; + const tool = createTool({ + name: "message", + execute: vi.fn(async () => toolResult), + }); + const bridge = createCodexDynamicToolBridge({ + tools: [tool], + signal: new AbortController().signal, + }); + + const result = await handleMessageToolCall(bridge, { + action: "send", + text: "song attached", + media: "/tmp/generated-song.mp3", + attachments: [{ filePath: "/tmp/generated-cover.png" }], + }); + + expect(result).toEqual(expectInputText("Sent.")); + expect(bridge.telemetry.didSendViaMessagingTool).toBe(true); + expect(bridge.telemetry.messagingToolSentMediaUrls).toEqual([ + "/tmp/generated-song.mp3", + "/tmp/generated-cover.png", + ]); + expect(bridge.telemetry.messagingToolSentTargets).toEqual([ + { + tool: "message", + provider: "message", + to: undefined, + threadId: undefined, + text: "song attached", + mediaUrls: ["/tmp/generated-song.mp3", "/tmp/generated-cover.png"], + }, + ]); + }); + it("records internal UI source replies separately from outbound messaging evidence", async () => { const toolResult = textToolResult("Sent to current chat.", { status: "ok", diff --git a/extensions/codex/src/app-server/dynamic-tools.ts b/extensions/codex/src/app-server/dynamic-tools.ts index 8cfe7327e02..96f315b85a1 100644 --- a/extensions/codex/src/app-server/dynamic-tools.ts +++ b/extensions/codex/src/app-server/dynamic-tools.ts @@ -417,11 +417,32 @@ function readFirstString(record: Record, keys: string[]): strin function collectMediaUrls(record: Record): string[] { const urls: string[] = []; - for (const key of ["mediaUrl", "media_url", "imageUrl", "image_url"]) { - const value = record[key]; + const pushMediaUrl = (value: unknown) => { if (typeof value === "string" && value.trim()) { urls.push(value.trim()); } + }; + const pushAttachment = (value: unknown) => { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return; + } + const attachment = value as Record; + for (const key of ["media", "mediaUrl", "path", "filePath", "fileUrl"]) { + pushMediaUrl(attachment[key]); + } + }; + for (const key of [ + "media", + "mediaUrl", + "media_url", + "path", + "filePath", + "fileUrl", + "imageUrl", + "image_url", + ]) { + const value = record[key]; + pushMediaUrl(value); } for (const key of ["mediaUrls", "media_urls", "imageUrls", "image_urls"]) { const value = record[key]; @@ -429,9 +450,13 @@ function collectMediaUrls(record: Record): string[] { continue; } for (const entry of value) { - if (typeof entry === "string" && entry.trim()) { - urls.push(entry.trim()); - } + pushMediaUrl(entry); + } + } + const attachments = record.attachments; + if (Array.isArray(attachments)) { + for (const attachment of attachments) { + pushAttachment(attachment); } } return urls; diff --git a/src/agents/pi-embedded-subscribe.handlers.tools.test.ts b/src/agents/pi-embedded-subscribe.handlers.tools.test.ts index eea91989636..9a1a73ad5cc 100644 --- a/src/agents/pi-embedded-subscribe.handlers.tools.test.ts +++ b/src/agents/pi-embedded-subscribe.handlers.tools.test.ts @@ -1187,6 +1187,43 @@ describe("messaging tool media URL tracking", () => { expect(ctx.state.pendingMessagingMediaUrls.has("tool-upload-file")).toBe(false); }); + it("commits message attachment aliases as delivery evidence", async () => { + const { ctx } = createTestContext(); + + const startEvt: ToolExecutionStartEvent = { + type: "tool_execution_start", + toolName: "message", + toolCallId: "tool-attachment-aliases", + args: { + action: "send", + to: "channel:123", + content: "track ready", + media: "/tmp/generated-song.mp3", + attachments: [{ filePath: "/tmp/generated-cover.png" }], + }, + }; + await handleToolExecutionStart(ctx, startEvt); + + const endEvt: ToolExecutionEndEvent = { + type: "tool_execution_end", + toolName: "message", + toolCallId: "tool-attachment-aliases", + isError: false, + result: { ok: true }, + }; + await handleToolExecutionEnd(ctx, endEvt); + + expect(ctx.state.messagingToolSentMediaUrls).toEqual([ + "/tmp/generated-song.mp3", + "/tmp/generated-cover.png", + ]); + expectRecordFields(requireSingleMessagingTarget(ctx), "messaging target", { + to: "channel:123", + text: "track ready", + mediaUrls: ["/tmp/generated-song.mp3", "/tmp/generated-cover.png"], + }); + }); + it("commits sendAttachment args as message delivery evidence", async () => { const { ctx } = createTestContext(); diff --git a/src/agents/pi-embedded-subscribe.handlers.tools.ts b/src/agents/pi-embedded-subscribe.handlers.tools.ts index 2bd534421f9..fbe7d819694 100644 --- a/src/agents/pi-embedded-subscribe.handlers.tools.ts +++ b/src/agents/pi-embedded-subscribe.handlers.tools.ts @@ -341,11 +341,23 @@ function pushUniqueMediaUrl(urls: string[], seen: Set, value: unknown): function collectMessagingMediaUrlsFromRecord(record: Record): string[] { const urls: string[] = []; const seen = new Set(); + const pushAttachment = (value: unknown) => { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return; + } + const attachment = value as Record; + pushUniqueMediaUrl(urls, seen, attachment.media); + pushUniqueMediaUrl(urls, seen, attachment.mediaUrl); + pushUniqueMediaUrl(urls, seen, attachment.path); + pushUniqueMediaUrl(urls, seen, attachment.filePath); + pushUniqueMediaUrl(urls, seen, attachment.fileUrl); + }; pushUniqueMediaUrl(urls, seen, record.media); pushUniqueMediaUrl(urls, seen, record.mediaUrl); pushUniqueMediaUrl(urls, seen, record.path); pushUniqueMediaUrl(urls, seen, record.filePath); + pushUniqueMediaUrl(urls, seen, record.fileUrl); const mediaUrls = record.mediaUrls; if (Array.isArray(mediaUrls)) { @@ -353,6 +365,12 @@ function collectMessagingMediaUrlsFromRecord(record: Record): s pushUniqueMediaUrl(urls, seen, mediaUrl); } } + const attachments = record.attachments; + if (Array.isArray(attachments)) { + for (const attachment of attachments) { + pushAttachment(attachment); + } + } return urls; }