Skip to content

fix(mobile): hydrate and refresh relay presence snapshots - #7382

Open
loganj wants to merge 1 commit into
mainfrom
fix/mobile-agent-presence-20260905
Open

fix(mobile): hydrate and refresh relay presence snapshots#7382
loganj wants to merge 1 commit into
mainfrom
fix/mobile-agent-presence-20260905

Conversation

@loganj

@loganj loganj commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

🤖

Summary

Mobile showed every person in a DM as Offline until they happened to publish a presence update while the app was connected. Presence was never loaded when the app opened, never refreshed, and a missing or failed read looked identical to actually being offline. This PR makes presence load and stay current: on connect or foreground, the app fetches the relay's presence snapshot for everyone visible, keeps live updates overlapping so nothing is missed in between, and re-checks every 60 seconds while the app is in the foreground.

  • Snapshot reads are authenticated like the rest of the session and batched by exact key (at most 100 per batch), instead of relying on live events only.
  • The live subscription and the snapshots overlap, so a gap between them can't silently drop an update; late results and same-status heartbeats are fenced so they can't overwrite newer state.
  • Refreshing every 60 foreground seconds observes relay lease expiry and retry failures; missing or failed evidence simply stays absent — it is never reported as Offline.
  • Disconnects, backgrounding, and account/community changes reset the cache cleanly. The app's existing subscription/recovery machinery is reused rather than duplicated.

Mirrors Desktop's presence hydration (desktop/src-tauri/src/commands/profile.rs::get_presence, desktop/src/features/presence/hooks.ts).

Related issue

Testing

  • Regressions cover cold-open hydration, live/snapshot races, lease expiry, failed reads and retries, disconnect/background/scope changes, and >100-key batching.
  • At the branch head: just mobile-check and the full mobile test suite pass independently, and every broader CI gate has successful evidence after isolated recovery runs (no monolithic local just ci run) — receipts in the exact-head evidence comment.
  • Verification is automated-test level; no native device or simulator run is claimed. No layout change in this slice.

Screenshots

No layout change in this slice — the renders below show the same existing profile sheet, with the presence status now following hydrated relay data. Flutter production-widget test renders — not native-device screenshots or acceptance captures.

Scenario Before After
Existing profile opened cold, with a signed relay snapshot saying Online Before: the profile stays Offline — the snapshot is never fetched After: the profile shows Online once the snapshot hydrates
Capture provenance

Rendered by the Flutter widget engine in a flutter test run (production widgets, production theme; no device or simulator). Before: this PR's declared base 3c7f288c60d67df78577b237e27c3dfc8831aaa1. After: its head a8e8028bfb9bb67de2db03ed448839e2f3ab2b0d.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is 813bbd14121edacc6cb4733301a3af12131aa10e...3ff2f290bac41811fe2db0b141961148740ebce5.
A new review must complete for this exact range. When manual authorization
is required, a Block organization member must comment exactly
@buzz-security-review 3ff2f290bac41811fe2db0b141961148740ebce5 to authorize a new review.
Any previous review applies only to its recorded range.

@loganj

loganj commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

@buzz-security-review a8e8028

@loganj

loganj commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Independent presence readiness evidence — 2026-09-07

Verified current range: main at 3c7f288c60d67df78577b237e27c3dfc8831aaa1a8e8028bfb9bb67de2db03ed448839e2f3ab2b0d; +493/−54 = 547 lines. Still OPEN/draft. No source changes or pushes were needed.

  • Exact-head mobile-check passed; full mobile suite 2,083 passed. Completed mobile validation was preserved, not repeated during continuation.
  • Broader local validation: repository-wide check passed. Original just ci returned 1 in Rust unit fixtures, not a clean monolithic pass. In-checkout TMPDIR made five agent hints tests read the real repository AGENTS.md; ACP shell/timer tests and one process-global DB waiter gauge also failed under parallel execution. After moving TMPDIR outside any git root (still private), complete affected ACP/agent/DB lanes passed with --test-threads=1, without altering code/assertions: ACP 915 + 9, agent 525 (1 ignored), DB 122 (252 ignored).
  • All previously unreached local lanes passed independently on this head: Desktop 6,450 tests, Desktop build, Tauri check, Tauri 3,266 tests across targets (20 ignored), web build. Thus every local gate component has successful evidence after documented recovery; the original exit-1 logs remain preserved.
  • Remote CI attempt 2 completed successfully. Retried Desktop shard 4: 345 passed, 3 flaky, zero final failures. The original structured-condition case also passed its exact-head targeted local reproduction. Neither this PR nor its UI child changes Desktop source/tests. Final head rollup: 32 success / 31 intentionally skipped, nothing pending/failing; path-filtered skips are not execution claims.

