lotusDeafen implements deafen by calling RemoteParticipant.setVolume(0, Track.Source.Microphone). Upstream EC uses the same API for its per-participant volume slider: createVolumeControls subscribes combineLatest([sink$, playbackVolume$]) and calls p.setVolume(volume) (default source = Microphone) every time either input emits. sink$ is derived from participant$, so it re-emits whenever a member's RemoteParticipant object appears or is replaced — i.e. on every join, and on participant re-resolution after a reconnect. The default playbackVolume$ is 1, so EC writes setVolume(1)after lotusDeafen wrote 0, and the LiveKit volumeMap keeps the last writer. Deafen therefore does not hold for anyone who joins (or reconnects) while you are deafened — exactly the case the module's docstring claims to cover via ParticipantConnected.
Second symptom (undeafen):applyToParticipant unconditionally writes 1 when not deafened, for both Microphone and ScreenShareAudio. EC exposes a per-participant volume slider and a per-tile "mute this participant" toggle backed by VolumeControls (playbackVolume$, togglePlaybackMuted). A deafen/undeafen cycle silently resets all of those to full volume, while playbackVolume$ — and therefore the slider position and the muted icon in GridTile/SpotlightTile — still shows the old value. The UI and the actual audio diverge with no way to resync short of nudging the slider.
Third symptom (first joiner audible while deafened): The module keys its RoomEvent.ParticipantConnected listeners off vm.livekitRoomItems$, which only surfaces a room once at least one matrix-validated, non-local member has a resolved LiveKit participant (CallViewModel.ts:648-654 returns null for local/unresolved). While you are alone in the call, rooms is empty and no listener is attached, so the very first joiner is covered only by the applyToRoom(room) call that runs on the subsequent stream emit — which lands after matrix membership resolution, i.e. potentially after their audio track has already subscribed and started playing through MatrixAudioRenderer. The sibling lotus modules (lotusDenoise.ts:116, lotusQuality.ts:87, lotusAudioInject.ts:46) all explicitly avoid livekitRoomItems$ for this reason; deafen is the one that still uses it.
How to trigger
Deafen in cinny, then have a second user join the call (or force a LiveKit reconnect for an existing remote). Their voice becomes audible while the cinny UI still shows deafened.
Also: Turn a participant down to 20% (or mute them) on their tile, then deafen and undeafen. They come back at 100% with the tile still showing 20% / muted.
Also: Deafen while alone in a call, then have someone join and speak immediately. A short burst of their audio is audible.
Suggested fix
Stop fighting setVolume; drive EC's existing global mute instead — setAudioEnabled$.next(false) feeds muteAllAudio$ (src/state/MuteAllAudioModel.ts:17), which InCallView already passes as muted to every LivekitRoomAudioRenderer plus the event/reaction renderers. That also covers Track.Source.Unknown soundboard audio (the known P6-2 divergence) without any setVolume conflict.
Also: Same as above — implement deafen as a global output mute rather than per-participant volume writes, so EC's per-participant state is left untouched.
Also: Switch to vm.allConnections$ like the sibling modules so the room (and its ParticipantConnected listener) exists from connect time; the global-mute fix above removes the problem entirely.
Filed from the September 2026 audit (branch lotus).
**Severity:** high · **Type:** bug · **Confidence:** high
**Location:** `src/lotus/lotusDeafen.ts:51-56`, `src/state/media/RemoteUserMediaViewModel.ts:53`, `src/state/VolumeControls.ts:80-90`
`src/lotus/lotusDeafen.ts:51-56`
`src/lotus/lotusDeafen.ts:75-95`, `src/state/CallViewModel/CallViewModel.ts:643-684`
### Problem
`lotusDeafen` implements deafen by calling `RemoteParticipant.setVolume(0, Track.Source.Microphone)`. Upstream EC uses the *same* API for its per-participant volume slider: `createVolumeControls` subscribes `combineLatest([sink$, playbackVolume$])` and calls `p.setVolume(volume)` (default source = `Microphone`) every time either input emits. `sink$` is derived from `participant$`, so it re-emits whenever a member's `RemoteParticipant` object appears or is replaced — i.e. on every join, and on participant re-resolution after a reconnect. The default `playbackVolume$` is `1`, so EC writes `setVolume(1)` *after* lotusDeafen wrote `0`, and the LiveKit `volumeMap` keeps the last writer. Deafen therefore does not hold for anyone who joins (or reconnects) while you are deafened — exactly the case the module's docstring claims to cover via `ParticipantConnected`.
**Second symptom (undeafen):** `applyToParticipant` unconditionally writes `1` when not deafened, for both `Microphone` and `ScreenShareAudio`. EC exposes a per-participant volume slider and a per-tile "mute this participant" toggle backed by `VolumeControls` (`playbackVolume$`, `togglePlaybackMuted`). A deafen/undeafen cycle silently resets all of those to full volume, while `playbackVolume$` — and therefore the slider position and the muted icon in `GridTile`/`SpotlightTile` — still shows the old value. The UI and the actual audio diverge with no way to resync short of nudging the slider.
**Third symptom (first joiner audible while deafened):** The module keys its `RoomEvent.ParticipantConnected` listeners off `vm.livekitRoomItems$`, which only surfaces a room once at least one *matrix-validated, non-local* member has a resolved LiveKit participant (`CallViewModel.ts:648-654` returns `null` for local/unresolved). While you are alone in the call, `rooms` is empty and no listener is attached, so the very first joiner is covered only by the `applyToRoom(room)` call that runs on the subsequent stream emit — which lands after matrix membership resolution, i.e. potentially after their audio track has already subscribed and started playing through `MatrixAudioRenderer`. The sibling lotus modules (`lotusDenoise.ts:116`, `lotusQuality.ts:87`, `lotusAudioInject.ts:46`) all explicitly avoid `livekitRoomItems$` for this reason; deafen is the one that still uses it.
### How to trigger
Deafen in cinny, then have a second user join the call (or force a LiveKit reconnect for an existing remote). Their voice becomes audible while the cinny UI still shows deafened.
Also: Turn a participant down to 20% (or mute them) on their tile, then deafen and undeafen. They come back at 100% with the tile still showing 20% / muted.
Also: Deafen while alone in a call, then have someone join and speak immediately. A short burst of their audio is audible.
### Suggested fix
Stop fighting `setVolume`; drive EC's existing global mute instead — `setAudioEnabled$.next(false)` feeds `muteAllAudio$` (`src/state/MuteAllAudioModel.ts:17`), which `InCallView` already passes as `muted` to every `LivekitRoomAudioRenderer` plus the event/reaction renderers. That also covers `Track.Source.Unknown` soundboard audio (the known P6-2 divergence) without any `setVolume` conflict.
Also: Same as above — implement deafen as a global output mute rather than per-participant volume writes, so EC's per-participant state is left untouched.
Also: Switch to `vm.allConnections$` like the sibling modules so the room (and its `ParticipantConnected` listener) exists from connect time; the global-mute fix above removes the problem entirely.
---
_Filed from the September 2026 audit (branch `lotus`)._
jared
added this to the EC fork audit 2026-09 · High milestone 2026-09-12 02:12:56 -04:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Severity: high · Type: bug · Confidence: high
Location:
src/lotus/lotusDeafen.ts:51-56,src/state/media/RemoteUserMediaViewModel.ts:53,src/state/VolumeControls.ts:80-90src/lotus/lotusDeafen.ts:51-56src/lotus/lotusDeafen.ts:75-95,src/state/CallViewModel/CallViewModel.ts:643-684Problem
lotusDeafenimplements deafen by callingRemoteParticipant.setVolume(0, Track.Source.Microphone). Upstream EC uses the same API for its per-participant volume slider:createVolumeControlssubscribescombineLatest([sink$, playbackVolume$])and callsp.setVolume(volume)(default source =Microphone) every time either input emits.sink$is derived fromparticipant$, so it re-emits whenever a member'sRemoteParticipantobject appears or is replaced — i.e. on every join, and on participant re-resolution after a reconnect. The defaultplaybackVolume$is1, so EC writessetVolume(1)after lotusDeafen wrote0, and the LiveKitvolumeMapkeeps the last writer. Deafen therefore does not hold for anyone who joins (or reconnects) while you are deafened — exactly the case the module's docstring claims to cover viaParticipantConnected.Second symptom (undeafen):
applyToParticipantunconditionally writes1when not deafened, for bothMicrophoneandScreenShareAudio. EC exposes a per-participant volume slider and a per-tile "mute this participant" toggle backed byVolumeControls(playbackVolume$,togglePlaybackMuted). A deafen/undeafen cycle silently resets all of those to full volume, whileplaybackVolume$— and therefore the slider position and the muted icon inGridTile/SpotlightTile— still shows the old value. The UI and the actual audio diverge with no way to resync short of nudging the slider.Third symptom (first joiner audible while deafened): The module keys its
RoomEvent.ParticipantConnectedlisteners offvm.livekitRoomItems$, which only surfaces a room once at least one matrix-validated, non-local member has a resolved LiveKit participant (CallViewModel.ts:648-654returnsnullfor local/unresolved). While you are alone in the call,roomsis empty and no listener is attached, so the very first joiner is covered only by theapplyToRoom(room)call that runs on the subsequent stream emit — which lands after matrix membership resolution, i.e. potentially after their audio track has already subscribed and started playing throughMatrixAudioRenderer. The sibling lotus modules (lotusDenoise.ts:116,lotusQuality.ts:87,lotusAudioInject.ts:46) all explicitly avoidlivekitRoomItems$for this reason; deafen is the one that still uses it.How to trigger
Deafen in cinny, then have a second user join the call (or force a LiveKit reconnect for an existing remote). Their voice becomes audible while the cinny UI still shows deafened.
Also: Turn a participant down to 20% (or mute them) on their tile, then deafen and undeafen. They come back at 100% with the tile still showing 20% / muted.
Also: Deafen while alone in a call, then have someone join and speak immediately. A short burst of their audio is audible.
Suggested fix
Stop fighting
setVolume; drive EC's existing global mute instead —setAudioEnabled$.next(false)feedsmuteAllAudio$(src/state/MuteAllAudioModel.ts:17), whichInCallViewalready passes asmutedto everyLivekitRoomAudioRendererplus the event/reaction renderers. That also coversTrack.Source.Unknownsoundboard audio (the known P6-2 divergence) without anysetVolumeconflict.Also: Same as above — implement deafen as a global output mute rather than per-participant volume writes, so EC's per-participant state is left untouched.
Also: Switch to
vm.allConnections$like the sibling modules so the room (and itsParticipantConnectedlistener) exists from connect time; the global-mute fix above removes the problem entirely.Filed from the September 2026 audit (branch
lotus).Fixed in
f1cfcc73, shipped in0.25.0-lotus.1(published by CI from tag v0.25.0-lotus.1) and consumed by cinny at 9a85a487.