Skip to content

fix(mobile): reconnect resync — surface real errors, resync worktrees, harden pong-liveness, replay notifications (#6784 #8498 #4500 #8129) - #8605

Closed
branben wants to merge 3 commits into
stablyai:mainfrom
branben:branben/sawfish
Closed

branben wants to merge 3 commits into
stablyai:mainfrom
branben:branben/sawfish

Conversation

@branben

@branben branben commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implements the four mobile-reconnect fixes documented in #8591 (and the
investigation spec). When the Orca mobile app returns from background/sleep,
it re-opens the WebSocket but previously performed no resync of
desktop-held state
— this PR closes that gap across notifications, terminal
input ownership, worktree snapshots, and real connection-error visibility.

Shipped as two isolated commits for clean review/rollback:

Fixes

Verification

Closes #8591.

Brandon Bennett added 2 commits July 13, 2026 12:22
…liveness

Implements stablyai#6784, stablyai#8498, stablyai#4500 from stablyai#8591.

- stablyai#6784: mirror the real socket error per host (getLastConnectionError +
  useLastConnectionError) and surface it via classifyConnection's hint
  instead of a canned Tailscale label.
- stablyai#8498: refetch worktrees on transition into 'connected' (not every poll
  tick), add pull-to-refresh, and write through setCachedWorktrees from the
  host screen so a reconnect can't serve a stale cached snapshot.
- stablyai#4500: reap a socket that is warm via app frames but silent on pong —
  split wsPonged from wsAlive so the liveness check releases the driver.

Extracted new hooks (use-worktree-resync, use-host-connection-state,
use-last-connection-error) to stay under the max-lines budget.

stablyai#8129 (notification replay) scoped to a follow-up bead.

Verified: pnpm run typecheck clean; vitest 23/23 (ws-transport) + 9/9
(connection-health) + 40 new lines (worktree-cache); max-lines ratchet OK.
…tablyai#8129)

Implements stablyai#8129 (the 4th fix from stablyai#8591), isolated from the
stablyai#6784/stablyai#8498/stablyai#4500 commit.

- Desktop: MobileNotificationReplayBuffer records every dispatch with a
  monotonic seq (cap 256, oldest-first evict). dispatchMobileNotification
  surfaces the seq to live listeners; getMissedNotificationsSince(seq)
  returns exactly seq > watermark -> idempotent by construction.
- Mobile: persist lastSeenSeq (AsyncStorage), send it as the catch-up
  watermark via notifications.getMissedSince on reconnect, and keep an
  in-memory seen-set keyed by id+seq so a replay re-including an already
  delivered event is dropped (defense-in-depth; a legit live re-delivery at
  a NEW seq is still allowed).
- Principal fix: deliverLive now marks events seen on every path (live +
  replay); previously only fetchMissed did, so a live-then-replay duplicate
  double-pushed. Re-verified 11/11.

Verified: pnpm run typecheck clean; vitest 7/7 (replay) + 11/11
(mobile-notifications, incl dedupe regression); max-lines ratchet OK.

No push.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1b4f50bd-e56d-4c91-bb97-16fb0cac044e

📥 Commits

Reviewing files that changed from the base of the PR and between 79d3805 and 745a118.

📒 Files selected for processing (3)
  • mobile/src/notifications/mobile-notifications.ts
  • mobile/src/transport/rpc-client.ts
  • mobile/src/transport/use-host-connection-state.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • mobile/src/transport/use-host-connection-state.ts
  • mobile/src/notifications/mobile-notifications.ts

📝 Walkthrough

Walkthrough

The mobile client now captures and surfaces socket errors in connection health verdicts, centralizes per-host connection state, and supports worktree resynchronization through reconnect-triggered fetches and pull-to-refresh. Worktree fetches update the cache. Desktop notification delivery now assigns sequence numbers and exposes replay data through RPC, while mobile subscriptions persist watermarks and deduplicate reconnect catch-up events. WebSocket heartbeat cleanup now requires protocol PONG responses in addition to inbound traffic.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers summary, fixes, and verification, but it omits several required template sections. Add the missing Screenshots, Testing checklist, AI Review Report, Security Audit, and Notes sections, following the template headings and required details.
Docstring Coverage ⚠️ Warning Docstring coverage is 39.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the reconnect-focused mobile fixes and references the main affected areas.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0c14f157-85bd-4c31-90e8-0acf8ef6e4f6

📥 Commits

Reviewing files that changed from the base of the PR and between ebc9a0a and 79d3805.