Advisory security: unavailable, not clean

The authorized exact-range advisory finished cancelled at 15:54:56 UTC. Its annotation states: “The job has exceeded the maximum execution time of 40m0s.” Authorization/checkout/range pin passed, but review never completed; salvage/post did not run, zero artifacts were published, and the review-job log endpoint returned 404. No findings were produced for inspection. No manual cancellation or blind retry was performed.

Next actor/action: the maintainer of the trusted Codex Security Review workflow must resolve the runner/action timeout/output loss, then rerun the authorized current range. Resume advisory sign-off only when actual current-range output is posted and findings have been reviewed. This is an external advisory limitation, not a failed presence test or a claim that security review passed.

Technical review found no substantive mobile defect: snapshot/live overlap, per-key revisions, generation fences, invalidation on transport/account/community/lifecycle changes, and bounded 60-second polling were checked. Unknown rendering is deliberately the separate stacked UI PR #7383. Presence is conversational availability, not substrate health.

Native relay-backed foreground/background/reconnect validation remains unavailable on this host (no Xcode/simulator). Widget renders are not native-runtime evidence. Both PRs remain drafts; no merge/readiness-state change was made.

Durable evidence: WORK_LOGS/PRESENCE_REVIEW_3FE2D8A4/REVIEW_REPORT.md, original *-ci.log/exit records, recover-ci-lanes.sh, per-head recovery logs/results, and final GitHub API snapshots.

@loganj
loganj marked this pull request as ready for review September 8, 2026 13:54
@loganj
loganj requested a review from a team as a code owner September 8, 2026 13:54

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing exact head a8e8028bfb9bb67de2db03ed448839e2f3ab2b0d against base 3c7f288c60d67df78577b237e27c3dfc8831aaa1, I found two user-visible correctness defects that need to be resolved before merge.

  1. Transient snapshot failures are rendered as authoritative Offline. PresenceCacheNotifier._refresh removes requested keys when the query fails (mobile/lib/features/profile/presence_cache_provider.dart:163-195), while each production consumer converts an absent key directly to offline (mobile/lib/features/profile/user_profile_sheet.dart:56-57, mobile/lib/features/channels/channel_detail_page/app_bar.dart:218-250, and mobile/lib/features/channels/channels_page/channel_tile.dart:177-182). A previously online/away person therefore flips visibly—and in profile accessibility semantics, audibly—to Offline during relay, Redis, auth, or network failure. The new test asserts that the failed refresh empties the helper cache, but does not exercise a widget consumer, so it codifies the state loss without protecting the user-facing contract.

    Required: preserve unknown/stale explicitly through the renderer (for example, neutral/hidden/loading, or retained last-known state marked stale), and add widget regression coverage for both no observation and known-online → failed refresh. Neither case may claim Offline without evidence.

  2. Snapshot freshness is not joined to live-event ordering. Snapshot settlement selects the latest result but does not write its createdAt into _timestamps (mobile/lib/features/profile/presence_cache_provider.dart:176-198). The next live event is consequently compared against zero in _handlePresenceEvent (:137-149). A production-notifier regression probe at this head demonstrated that snapshot online @ 20 followed by delayed live offline @ 10 yields offline (expected online). HTTP snapshot and WebSocket delivery are independent, so delayed pre-snapshot delivery can regress visible presence until another heartbeat or the next refresh.

    Required: use one monotonic freshness policy across snapshot and live settlement, define equal-second conflict behavior, account for relay-synthesized snapshot timestamps versus signer clock skew, and add the deterministic snapshot-20/live-10 regression.

