Stale m.call.member ('waiting for media') after unclean client disconnect — blocked on homeserver MSC4140 (delayed events) #25
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
dark/nether-voicebridge#25
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 call participant appears stuck as a perpetually-loading tile / "waiting for media from " in Matrix clients (seen in Commet and Element on the Light Voice call), even though they are not transmitting anything.
Diagnosis (live-traced 2026-06-17). The example was
@victor:nether.im— a real Matrix user, not a bridge ghost. From the bridge log + the room'sm.call.memberstate:nwhe0pUtdu) joined LiveKit at 23:26:22, published audio, then disconnected from the SFU at 23:26:32 (remote participant disconnected/TrackUnpublished … Disconnected).m.call.memberwas never retracted — still active 20+ min later,expires: 14400000(4h). So other clients keep him in the roster but receive no track → "waiting for media." It only clears at the 4h expiry or on rejoin.Root cause = no MSC4140 (Delayed Events) on the homeserver. Delayed events let a client arm a server-side "leave" that auto-fires if it stops heartbeating — the crashed/unclean-disconnect cleanup that is missing here. On any unclean exit (crash, killed tab, network drop), the membership lingers ~4h.
This is blocked on the homeserver (Tuwunel), not on a client and not on this bridge:
org.matrix.msc4140flag in/_matrix/client/versions. Its MatrixRTC docs (docs/calls/matrix_rtc.md) and issue tracker have no MSC4140 / delayed-events entry — so it is not on a visible roadmap and not imminent. The conduwuit family tracks it unimplemented (sibling fork continuwuity issue #903). MSC4140 itself is still an unstable MSC (PR #4140, not in the stable spec).UNSTABLE_MSC4140_DELAYED_EVENTS); Commet via the famedly matrix-dart-sdkmsc_4140_delayed_eventslibrary. So we are not waiting on Commet or Element.m.call.memberand should not reap another user's state. The only fix for what humans see is upstream (homeserver delayed events, the 4h expiry, or the owning client rejoining).Lever to move it: if we want this fixed, file a feature request on
matrix-construct/tuwunel(none exists today) and/or wait for Tuwunel to prioritize Element Call membership reliability.Downstream effect on the bridge. Because the bridge keys Discord-puppet presence off
m.call.member(deliberately, to absorb Element Call's normal ~90s membership flapping —LEAVE_GRACEincrates/matrix-rtc/src/matrix.rs), a stale human membership also keeps a zombie[Matrix] <name>Discord puppet alive until expiry. Root cause is still upstream.Bridge-side mitigations available if we ever want them (NOT doing now):
LEAVE_GRACEflap-absorption.expires+ heartbeat refresh — so the bridge's own ghost memberships self-clean faster on a bridge crash without needing delayed events (crates/matrix-rtc/src/rtc/membership.rscurrently defaults to the 4h expiry).Re-check the gate any time:
curl -s https://nether.im/_matrix/client/versions→ look fororg.matrix.msc4140inunstable_features(absent today). When it appears, revisit mitigation #3.Filed from a live investigation; tracking-only — no code change yet.
I wanted to add some extra context to this one too since I'm familiar with MSC4140 - this isn't a bridge-solvable issue.
Element Call utilizes the scheduled events feature to make clients auto-kick themselves in case of disconnect/crash. It's used as a heartbeat.
The client schedules an event in the near future (10 seconds? I forget the exact duration but it's not long). The scheduled event is that the client leaves the call. While the client is active/alive and has a working connection, once every few seconds (2-5) it updates the scheduled event to delay the event ~10 seconds from current time. This continues perpetually as long as the client is alive.
Once the client stops doing this due to network problems or otherwise, the server eventually executes the action and processes that the dead client has left the room.
The server doesn't do any other processing or tracking of participant health... at least on Synapse. I can't speak to Tuwunel or Continuwuity, etc etc... my understanding is these (largely) don't support MSC4140 at all so all livekit calls will be plagued by phantom participants.
I'm not sure I'd try any mitigations for this as on the bridge they will all have down-sides. But of course, this is just an opinion/voice.
Closing as mitigated bridge-side; the real fix stays upstream, as @shadowdrake laid out.
What shipped since this was filed:
m.call.memberentries are reaped, and the #54 fix (v0.3.0-alpha.6) made reaping also release the Discord puppet — live-verified ~75 s from client kill to full release.So a crashed client's tile no longer lingers indefinitely and no longer wedges anything. The proper fix — the client's own MSC4140 delayed-event heartbeat kicking its membership — is Tuwunel's to implement; nothing further is actionable in the bridge. Reopen if a stale tile ever survives the reaper.