Skip to content

feat(discord): inbound + outbound media attachments#175

Merged
williamwa merged 3 commits into
mainfrom
feat/discord-attachments
May 29, 2026
Merged

feat(discord): inbound + outbound media attachments#175
williamwa merged 3 commits into
mainfrom
feat/discord-attachments

Conversation

@williamwa
Copy link
Copy Markdown
Collaborator

@williamwa williamwa commented May 29, 2026

Second channel in the attachment-support rollout (after #174 WhatsApp).

Discord was fully text-only — inbound attachments ignored, agent couldn't send files. Wires the channel to the already-complete core attachment infrastructure both directions. No core/protocol/gateway changes.

Inbound (Discord → agent)

  • Message attachments (guild-message path + DM gateway dispatch) are fetched from the Discord CDN and uploaded as session attachments — images become vision input automatically.
  • Audio attachments are transcribed via client.transcribeAudio and appended as [Transcribed voice message] text.
  • Media-only messages now trigger the agent instead of being dropped.
  • Attachments over the 25 MiB cap are skipped (logged).

Outbound (agent → Discord)

  • The agent's attachment_send deliveries (SSE attachment event) are sent as Discord file uploads via AttachmentBuilder, caption as message content.

Notes

  • Discord is a private package bundled into the openhermit CLI, so this ships with the next CLI release — no standalone npm publish. Version bumped 0.2.0 → 0.3.0 for changelog clarity.
  • Outbound TTS voice replies are intentionally not added for Discord (no native voice-note affordance like WhatsApp/Telegram); inbound audio is still transcribed.
  • Adds the first unit tests for this package (mapRawAttachments).

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Discord channel adapter now supports media attachments: images, files, and audio
    • Images processed as vision input; audio transcribed and included in agent context
    • Agent can send files back to Discord in responses
    • Attachments exceeding 25 MiB limit are skipped
  • Documentation

    • Added media handling documentation for Discord adapter
  • Tests

    • Added test coverage for attachment processing

Review Change Stack

Discord was fully text-only: inbound attachments were ignored and the
agent couldn't send files. Wire the channel to the existing attachment
infrastructure both directions.

Inbound: message attachments (guild messages + DM gateway dispatch) are
fetched from the Discord CDN and uploaded as session attachments (images
become vision input); audio attachments are transcribed via the agent's
STT and appended as text. Media-only messages now trigger the agent.
Attachments over the 25 MiB cap are skipped.

Outbound: the agent's attachment_send deliveries (SSE 'attachment' event)
are sent back as Discord file uploads via AttachmentBuilder, with any
caption as the message content.

Discord is bundled into the CLI (private package), so this ships with the
next openhermit release rather than a standalone publish. Bumps 0.2.0 ->
0.3.0 for changelog clarity. Adds the first unit tests for this package.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

Warning

Review limit reached

@williamwa, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 2 minutes and 52 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 814cfc43-091a-44b7-a610-7c0540641744

📥 Commits

Reviewing files that changed from the base of the PR and between 16d6f92 and b262830.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • apps/channels/discord/src/bridge.ts
  • apps/gateway/package.json
📝 Walkthrough

Walkthrough

This PR adds comprehensive media attachment support to the Discord channel adapter. It introduces attachment type contracts, extracts and maps attachments from Discord messages (both raw gateway events and discord.js objects), resolves them with size validation and audio transcription, and delivers agent-generated attachments back to Discord via file uploads.

Changes

Discord media attachment support

Layer / File(s) Summary
Attachment type contracts
src/discord-api.ts
New DiscordIncomingAttachment interface (url, name, optional contentType/size), updated DiscordMessageEvent with optional attachments array, and new DiscordApi.sendFile method for Discord file uploads.
Message extraction and attachment mapping
src/bot.ts
Raw gateway MESSAGE_CREATE handler and handleMessage now extract attachments from Discord messages, normalize them to neutral shape via mapMessageAttachments and mapRawAttachments helpers, and allow text-only-empty messages through when attachments present.
Extraction test suite
test/bot.test.ts
Tests for mapRawAttachments covering gateway field normalization (filename→name, content_type→contentType) and defensive input validation (empty/invalid inputs, missing url).
Inbound attachment resolution
src/bridge.ts
New 25 MiB size cap constant, ResolvedInbound type for final resolved content, and resolveInbound() method that downloads Discord CDN attachments, skips oversized files, transcribes audio attachments, and uploads non-audio to agent-local session; sendToAgent() integrates resolved output.
Outbound attachment delivery
src/bridge.ts
New deliverAttachment() helper downloads bytes from agent-local API and uploads to Discord with filename/caption; SSE loop in waitForAgentResponse() recognizes attachment frames and routes them to delivery.
Version and test infrastructure
package.json
Package version bumped to 0.3.0; new test npm script using Node --test runner with tsx.
Documentation updates
README.md, docs/channel-adapter.md, docs/manual/17-channels.md
Adapter README and channel docs expanded to document media handling: inbound attachments uploaded as vision or transcribed, outbound via attachment_send, 25 MiB limit enforced.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • HCF-STUDIOS/openhermit#124: Adds AgentLocalClient session attachment upload/download APIs that power the inbound attachment resolution and outbound delivery flows in this PR's bridge implementation.
  • HCF-STUDIOS/openhermit#164: Also modifies apps/channels/discord/src/bridge.ts SSE event handling in waitForAgentResponse(); this PR adds attachment frame support while the related PR handles text token processing.

Poem

📎 A rabbit hops through Discord's gate,
With files and images carried late;
Transcribes the audio, size-checked fast,
Then sends them back—attachments at last! 🐰

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: bidirectional media attachment support for Discord, which is the primary focus across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/discord-attachments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/channels/discord/src/bridge.ts (1)

117-124: 💤 Low value

Consider adding a timeout for CDN fetch.

The fetch(att.url) call has no timeout, so a stalled Discord CDN connection could block the message queue indefinitely. Adding an AbortSignal.timeout() would bound the wait time and improve resilience.

💡 Suggested timeout
       let bytes: Uint8Array;
       try {
-        const res = await fetch(att.url);
+        const res = await fetch(att.url, { signal: AbortSignal.timeout(30_000) });
         if (!res.ok) throw new Error(`status ${res.status}`);
         bytes = new Uint8Array(await res.arrayBuffer());
       } catch (err) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/channels/discord/src/bridge.ts` around lines 117 - 124, Add a bounded
timeout to the CDN fetch call so a stalled request can't block processing: when
calling fetch(att.url) (in the attachment fetch block that sets bytes from await
res.arrayBuffer()), create an AbortSignal with AbortSignal.timeout(ms) (or a
manual AbortController for older runtimes), pass that signal into fetch({ signal
}), and handle the abort case in the existing catch (log a clear timeout/abort
message via this.log and continue). Ensure the same error handling still covers
non-OK status and other errors and that you clean up any controller if you use
one.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/channels/discord/package.json`:
- Line 10: The package.json test script references "tsx" but this package lacks
tsx in its dependencies so running npm test fails; update
apps/channels/discord/package.json by adding "tsx" to devDependencies (e.g., add
a devDependency entry for "tsx" with an appropriate version) or adjust the
workspace/hoisting configuration so the package can resolve the existing tsx
installation; ensure the change targets the package.json that defines the
"test": "node --import tsx --test test/*.test.ts" script and run an install to
verify tests run.
- Line 4: Update apps/gateway's optionalDependencies entry for
`@openhermit/channel-discord` from "0.2.0" to "0.3.0" so the gateway picks up the
new attachment behavior; locate the optionalDependencies block in
apps/gateway/package.json and bump the version string for
`@openhermit/channel-discord`. Also, if this package is intended to be runnable
outside the monorepo, add "tsx" to the devDependencies of
apps/channels/discord/package.json (so the "test" script using node --import tsx
works when installed standalone).

---

Nitpick comments:
In `@apps/channels/discord/src/bridge.ts`:
- Around line 117-124: Add a bounded timeout to the CDN fetch call so a stalled
request can't block processing: when calling fetch(att.url) (in the attachment
fetch block that sets bytes from await res.arrayBuffer()), create an AbortSignal
with AbortSignal.timeout(ms) (or a manual AbortController for older runtimes),
pass that signal into fetch({ signal }), and handle the abort case in the
existing catch (log a clear timeout/abort message via this.log and continue).
Ensure the same error handling still covers non-OK status and other errors and
that you clean up any controller if you use one.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5dda98a6-6cd4-4f80-b4af-d2d5ae9a7122

