fix: Discord attachment download support#2
Conversation
- Store attachment URLs in Map<id, url> during message handling - Detect image attachments via contentType (kind: 'photo' vs 'document') - Implement downloadAttachment: fetch from Discord CDN → write to inboxDir - Fixes: Discord image attachments received as blank messages
There was a problem hiding this comment.
Summary
This PR successfully implements Discord attachment download support, enabling image attachments to be processed correctly. However, there are 3 critical issues that must be addressed before merge:
Critical Issues Found
- Memory Leak Risk (Line 58): Attachment URLs accumulate indefinitely when downloads don't occur or fail
- File Collision Risk (Lines 379-380): Unsanitized filenames can cause overwrites and data loss
- Resource Leak (Lines 382-388): Failed downloads leave URLs in memory due to missing cleanup guarantees
Required Actions
All three issues have blocking severity and require fixes. Code suggestions have been provided for issues #2 and suzuke#3. Issue #1 requires implementing a cleanup strategy (TTL-based expiration or size-based eviction).
Once these are resolved, the implementation will correctly match the Telegram adapter pattern as intended.
You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.
| private generalChannelId?: string; | ||
| private queue: MessageQueue; | ||
| private lastChatId: string | null = null; | ||
| private attachmentUrls = new Map<string, string>(); |
There was a problem hiding this comment.
🛑 Memory Leak Risk: Attachment URLs are stored indefinitely in the Map but only deleted after successful download. If downloadAttachment is never called (e.g., for non-photo attachments) or download fails, URLs accumulate in memory over the bot's lifetime. Consider implementing a cleanup mechanism such as TTL-based expiration or size-based eviction to prevent unbounded memory growth.
| const filename = new URL(url).pathname.split("/").pop() ?? fileId; | ||
| const localPath = join(this.inboxDir, filename); |
There was a problem hiding this comment.
🛑 File Collision Risk: Using unsanitized filenames from Discord CDN URLs can cause overwrites when multiple attachments share the same name. Add unique identifier (timestamp or fileId) to prevent data loss from concurrent or sequential attachments with identical names.
| const filename = new URL(url).pathname.split("/").pop() ?? fileId; | |
| const localPath = join(this.inboxDir, filename); | |
| const filename = new URL(url).pathname.split("/").pop() ?? fileId; | |
| const localPath = join(this.inboxDir, `${fileId}-${filename}`); |
| const response = await fetch(url); | ||
| if (!response.ok) throw new Error(`Failed to download attachment: ${response.status}`); | ||
| if (!response.body) throw new Error("No response body"); | ||
|
|
||
| await pipeline(Readable.fromWeb(response.body as import("stream/web").ReadableStream), createWriteStream(localPath)); | ||
| this.attachmentUrls.delete(fileId); | ||
| return localPath; |
There was a problem hiding this comment.
🛑 Resource Leak: If pipeline fails after fetch succeeds, the URL remains in the Map permanently because cleanup only executes after successful pipeline completion. Wrap cleanup in try-finally to ensure deletion regardless of download outcome.
| const response = await fetch(url); | |
| if (!response.ok) throw new Error(`Failed to download attachment: ${response.status}`); | |
| if (!response.body) throw new Error("No response body"); | |
| await pipeline(Readable.fromWeb(response.body as import("stream/web").ReadableStream), createWriteStream(localPath)); | |
| this.attachmentUrls.delete(fileId); | |
| return localPath; | |
| const response = await fetch(url); | |
| if (!response.ok) throw new Error(`Failed to download attachment: ${response.status}`); | |
| if (!response.body) throw new Error("No response body"); | |
| try { | |
| await pipeline(Readable.fromWeb(response.body as import("stream/web").ReadableStream), createWriteStream(localPath)); | |
| return localPath; | |
| } finally { | |
| this.attachmentUrls.delete(fileId); | |
| } |
Fallback to tmux capture-pane when statusline.json has no context data. Parses Kiro CLI's 'X% !>' prompt pattern from the pane output.
- Add parseContextFromPane() optional method to CliBackend interface - Implement in KiroBackend with classic + TUI mode patterns - Replace hardcoded regex in cli.ts with contextParsers map - New backends can add a parser entry without touching cli.ts
…Backend contextParsers map in cli.ts is the sole consumer — no need for the interface method or backend implementation.
Fetch Discord CDN URLs during message handling instead of deferring to downloadAttachment(). Prevents CDN URL expiration issues. - attachmentUrls Map → attachmentPaths Map (stores local paths) - Download happens in _registerHandlers loop, failures skip attachment - downloadAttachment() becomes a simple path lookup + cleanup
All this.adapter references that should be per-adapter are now fixed: P0 (functionality broken): #1 agent-endpoint: routeToolCall uses getAdapterForInstance #2 Telegram classic replies: use adapters.get(msg.adapterId) suzuke#3 Topic-mode inbound react+processAttachments: per-adapter suzuke#4 handleClassicChannelMessage: react, processAttachments, downloadAttachment all use per-adapter P1 (notifications to wrong platform): suzuke#5 outbound-handlers cross-instance notifications: per-instance adapter suzuke#6 sendHangNotification: getAdapterForInstance suzuke#7 setTopicIcon: getAdapterForInstance suzuke#8 reactMessageStatus: check all adapters for Discord type suzuke#9 warnIfRateLimited: adapters.get(msg.adapterId) suzuke#10 web-api Web→Chat sync: getAdapterForInstance OutboundContext interface extended with adapters, getAdapterForInstance, getChannelConfig. WebApiContext extended with getAdapterForInstance.
Summary
Fixes #2 — Discord image attachments were received as blank messages.
Changes (
plugins/agend-plugin-discord/src/discord-adapter.ts)attachmentUrlsMap to cache Discord CDN URLs during message handlingkind: 'photo'forimage/*content types soprocessAttachments()auto-downloads them (previously hardcoded to'document')downloadAttachment: Fetch from stored CDN URL → write toinboxDir→ return local path (matching Telegram adapter pattern)Before
downloadAttachment()threwnot yet implementeddocument, so image auto-download never triggeredAfter
processAttachments()image_pathmetadata and can read the image directly