From 0c9f4cbf0b8d908eef4fcdb71d9838f2557985b8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 28 Sep 2026 10:48:31 -0400 Subject: [PATCH] fix(marmot): a Welcome that ran out of retries waits for the next app start Relays re-deliver two days of gift wraps on every re-subscription, and each delivery of a Welcome that kept failing started a fresh 30s/2m/10m chain, so a Welcome that can never apply was re-run through MLS for as long as relays kept it. Exhausted Welcomes are now remembered for the session: re-deliveries skip them, and the next app start still gives them one more try. Co-Authored-By: Claude Opus 5.5 --- .../amethyst/model/AccountMarmotActions.kt | 19 +++++++++++++++++-- .../loggedIn/DecryptAndIndexProcessor.kt | 7 ++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountMarmotActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountMarmotActions.kt index e767ce649d..4e2d9a7f8d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountMarmotActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountMarmotActions.kt @@ -94,10 +94,25 @@ class AccountMarmotActions( // Welcome rumor ids with a retry already scheduled, so a relay re-delivering the same // wrap while one waits does not start a second chain of retries. private val welcomeRetries = mutableSetOf() + + // Welcome rumor ids whose retries ran out this session. Relays re-deliver two days of gift + // wraps on every re-subscription, and each delivery used to start a fresh chain, so a + // Welcome that can never apply was re-run through MLS for as long as relays kept it. + // In memory on purpose: the next app start still gives it one more try. + private val welcomeRetriesExhausted = mutableSetOf() private val welcomeRetriesLock = KmpLock() - /** True when [welcomeId] had no retry pending and now has one. */ - fun claimWelcomeRetry(welcomeId: HexKey): Boolean = welcomeRetriesLock.withLock { welcomeRetries.add(welcomeId) } + /** True when [welcomeId] had no retry pending, has retries left, and now has one pending. */ + fun claimWelcomeRetry(welcomeId: HexKey): Boolean = + welcomeRetriesLock.withLock { + welcomeId !in welcomeRetriesExhausted && welcomeRetries.add(welcomeId) + } + + fun markWelcomeRetriesExhausted(welcomeId: HexKey) { + welcomeRetriesLock.withLock { welcomeRetriesExhausted.add(welcomeId) } + } + + fun welcomeRetriesExhausted(welcomeId: HexKey): Boolean = welcomeRetriesLock.withLock { welcomeId in welcomeRetriesExhausted } fun releaseWelcomeRetry(welcomeId: HexKey) { welcomeRetriesLock.withLock { welcomeRetries.remove(welcomeId) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index 71178e26dc..b9924db662 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -406,6 +406,8 @@ private suspend fun processMarmotWelcomeFlow( // A Welcome that already joined (or never can) is done. Replays of its wrap are routine: // every re-subscription re-delivers the last two days of gift wraps. if (manager.isTerminallyIngested(innerEvent.id)) return + // Out of retries this session: a re-delivered wrap waits for the next app start. + if (attempt == 0 && account.marmot.welcomeRetriesExhausted(innerEvent.id)) return // "h" tag is optional per MIP-02 — some senders (e.g. whitenoise-rs) omit it. // nostrGroupId is derived from the MLS GroupContext's NostrGroupData extension instead. @@ -480,7 +482,10 @@ private fun scheduleWelcomeRetry( account: Account, attempt: Int, ) { - if (attempt >= WELCOME_RETRY_DELAYS_MS.size) return + if (attempt >= WELCOME_RETRY_DELAYS_MS.size) { + account.marmot.markWelcomeRetriesExhausted(welcome.id) + return + } if (!account.marmot.claimWelcomeRetry(welcome.id)) return account.scope.launch(Dispatchers.IO) { delay(WELCOME_RETRY_DELAYS_MS[attempt])