📥 Commits

Reviewing files that changed from the base of the PR and between 38db5d9 and 16d6f92.

📒 Files selected for processing (8)
  • apps/channels/discord/README.md
  • apps/channels/discord/package.json
  • apps/channels/discord/src/bot.ts
  • apps/channels/discord/src/bridge.ts
  • apps/channels/discord/src/discord-api.ts
  • apps/channels/discord/test/bot.test.ts
  • docs/channel-adapter.md
  • docs/manual/17-channels.md

Comment thread apps/channels/discord/package.json
"scripts": {
"build": "tsc -b",
"typecheck": "tsc -p tsconfig.typecheck.json --pretty false",
"test": "node --import tsx --test test/*.test.ts",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if tsx is declared in this package or workspace root
cat apps/channels/discord/package.json | jq -r '.dependencies.tsx // .devDependencies.tsx // "not found"'

# Check workspace root if this is a monorepo
if [ -f package.json ]; then
  echo "Workspace root tsx:"
  cat package.json | jq -r '.dependencies.tsx // .devDependencies.tsx // "not found"'
fi

# Try running the test script to see if it fails
cd apps/channels/discord && npm test 2>&1 | head -20

Repository: HCF-STUDIOS/openhermit

Length of output: 1115


Fix test script so tsx is available to this package.

apps/channels/discord/package.json has no tsx in dependencies/devDependencies, and running npm test from apps/channels/discord fails with ERR_MODULE_NOT_FOUND: Cannot find package 'tsx'. Add tsx to this package’s devDependencies (or fix workspace install/hoisting so this package can resolve it).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/channels/discord/package.json` at line 10, The package.json test script
references "tsx" but this package lacks tsx in its dependencies so running npm
test fails; update apps/channels/discord/package.json by adding "tsx" to
devDependencies (e.g., add a devDependency entry for "tsx" with an appropriate
version) or adjust the workspace/hoisting configuration so the package can
resolve the existing tsx installation; ensure the change targets the
package.json that defines the "test": "node --import tsx --test test/*.test.ts"
script and run an install to verify tests run.

williamwa and others added 2 commits May 29, 2026 18:59
The gateway optionalDependencies still pinned @openhermit/channel-discord
at 0.2.0 while the workspace moved to 0.3.0, so a fresh install wouldn't
link the updated adapter and the media changes wouldn't take effect in the
bundled CLI. Bump the gateway pin and lockfile to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A stalled Discord CDN connection during inbound attachment download could
block the per-channel message queue indefinitely. Add a 15s
AbortSignal.timeout() to the fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@williamwa
Copy link
Copy Markdown
Collaborator Author

Addressed the review:

  • Gateway pin (Major): fixed in 5486f64apps/gateway/package.json + lockfile now pin @openhermit/channel-discord at 0.3.0.
  • CDN fetch timeout (nitpick): added a 15s AbortSignal.timeout() to the inbound attachment fetch so a stalled CDN connection can't block the channel queue (b262830).

On the tsx "Critical" finding — this is a false positive for the monorepo: tsx lives in the workspace-root devDependencies and is hoisted to the root node_modules, so node --import tsx resolves when running npm test from the package (which passes). This matches every other channel package's setup, and test/devDeps aren't consumed by installers anyway since this is a bundled, private package. No change needed.

@williamwa williamwa merged commit 4d54ed4 into main May 29, 2026
1 check passed
@williamwa williamwa deleted the feat/discord-attachments branch May 29, 2026 11:53
williamwa added a commit that referenced this pull request May 29, 2026
Ship the bundled channel media support — Discord/Slack/Telegram
attachments (#175, #176, #177) and their gateway pins (now all 0.3.0) —
in the published CLI. Also syncs the stale lockfile cli entry (0.9.0 ->
0.9.2) left by the earlier 0.9.1 bump.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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