discord_radio: presence follows voice; name why a voice join fails #38

Merged
logan merged 2 commits from fix/discord-voice-join into main 2026-10-04 22:52:31 -04:00
Owner

Live symptom (prod fabdf58, nodes on v1 = 44e41b3): the radio bot comes online in Discord with the right presence but never joins the voice channel.

What the investigation found

  • Presence lied. On v1 the edge sets "Listening to " only after channel.connect() succeeds, then never clears it. So that presence means the bot did join at least once and voice later dropped or keeps failing to rejoin, with nothing visible in Discord. It now follows voice on every reconcile pass.
  • The voice-failure reason was unreadable. Discord silently ignores a join the bot has no permission for. discord.py then times out after 30 s and the admin page showed Could not connect to the voice channel (TimeoutError: ). The edge now checks View Channel, Connect and Speak and the user limit before joining, and reports missing_permissions or channel_full naming the missing permission. Timeouts get a code of their own, connect_timeout, with readable text.
  • DAVE (hypothesis a) is not it for current images. Discord has required E2EE (DAVE) voice since 2026-03-01. discord.py 2.7.0+ supports it through davey, which the [voice] extra pulls in. uv pip compile of this requirements.txt for py3.14 on aarch64 and x86_64 resolves discord.py 2.7.1 + davey 0.1.6 + PyNaCl 1.5.0. The Dockerfile has ffmpeg, libopus0 and libpulse0. Without davey, discord.py raises RuntimeError: davey library needed in order to use voice, and the admin page would show that text. >=2.7 is now a floor.
  • Desired state not reaching the edge (b): ruled out for the edge's bot. The edge logs a radio bot in only when a desired stream names its token. It sets presence only after a successful join.
  • Guild/channel lookup (c): already reported as not_in_guild / bad_channel. Neither error is swallowed (d).

Not covered here

The root cause on live is still unconfirmed. Tracked in the linked issue, along with the reading that tells the cases apart.

Tests

pytest (drb-edge-node): 303 passed, 12 skipped. 4 new tests, all failing on v1 and passing here. flake8 --max-line-length=120: touched files clean. The one violation left is the existing tests/test_config_manager.py:6:1: F401 (DEFERRED.md).

🤖 Generated with Claude Code

Live symptom (prod fabdf58, nodes on v1 = 44e41b3): the radio bot comes online in Discord with the right presence but never joins the voice channel. ## What the investigation found - **Presence lied.** On v1 the edge sets "Listening to <system>" only after `channel.connect()` succeeds, then never clears it. So that presence means the bot *did* join at least once and voice later dropped or keeps failing to rejoin, with nothing visible in Discord. It now follows voice on every reconcile pass. - **The voice-failure reason was unreadable.** Discord silently ignores a join the bot has no permission for. discord.py then times out after 30 s and the admin page showed `Could not connect to the voice channel (TimeoutError: )`. The edge now checks View Channel, Connect and Speak and the user limit before joining, and reports `missing_permissions` or `channel_full` naming the missing permission. Timeouts get a code of their own, `connect_timeout`, with readable text. - **DAVE (hypothesis a) is not it for current images.** Discord has required E2EE (DAVE) voice since 2026-03-01. discord.py 2.7.0+ supports it through `davey`, which the `[voice]` extra pulls in. `uv pip compile` of this requirements.txt for py3.14 on aarch64 and x86_64 resolves discord.py 2.7.1 + davey 0.1.6 + PyNaCl 1.5.0. The Dockerfile has ffmpeg, libopus0 and libpulse0. Without davey, discord.py raises `RuntimeError: davey library needed in order to use voice`, and the admin page would show that text. `>=2.7` is now a floor. - **Desired state not reaching the edge (b): ruled out *for the edge's bot*.** The edge logs a radio bot in only when a desired stream names its token. It sets presence only after a successful join. - **Guild/channel lookup (c): already reported** as `not_in_guild` / `bad_channel`. Neither error is swallowed (d). ## Not covered here The root cause on live is still unconfirmed. Tracked in the linked issue, along with the reading that tells the cases apart. ## Tests `pytest` (drb-edge-node): **303 passed, 12 skipped**. 4 new tests, all failing on v1 and passing here. `flake8 --max-line-length=120`: touched files clean. The one violation left is the existing `tests/test_config_manager.py:6:1: F401` (DEFERRED.md). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
logan added 1 commit 2026-10-04 22:43:03 -04:00
discord_radio: presence follows voice; name the reason a voice join fails
CI / lint (push) Successful in 6s
CI / test-op25 (push) Successful in 14s
CI / lint (pull_request) Successful in 7s
CI / test-op25 (pull_request) Successful in 14s
CI / test (push) Successful in 47s
CI / test (pull_request) Successful in 43s
786e06a7a3
Live symptom: the radio bot shows "Listening to <system>" but is not in the
voice channel.

- Presence was set once on the first successful join and never cleared, so a
  bot whose voice dropped (or whose rejoin kept failing) still looked healthy.
  Presence is now synced at the end of every reconcile: set only while a voice
  connection for that bot is up, cleared otherwise.
- Pre-check the bot's permissions (View Channel, Connect, Speak) and the
  channel's user limit before joining. Discord silently ignores a join the bot
  may not make; discord.py then times out after 30s and the admin page read
  "Could not connect to the voice channel (TimeoutError: )". Now: code
  missing_permissions / channel_full with the missing permission named.
- A connect timeout gets its own code (connect_timeout) and readable text.
- requirements: discord.py[voice]>=2.7 — Discord has required DAVE E2EE voice
  since 2026-03-01 and 2.7 is the first discord.py with it (the extra pulls
  davey). Today's resolution was already 2.7.1 + davey 0.1.6; this stops an
  older resolution from ever being accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Root cause still open: #39 (what to read on the admin page after this deploys).

Root cause still open: #39 (what to read on the admin page after this deploys).
logan added 1 commit 2026-10-04 22:50:00 -04:00
discord_radio: don't count the bot's own voice state toward a full channel; timeout text points at the handshake
CI / lint (push) Successful in 8s
CI / lint (pull_request) Successful in 10s
CI / test-op25 (push) Successful in 14s
CI / test-op25 (pull_request) Successful in 16s
CI / test (push) Successful in 48s
CI / test (pull_request) Successful in 47s
2065446393
Permissions are pre-checked now, so a connect timeout is the voice handshake
(pre-2.7 discord.py: DAVE close 4017 retried to the 30s timeout) or UDP egress,
not permissions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
logan merged commit 5770ecae49 into main 2026-10-04 22:52:31 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: logan/node-26#38