Puppet not re-joining its assigned channel after a mod force-move #62

Closed
opened 2026-07-06 17:42:12 +00:00 by robocub · 1 comment
Collaborator

Bug (known gap, surfaced by the #38 test-framework design)

When a guild moderator force-moves a bridge puppet bot to a different voice channel, the puppet stays there — it never re-joins its assigned channel. The bridged Matrix speaker keeps "speaking" into the wrong channel (or effectively goes silent for the bridged call) until the puppet is next re-allocated.

Why

  • The puppet pool ignores the bot's own voice_state_update: crates/discord/src/puppet.rs:1982 early-returns on is_bridge_bot(user_id), so an externally-caused channel change is invisible to the allocator.
  • The #46 DriverDisconnect recovery (epoch-checked rejoin + sweep_voice_health backstop) does not cover this case: a move keeps the voice connection alive, so no DriverDisconnect fires and the health sweep sees a healthy (but misplaced) connection.

A mod disconnect of a puppet, by contrast, IS recovered since #46 — that's the adjacent case and it works.

Expected behavior

Watch the puppet's own voice_state_update, compare the actual channel against the assigned one, and re-issue a join to the assigned channel (epoch-guarded, same shape as the disconnect recovery).

Test-framework linkage

This is scenario 10, force_move_no_rejoin in the catalogue (docs/nether-voicebridge-testing-design.md): the live E2E suite encodes the expected behavior and marks it KnownFail referencing this issue. Per the framework's locked policy, the fix happens here — never inside test work. When this is fixed, the scenario flips to a must-pass regression guard (the suite reports "unexpectedly fixed" until the expectation is updated).

## Bug (known gap, surfaced by the #38 test-framework design) When a guild moderator **force-moves** a bridge puppet bot to a different voice channel, the puppet stays there — it never re-joins its assigned channel. The bridged Matrix speaker keeps "speaking" into the wrong channel (or effectively goes silent for the bridged call) until the puppet is next re-allocated. ### Why - The puppet pool ignores the bot's **own** `voice_state_update`: `crates/discord/src/puppet.rs:1982` early-returns on `is_bridge_bot(user_id)`, so an externally-caused channel change is invisible to the allocator. - The #46 `DriverDisconnect` recovery (epoch-checked rejoin + `sweep_voice_health` backstop) does **not** cover this case: a move keeps the voice connection alive, so no `DriverDisconnect` fires and the health sweep sees a healthy (but misplaced) connection. A mod **disconnect** of a puppet, by contrast, IS recovered since #46 — that's the adjacent case and it works. ### Expected behavior Watch the puppet's own `voice_state_update`, compare the actual channel against the assigned one, and re-issue a join to the assigned channel (epoch-guarded, same shape as the disconnect recovery). ### Test-framework linkage This is **scenario 10, `force_move_no_rejoin`** in the catalogue (`docs/nether-voicebridge-testing-design.md`): the live E2E suite encodes the expected behavior and marks it `KnownFail` referencing this issue. Per the framework's locked policy, the fix happens here — never inside test work. When this is fixed, the scenario flips to a must-pass regression guard (the suite reports "unexpectedly fixed" until the expectation is updated).
Author
Collaborator

Fixed: merged to master 5f7587d (rides v0.3.0-alpha.11).

As filed: the pool discarded its own bots' voice_state_updates (echo-exclusion), and neither the #46 disconnect recovery (no DriverDisconnect fires on a move) nor the health sweep (current_channel().is_none() — a moved bot is still in a channel) covered a moderator force-move.

Fix, event + sweep, mirroring #46's shape:

  • The pool now maps bot user-id → slot (token-decode seeded, ready-confirmed). A bot seeing its OWN voice-state event forwards a VerifyConnected probe to its command loop, delayed 1 s so songbird's view settles. The probe carries no channel snapshot — it re-evaluates songbird's current state at processing time, so probes queued behind an in-flight recovery are no-ops (an earlier snapshot design replayed stale displacement and double-rejoined; caught live).
  • The probe's check is now voice_displaced: out of voice entirely OR connected to a channel other than the assigned GuildConn.chan.channel_id. The periodic sweep gets the same check as backstop.
  • rejoin_puppet lets the gateway leave settle (1.5 s) when the connection was LIVE before rejoining — a live connection's leave echo used to cancel the immediate rejoin ("request was cancelled/dropped"), needlessly quarantining the bot and failing over to a different one (caught live). The dead-connection recovery keeps its proven full-speed timing.

Live-verified on staging 2026-07-07: three force-moves, each recovered by the SAME bot back in its assigned channel in ~3 s (1 s probe delay + 1.5 s settle + join), zero quarantines, zero failovers, zero errors. 236 tests + clippy green.

**Fixed: merged to master `5f7587d` (rides v0.3.0-alpha.11).** As filed: the pool discarded its own bots' `voice_state_update`s (echo-exclusion), and neither the #46 disconnect recovery (no `DriverDisconnect` fires on a move) nor the health sweep (`current_channel().is_none()` — a moved bot is still in *a* channel) covered a moderator force-move. Fix, event + sweep, mirroring #46's shape: - The pool now maps bot user-id → slot (token-decode seeded, `ready`-confirmed). A bot seeing its OWN voice-state event forwards a `VerifyConnected` probe to its command loop, delayed 1 s so songbird's view settles. The probe carries **no channel snapshot** — it re-evaluates songbird's current state at processing time, so probes queued behind an in-flight recovery are no-ops (an earlier snapshot design replayed stale displacement and double-rejoined; caught live). - The probe's check is now `voice_displaced`: out of voice entirely OR connected to a channel other than the assigned `GuildConn.chan.channel_id`. The periodic sweep gets the same check as backstop. - `rejoin_puppet` lets the gateway leave settle (1.5 s) when the connection was LIVE before rejoining — a live connection's leave echo used to cancel the immediate rejoin ("request was cancelled/dropped"), needlessly quarantining the bot and failing over to a different one (caught live). The dead-connection recovery keeps its proven full-speed timing. **Live-verified on staging 2026-07-07**: three force-moves, each recovered by the SAME bot back in its assigned channel in ~3 s (1 s probe delay + 1.5 s settle + join), zero quarantines, zero failovers, zero errors. 236 tests + clippy green.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
dark/nether-voicebridge#62
No description provided.