Validation at this exact head: just mobile-check passed; the full just mobile-test passed 2,083 tests; focused shipped presence suites passed 17 tests; git diff --check passed. The adversarial stale-order probe failed as described and was then removed, leaving its review worktree clean. Identity/tenant boundaries, subscribe-before-snapshot fencing, scope reset, cancellation/disposal, retry, batching, and Redis tenant TTL storage otherwise held under source/test review. Green gates do not cover the two production seams above.

Confidence gaps, not additional author defects: no native iOS foreground/background/reconnect or accessibility journey was observed, and the mobile client relies on authenticated transport plus server validation/synthesis rather than locally verifying inbound Nostr signatures. Native relay-backed verification should follow the fixes when a simulator/device-capable owner is available.

@loganj loganj left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposition for review 5142925666 at head a8e8028bf — both items are addressed in this stack; this branch itself is intentionally left unchanged so each stack delta stays small and reviewable.

  1. Transient snapshot failures rendered as authoritative Offline. The consumer-side fix is in #7383 (this PR's descendant): it removes the null→offline mapping at all three renderers, hides unknown dots/notches, and labels unknown presence explicitly. Landing #7382 together with #7383 ships the unknown semantics without the intermediate false-Offline UI, so I have not duplicated those renderer changes onto this branch.

  2. Snapshot freshness not joined to live-event ordering. Fixed in #7526, a focused follow-up based on #7383's branch rather than further growth here. It records each settled snapshot's observation second, never compares the relay-synthesized snapshot timestamp against signer event time directly (snapshot records carry the relay clock at synthesis; live kind:20001 events carry the subject's signer clock), and confirms a live event older than the observation second against a fresh snapshot — the relay ingests presence before fan-out, so that observation always reflects the event or a newer one. Equal-second conflicts settle last-arrival-wins, and a bounded event-id memory keeps redelivered ambiguous events from re-arming confirmations. The deterministic snapshot-20/live-10 regression is included, plus lagging-clock confirm, equal-second, and redelivery bounds; full mobile suite 2090/2090 at its head 08d20f9.

Happy to reshape the stack if maintainers prefer a different ordering.

loganj added a commit that referenced this pull request Sep 10, 2026
## Summary
- Reject kind:20001 presence events with `OK false` / `error: presence
storage unavailable` when Redis SET or DEL fails, before publishing,
local fan-out, or local-event marking.
- Preserve the producer contract needed by snapshot-confirming
consumers: delivered live presence must follow successful mutation of
the Redis state read by snapshots.
- Classify those backend rejections with the existing `IngestError`
taxonomy so a presence storage outage counts as
`buzz_events_rejected_total{transport="ws",reason="error"}`, not client
`reason="invalid"`; genuine client-input refusals (verification failure,
membership gates) stay `invalid`, and every wire message is an unchanged
fixed sanitized string (review follow-up, no protocol wording change).
- Add actual `handle_event` integration coverage for rejected
online/offline transitions, healthy online→offline
accepted/stored/fanned-out behavior, and the rejection-counter routing
on storage failure with an invalid-signature control.

This is standalone on main; it does not depend on the mobile
implementation. Deploy this relay prerequisite before relying on #7526's
snapshot-confirmation policy. Existing
pubsub-failure-after-successful-storage behavior and disconnect TTL
cleanup are deliberately unchanged. A storage error may be an ambiguous
write outcome, not a rollback guarantee; the rejected event is not
published by this handler. Clients may retry the generic `error:`
rejection. Desktop's 60s heartbeat retries non-offline presence, not
every explicit offline transition.

