From ea08c43b71455d24045b761312ad88ed187d9611 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 21:27:35 +0000 Subject: [PATCH] =?UTF-8?q?fix(nests):=20don't=20teardown=20on=20wrapper?= =?UTF-8?q?=20Closed=20=E2=80=94=20let=20orchestrator=20reconnect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cliff-detector recycle path emits Closed → Reconnecting → Connecting → Connected from the wrapper. The Closed branch was calling teardown(), which cancels cliffDetectorJob, announcesJob, and the wrapper itself, preventing the orchestrator from ever reopening the inner listener. Result pre-fix (visible in receiver log): cliff-detector fires after 4s of silence, teardown runs ~500ms later, wrapper never reopens, room permanently dead. User-initiated close goes through disconnect() / onCleared() which call teardown directly; the wrapper's subsequent Closed emission is redundant for those paths, so no-op'ing it is safe. UI still reflects Closed via state.toUiState(ui.connection). https://claude.ai/code/session_01UHN3fnXzWdj8UbSXnxSwwv --- .../commons/viewmodels/NestViewModel.kt | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt index 6776346a09..c221bdc20e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt @@ -883,11 +883,34 @@ class NestViewModel( // wait for a manual reconnect tap. } - // Server-initiated Closed: tear down stale - // local state so any later user-driven reconnect - // starts fresh. NestsListenerState.Closed -> { - teardown(targetState = ConnectionUiState.Closed) + // Closed from the wrapper is *almost always* + // transient — the orchestrator emits Closed + // → Reconnecting → Connecting → Connected + // around every cliff-detector recycle and + // every JWT refresh. We must NOT teardown + // here: teardown cancels `cliffDetectorJob`, + // `announcesJob`, etc., AND calls + // `wrapper.close()`, which cancels the + // wrapper's orchestrator before it can + // reopen the inner listener. End result + // pre-fix: the very first cliff-detector + // recycle permanently kills the room + // instead of recovering it (visible in the + // 15:56:25 receiver log: cliff fires → + // teardown fires 521 ms later → wrapper + // never reopens → `cliff-detector EXITED + // closed=false`). + // + // User-initiated close goes through + // `disconnect()` / `onCleared()` which call + // `teardown` directly; the wrapper's + // subsequent Closed emission here is a + // redundant signal — no-op'ing it doesn't + // change anything for those paths. The UI + // still picks up the Closed via + // `state.toUiState(ui.connection)` above + // for visual feedback. } else -> { /* no extra side effect */ }