Discord Bot Setup¶
Detailed guide for creating and configuring a Discord bot for rig-agent-runtime.
Create the Application¶
- Go to discord.com/developers/applications
- Click New Application
- Name it (e.g., "My Agent") and click Create
Get the Bot Token¶
- Go to the Bot tab in the left sidebar
- Click Reset Token
- Copy the token -- you'll need this as
DISCORD_BOT_TOKEN - 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:
- Go to the Installation tab
- Set Install Link to None
- Go back to the Bot tab
- Disable Public Bot
Invite to Your Server¶
- Go to the OAuth2 tab
- In OAuth2 URL Generator, check the
botscope - Under Bot Permissions, select:
- View Channels
- Send Messages
- Create Public Threads
- Send Messages in Threads
- Read Message History
- Copy the generated URL
- 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:
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:
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:
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.js—buildDiscordCliPrompt(focus constraint, rar#232)src/agent/providers/claude-cli.js— branches oncontext.discordModesrc/index.js— setsdiscordMode: truein DM and threadmessageCreatehandlers