Skip to content

Add Discord bot with Gateway WebSocket forwarding#567

Open
RSO wants to merge 19 commits intomainfrom
feat/discord-bot
Open

Add Discord bot with Gateway WebSocket forwarding#567
RSO wants to merge 19 commits intomainfrom
feat/discord-bot

Conversation

@RSO
Copy link
Contributor

@RSO RSO commented Feb 25, 2026

This PR adds a Kilo for Discord implementation, heavily based on Kilo for Slack.

Instead of abstracting shared code paths, I've opted to duplicate a bunch of logic. The main reasoning here is that I want to move as fast as possible, and I'm currently reviewing switching the whole infrastructure over to using the vercel/chat SDK anyway, so I don't want to waste time abstracting code that I'm going to remove anyway.

I've tested the bot locally to make sure that it can do everything that the Slack bot can do:

CleanShot 2026-02-27 at 16 46 45@2x

Review Plan

Honestly, I wouldn't expect you to read through all the code. I mainly made sure that the code is on-par with the Slack implementation, so if Kilo Code Reviewer agrees with that I think that's great 😄

Base automatically changed from feat/discord-integration to main February 26, 2026 12:21
@RSO RSO force-pushed the feat/discord-bot branch from 3fd2bcf to 9ed1bed Compare February 26, 2026 12:23
RSO added 4 commits February 27, 2026 13:32
Implement the full Discord bot following the Vercel chat architecture pattern:

- Gateway WebSocket listener (/api/discord/gateway) connects to Discord
  via discord.js, receives MESSAGE_CREATE events, and forwards them to
  the webhook handler for unified processing
- Webhook handler (/discord/webhook) processes both Discord HTTP
  Interactions (PING, slash commands) and forwarded Gateway events
- Bot processor reuses the platform-agnostic runBot() engine with
  spawn_cloud_agent tool support, conversation context, and GitHub
  repository context
- All bot API calls use the DISCORD_BOT_TOKEN (Bot auth), not OAuth tokens
- Message posting, reactions, and channel context fetching via Discord
  REST API v10

New dependencies: discord.js, discord-interactions, discord-api-types
New env var: DISCORD_PUBLIC_KEY (Ed25519 verification for HTTP Interactions)
discord.js depends on optional native modules (zlib-sync, bufferutil,
utf-8-validate) that webpack cannot bundle. Add them to
serverExternalPackages so Next.js resolves them at runtime via Node.js
instead of trying to bundle them.
@RSO RSO force-pushed the feat/discord-bot branch from 9ed1bed to 99ed1e1 Compare February 27, 2026 12:40
RSO added 10 commits February 27, 2026 13:50
- Capture bot user ID from discord.js client on ready event
- Include botUserId in forwarded Gateway events
- Filter messages in webhook to only process those mentioning the bot
- Use correct botUserId for stripping bot mention from message

Previously the code incorrectly assumed the first non-author mention
was the bot, which failed when users mentioned others before the bot.
Each heartbeat iteration added an abort listener that was never removed
when the timer resolved normally. Over a 10-minute run this accumulated
~60 stale listeners on the AbortSignal. Use a shared done() callback
that both the timer and abort paths call, which clears the timer and
removes the event listener.
When runGatewayListener rejected (e.g. login failure), the catch block
returned an error response without aborting the heartbeat or awaiting
its promise, leaving background DB polling alive. Move abort+await into
a finally block so cleanup always runs.
The webhook handler defers long-running bot processing via after(),
which needs the same 800s function timeout as the other bot routes.
Without this export, Vercel applies a shorter default limit that can
cut off background work mid-processing.
The Ed25519 signature check validates integrity but not age — a
captured valid request could be replayed later. Add a 5-minute
staleness window (matching the Slack verifier) that rejects requests
with timestamps too far in the past or future before calling verifyKey.
Move claimActiveListener + heartbeat start into the ClientReady
callback so a failed Discord login never evicts the previous healthy
listener from the active-listener DB slot. Also gate event forwarding
on isLeader to avoid forwarding before ownership is confirmed.
…client

The Discord bot was using the old createCloudAgentClient (SSE-based) which
connects to an endpoint that is no longer available, causing ECONNREFUSED.
Migrate to createCloudAgentNextClient + runSessionToCompletion (WebSocket-based)
to match the working Slack bot implementation.
Discord was hardcoding 'anthropic/claude-sonnet-4' while Slack properly
resolved the configured model from integration metadata. Add model_slug
storage, getModel/updateModel to discord-service, a TRPC updateModel
procedure, and a model selector to the Discord settings UI.
@vercel
Copy link

vercel bot commented Feb 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
kilocode-gateway Error Error Feb 27, 2026 3:47pm

Request Review

Retry the processing emoji removal if the initial parallel attempt
fails due to Discord API rate limiting.
@RSO RSO marked this pull request as ready for review February 27, 2026 15:50
@kilo-code-bot
Copy link
Contributor

kilo-code-bot bot commented Feb 27, 2026

Code Review Summary

Status: 12 Issues Found | Recommendation: Address before merge

Fix these issues in Kilo Cloud

Overview

Severity Count
CRITICAL 0
WARNING 7
SUGGESTION 5
Issue Details (click to expand)

WARNING

