ADR 0045: Official-bot group voice uses the maintained Discord media stack
Status: accepted (2026-07-25). Current-status addendum (2026-08-19):
ADR 0128 supersedes this ADR's
@discordjs/voice media-owner, Node Opus readiness, and split-stack rollout
decisions. Consent, Discord attribution, DAVE-positive join, allowlists, floor,
memory-only PCM, and live-evidence requirements remain authoritative here.
ADR 0057 defines the realtime
speech path. The rationale and diagram below are retained as historical.
Context
Clankie needs an official Discord bot that can participate in a multi-person
voice channel with speaker attribution, transcription, the same pi captain
identity, governed person memory, and spoken responses. ADR 0025 selected a
selective import of the v1 Rust ClankVox media owner. ADR 0100 records that
source's AGPL-3.0-or-later disposition as an isolated native package inside
this otherwise Apache-2.0 repository.
The maintained @discordjs/voice@0.19.2 package already owns the official-bot
voice WebSocket, UDP, RTP/Opus, transport encryption, receive subscriptions,
and DAVE session used by the bridge. It is Apache-2.0, exposes each received
speaker as a Discord user id, and defaults DAVE encryption on. A second media
implementation is unnecessary for the bounded group-voice outcome.
Decision
The official-bot group-voice session lives in apps/discord-bridge and has one
media owner: @discordjs/voice. This decision supersedes ADR 0025's ClankVox
placement and direct guild.voiceAdapterCreator plan for official-bot voice.
The AGPL Vox source now lives in the explicit mixed-license apps/vox package
under ADR 0100, but it is not executed for official-bot voice.

Consent and privacy
DISCORD_VOICE_ENABLED=trueand an explicit guild allowlist enable the capability. It is off by default.DISCORD_VOICE_CHANNEL_IDSis optional refinement, not a second gate. Empty admits every voice channel inside an allowlisted guild; listing channels narrows it further. The guild allowlist is never skipped, so voice reach is always bounded to servers the owner chose, and joining still requires the voice presence check on/clankie joinplus per-participant consent.DISCORD_INGRESS_CHANNEL_IDSfollows the same rule, so one mental model covers both planes: the guild allowlist bounds reach, the channel list refines it. Text has no per-speaker consent gate, so every message in those admitted channels reaches the captain by default; he still decides whether to reply. The explicitaddressedresource policy narrows that spend when wanted./clankie joinis gated by the voice presence tier (ADR 0050), joins the invoker's allowlisted channel, discloses DAVE, the selected realtime provider's data handling and AI-generated speech, and opts in only the invoker.- Under the default
explicitpolicy every other human uses/clankie voice-consent opt-in. ADR 0071 also permits an owner-selectedpresencepolicy for disclosed private rooms; explicit opt-out always wins. Leaving the channel, bot leave, emergency shutdown, or process restart revokes ephemeral consent. - The receiver subscribes only to consented Discord user ids, so unconsented audio never reaches the realtime input buffer. Local raw and generated PCM buffers are memory-only and zeroed after use. The join disclosure names the configured provider and its data handling (ADR 0057). Voice receipts reject transcript, response, prompt, audio, and PCM fields. The separate, owner-enabled private development transcript in ADR 0121 may retain exact consented text; raw audio remains memory-only.
- Spoken input remains ambient authority and cannot approve privileged work. Approval-shaped results become a generic authenticated-surface handoff.
Conversation, overlap, and memory
Conversation is answered by the engaged realtime session; anything that
touches the world goes through its single ask_clankie tool to the continuing
discord_voice pi session for the active guild/channel
(ADR 0057). The Clankie service,
not the bridge request, resolves approved person-memory facts for consented
guild/user identities and supplies the bounded briefing projection. Voice can
use approved person memory but cannot mutate it. The realtime model has no
direct memory writer; when it chooses to keep part of a conversation as its own
experience, ask_clankie lets the captain write a self-authored episode. Exact
final speech is persisted only when the owner enables ADR 0121's separate
development transcript log.
Concurrent speakers retain separate Opus subscriptions and separate transcription sessions, then converge as gateway-attributed text in one shared floor and engaged conversation. Overlap emits content-free evidence. Barge-in is deliberate: the floor holder speaking over him, or a re-address, truncates playback, while crosstalk between other people lets him finish. The utterance's gateway user id and delivery id remain immutable through captain handoff and response evidence, even if another participant takes the floor meanwhile. Captain handoffs and playback remain serialized so two responses do not talk over one another.
Credentials and evidence
The official discord_bot token and the existing openai API key are read
from the credential broker; environment copies are startup errors. The control
plane mints distinct clankie_discord_bridge and
clankie_discord_voice_bridge bearers. The latter authenticates only the
discord_voice captain source.
Readiness checks the brokered identities, OpenAI speech configuration, native Opus, allowlists, live bot/application/guild identity, and Clankie service composition. The live gate requires one positive DAVE protocol, three unique explicit consents, three attributed speakers with captain/TTS round trips, no media failure, and a clean leave.
Options weighed
- Use v1 ClankVox for official-bot voice — rejected for this outcome because the maintained library supplies the required bot media and DAVE paths. Vox remains the separately licensed user-session media owner under ADR 0100.
- Write a new Rust sidecar — rejected because it duplicates an actively maintained protocol implementation without adding a required isolation boundary.
- Normal-user voice or Go Live transport — rejected for official-bot voice. The separately opted-in screen-watch/publish path later shipped in ADR 0098 (user-session shares) and never shares this session.
- Local temporary-file STT/TTS — rejected because the existing brokered speech provider supports a bounded memory-only path and avoids raw-audio filesystem retention.
Consequences
@discordjs/opusis an approved native build dependency and must load in readiness.- A live Discord server and three consenting humans remain necessary to call the group-voice row complete; deterministic tests cannot substitute for it.
- Discord screen watch/publish uses the isolated user-session Vox media owner from ADR 0100. This ADR grants no normal-user token capability.
- ADR 0048 reuses this media owner behind the personal-lab user session through a custom gateway adapter. The single-media-owner decision constrains the media stack, not which credential opened the gateway, so DAVE, consent, per-speaker capture, and the local memory-only audio discipline hold identically on both bodies.
