Mobile: agent presence shows offline after relay reconnect — no snapshot backstop, unlike desktop
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## 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:
```dart
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:
```dart
/// 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 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`:
```ts
// 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.
Contributor guide
Research direction
Start in mobile/lib/features/profile/presence_cache_provider.dart, especially PresenceCacheNotifier, _subscribePresenceUpdates(), and track(); then compare the snapshot and merge behavior in desktop/src/features/presence/lib/presence.ts. Reproduce by forcing a relay reconnect while observing an inactive agent from mobile and desktop. Done means mobile presence remains available after reconnect instead of waiting for a new kind:20001 event.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart, typescript
- Domain
- mobile
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100