fix(lotus-denoise): reliability — never-silent watchdog, resume timeout, cache + activation

Track-A robustness fixes from the engine review; no quality/model changes.

- H1: auto-resume the AudioContext on `statechange` if it suspends mid-call
  (mobile backgrounding / audio interruption). Previously the dest node emitted
  digital silence with no recovery — a silent mute of the sender.
- H2: `resumeCtx()` races `resume()` against a timeout. A suspended context can
  only resume on a user gesture; the action can arrive via postMessage, so a
  bare `await resume()` inside LiveKit's track-change lock could hang and
  deadlock all later mute/unmute/device-switch. Now it proceeds and the H1
  watcher heals it.
- M1: don't cache a REJECTED wasm fetch — a transient blip during a reconnect
  used to permanently disable denoise for the session. Evict on failure.
- M2: activate denoise off `allConnections$` (local participant's connections)
  instead of `livekitRoomItems$`, which excludes the local participant and only
  surfaces rooms with a remote member — so denoise now also runs when you're
  alone and no longer couples to a remote-render concern.
- Context lifecycle: `closeContext()` removes the state watcher before closing;
  `ensureContext()` closes a half-initialised context on any failure (no leak).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Lotus CI
2026-06-30 23:27:44 -04:00
co-authored by Claude Opus 4.8
parent a4309f3e0c
commit 9a4e9bf7da
2 changed files with 79 additions and 26 deletions
+6 -2
View File
@@ -101,8 +101,12 @@ export function startLotusDenoise(vm: CallViewModel): () => void {
const roomListeners = new Map<LivekitRoom, () => void>();
let rooms: LivekitRoom[] = [];
const sub = vm.livekitRoomItems$.subscribe((items) => {
const next = items.map((i) => i.livekitRoom);
// Drive activation off the LOCAL participant's connection(s), not
// `livekitRoomItems$` — that stream excludes the local participant and only
// surfaces rooms with ≥1 remote member, so it wouldn't denoise you while
// you're alone and is a fragile coupling to a remote-render concern.
const sub = vm.allConnections$.subscribe((data) => {
const next = data.getConnections().map((c) => c.livekitRoom);
rooms = next;
for (const [room, off] of roomListeners) {
if (!next.includes(room)) {