Environment
- OpenClaw version: 2026.2.21-2
- OS: macOS Darwin 25.3.0 (arm64)
- Setup: Multi-account Discord config with named accounts only (
coach_claw, cheuk_claw, info_claw), no "default" account
Description
When using named Discord accounts (i.e. all tokens are under channels.discord.accounts.<name>.token with no root channels.discord.token), ack reactions never appear and the failure is completely silent at debug log level — it only surfaces with --verbose.
Root Cause
In processDiscordMessage, the statusReactionController adapter calls reactMessageDiscord without passing accountId:
// src/discord/monitor/message-handler.process.ts (simplified)
adapter: {
setReaction: async (emoji) => {
await reactMessageDiscord(messageChannelId, message.id, emoji, {
rest: client.rest
// ⚠️ accountId is never passed here
})
}
}
This causes createDiscordRestClient to fall back to accountId = "default":
reactMessageDiscord({ rest: client.rest }) // no accountId
→ resolveDiscordAccount(undefined)
→ normalizeAccountId(undefined) → "default"
→ channels.discord.accounts["default"] → undefined
→ channels.discord.token → undefined (not set)
→ DISCORD_BOT_TOKEN env var → undefined
→ token = ""
→ resolveToken throws: "Discord bot token missing for account 'default'"
The error is caught in applyEmoji's try/catch and forwarded to onError, which logs via logVerbose — only visible with --verbose flag, invisible at debug level.
Verbose log output:
discord ack cleanup failed target=<channelId>/<messageId>: Error: Discord bot token missing for account "default"
Why It's Hard to Diagnose
- No error is visible in standard
debug logs — requires --verbose gateway startup flag
- The agent processes messages and responds normally (the WS/inbound path is unaffected)
- Config looks correct;
ackReactionScope: "all" and per-account ackReaction emoji are both set
Workaround
Set channels.discord.token to any valid bot token. This satisfies the token resolver for the "default" fallback path. Crucially, since opts.rest is already provided by the caller, resolveRest(token, opts.rest) returns opts.rest directly — so the correct per-account REST client is still used, and the workaround works for all named accounts without per-account changes.
{
channels: {
discord: {
token: "<any_valid_bot_token>", // workaround: satisfies token resolver
accounts: {
coach_claw: { token: "...", ackReaction: "👨🏻🏫" },
cheuk_claw: { token: "...", ackReaction: "🦞" },
info_claw: { token: "...", ackReaction: "🔍" }
}
}
}
}
Suggested Fix
Pass accountId through to the reaction adapter in processDiscordMessage:
setReaction: async (emoji) => {
await reactMessageDiscord(messageChannelId, message.id, emoji, {
rest: client.rest,
accountId // ← add this
})
},
removeReaction: async (emoji) => {
await removeReactionDiscord(messageChannelId, message.id, emoji, {
rest: client.rest,
accountId // ← add this
})
}
This eliminates the need for the "default" account fallback entirely in this code path.
Environment
coach_claw,cheuk_claw,info_claw), no"default"accountDescription
When using named Discord accounts (i.e. all tokens are under
channels.discord.accounts.<name>.tokenwith no rootchannels.discord.token), ack reactions never appear and the failure is completely silent atdebuglog level — it only surfaces with--verbose.Root Cause
In
processDiscordMessage, thestatusReactionControlleradapter callsreactMessageDiscordwithout passingaccountId:This causes
createDiscordRestClientto fall back toaccountId = "default":The error is caught in
applyEmoji's try/catch and forwarded toonError, which logs vialogVerbose— only visible with--verboseflag, invisible atdebuglevel.Verbose log output:
Why It's Hard to Diagnose
debuglogs — requires--verbosegateway startup flagackReactionScope: "all"and per-accountackReactionemoji are both setWorkaround
Set
channels.discord.tokento any valid bot token. This satisfies the token resolver for the"default"fallback path. Crucially, sinceopts.restis already provided by the caller,resolveRest(token, opts.rest)returnsopts.restdirectly — so the correct per-account REST client is still used, and the workaround works for all named accounts without per-account changes.Suggested Fix
Pass
accountIdthrough to the reaction adapter inprocessDiscordMessage:This eliminates the need for the
"default"account fallback entirely in this code path.