📒 Files selected for processing (20)
  • mobile/app/h/[hostId]/index.tsx
  • mobile/app/h/[hostId]/session/[worktreeId].tsx
  • mobile/app/h/[hostId]/tasks.tsx
  • mobile/app/index.tsx
  • mobile/src/cache/worktree-cache.test.ts
  • mobile/src/notifications/mobile-notifications.test.ts
  • mobile/src/notifications/mobile-notifications.ts
  • mobile/src/transport/client-context.tsx
  • mobile/src/transport/connection-health.test.ts
  • mobile/src/transport/connection-health.ts
  • mobile/src/transport/rpc-client.ts
  • mobile/src/transport/use-host-connection-state.ts
  • mobile/src/transport/use-last-connection-error.ts
  • mobile/src/transport/use-worktree-resync.ts
  • src/main/runtime/mobile-notification-replay.test.ts
  • src/main/runtime/mobile-notification-replay.ts
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/rpc/methods/notifications.ts
  • src/main/runtime/rpc/ws-transport.test.ts
  • src/main/runtime/rpc/ws-transport.ts

Comment thread mobile/src/transport/rpc-client.ts Resolved
Comment on lines +16 to +18
export type ReplayableMobileNotification = MobileNotificationEvent & {
seq: number
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: check how the mobile client reads the watermark from live vs replayed events.
rg -n "notificationSeq|lastSeenSeq|getMissedSince|\.seq\b" -g '!**/node_modules/**' mobile/src/notifications/mobile-notifications.ts mobile/src/notifications/mobile-notifications.test.ts 2>/dev/null

Repository: stablyai/orca

Length of output: 3044


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the replay implementation and the live notification path that sets the watermark field.
ast-grep outline src/main/runtime/mobile-notification-replay.ts --view expanded || true
printf '\n---\n'
ast-grep outline src/main/runtime/orca-runtime.ts --view expanded || true
printf '\n---\n'
sed -n '1,220p' src/main/runtime/mobile-notification-replay.ts
printf '\n--- ORCA RUNTIME MATCHES ---\n'
rg -n "notificationSeq|seq\b|dispatchMobileNotification|getMissedSince|record\(" src/main/runtime/orca-runtime.ts src/main/runtime/mobile-notification-replay.ts
printf '\n--- CLIENT MATCHES ---\n'
sed -n '1,220p' mobile/src/notifications/mobile-notifications.ts
printf '\n--- CLIENT REPLAY/DELIVERY LOGIC ---\n'
sed -n '300,420p' mobile/src/notifications/mobile-notifications.ts

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,120p' src/main/runtime/mobile-notification-replay.ts
printf '\n---\n'
sed -n '7350,7405p' src/main/runtime/orca-runtime.ts
printf '\n---\n'
sed -n '1,120p' mobile/src/notifications/mobile-notifications.ts
printf '\n---\n'
sed -n '330,390p' mobile/src/notifications/mobile-notifications.ts
printf '\n---\n'
sed -n '410,455p' mobile/src/notifications/mobile-notifications.test.ts
printf '\n---\n'
sed -n '470,540p' mobile/src/notifications/mobile-notifications.test.ts

Repository: stablyai/orca

Length of output: 16981


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find every place that mentions the replay watermark field or the getMissedSince RPC.
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  "getMissedSince|notificationSeq\b|ReplayableMobileNotification|seq\b" \
  src mobile shared packages

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '2038,2072p' src/main/runtime/orca-runtime.ts
printf '\n---\n'
sed -n '118,140p' mobile/src/notifications/mobile-notifications.ts
printf '\n---\n'
sed -n '320,380p' mobile/src/notifications/mobile-notifications.ts
printf '\n---\n'
sed -n '1,130p' src/main/runtime/mobile-notification-replay.test.ts

Repository: stablyai/orca

Length of output: 9796


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the replay handling in the mobile client and the RPC wiring for notifications.getMissedSince.
sed -n '380,460p' mobile/src/notifications/mobile-notifications.ts
printf '\n---\n'
rg -n "notifications.getMissedSince|getMissedNotificationsSince|mobileNotificationReplay|dispatchMobileNotification" src/main/runtime/orca-runtime.ts src/main/runtime/*.ts src/main/runtime/rpc/*.ts
printf '\n---\n'
sed -n '7370,7405p' src/main/runtime/orca-runtime.ts
printf '\n---\n'
sed -n '1,220p' src/main/runtime/runtime-rpc.ts

Repository: stablyai/orca

Length of output: 14446


Replay events need notificationSeq

ReplayableMobileNotification and record() currently stamp replayed events as seq, but the live mobile path and reconnect logic read notificationSeq. notifications.getMissedSince() therefore returns payloads the client won’t watermark or dedupe correctly. Unify the field name at the replay source, or translate it at the RPC boundary.

- stablyai#6784 (CRITICAL): rpc-client.ts ws.onerror called an undefined
  captureConnectionError(); replace with the real lastConnectionError
  assignment so the connection-error capture actually works at runtime.
- stablyai#8498: HostClientEntry now includes state (matches useAllHostClients
  shape) in use-host-connection-state.ts.
- stablyai#8129: watermark load uses Math.max(lastDeliveredSeq, seq) so an
  async storage read can't move the replay watermark backward.

C4 (replay seq vs notificationSeq) is a false positive: notificationSeq is
assigned the buffer's seq at the dispatch boundary, so the client watermark
matches the desktop cut. No change.

Verified: pnpm run typecheck clean; vitest rpc-client 29/29,
connection-health 9/9, mobile-notifications 11/11, replay 7/7;
max-lines ratchet OK.
@branben

branben commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

CodeRabbit review — triage & fixes

Thanks for the review. Triage of the 4 comments:

Fixed (3 real):

  • Critical captureConnectionError undefined (rpc-client.ts:702): confirmed — ws.onerror called a non-existent helper, so the Mobile: surface a Tailscale recommendation when the remote runtime is unreachable #6784 error-capture was dead at runtime. Replaced with the real lastConnectionError = message assignment. Verified rpc-client suite 29/29.
  • HostClientEntry missing state (use-host-connection-state.ts:5): confirmed — the real useAllHostClients() return type includes state, so the local type was incomplete. Added state: ConnectionState.
  • Watermark can move backward (mobile-notifications.ts:407): confirmed race — the async loadLastSeenSeq could overwrite a higher live lastDeliveredSeq. Now Math.max(lastDeliveredSeq, seq).

Not a bug (1 false positive):

  • Replay seq vs notificationSeq (mobile-notification-replay.ts:18): the concern that the client "won't watermark or dedupe correctly" is incorrect. In orca-runtime.ts, dispatchMobileNotification assigns notificationSeq = seq (the replay buffer's monotonic seq) to the live listener, and getMissedNotificationsSince cuts by that same seq. So the client's lastDeliveredSeq (driven by notificationSeq) and the desktop's cut (by seq) are the same value space — the watermark matches. notificationSeq is preserved on replayed events alongside the internal seq, so dedup works. Tests confirm: replay 7/7 + mobile-notifications 11/11 (incl. the dedupe regression).

All three fixes are in commit 745a1186f, pushed. Full verification: pnpm run typecheck clean; vitest 29/29 + 9/9 + 11/11 + 7/7; check-max-lines-ratchet OK.

nwparker added a commit that referenced this pull request Jul 14, 2026
… reconnect (#8498 #8129)

Take-over of #8605 (issue #8591). Ships #8498 (worktree resync + pull-to-refresh + cache write-through) and #8129 (idempotent notification replay on reconnect). Fixes the original PR's field mismatch (seq vs notificationSeq) and adds the missing notifications.getMissedSince mobile RPC allowlist entry. #6784 and #4500 held back to avoid conflicting with the relay work (#8536). Co-authored-by: Brandon Bennett (@branben).
@nwparker

Copy link
Copy Markdown
Contributor

Thank you for this, @branben — the root-cause investigation in #8591 and the architecture here were genuinely excellent, and the idempotent-replay design in particular was the right shape.

I've taken the two fixes that are clear of the in-flight relay work (#8536) and shipped them, corrected, in #8690 (now merged to main):

A few things I found in review, for future reference:

  • The mobile app is typechecked separately (mobile.yml → cd mobile && npx tsc --noEmit), not by the root pnpm typecheck. Running it surfaced three breakages in the Mobile: surface a Tailscale recommendation when the remote runtime is unreachable #6784 files: use-host-connection-state.ts imported ./transport/types / ./transport/rpc-client (→ nonexistent paths) and use-last-connection-error.ts imported useCtx, which isn't exported from client-context (runtime crash on the host/session/tasks screens).
  • The replay path returned seq, but the live fan-out and the client both use notificationSeq, so a replay-only delivery never advanced/persisted the watermark — a latent duplicate-notification regression (the [Bug]: Mobile app shows duplicate notifications for every agent completion #5070 class). The CodeRabbit comment that flagged this was actually correct; fixed by making the buffer speak notificationSeq end-to-end.
  • notifications.getMissedSince also needed a MOBILE_RPC_METHOD_ALLOWLIST entry, or it's rejected at runtime before dispatch.

I held #6784 and #4500 back on purpose: they live in the connection-health / ws-heartbeat surface that the relay PR #8536 is actively rewriting, so changing them now would conflict. (#6784's raw-error approach was also a bit different from what the issue asks for — a Tailscale recommendation.)

Closing in favor of #8690 — but huge thanks, this was a high-quality contribution.

@nwparker nwparker closed this Jul 14, 2026
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
… reconnect (stablyai#8498 stablyai#8129)

Take-over of stablyai#8605 (issue stablyai#8591). Ships stablyai#8498 (worktree resync + pull-to-refresh + cache write-through) and stablyai#8129 (idempotent notification replay on reconnect). Fixes the original PR's field mismatch (seq vs notificationSeq) and adds the missing notifications.getMissedSince mobile RPC allowlist entry. stablyai#6784 and stablyai#4500 held back to avoid conflicting with the relay work (stablyai#8536). Co-authored-by: Brandon Bennett (@branben).
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.

Mobile reconnect is socket-only: missed notifications, stuck terminal driver, stale worktree list (#8129 #4500 #8498 #6784)

2 participants