fix(dm): pin the history floor so un-delivered relays don't re-page; add sentinel/bootstrap trace logs

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).
This commit is contained in:
Claude
2026-06-04 21:41:07 +00:00
parent 9ac835557a
commit c4b1eaaa3d
4 changed files with 33 additions and 11 deletions
@@ -100,8 +100,13 @@ class AccountGiftWrapsHistoryEoseManager(
private val _relayProgress = MutableStateFlow<Map<NormalizedRelayUrl, RelayPagingProgress>>(emptyMap())
val relayProgress: StateFlow<Map<NormalizedRelayUrl, RelayPagingProgress>> = _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<HexKey, Long>()
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 =
@@ -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()
}
}
}
}
@@ -91,7 +91,11 @@ class ChatroomNip04HistorySubAssembler(
private var activeConvo: ConvoKey? = null
private val exhaustedByConvo = ConcurrentHashMap<ConvoKey, Boolean>()
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<ConvoKey, Long>()
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 =
@@ -79,7 +79,11 @@ class ChatroomListNip04HistorySubAssembler(
private val _relayProgress = MutableStateFlow<Map<NormalizedRelayUrl, RelayPagingProgress>>(emptyMap())
val relayProgress: StateFlow<Map<NormalizedRelayUrl, RelayPagingProgress>> = _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<HexKey, Long>()
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 =