457e0f5997
The channelFlow conversion inf17e7adfixed `MoqLiteNestsListener.announces` but the SAME `IllegalStateException: Flow invariant is violated` kept firing in the next two-phone repro (commitf17e7ad, run 15:46:51) — this time the offending `flow{}` was one layer up, in `ReconnectingNestsListener.announces`: override fun announces(): Flow<RoomAnnouncement> = flow { activeListener.collectLatest { listener -> ... runCatching { listener.announces().collect { emit(it) } // <-- HERE } } } `Flow.collectLatest { lambda }` cancels and restarts a CHILD coroutine for each upstream emission. The lambda body runs in that child, NOT in the surrounding flow{} body's coroutine. So when the lambda invokes `emit(it)`, it's emitting from a child coroutine. `flow{}`'s SafeFlow guard rejects this with the same "Emission from another coroutine is detected" error the inner listener was throwing before my last fix. Net effect on the user's repro: the inner channelFlow now correctly sends RoomAnnouncement to the wrapper's `listener.announces().collect` lambda, but that lambda's `emit(it)` to the wrapper's flow{} body fails the same SafeFlow check, the wrapper's runCatching swallows the exception (as `iter=2 inner collect ended IllegalStateException ... fwd=1`), `_announcedSpeakers` stays empty, cliff detector never fires. Fix: convert `ReconnectingNestsListener.announces` from `flow{} + emit` to `channelFlow{} + send`, matching the inner listener's shape. The combination of `channelFlow + collectLatest + send` is the canonical pattern in kotlinx-coroutines for "switch-map" semantics with cross- coroutine production. `awaitClose { }` is empty because `collectLatest` on an infinite-StateFlow never completes naturally — cancellation propagates through structured concurrency when the consumer cancels the channelFlow. Tests: every nestsClient + commons unit test still passes, including the 12 CliffDetectorTest cases pinning the predicate's behaviour. Together withf17e7ad(inner channelFlow), this should finally close the chain: inner emits RoomAnnouncement on its channelFlow → wrapper's collectLatest receives it inside its child coroutine → wrapper's channelFlow.send forwards across the channel → consumer's observeAnnounces collect receives → `_announcedSpeakers` populates → cliff detector tick reports `announced=1` → on a real cliff event the recycle fires.