Skip to content

fix: Discord attachment download support#2

Open
changhansung wants to merge 9 commits into
mainfrom
dev2
Open

fix: Discord attachment download support#2
changhansung wants to merge 9 commits into
mainfrom
dev2

Conversation

@changhansung

Copy link
Copy Markdown
Owner

Summary

Fixes #2 — Discord image attachments were received as blank messages.

Changes (plugins/agend-plugin-discord/src/discord-adapter.ts)

  • Store attachment URLs: Added attachmentUrls Map to cache Discord CDN URLs during message handling
  • Detect image attachments: Set kind: 'photo' for image/* content types so processAttachments() auto-downloads them (previously hardcoded to 'document')
  • Implement downloadAttachment: Fetch from stored CDN URL → write to inboxDir → return local path (matching Telegram adapter pattern)
  • Prevent memory leak: Clean up URL from map after successful download

Before

  • Discord images arrived as blank messages — downloadAttachment() threw not yet implemented
  • All attachments were tagged as document, so image auto-download never triggered

After

  • Images correctly identified and auto-downloaded via processAttachments()
  • Agents receive image_path metadata and can read the image directly
  • Behavior matches Telegram adapter

- 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

@amazon-q-developer amazon-q-developer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. Memory Leak Risk (Line 58): Attachment URLs accumulate indefinitely when downloads don't occur or fail
  2. File Collision Risk (Lines 379-380): Unsanitized filenames can cause overwrites and data loss
  3. 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>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 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.

Comment on lines +379 to +380
const filename = new URL(url).pathname.split("/").pop() ?? fileId;
const localPath = join(this.inboxDir, filename);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 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.

Suggested change
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}`);

Comment on lines +382 to +388
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 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.

Suggested change
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
changhansung added a commit that referenced this pull request May 25, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant