Reaped phantom m.call.member never releases the Discord puppet #54
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
dark/nether-voicebridge#54
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
A Matrix user closes their client without leaving the call (unclean disconnect). The #25 reaper correctly empties their phantom
m.call.memberafter the 45s grace — but their Discord puppet stays in the voice channel indefinitely (until a bridge restart, or until the user rejoins and leaves cleanly).Live occurrence (staging, 2026-07-05, Purple Voice):
Root cause
The reaper's retraction is a state event sent by the bridge bot on the phantom's state_key (
send_state_event_for_key, permitted because the modern_@user_DEVICEkey is not owner-restricted). When that event echoes back through sync, the membership handler keys the participant onorig.sender— which is now the bot, not the phantom's owner:crates/matrix-rtc/src/matrix.rssync handler:let user_id = orig.sender.clone();apply_member_eventthen returns at the bot echo-exclusion (if &user_id == bot_user_id { return; }) before touching the participant set.So the
(@user, state_key)entry is never removed, the 12sLEAVE_GRACErelease path never starts, androuter.release()is never called. Room state and Discord-side presence silently diverge.Note the
ParticipantSetdoc comment already states the intent: state_key is "the unique handle … This lets us remove exactly the right entry when an Empty event arrives for a state_key, without needing to parse the state_key format" — the code just never uses that for foreign-sender retractions.Fix sketch
In
apply_member_event:is_active == false), resolve the affected user by looking up the matchingstate_keyin the participant set and using that entry's owner instead of the sender; active events keep sender keying.retract_stale_self_membershipand ghost teardowns have no set entry → resolve to sender → still excluded; a normal user leave has owner == sender). Bonus: any third-party retraction (e.g. a moderator emptying someone's membership) now also releases the puppet.Workaround
Restart the bridge, or have the affected user rejoin the call and leave cleanly (their own Empty event releases the puppet normally).
Related: #25 (reaper), constraint #5 echo-exclusion (which is exactly what eats the retraction event).
Fixed in
515db4b(released in v0.3.0-alpha.6): retractions are now attributed to the membership's owner via a state_key lookup in the participant set (resolve_affected_user), so the reaper's bot-sent retraction flows through the normal 12s leave grace and releases the puppet.Live-verified 2026-07-05 02:10 UTC on staging: force-killed Element mid-call →
reaped phantom m.call.member(45s reaper grace)membership gone — starting leave grace timer← the fix (never fired before)leave grace elapsed — releasing puppet+ puppet left the Discord channel~75s from kill to release. Unit tests cover owner resolution (foreign retraction → owner, active event → sender, unknown state_key → sender fallback).