### Related issue
Addresses the relay prerequisite identified in [#7526 review
5157607827](#7526 (review)).
Searched open presence/storage PRs; no duplicate relay storage-error
rejection fix found. #7382/#7383/#7526 heads and bases are unchanged.

### Testing
Exact head: `389174df29cc02d0f885c03209eff661d8bb2ec0` (+380/-13; 393
total), one commit `389174df2` on top of the reviewed `c031d6eb1`
(DCO-signed; base `bfc384855889432df4a333a0edf3080f332ee169` unchanged).

- PASS: `cargo fmt --all -- --check`, `cargo clippy -p buzz-relay
--all-targets -- -D warnings`, `git diff --check`, `just
file-size-check`, PostgreSQL discovery validation — all run at the exact
final head with a clean tree before and after.
- PASS: documented native `scripts/postgres-test-run.sh -p buzz-relay
--lib --tests`: **89/89** actual integration tests, including the four
presence cases (online/offline storage rejection, healthy
online→offline, and the new rejection-classification case). Owned
PostgreSQL 17/Redis on isolated loopback ports, schema plus
reconciliation applied; no shared development database.
- PASS: explicit `cargo test -p buzz-relay presence_storage -- --ignored
--nocapture`: **4/4**, not skipped.
- Full isolated relay crate suite at the final head (`cargo nextest run
-p buzz-relay --lib --tests`): **1062 run: 1062 passed, 94 skipped**.
The previously failing
`api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo` passed
in this run (1.5s); it is a known timing-sensitive main baseline failure
tracked open in #7140 and untouched by this PR, so this single passing
run is reported as-is and does not claim environmental clearance or
close #7140. No full-suite-green claim is made beyond this run.
- Mobile is untouched; #7526's existing 2090-test/format/analyze
evidence remains scoped to its unchanged head. Its separate Desktop
Smoke E2E (2) failure remains red; no CI retries requested.

[Production-seam regression
coverage](https://github.com/block/buzz/blob/389174df29cc02d0f885c03209eff661d8bb2ec0/crates/buzz-relay/src/handlers/event.rs#L1491-L1803):
the metric case drives real `handle_event` traffic against a genuinely
dead Redis endpoint with a seeded active PostgreSQL community and a
registered presence watcher, asserts the storage rejection counts
`reason="error"` while a tampered-signature control through the same
dispatcher arm stays `reason="invalid"`, and re-asserts the rejected
ACK, no fan-out, and no local-event marker. Counter assertions use a
thread-local recorder guard held across `.await` points (the buzz-db
counter-test convention) inside the per-process nextest postgres-ci
lane, so no parallel test can race the counter snapshot.

No UI change or screenshot. Local logs and reproducible service/gate
scripts are retained under
`WORK_LOGS/MOBILE_FEEDBACK_PRESENCE_20260909/relay_prerequisite/metric_correction/`
in the engineering workspace. This PR is a review candidate, not merge
clearance.

Causal checks: restoring only the pre-fix production mutation block
makes both original rejection tests fail (`OK true` instead of `false`);
healthy success still passes. Reverting only the typed classification
(mapping the ephemeral `Internal` arm back to `invalid`) makes the new
metric regression fail with the outage counted as `[("ws","invalid",2)]`
instead of `[("ws","error",1),("ws","invalid",1)]`. The unchanged mesh
echo case also failed 504/200 with the main-production block restored in
the prior run, supporting its separation from this change without
claiming environmental clearance. Candidate source restored
byte-for-byte after each mutation. Repository-wide `just ci` was not
rerun; the scoped relay gates above are the new evidence.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
@loganj
loganj force-pushed the fix/mobile-agent-presence-20260905 branch from a8e8028 to 2e3eae6 Compare September 11, 2026 02:22
@loganj
loganj requested a review from jedwards27 September 11, 2026 02:22
@loganj

loganj commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

Both blocking items from review 5142925666 are now included directly in this PR at 2e3eae681, based on main 813bbd141. This supersedes my earlier stack-based disposition: this root no longer relies on #7383/#7526 to supply the fixes.

  • Failed refresh ≠ Offline: all three consumers preserve Unknown (hidden dot/notch, explicit Unknown label and profile semantics). Widget regressions cover no observation and Online → failed refresh, plus known states. See header, tile, profile, and renderer regressions.
  • Snapshot/live ordering: provider separates relay observation time from signer ordering, confirms ambiguous older live events, and fences older in-flight results by per-key revision. Equal-second nonduplicate live conflicts are last-arrival-wins; bounded event-ID memory prevents repeated confirmations. Deterministic tests cover snapshot Online@20/live Offline@10, lagging signer confirmation, equal-second conflict, redelivery and stale completion.

Complete main → head diff is 8 Mobile files, +802/−96 = 898 including four test files. Local evidence is reused by exact whole-Mobile-subtree identity with the independently reviewed/tested candidate: 2,113 passed / 0 failed, six targeted presence/geometry controls, analyzer clean, formatter 552 files unchanged. These were not rerun on this new commit; intervening main changes touch no Mobile validation inputs. This is not a new CI success claim. Native device/live-relay/accessibility journeys and actual relay rollout remain unverified.

Requested re-review from @jedwards27 through GitHub's review-request API (accepted and verified pending). Request delivery is not approval.

Signed-off-by: Logan Johnson <loganj@squareup.com>
@loganj
loganj force-pushed the fix/mobile-agent-presence-20260905 branch from 2e3eae6 to 3ff2f29 Compare September 11, 2026 03:52
@loganj

loganj commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

@jedwards27 — requesting exact-new-head re-review after independent review and parent-first publication.

Head: 3ff2f290bac41811fe2db0b141961148740ebce5
Tree: 3572f81790cad53dfce740efaabae92d6ffe61f1
Sole parent / actual target: 813bbd14121edacc6cb4733301a3af12131aa10e
Complete target-pair diff: 899 added+deleted lines (899 cap). Existing PR/base branch retained.

This governing root now directly owns both original findings from review 5142925666: failed/absent observations render Unknown (hidden unconfirmed dots, one truthful profile accessibility label), and snapshot/live ambiguity is confirmed rather than immediately regressing settled state. The equality defect identified in #7526 reviews 5174631081 / 5174636673 is repaired here with event.createdAt <= snapshotAt. No reliance on a descendant to repair root production remains.

Freshness/producer evidence: the root increments the per-key revision before queuing ambiguous evidence. A pre-event in-flight response cannot settle that key; the pending loop issues a distinct subsequent query. Idle coalescing groups keys, not response evidence. RelaySessionNotifier.queryRelay signs and sends a new HTTP POST, and RelayHttpQueryClient reuses connections, not cached responses/futures. Successful empty Offline snapshots retain the previous observed second. Failure remains Unknown. Root busy-query tests and child idle-query tests cover both directions; newer equal-second state is obtained from confirmation, not discarded. Merged #7532 squash 00209076c7a10d9e4a475466c313e8ebecf041f5 is structurally in this lineage; storage failures return before live fanout.

Local evidence (one composed leaf execution, not separate 2126-test runs per ancestor): immutable leaf 21818212e5b2b2e8cae55e07de0132ef2a74b8b3 / tree 6f3b392656e2647ede1e8656b9dcce41dee7ccef passed the full Mobile package 2126 tests, analyzer clean, formatter 555 files / 0 changed. Both child deltas are tests-only: all ancestor Mobile production/dependency/build inputs are preserved. Root consumer tests run within the composed child harness; #7383 existing Mobile tests are unchanged by #7526. Private offline lock-enforced 222 package roots, gpt_markdown 1.2.1, candidate source contents/modes and restored provider verified independently against Git objects. Evidence retained locally in WORK_LOGS/MOBILE_FEEDBACK_CLASSIFICATION_20260909/MAIN_ROOT_ENV_1089D92A/runs/equal22c6385e_v4/receipts/ and the independent review report.

Causal rollback: minimal <=< fails all six equality regressions: four child cases first fail at presence_ordering_test.dart:97, two root cases at presence_snapshot_test.dart:117, with wrong visible Online/Offline states before query-count assertions. Restoring exact provider bytes returns 22 focused passes and source/mode/binding verification succeeds. No author/test fixes were made by the independent reviewer.

Outstanding: initial exact-head CI is pending/in progress, not green; old-head CI is not evidence for these commits. Re-review and required new-head CI remain merge gates. No new Rust/Desktop runtime, simulator/native or live deployment acceptance is claimed. Structural #7532 ancestry is not a deployed relay receipt: release ownership must still establish the deployed producer guard and relay-before-client rollout/native acceptance. No merge or release clearance is requested by this evidence post.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants