Skip to content

Commit 5ad1808

Browse files
waleedlatif1claude
andcommitted
fix(inbox): fetch real attachment binary from presigned URL and persist for chat display
The AgentMail attachment endpoint returns JSON metadata with a download_url, not raw binary. We were base64-encoding the JSON text and sending it to the LLM, causing provider rejection. Now we parse the metadata, fetch the actual file from the presigned URL, upload it to copilot storage, and persist it on the chat message so images render inline with previews. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 56ac180 commit 5ad1808

File tree

2 files changed

+75
-33
lines changed

2 files changed

+75
-33
lines changed

apps/sim/lib/mothership/inbox/agentmail-client.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -122,28 +122,21 @@ export async function getMessage(inboxId: string, messageId: string): Promise<Ag
122122
)
123123
}
124124

125+
interface AttachmentMetadata {
126+
download_url: string
127+
}
128+
125129
export async function getAttachment(
126130
inboxId: string,
127131
messageId: string,
128132
attachmentId: string
129133
): Promise<ArrayBuffer> {
130134
const path = `/inboxes/${encodeURIComponent(inboxId)}/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`
131-
return requestRaw(path)
132-
}
133-
134-
async function requestRaw(path: string): Promise<ArrayBuffer> {
135-
const url = `${BASE_URL}${path}`
136-
const response = await fetch(url, {
137-
headers: {
138-
Authorization: `Bearer ${getApiKey()}`,
139-
},
140-
})
135+
const metadata = await request<AttachmentMetadata>(path)
141136

137+
const response = await fetch(metadata.download_url)
142138
if (!response.ok) {
143-
const body = await response.text().catch(() => '')
144-
logger.error('AgentMail API error', { status: response.status, path, body })
145-
throw new Error(`AgentMail API error: ${response.status} ${body}`)
139+
throw new Error(`Failed to download attachment from presigned URL: ${response.status}`)
146140
}
147-
148141
return response.arrayBuffer()
149142
}

apps/sim/lib/mothership/inbox/executor.ts

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as agentmail from '@/lib/mothership/inbox/agentmail-client'
1313
import { formatEmailAsMessage } from '@/lib/mothership/inbox/format'
1414
import { sendInboxResponse } from '@/lib/mothership/inbox/response'
1515
import type { AgentMailAttachment } from '@/lib/mothership/inbox/types'
16+
import { uploadFile } from '@/lib/uploads/core/storage-service'
1617
import { createFileContent, type MessageContent } from '@/lib/uploads/utils/file-utils'
1718
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
1819

@@ -146,13 +147,14 @@ export async function executeInboxTask(taskId: string): Promise<void> {
146147
logger.warn('Failed to fetch attachment metadata', { taskId, attachErr })
147148
}
148149
}
149-
const fileAttachments = await downloadAttachmentContents(
150+
const downloaded = await downloadAttachmentContents(
150151
attachments,
151152
ws.inboxProviderId,
152153
inboxTask.agentmailMessageId,
153-
taskId
154+
taskId,
155+
userId
154156
)
155-
return { attachments, fileAttachments }
157+
return { attachments, ...downloaded }
156158
}
157159

158160
const [attachmentResult, workspaceContext, integrationTools, userPermission] =
@@ -162,7 +164,7 @@ export async function executeInboxTask(taskId: string): Promise<void> {
162164
buildIntegrationToolSchemas(userId),
163165
getUserEntityPermissions(userId, 'workspace', ws.id).catch(() => null),
164166
])
165-
const { attachments, fileAttachments } = attachmentResult
167+
const { attachments, fileAttachments, storedAttachments } = attachmentResult
166168

