Skip to content

Mobile: agent presence shows offline after relay reconnect — no snapshot backstop, unlike desktop #6200

Open

Description

@shawnhank

Summary

On mobile, agents (and other users) can show as "offline" in the app even while genuinely online and visible from desktop, for extended stretches after any relay reconnect. This is a confirmed asymmetry between the mobile and desktop presence implementations, not a hypothesis.

Root cause

PresenceCacheNotifier (mobile/lib/features/profile/presence_cache_provider.dart) is subscribe-only:

if (sessionState.status == SessionStatus.connected) {
  _subscribePresenceUpdates();
}
return {};

_subscribePresenceUpdates() opens a live kind:20001 subscription with limit: 0 — no snapshot of current state, only future events. track() (line 40-46) is explicitly a no-op for the actual fetch:

/// Currently a no-op for the actual fetch — we rely on live kind:20001
/// events. The tracked set is still used to filter incoming events so the
/// cache doesn't grow unbounded.
void track(List<String> pubkeys) { ... }
// TODO(presence): once the relay supports a `presence:true` filter
// extension, issue a one-shot fetch here for the latest known state per
// pubkey. Until then, presence is "online whenever they publish".

Every time sessionState transitions and build() re-fires — which happens on every relay reconnect — the presence map resets to {} and stays empty until a tracked pubkey happens to publish a fresh presence event during that session. iOS backgrounds/suspends the WebSocket aggressively (app backgrounding, network changes, lock screen), so mobile reconnects far more often than desktop ever does, and each reconnect blanks presence until something happens to re-publish.

Desktop has a real backstop for exactly this gap. desktop/src/features/presence/lib/presence.ts:40-50:

// get_presence omits offline/unknown pubkeys, so a live online event often
// targets a pubkey absent from the lookup — merge it in rather than dropping it.
export function mergePresenceUpdate(...)

Desktop calls a get_presence snapshot query and merges it with the live kind:20001 stream, so a reconnect doesn't wipe known state. Mobile has no equivalent call anywhere in presence_cache_provider.dart — this was apparently deferred pending a relay-side presence:true filter extension per the TODO, not an oversight introduced by a recent regression.

Suggested fix

Short term: add a one-shot presence snapshot fetch (mirroring desktop's get_presence call) in _subscribePresenceUpdates(), issued once per successful (re)connect, merged into state the same way desktop merges live updates into its query cache. Long term: swap to the relay-side presence:true filter extension mentioned in the TODO once it exists, to avoid a separate REST/query round trip.

Repro

Force a mobile relay reconnect (background the app for >30s, then foreground it, or toggle airplane mode briefly) while an agent that isn't actively posting is a member of a visible channel/DM. The agent will show offline on mobile until it next publishes, while desktop (open at the same time, same relay) shows it online throughout.

Activity

  1. KostaGorod commented on Sep 3, 2026

    @KostaGorod

    Cross-linking a related report with additional detail: #7295 covers the cold-open variant of this (presence cache starts empty on app launch — agents trickle online one-by-one over ~30-60s due to staggered heartbeats) and lays out a fix matrix including a relay-side WS REQ synthesis option that would fix both mobile and desktop in one place.

  2. marcelogarciadoo commented on Oct 3, 2026

    @marcelogarciadoo

    Supporting the request for an authoritative presence snapshot on mobile connection/reconnection: an agent that can respond should not appear offline solely because the phone has not seen its next heartbeat. Please keep current presence hydration and live heartbeat updates consistent.

    A separate integration caveat matters: an external adapter that does not publish kind:20001 heartbeats will also appear offline. That is not evidence of the mobile cache defect, and fixing mobile hydration alone cannot repair a missing publisher. Please document the required external-agent presence lifecycle and distinguish unknown presence from a confirmed offline state where possible.

  3. marcelogarciadoo commented on Oct 4, 2026

    @marcelogarciadoo

    Suggested regression coverage for this issue and #7295:

    • A valid kind:20001 online update is reflected in an authoritative kind:40902 presence snapshot.
    • A cold-open or reconnected client hydrates from current presence before waiting for the next heartbeat.
    • Orderly offline updates clear presence, and missing heartbeats expire according to the protocol window.
    • Switching communities does not reuse another community's cached presence.
    • Missing initial presence data is distinguished from a confirmed offline state.

    These are proposed generic test cases, not a claim that a client fix has shipped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions