Follow users across Discord voice channels
Configure voice.followUsers so the Discord bot trails chosen users between voice channels and leaves when they disconnect. Covers ID formats, followUsersEnabled, and access rules.
Read this when
- Configuring voice.followUsers so the bot follows people between voice channels
- Debugging follow-mode joins, barge-in, or echo handling
Follow mode lets the bot trail chosen users across Discord voice channels as they switch.
Follow users in voice
Reach for voice.followUsers when the Discord voice bot should stick with one or more Discord users you already know, rather than joining a set channel at startup or waiting on /vc join.
{
channels: {
discord: {
voice: {
enabled: true,
followUsersEnabled: true,
followUsers: ["discord:123456789012345678"],
allowedChannels: [
{
guildId: "123456789012345678",
channelId: "234567890123456789",
},
],
},
},
},
}
How it behaves:
- Both raw Discord user IDs and
discord:<id>values are accepted byfollowUsers. OpenClaw normalizes either form before it matches voice-state events. - With
followUsersconfigured,followUsersEnabledfalls back totrue. Setfalseto retain the stored list while halting automatic voice following. followUsersgoverns voice residency alone. Speaker access and owner authority are not granted by it; set upcommands.ownerAllowFromplus guild or channel users and roles on their own.- OpenClaw joins the channel an allowed voice channel that a followed user enters. Should that user relocate, OpenClaw relocates too. Once the active followed user disconnects, OpenClaw departs.
- Where several followed users share a guild and the active one leaves, OpenClaw switches to another tracked followed user's channel first, departing the guild only after that. When multiple followed users move simultaneously, the most recent observed voice-state event takes precedence.
allowedChannelscontinues to apply. A followed user sitting in a disallowed channel is skipped, and a follow-owned session either shifts to another followed user or leaves.- On startup and on a bounded interval, OpenClaw reconciles voice-state events it missed. Each reconciliation run samples the configured guilds and limits REST lookups, so very large
followUserslists can need more than one interval to settle. - Should Discord or an admin relocate the bot mid-follow, OpenClaw rebuilds the voice session and retains follow ownership provided the destination is allowed. If the bot lands outside
allowedChannels, OpenClaw departs and rejoins the configured target when one exists. - DAVE receive recovery can leave and rejoin the same channel after repeated decrypt failures. Follow-owned sessions retain their follow ownership across that recovery path, so a disconnect by a followed user later still causes a channel exit.
Picking a join mode:
- Choose
followUsersfor personal or operator setups where voice presence should be automatic whenever you are in voice. - Choose
autoJoinfor fixed rooms. AddwhenOccupied: trueso the bot is present only while humans occupy that room; leave it out for always-on voice presence. - Choose
/vc joinfor one-off joins or rooms where automatic voice presence would come as a surprise.
Discord voice codec:
- Voice receive logs display
discord voice: opus decoder: libopus-wasm. - For realtime playback, raw 48 kHz stereo PCM is encoded to Opus using the same bundled
libopus-wasmpackage, then packets are handed to@discordjs/voice. - File and provider-stream playback is transcoded by ffmpeg to raw 48 kHz stereo PCM, after which
libopus-wasmproduces the Opus packet stream delivered to Discord.
STT plus TTS pipeline:
- Captured Discord PCM is written to a WAV temp file.
- STT is handled by
tools.media.audio, for exampleopenai/gpt-4o-mini-transcribe. - Batch capture honors the largest applicable audio input limit, including configured model and CLI fallbacks. Captures that exceed it halt with a warning to speak a shorter segment; partial audio is never transcribed. Direct realtime streams are neither buffered nor truncated by this limit.
- The transcript goes through Discord ingress and routing while the response LLM runs under a voice-output policy that hides the agent
ttstool and requests returned text, since Discord voice owns final TTS playback. voice.model, when set, overrides the response LLM for this voice-channel turn only.voice.ttsis merged overtts; providers capable of streaming feed the player directly, and otherwise the resulting audio file plays in the joined channel.
Default agent-proxy voice-channel session example:
{
channels: {
discord: {
voice: {
enabled: true,
model: "openai/gpt-6-astra",
followUsersEnabled: true,
followUsers: ["123456789012345678"],
realtime: {
provider: "openai",
model: "gpt-realtime-2.1",
speakerVoice: "cedar",
},
},
},
},
}
Absent any voice.agentSession block, every voice channel receives its own routed OpenClaw session. For instance, /vc join channel:234567890123456789 talks to the session belonging to that Discord voice channel. The realtime model serves only as the voice front end; substantive requests get handed to the configured OpenClaw agent. Should the realtime model emit a final transcript without invoking the consult tool, OpenClaw forces the consult as a fallback, so the default still feels like talking to the agent.
Legacy STT plus TTS example:
{
channels: {
discord: {
voice: {
enabled: true,
mode: "stt-tts",
model: "openai/gpt-5.4-mini",
tts: {
provider: "openai",
providers: {
openai: {
model: "gpt-4o-mini-tts",
speakerVoice: "cedar",
},
},
},
},
},
},
}
Realtime bidi example:
{
channels: {
discord: {
voice: {
enabled: true,
mode: "bidi",
model: "openai/gpt-6-astra",
realtime: {
provider: "openai",
model: "gpt-realtime-2.1",
speakerVoice: "cedar",
toolPolicy: "safe-read-only",
consultPolicy: "always",
},
},
},
},
}
Voice as an extension of an existing Discord channel session:
{
channels: {
discord: {
voice: {
enabled: true,
mode: "agent-proxy",
model: "openai/gpt-6-astra",
agentSession: {
mode: "target",
target: "channel:123456789012345678",
},
realtime: {
provider: "openai",
model: "gpt-realtime-2.1",
speakerVoice: "cedar",
},
},
},
},
}
Under agent-proxy mode the bot joins the configured voice channel, yet OpenClaw agent turns use the target channel's usual routed session and agent. Spoken results are returned into the voice channel by the realtime voice session. Depending on its tool policy, the supervisor agent may still use normal message tools, including sending a separate Discord message when that is the right action.
While a realtime voice call is running, you can ask the agent to list the available voices or to switch to one. The talk_voice tool swaps the current room's voice and reconnects its realtime provider connections while keeping the Discord connection, conversation, and in-flight agent work intact. The agent confirms once the replacement is ready. Should the switch fail, OpenClaw retains or restores the prior voice. If restoration fails too, it asks you to join voice again; accepted agent work is still able to finish.
The selection covers subsequent speakers in that room and holds until the room disconnects; voice.realtime.speakerVoice stays the default for future calls. An owner-authorized voice turn, agent delegation, and access to the talk_voice tool are required for this.
Changing voices mid-call needs a provider voice catalog. Google Live supports the same talk_voice flow: OpenClaw opens a fresh connection using the selected voice and keeps unfinished agent work attached to the Discord call. Answers queued for sending follow the replacement. A response already submitted to the previous connection may be interrupted by the switch and is not replayed, which avoids duplicate speech.
Whenever a delegated OpenClaw run is active, command-authorized Discord conversation transcripts are handled as live run control before any new agent turn starts. Utterances like "status", "cancel that", "use the smaller fix", or "when you're done also check tests" get classified as status, cancel, steering, or follow-up input for the running session. Status, cancel, accepted steering, and follow-up outcomes are spoken back into the voice channel, so the caller learns whether OpenClaw handled the request.
When OpenClaw cancels a delegated consult, Discord logs a cancellation rather than a failure and skips the generic error fallback. Late provider tool calls that match get the same terminal cancellation instead of restarting the work. The voice session stays available for the next request; timeouts and genuine failures keep their usual error handling.
Useful target forms:
target: "channel:123456789012345678"routes through a Discord text channel session.target: "123456789012345678"is handled as a channel target.target: "dm:123456789012345678"ortarget: "user:123456789012345678"routes through that direct-message session.
Echo-heavy OpenAI Realtime example:
{
channels: {
discord: {
voice: {
enabled: true,
mode: "bidi",
model: "openai/gpt-6-astra",
realtime: {
provider: "openai",
model: "gpt-realtime-2.1",
speakerVoice: "cedar",
bargeIn: true,
minBargeInAudioEndMs: 500,
consultPolicy: "always",
providers: {
openai: {
interruptResponseOnInputAudio: false,
},
},
},
},
},
},
}
Reach for this when the model picks up its own Discord playback through an open mic but you still want to interrupt it by speaking. OpenClaw stops OpenAI from auto-interrupting on raw input audio, while bargeIn: true lets Discord speaker-start events and already-active speaker audio cancel active realtime responses before the next captured turn reaches OpenAI. Very early barge-in signals whose audioEndMs is below minBargeInAudioEndMs are treated as probable echo or noise and ignored, so the model does not cut off on the first playback frame.
Expected voice logs:
- On join:
discord voice: joining ... voiceSession=... supervisorSession=... agentSessionMode=... voiceModel=... realtimeModel=... - On realtime start:
discord voice: realtime bridge starting ... autoRespond=false interruptResponse=false bargeIn=false minBargeInAudioEndMs=... - On speaker audio:
discord voice: realtime speaker turn opened ...,discord voice: realtime input audio started ... outputAudioMs=... outputActive=..., anddiscord voice: realtime speaker turn closed ... chunks=... discordBytes=... realtimeBytes=... interruptedPlayback=... - On skipped stale speech:
discord voice: realtime forced agent consult skipped reason=incomplete-transcript ...orreason=non-actionable-closing ... - On realtime response completion:
discord voice: realtime audio playback finishing reason=completed ... audioMs=... chunks=...; buffered audio may still be playing once this line is logged. - On ordinary playback backpressure:
discord voice: realtime audio playback buffering ... bufferedBytes=...; playback resumes when Discord drains the buffered audio. - Discord acknowledges scoped provider playback marks once their PCM has been consumed. xAI waits for those acknowledgments before it starts a following response; audio that is cleared and discarded is not reported as played.
- On playback stop/reset:
discord voice: realtime audio playback stopped reason=... audioMs=... elapsedMs=... chunks=... - On realtime consult:
discord voice: realtime consult requested ... voiceSession=... supervisorSession=... question=... - On agent answer:
discord voice: agent turn answer ... - On queued exact speech:
discord voice: realtime exact speech queued ... queued=... outputAudioMs=... outputActive=..., followed bydiscord voice: realtime exact speech dequeued reason=player-idle ... - On barge-in detection:
discord voice: realtime barge-in detected source=speaker-start ...ordiscord voice: realtime barge-in detected source=active-speaker-audio ..., followed bydiscord voice: realtime barge-in requested reason=... outputAudioMs=... outputActive=... - On realtime interruption:
discord voice: realtime model interrupt requested client:response.cancel reason=barge-in, followed by eitherdiscord voice: realtime model audio truncated client:conversation.item.truncate reason=barge-in audioEndMs=...ordiscord voice: realtime model interrupt confirmed server:response.done status=cancelled ... - On ignored echo/noise:
discord voice: realtime model interrupt ignored client:conversation.item.truncate.skipped reason=barge-in audioEndMs=0 minAudioEndMs=250 - On disabled barge-in:
discord voice: capture ignored: ... reason=protected playback - On idle playback:
discord voice: realtime barge-in ignored reason=... outputActive=false ... playbackChunks=0
When audio gets cut off, read the realtime voice logs as a timeline:
realtime audio playback startedtells you Discord has started playing assistant audio. From this point the bridge begins counting assistant output chunks, Discord PCM bytes, provider realtime bytes, and synthesized audio duration.realtime speaker turn openedrecords that a Discord speaker has become active. When playback is already running andbargeInis enabled,barge-in detected source=speaker-startcan follow.realtime input audio startedrecords the first real audio frame that arrives for that speaker turn. A nonzerooutputAudioMsoroutputActive=trueat this spot indicates the mic is delivering input while assistant playback is still running.barge-in detected source=active-speaker-audioindicates OpenClaw observed live speaker audio during active assistant playback. That helps you tell a genuine interruption apart from a Discord speaker-start event that carried no useful audio.barge-in requested reason=...indicates OpenClaw requested that the realtime provider cancel or truncate the active response. Generated audio is described byoutputAudioMsandplaybackChunks; theaudioEndMstruncation field that follows records consumed audio per native item. Local consumption is measured by Discord from Opus packets prepared by AudioPlayer at 20 ms granularity; playout at remote listeners cannot be measured.realtime audio playback stopped reason=...is where local Discord playback resets.player-idleindicates Discord has finished consuming the audio; providerresponse.doneand encoder completion on their own do not mean playback is done. Other reasons includebarge-in,provider-clear-audio,forced-agent-consult,stream-close,output-audio-overflow, andsession-close.realtime speaker turn closedsummarizes the captured input turn.chunks=0orhasAudio=falseindicates the speaker turn opened but no usable audio ever reached the realtime bridge.interruptedPlayback=trueindicates that input turn overlapped assistant output and set off barge-in logic.
Fields worth checking:
outputAudioMs: duration of assistant audio the realtime provider generated before this log line.audioMs: duration of assistant audio OpenClaw had counted when playback stopped.elapsedMs: wall-clock time elapsed between opening and closing the playback stream or speaker turn.discordBytes: 48 kHz stereo PCM bytes sent to or received from Discord voice.realtimeBytes: PCM bytes in provider format sent to or received from the realtime provider.playbackChunks: assistant audio chunks forwarded to Discord for the active response.sinceLastAudioMs: gap between the last captured speaker audio frame and the closing of the speaker turn.
Common patterns:
- A sharp stop while
source=active-speaker-audiois active,outputAudioMsis small, and the same user is close by typically means speaker output is leaking into the mic. Turnvoice.realtime.minBargeInAudioEndMsup, reduce the speaker level, switch to headphones, or configurevoice.realtime.providers.openai.interruptResponseOnInputAudio: false. - When
source=speaker-startappears andspeaker turn closed ... hasAudio=falsefollows, Discord signalled a speaker start but no audio arrived at OpenClaw. Possible causes: a transient Discord voice event, noise gate behaviour, or a client briefly keying the mic. audio playback stopped reason=output-audio-overflowindicates that ongoing delivery trouble went past the bounded pending-audio queue. Inspect the relatedDiscord realtime audio playback overflowerror plus earlier provider or Discord connection diagnostics; normal playback backpressure should not trigger this error.Discord voice receive backlog exceededindicates that identity lookup, decoding, or the local disk fell behind the incoming audio. OpenClaw caps every receive stream at 1,000 pending packets and 1 MiB of encoded audio; speak again once the bottleneck clears. Other speakers stay active, as does any registered capture, and partial speech cannot turn into a new conversation command.- A conversation limit was hit when you see
conversation audio backlog exceeded,conversation is busy, orconversation transcript limit exceeded. Recording keeps going; either wait for pending requests to complete or repeat a shorter, complete request. Discord voice recording backlog exceededindicates that pending WAV files hit the recording queue's chunk or byte limit. The utterance in question stops, and the registered capture becomes available again once pending audio processing catches up.audio playback stopped reason=stream-closewith no nearby barge-in orprovider-clear-audiomeans the local Discord playback stream stopped unexpectedly. Review the preceding provider and Discord player logs.capture ignored: ... reason=protected playbackmeans OpenClaw deliberately held back conversational input while assistant audio was playing. Speech is still recorded by an explicitly started capture. Turn onvoice.realtime.bargeInif you want speech to interrupt playback.barge-in ignored ... outputActive=falsemeans Discord or provider VAD detected speech, yet OpenClaw had no active playback to interrupt. Audio should not be cut off in this case.
Credentials are resolved per component: voice.model uses LLM route auth, tools.media.audio uses STT auth, tts/voice.tts use TTS auth, and voice.realtime.providers or the provider's normal auth config supplies realtime provider auth.