Skip to content

Discord Bot Setup

Detailed guide for creating and configuring a Discord bot for rig-agent-runtime.

Create the Application

  1. Go to discord.com/developers/applications
  2. Click New Application
  3. Name it (e.g., "My Agent") and click Create

Get the Bot Token

  1. Go to the Bot tab in the left sidebar
  2. Click Reset Token
  3. Copy the token -- you'll need this as DISCORD_BOT_TOKEN
  4. Never share this token or commit it to git

Enable Required Intents

Still on the Bot tab, scroll down to Privileged Gateway Intents and enable:

  • Message Content Intent -- required for the bot to read message text

Without this, the bot connects but can't see what users write.

Disable Public Access

To prevent random users from adding your bot to their servers:

  1. Go to the Installation tab
  2. Set Install Link to None
  3. Go back to the Bot tab
  4. Disable Public Bot

Invite to Your Server

  1. Go to the OAuth2 tab
  2. In OAuth2 URL Generator, check the bot scope
  3. Under Bot Permissions, select:
    • View Channels
    • Send Messages
    • Create Public Threads
    • Send Messages in Threads
    • Read Message History
  4. Copy the generated URL
  5. Open it in your browser, select your server, and authorize

Configure the Agent

In your character.json, set the channels the bot should listen on:

{
  "discord": {
    "channels": ["#general", "#support"]
  }
}

Channel names must match exactly, with the # prefix.

Thread Mode

When someone sends a message in a monitored channel, the bot creates a thread for the conversation. This keeps the main channel clean and gives each conversation its own context.

DM Support

The bot automatically responds to direct messages. No configuration needed.

Allow Other Bots

By default, the bot ignores messages from other bots. To allow specific bots:

{
  "discord": {
    "allowBots": ["123456789012345678"]
  }
}

Use the bot's user ID (not application ID). You can find this by enabling Developer Mode in Discord, then right-clicking the bot user.

Run the Agent

export CHARACTER_FILE=./character.json
export DISCORD_BOT_TOKEN=your-token-here
export ANTHROPIC_API_KEY=sk-ant-...

npm start

You should see:

[Rig-Agent] messageCreate handler registered
[Rig-Agent] Logged in as My Agent#1234
[Rig-Agent] Listening on channels: #general, #support
[Rig-Agent] DMs: enabled

Send a message in one of the configured channels -- the bot creates a thread and responds.

Diagnostics

Every incoming Discord message now logs a single stdout line before any filtering:

[Rig-Agent] messageCreate: SomeUser (bot=false) channelId=123456789

Use this to distinguish three failure modes:

Symptom Likely cause
Line absent for a message Discord gateway not delivering the event (intent issue or reconnect race)
Line present, then dropping — bot X not in allowBots Bot ID missing from allowBots in character config
Line present, then no base channel resolved Thread parent not cached on cold start — auto-retried via REST fetch
Line present, no further output, no Discord reply agent.process hung or threw before replying

The messageCreate handler registered line at startup confirms the handler was installed before client.login().

Thread parent cache miss

After a pod restart, Discord.js's channel cache is cold. Messages received in threads (rather than the main channel) may arrive with message.channel.parent === null when Partials.Channel is configured. The runtime detects this and fetches the parent via client.channels.fetch(message.channel.parentId) before evaluating the channel allowlist. A warning is logged if the fetch fails.

Discord-mode prompt assembly

When the runtime handles a Discord message (DM or guild thread), it sets discordMode: true in the agent context and uses buildDiscordCliPrompt instead of the standard buildCliPrompt.

Why this matters

Persona prompts are task-oriented (e.g. "post a TaskSpec as a GitHub comment"). When a conversational Discord message arrives with no GitHub issue context, the model correctly finds "nothing actionable" and exits cleanly — producing an empty reply (category=idle).

The Discord-mode prompt injects a framing block that explicitly overrides this behaviour:

What it tells the model Effect
"Your text reply IS the Discord output" Model writes text instead of calling GitHub tools
"You MUST write substantive content" Prevents silent idle exit
"GitHub interactions are opt-in" Suppresses unwanted issue/PR calls during chat
"Answer ONLY the most recent message" Prevents hallucinated answers to topics not in this thread (rar#232)
"Do NOT re-engage prior topics" Stops persona domain knowledge from bleeding into unasked follow-ups
Recent conversation history included Gives turn-by-turn context without a session ID

Conversation history scoping

Conversation history (getConversation) is keyed strictly by threadId (the Discord thread ID) in both the Postgres and in-memory stores. Cross-thread bleed at the storage layer is not possible — each Discord thread gets its own isolated history.

The focus constraint in the prompt (Answer ONLY the most recent message) is the guard against the model over-generating based on persona domain knowledge rather than stale data.

Category tagging

Discord chat runs are tagged category=work (not idle) in reportTokenUsage so that cost attribution is accurate even when no CONDUCTOR_REPO/CONDUCTOR_ISSUE_NUMBER env vars are set.

Affected code paths

  • src/agent/shared.jsbuildDiscordCliPrompt (focus constraint, rar#232)
  • src/agent/providers/claude-cli.js — branches on context.discordMode
  • src/index.js — sets discordMode: true in DM and thread messageCreate handlers