Matrix call-end teardown overrides the capture-only-anchor grace — bridge dormant with Discord humans still in channel #59

Closed
opened 2026-07-05 18:03:55 +00:00 by robocub · 1 comment
Collaborator

Matrix call-end teardown overrides the capture-only-anchor grace — bridge goes dormant with Discord humans still in channel

Observed twice on prod (v0.3.0-alpha.7) during the #46 cross-guild test session, 2026-07-05 UTC.

What happens

When the last Matrix call member leaves while Discord humans remain in the bridged channel, the puppet layer decides to keep a capture-only anchor — and then the Matrix call-end teardown releases it anyway, ~0.5–0.7 s later:

16:56:58.328 matrix: leave grace elapsed — releasing puppet mxid=@dark:nether.im
16:56:58.329 puppet: last puppet released but Discord speakers remain — keeping capture-only anchor (grace before release) bridge="Purple Voice" key="@dark:nether.im" idx=0
16:56:58.329 matrix: call ended (after grace)
16:56:58.329 rtc: call ended — disconnecting subscriber
16:56:58.812 puppet: puppet left voice and released idx=0 bridge=Purple Voice

Identical sequence again at 18:02:04.45118:02:05.108 (same bridge, same session — fully reproducible).

At 16:56 the channel still had 2 human members (voicetick ... silent=2 right up to the teardown). The bridge then sat dormant for ~36 minutes with humans in the channel: their idle presence generates no gateway events, so nothing re-activates the bridge until a NEW voice-state event arrives (a user joining at 17:32 re-minted presence ghosts; anchor voice-join additionally required a Matrix user joining the call — by design).

Why it matters

  • The keeping capture-only anchor (grace before release) path appears to be dead code in practice — whatever grace it intends never survives the call ended teardown that runs in the same instant.
  • Open question: do the remaining Discord users' presence ghosts survive the call-end teardown? At re-activation the log shows seeding ghosts for existing voice members count=3, which suggests they had to be rebuilt — i.e. between call-end and the next voice-state event, Discord users idling in the channel may be invisible in the Matrix room (no ghosts posted), so a Matrix user opening the room sees an empty call instead of the waiting Discord participants.
  • If it IS intended that no voice connection persists without a Matrix listener (reasonable — capture with no consumer is pointless), the fix is likely: (a) drop/reword the misleading "keeping capture-only anchor" log + prune the dead path, and (b) ensure presence ghosts persist (or are re-posted) after call-end teardown so the Matrix side still shows who's waiting on Discord.

