Skip to content

Mobile: presence cache starts empty on app open — agents trickle 'online' one-by-one over 30-60s (staggered heartbeats); fix options incl. relay-side WS presence synthesis #7295

Description

@KostaGorod

Summary

Related to #6200 (same root cause class — subscribe-only presence cache on mobile), but this report adds the cold-open timing signature and a broader fix matrix including relay-side options that would also fix desktop's cold start.

Observed: opening the mobile app shows all agents offline, then they flip online one at a time over ~30–60 seconds. Which agent comes online first varies between runs.

Observed behavior (live, relay-side verified)

Environment: self-hosted relay (buzz-relay), 5 identities (1 human + 4 agents) each running an adapter that maintains an authenticated WebSocket and publishes kind:20001 heartbeats on a ~60s cycle.

At the moment the mobile app showed "all agents offline":

  • Relay Redis presence keys: all identities online, TTLs ~120–180s and continuously refreshing (verified directly in Redis: GETonline, TTL → counting down and resetting)
  • HTTP bridge POST /query with kinds:[20001] + authors:[...]: returns online for every requested pubkey (this is the path desktop uses as its backstop)
  • buzz users presence --pubkeys ... CLI against the same relay: online for all

So the authoritative state is fully available server-side; only the mobile client renders offline.

Root cause

mobile/lib/features/profile/presence_cache_provider.dartPresenceCacheNotifier:

  1. build() returns {} and opens a live subscription (kinds: [EventKind.presenceUpdate], limit: 0) — future events only, no snapshot.
  2. track() (lines 40–46) is an explicit no-op for fetching:
/// 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".
}
  1. Heartbeats from independent agents are staggered (each adapter process has its own ~60s cycle). As each agent's next heartbeat fires after the app subscribes, the relay fans the ephemeral event out and that one agent flips online. An agent whose heartbeat fired just before app open stays "offline" for up to a full cycle.

That produces the exact observed signature: empty cache on open → agents appear one-by-one inside a ~60s window, order varying run to run.

Note on the WS path: relay-side, presence synthesis from Redis exists only on the HTTP bridge (crates/buzz-relay/src/api/bridge.rs, synthesize_presence() around line 2188 — intercepts /query filters targeting kind:20001/40902 with authors, answers from pubsub.get_presence_bulk). The WS REQ handler (crates/buzz-relay/src/handlers/req.rs) has no equivalent, so a WS limit: 0 subscription legitimately returns nothing for current state — ephemeral events are never stored.

Fix options

Option 1 — mobile one-shot snapshot via the existing HTTP bridge (smallest, no relay change)

On track(pubkeys) (or once per successful (re)connect), issue a single POST /query with kinds:[20001], authors:[tracked] and seed the cache from the relay-synthesized events, then keep the live WS subscription for updates. The relay-side synthesis already exists and is what desktop's get_presence (desktop/src-tauri/src/commands/profile.rs:338) already consumes; note it returns relay-signed events whose subject is in a p tag, which desktop already parses (profile.rs ~line 360–374). Mirrors #6200's suggested short-term fix; also fixes this cold-open variant.

Option 2 — relay-side presence:true filter extension (the anticipated TODO)

Add a REQ filter extension so a client can ask for the relay-synthesized presence snapshot directly over WS in the same subscription that delivers live updates. This is what the mobile TODO in track() anticipates. Removes the separate REST round trip and the double source-of-truth, but is a protocol/relay change.

Option 3 — relay-side WS REQ synthesis for kind:20001 with authors

Generalize the existing synthesize_presence() interception to the WS REQ path: when a REQ targets only kind:20001 (or 40902) with a non-empty authors list and limit: 0, emit the current Redis-backed snapshot as events before EOSE, in addition to the live fan-out. One implementation fixes both clients at once — mobile's subscribe-only cache and desktop's cold start (desktop currently paints from a rate-limit-queued REST call; with WS synthesis its subscription would immediately deliver authoritative state).

Options 1 and 3 are complementary: 1 is an immediate client-side patch, 3 removes the architectural asymmetry that keeps producing this class of bug (#6200 reconnect case, this cold-open case, #6560 members-list indicators).

Repro

Self-hosted relay with ≥2 agents publishing staggered presence heartbeats. Open the mobile app cold (or kill/relaunch) and watch the roster/agents view: all agents start offline, then flip online individually over the next ~30–60s. Meanwhile POST /query kinds:[20001] authors:[...] on the same relay returns online for all of them instantly.

Activity

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