167169
const truncatedTask = {
168170
...inboxTask,
@@ -197,10 +199,17 @@ export async function executeInboxTask(taskId: string): Promise<void> {
197199
const cleanContent = stripThinkingTags(result.content || '')
198200

199201
if (chatId) {
200-
await persistChatMessages(chatId, userId, userMessageId, messageContent, {
201-
...result,
202-
content: cleanContent,
203-
})
202+
await persistChatMessages(
203+
chatId,
204+
userId,
205+
userMessageId,
206+
messageContent,
207+
{
208+
...result,
209+
content: cleanContent,
210+
},
211+
storedAttachments
212+
)
204213
}
205214

206215
const finalStatus = result.success ? 'completed' : 'failed'
@@ -295,7 +304,8 @@ async function persistChatMessages(
295304
userId: string,
296305
userMessageId: string,
297306
userContent: string,
298-
result: OrchestratorResult
307+
result: OrchestratorResult,
308+
storedAttachments: StoredAttachment[] = []
299309
): Promise<void> {
300310
try {
301311
const now = new Date().toISOString()
@@ -305,6 +315,7 @@ async function persistChatMessages(
305315
role: 'user' as const,
306316
content: userContent,
307317
timestamp: now,
318+
...(storedAttachments.length > 0 ? { fileAttachments: storedAttachments } : {}),
308319
}
309320

310321
const assistantMessage = {
@@ -351,17 +362,33 @@ async function markTaskFailed(taskId: string, errorMessage: string): Promise<voi
351362

352363
const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024
353364

365+
interface StoredAttachment {
366+
id: string
367+
key: string
368+
filename: string
369+
media_type: string
370+
size: number
371+
}
372+
373+
interface DownloadedAttachments {
374+
fileAttachments: Array<MessageContent & { filename: string }>
375+
storedAttachments: StoredAttachment[]
376+
}
377+
354378
/**
355-
* Download attachment content from AgentMail and convert to file content objects
356-
* that the orchestrator can pass to the LLM as multimodal content.
379+
* Download attachment content from AgentMail, convert to file content objects
380+
* for the LLM, and upload to copilot storage for chat display.
357381
*/
358382
async function downloadAttachmentContents(
359383
attachments: AgentMailAttachment[],
360384
inboxProviderId: string | null,
361385
messageId: string | null,
362-
taskId: string
363-
): Promise<Array<MessageContent & { filename: string }>> {
364-
if (!inboxProviderId || !messageId || attachments.length === 0) return []
386+
taskId: string,
387+
userId: string
388+
): Promise<DownloadedAttachments> {
389+
if (!inboxProviderId || !messageId || attachments.length === 0) {
390+
return { fileAttachments: [], storedAttachments: [] }
391+
}
365392

366393
const eligible = attachments.filter((a) => {
367394
if (a.size > MAX_ATTACHMENT_SIZE) {
@@ -381,15 +408,37 @@ async function downloadAttachmentContents(
381408
const buffer = Buffer.from(arrayBuffer)
382409
const fileContent = createFileContent(buffer, attachment.content_type)
383410
if (!fileContent) return null
384-
return { ...fileContent, filename: attachment.filename }
411+
412+
const storageKey = `copilot/${Date.now()}-${attachment.attachment_id}-${attachment.filename}`
413+
const uploaded = await uploadFile({
414+
file: buffer,
415+
fileName: attachment.filename,
416+
contentType: attachment.content_type,
417+
context: 'copilot',
418+
customKey: storageKey,
419+
preserveKey: true,
420+
metadata: { userId, originalName: attachment.filename },
421+
})
422+
423+
const stored: StoredAttachment = {
424+
id: attachment.attachment_id,
425+
key: uploaded.key,
426+
filename: attachment.filename,
427+
media_type: attachment.content_type,
428+
size: buffer.length,
429+
}
430+
431+
return { fileContent: { ...fileContent, filename: attachment.filename }, stored }
385432
})
386433
)
387434

388-
const results: Array<MessageContent & { filename: string }> = []
435+
const fileAttachments: Array<MessageContent & { filename: string }> = []
436+
const storedAttachments: StoredAttachment[] = []
389437
for (let i = 0; i < settled.length; i++) {
390438
const outcome = settled[i]
391439
if (outcome.status === 'fulfilled' && outcome.value) {
392-
results.push(outcome.value)
440+
fileAttachments.push(outcome.value.fileContent)
441+
storedAttachments.push(outcome.value.stored)
393442
} else if (outcome.status === 'rejected') {
394443
const attachment = eligible[i]
395444
logger.warn('Failed to download attachment', {
@@ -404,8 +453,8 @@ async function downloadAttachmentContents(
404453
logger.info('Downloaded attachment contents', {
405454
taskId,
406455
total: attachments.length,
407-
downloaded: results.length,
456+
downloaded: fileAttachments.length,
408457
})
409458

410-
return results
459+
return { fileAttachments, storedAttachments }
411460
}

0 commit comments

Comments
 (0)