From c4b1eaaa3d3d386412415596e40401c654a423a3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 21:41:07 +0000 Subject: [PATCH] fix(dm): pin the history floor so un-delivered relays don't re-page; add sentinel/bootstrap trace logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The floor (now - 7d) was recomputed on every call, so it drifted forward over time. For a relay that hadn't delivered yet (reachedUntil == null), reachedUntilFor returned that drifting floor, so its marker's reached cursor kept changing, the sentinel's LaunchedEffect(reachedUntil) key changed, and it re-fired advance() — observed as rooms.nip04.history re-advancing a silent vitor.nostr1.com every ~15s, with the retry's 'until' moving NEWER each time. Pin the floor once per account/conversation for the session (matching the pre-rewrite behavior) in all three history loaders, so an un-delivered relay parks at a stable cursor and its sentinel fires once. Also add diagnostics to catch this class of bug quickly: one log line per marker sentinel fire (key + reachedUntil — a loop repeats the same key, a drift shows a moving cursor) and one per advanceAll (empty-feed bootstrap). --- .../AccountGiftWrapsHistoryEoseManager.kt | 14 ++++++++++---- .../chats/feed/layouts/RelayReachMarker.kt | 8 +++++++- .../datasource/ChatroomNip04HistorySubAssembler.kt | 11 ++++++++--- .../ChatroomListNip04HistorySubAssembler.kt | 11 ++++++++--- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsHistoryEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsHistoryEoseManager.kt index ff05a9b22d..8630cd87fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsHistoryEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsHistoryEoseManager.kt @@ -100,8 +100,13 @@ class AccountGiftWrapsHistoryEoseManager( private val _relayProgress = MutableStateFlow>(emptyMap()) val relayProgress: StateFlow> = _relayProgress.asStateFlow() - // History starts just below the live tail's one-week floor and pages backward from there. - private fun startUntil() = TimeUtils.now() - AccountGiftWrapsEoseManager.LIVE_TAIL_SECONDS + // History starts just below the live tail's one-week floor and pages backward from there. Pinned per + // account for the session: it must NOT drift forward on every recompute, or an un-delivered relay's + // marker (which sits at this floor) would keep changing and re-trigger its on-screen sentinel. The + // live tail covers everything newer than the floor. + private val pinnedFloor = ConcurrentHashMap() + + private fun startUntil(pk: HexKey) = pinnedFloor.getOrPut(pk) { TimeUtils.now() - AccountGiftWrapsEoseManager.LIVE_TAIL_SECONDS } private fun daysAgo(epochSeconds: Long) = (TimeUtils.now() - epochSeconds) / TimeUtils.ONE_DAY @@ -144,6 +149,7 @@ class AccountGiftWrapsHistoryEoseManager( account.dmRelays.flow.value .forEach { if (arm(user, it)) any = true } if (any) { + Log.d(TAG) { "[giftwrap.history] advanceAll (empty-feed bootstrap)" } _exhausted.value = false updateStatus(user) invalidateFilters() @@ -159,7 +165,7 @@ class AccountGiftWrapsHistoryEoseManager( val account = accounts[user.pubkeyHex] ?: return false if (relay !in account.dmRelays.flow.value) return false if (loadTracker.isInFlight(relay)) return false - if (!pager.advance(user.pubkeyHex, relay, startUntil())) return false + if (!pager.advance(user.pubkeyHex, relay, startUntil(user.pubkeyHex))) return false stalledRelays[user.pubkeyHex]?.remove(relay) loadTracker.bind(account.scope) loadTracker.onAdvance(relay) @@ -190,7 +196,7 @@ class AccountGiftWrapsHistoryEoseManager( if (activeUser != user.pubkeyHex) return val relays = accounts[user.pubkeyHex]?.dmRelays?.flow?.value ?: emptySet() _relayCount.value = loadTracker.count() - val start = startUntil() + val start = startUntil(user.pubkeyHex) _reachedBack.value = pager.deepestReached(user.pubkeyHex, relays, start) val stalled = stalledRelays[user.pubkeyHex] ?: emptySet() _relayProgress.value = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/RelayReachMarker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/RelayReachMarker.kt index 2151bbe6c6..cd7da3264b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/RelayReachMarker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/RelayReachMarker.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.HalfPadding +import com.vitorpamplona.quartz.utils.Log /** How far one relay has paged into the conversation, for an in-stream progress marker. */ enum class RelayReachState { @@ -109,7 +110,12 @@ fun RelayWindowLimitMarkers( // Keyed identity so the effect isn't torn down on reorder; keyed on the reached cursor ONLY so // it re-fires per landed page (continue while visible) but NOT on stall/unstall churn. key(lim.key) { - LaunchedEffect(lim.reachedUntil) { lim.advance() } + LaunchedEffect(lim.reachedUntil) { + // One line per sentinel fire — a re-fire LOOP shows the same key firing over and over + // (and whether its reached cursor is drifting, which would point at a non-pinned floor). + Log.d("DMPagination") { "marker fire ${lim.key} reachedUntil=${lim.reachedUntil}" } + lim.advance() + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomNip04HistorySubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomNip04HistorySubAssembler.kt index 642618b3f5..b358bf348b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomNip04HistorySubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomNip04HistorySubAssembler.kt @@ -91,7 +91,11 @@ class ChatroomNip04HistorySubAssembler( private var activeConvo: ConvoKey? = null private val exhaustedByConvo = ConcurrentHashMap() - private fun startUntil() = TimeUtils.now() - AccountGiftWrapsEoseManager.LIVE_TAIL_SECONDS + // Pinned per conversation for the session — must not drift forward, or an un-delivered relay's marker + // would keep moving and re-trigger its sentinel. See AccountGiftWrapsHistoryEoseManager. + private val pinnedFloor = ConcurrentHashMap() + + private fun startUntil(pk: ConvoKey) = pinnedFloor.getOrPut(pk) { TimeUtils.now() - AccountGiftWrapsEoseManager.LIVE_TAIL_SECONDS } override fun user(key: ChatroomQueryState) = key.account.userProfile() @@ -141,6 +145,7 @@ class ChatroomNip04HistorySubAssembler( relays.all.forEach { if (arm(key, it)) any = true } } if (any) { + Log.d("DMPagination") { "[convo.nip04.history] advanceAll (empty-thread bootstrap)" } _exhausted.value = false updateStatus() invalidateFilters() @@ -155,7 +160,7 @@ class ChatroomNip04HistorySubAssembler( if (relay !in relays.all) return false val pk = convoKey(key) if (loadTracker.isInFlight(relay)) return false - if (!pager.advance(pk, relay, startUntil())) return false + if (!pager.advance(pk, relay, startUntil(pk))) return false stalledRelays[pk]?.remove(relay) loadTracker.bind(key.account.scope) loadTracker.onAdvance(relay) @@ -182,7 +187,7 @@ class ChatroomNip04HistorySubAssembler( val pk = activeConvo ?: return val relays = relaysFor(pk) ?: return _relayCount.value = loadTracker.count() - val start = startUntil() + val start = startUntil(pk) _reachedBack.value = pager.deepestReached(pk, relays.all, start) val stalled = stalledRelays[pk] ?: emptySet() _relayProgress.value = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04HistorySubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04HistorySubAssembler.kt index 02f2b6922c..2ea913b2a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04HistorySubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04HistorySubAssembler.kt @@ -79,7 +79,11 @@ class ChatroomListNip04HistorySubAssembler( private val _relayProgress = MutableStateFlow>(emptyMap()) val relayProgress: StateFlow> = _relayProgress.asStateFlow() - private fun startUntil() = TimeUtils.now() - AccountGiftWrapsEoseManager.LIVE_TAIL_SECONDS + // Pinned per account for the session — must not drift forward, or an un-delivered relay's marker + // would keep moving and re-trigger its sentinel. See AccountGiftWrapsHistoryEoseManager. + private val pinnedFloor = ConcurrentHashMap() + + private fun startUntil(pk: HexKey) = pinnedFloor.getOrPut(pk) { TimeUtils.now() - AccountGiftWrapsEoseManager.LIVE_TAIL_SECONDS } override fun user(key: ChatroomListState) = key.account.userProfile() @@ -123,6 +127,7 @@ class ChatroomListNip04HistorySubAssembler( var any = false allRelays(account).forEach { if (arm(user, it)) any = true } if (any) { + Log.d("DMPagination") { "[rooms.nip04.history] advanceAll (empty-feed bootstrap)" } _exhausted.value = false updateStatus(user) invalidateFilters() @@ -136,7 +141,7 @@ class ChatroomListNip04HistorySubAssembler( val account = accounts[user.pubkeyHex] ?: return false if (relay !in allRelays(account)) return false if (loadTracker.isInFlight(relay)) return false - if (!pager.advance(user.pubkeyHex, relay, startUntil())) return false + if (!pager.advance(user.pubkeyHex, relay, startUntil(user.pubkeyHex))) return false stalledRelays[user.pubkeyHex]?.remove(relay) loadTracker.bind(account.scope) loadTracker.onAdvance(relay) @@ -168,7 +173,7 @@ class ChatroomListNip04HistorySubAssembler( val account = accounts[user.pubkeyHex] val relays = account?.let { allRelays(it) } ?: emptySet() _relayCount.value = loadTracker.count() - val start = startUntil() + val start = startUntil(user.pubkeyHex) _reachedBack.value = pager.deepestReached(user.pubkeyHex, relays, start) val stalled = stalledRelays[user.pubkeyHex] ?: emptySet() _relayProgress.value =