File Line Issue
src/lib/discord-bot.ts 325 Unhandled JSON.parse on toolCall.function.arguments — malformed LLM output will crash
src/lib/discord-bot.ts 190 as cast on args.mode bypasses type safety
src/app/discord/webhook/route.ts 63 JSON.parse(rawBody) as ForwardedGatewayEvent — can throw + unsafe cast
src/app/discord/webhook/route.ts 85 Unhandled JSON.parse(rawBody) on HTTP Interaction path — malformed body crashes handler
src/lib/discord-bot/discord-channel-context.ts 146 Off-by-one in truncatetext.slice(0, maxLen - 1) + '...' produces maxLen + 2 chars
src/app/api/discord/gateway/route.ts 262 x-discord-gateway-token header sends raw DISCORD_BOT_TOKEN
src/lib/integrations/discord-service.ts 191 Multiple as casts on metadata (lines 191, 341, 380, and router line 32) bypass type safety

SUGGESTION

File Line Issue
src/app/api/discord/gateway/route.ts 30 Comment says "every 3 minutes" but cron is */9 * * * * (every 9 minutes)
src/lib/discord-bot/discord-utils.ts 70 getDiscordBotUserId is exported but never used — dead code
src/routers/discord-router.ts N/A Audit log created regardless of whether updateModel succeeded
Other Observations (not in diff)

Issues found in unchanged code or general patterns that cannot receive inline comments:

File Line Issue
src/routers/discord-router.ts 100-108 Audit log for updateModel is created even when result.success is true, but the check if (input.organizationId && result.success) is correct. However, the uninstallApp audit log at line 65-74 is created unconditionally — if the uninstall throws, the audit log is still attempted (though the throw would prevent reaching it). Pattern is consistent.
General No tests were added for any of the new Discord bot functionality (discord-bot.ts, discord-utils.ts, discord-channel-context.ts, verify-request.ts, auth.ts, webhook route, gateway route, discord-service additions). Consider adding unit tests for the pure utility functions at minimum.
Files Reviewed (24 files)
  • next.config.mjs - 0 issues
  • package.json - 0 issues (generated)
  • pnpm-lock.yaml - 0 issues (generated)
  • src/app/api/discord/gateway/route.ts - 2 issues
  • src/app/api/openrouter/[...path]/route.ts - 0 issues
  • src/app/api/trpc/[trpc]/route.ts - 0 issues
  • src/app/discord/webhook/route.ts - 2 issues
  • src/app/slack/webhook/route.ts - 0 issues
  • src/components/integrations/DiscordIntegrationDetails.tsx - 0 issues
  • src/db/migrations/0033_third_shiver_man.sql - 0 issues (generated)
  • src/db/migrations/meta/0033_snapshot.json - 0 issues (generated)
  • src/db/migrations/meta/_journal.json - 0 issues (generated)
  • src/db/schema.ts - 0 issues
  • src/lib/bot-users/types.ts - 0 issues
  • src/lib/config.server.ts - 0 issues
  • src/lib/discord-bot.ts - 2 issues
  • src/lib/discord-bot/discord-channel-context.ts - 1 issue
  • src/lib/discord-bot/discord-utils.ts - 1 issue
  • src/lib/discord/auth.ts - 0 issues
  • src/lib/discord/verify-request.ts - 0 issues
  • src/lib/feature-detection.ts - 0 issues
  • src/lib/integrations/discord-service.ts - 1 issue
  • src/routers/discord-router.ts - 1 issue
  • vercel.json - 0 issues

RSO and others added 2 commits February 27, 2026 17:03
Co-authored-by: kilo-code-bot[bot] <240665456+kilo-code-bot[bot]@users.noreply.github.com>
return new NextResponse('Invalid signature', { status: 401 });
}

const body = JSON.parse(rawBody);
Copy link
Contributor

Choose a reason for hiding this comment

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

[WARNING]: Unhandled JSON.parse — this can throw on malformed request bodies from Discord HTTP Interactions, crashing the handler with a 500.

The gateway path at line 63 has the same issue (already flagged), but this is a separate code path for Discord's HTTP Interaction endpoint. A malformed body here would bypass the signature verification that already passed and crash before the PING/command handling.

Wrap in try-catch or use a safe parse utility.

* Discord Gateway listener with Postgres-based coordination.
*
* Architecture:
* - This route is triggered by a cron job (e.g., every 3 minutes)
Copy link
Contributor

Choose a reason for hiding this comment

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

[SUGGESTION]: Comment says "every 3 minutes" but the actual cron schedule in vercel.json is */9 * * * * (every 9 minutes).

Suggested change
* - This route is triggered by a cron job (e.g., every 3 minutes)
* - This route is triggered by a cron job (every 9 minutes)

/**
* Get the bot's own user ID by calling /users/@me with the bot token.
*/
export async function getDiscordBotUserId(): Promise<string | null> {
Copy link
Contributor

Choose a reason for hiding this comment

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

[SUGGESTION]: getDiscordBotUserId is exported but never referenced anywhere in the codebase. If it's intended for future use, consider removing it until needed to avoid dead code.


if (existing) {
// Preserve existing model_slug when re-authorizing
const existingMetadata = (existing.metadata || {}) as Record<string, unknown>;
Copy link
Contributor

Choose a reason for hiding this comment

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

[WARNING]: as Record<string, unknown> cast bypasses type safety (per project coding style: "STRONGLY AVOID typescript's as operator"). This pattern is repeated at lines 191, 341, and 380 in this file, plus line 32 in discord-router.ts.

Consider defining a typed metadata schema (e.g. with zod) and parsing it, or using satisfies to validate the shape. This would catch issues if the metadata shape changes unexpectedly.

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