Where to look

  • crates/discord/src/puppet.rs — the "last puppet released but Discord speakers remain — keeping capture-only anchor" site vs. the release path that wins.
  • The call ended (after grace) / call ended — disconnecting subscriber teardown in crates/matrix-rtc — whether it unconditionally releases the channel's puppets.
  • Related prior art: the presence-ghost/eager-connect work (#17) intended Discord users to be visible with no Matrix user present.
## Matrix call-end teardown overrides the capture-only-anchor grace — bridge goes dormant with Discord humans still in channel Observed twice on prod (v0.3.0-alpha.7) during the #46 cross-guild test session, 2026-07-05 UTC. ### What happens When the **last Matrix call member** leaves while **Discord humans remain in the bridged channel**, the puppet layer decides to keep a capture-only anchor — and then the Matrix call-end teardown releases it anyway, ~0.5–0.7 s later: ``` 16:56:58.328 matrix: leave grace elapsed — releasing puppet mxid=@dark:nether.im 16:56:58.329 puppet: last puppet released but Discord speakers remain — keeping capture-only anchor (grace before release) bridge="Purple Voice" key="@dark:nether.im" idx=0 16:56:58.329 matrix: call ended (after grace) 16:56:58.329 rtc: call ended — disconnecting subscriber 16:56:58.812 puppet: puppet left voice and released idx=0 bridge=Purple Voice ``` Identical sequence again at `18:02:04.451` → `18:02:05.108` (same bridge, same session — fully reproducible). At 16:56 the channel still had 2 human members (`voicetick ... silent=2` right up to the teardown). The bridge then sat **dormant for ~36 minutes with humans in the channel**: their idle presence generates no gateway events, so nothing re-activates the bridge until a NEW voice-state event arrives (a user joining at 17:32 re-minted presence ghosts; anchor voice-join additionally required a Matrix user joining the call — by design). ### Why it matters - The `keeping capture-only anchor (grace before release)` path appears to be dead code in practice — whatever grace it intends never survives the `call ended` teardown that runs in the same instant. - Open question: do the remaining Discord users' **presence ghosts** survive the call-end teardown? At re-activation the log shows `seeding ghosts for existing voice members count=3`, which suggests they had to be rebuilt — i.e. between call-end and the next voice-state event, Discord users idling in the channel may be invisible in the Matrix room (no ghosts posted), so a Matrix user opening the room sees an empty call instead of the waiting Discord participants. - If it IS intended that no voice connection persists without a Matrix listener (reasonable — capture with no consumer is pointless), the fix is likely: (a) drop/reword the misleading "keeping capture-only anchor" log + prune the dead path, and (b) ensure presence ghosts persist (or are re-posted) after call-end teardown so the Matrix side still shows who's waiting on Discord. ### Where to look - `crates/discord/src/puppet.rs` — the "last puppet released but Discord speakers remain — keeping capture-only anchor" site vs. the release path that wins. - The `call ended (after grace)` / `call ended — disconnecting subscriber` teardown in `crates/matrix-rtc` — whether it unconditionally releases the channel's puppets. - Related prior art: the presence-ghost/eager-connect work (#17) intended Discord users to be visible with no Matrix user present.
Author
Collaborator

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

Root cause confirmed exactly as filed: the pool's release correctly kept a capture-only anchor when Discord speakers remained, but the call-end teardown (call_active=falseteardown(release_routes=true)release_all) freed it unconditionally ~0.5 s later — release_all_locked never consulted speakers_present and explicitly released the capture-only-held anchor (its "return to baseline" branch).

Fix (keep-anchor design): release_all_locked gained a keep_capture_anchor mode — the call-end path now mirrors release's keep semantics (anchor demoted to capture-only, playback identity cleared, ANCHOR_IDLE_GRACE armed), while unregister_channel (process/tenant drain) still drains unconditionally, so teardown-to-baseline is preserved where it matters. A Matrix user returning within the 90 s grace reuses the live session instantly (no join latency, no SSRC-map reset). The idling anchor wears a configurable standby label — [puppets] standby_nickname, default "[Matrix: Standby]", a whole nickname rather than the prefix path — so it doesn't read as a participant named after the bare prefix.

Live-verified on staging 2026-07-07: the exact prod sequence (leave grace → keep decision → call-end 34 ms later) now logs "call ended but Discord speakers remain — keeping capture-only anchor" instead of releasing; EC rejoin within grace reused the anchor with instant audio; grace expiry released to baseline at +90 s on the dot; standby label confirmed visually. Unit tests cover keep-on-call-end, demote-active-anchor-free-the-rest, drain-without-speakers, and the unregister path (236 tests + clippy green).

Note: the open question about presence ghosts between call-end and the next voice event was NOT addressed here (out of scope); it remains observable-but-unfixed if it reproduces.

**Fixed: merged to master `5f7587d` (rides v0.3.0-alpha.11).** Root cause confirmed exactly as filed: the pool's `release` correctly kept a capture-only anchor when Discord speakers remained, but the call-end teardown (`call_active=false` → `teardown(release_routes=true)` → `release_all`) freed it unconditionally ~0.5 s later — `release_all_locked` never consulted `speakers_present` and explicitly released the capture-only-held anchor (its "return to baseline" branch). Fix (keep-anchor design): `release_all_locked` gained a `keep_capture_anchor` mode — the call-end path now mirrors `release`'s keep semantics (anchor demoted to capture-only, playback identity cleared, `ANCHOR_IDLE_GRACE` armed), while `unregister_channel` (process/tenant drain) still drains unconditionally, so teardown-to-baseline is preserved where it matters. A Matrix user returning within the 90 s grace reuses the live session instantly (no join latency, no SSRC-map reset). The idling anchor wears a configurable standby label — `[puppets] standby_nickname`, default `"[Matrix: Standby]"`, a whole nickname rather than the prefix path — so it doesn't read as a participant named after the bare prefix. **Live-verified on staging 2026-07-07**: the exact prod sequence (leave grace → keep decision → call-end 34 ms later) now logs "call ended but Discord speakers remain — keeping capture-only anchor" instead of releasing; EC rejoin within grace reused the anchor with instant audio; grace expiry released to baseline at +90 s on the dot; standby label confirmed visually. Unit tests cover keep-on-call-end, demote-active-anchor-free-the-rest, drain-without-speakers, and the unregister path (236 tests + clippy green). Note: the open question about presence ghosts between call-end and the next voice event was NOT addressed here (out of scope); it remains observable-but-unfixed if it reproduces.
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#59
No description provided.