Skip to content

Fix mobile browser frames after reconnect (STA-4128) - #16122

Closed
brennanb2025 wants to merge 4 commits into
mainfrom
brennanb2025/sta-4128-mobile-pane-freeze
Closed

brennanb2025 wants to merge 4 commits into
mainfrom
brennanb2025/sta-4128-mobile-pane-freeze

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​347 $\color{#cf222e}{\Huge{\mathbf{−}}}$​24 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​323
Prod 2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​189 $\color{#cf222e}{\Huge{\mathbf{−}}}$​88 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​101

Author: @BrennanKB5

ELI5

When mobile reconnects while the next browser screenshot is still loading offscreen, that hidden image can stop reporting that it loaded. Orca now replaces only that stuck hidden image and keeps the last good screenshot visible until a fresh frame is ready.

In plainer words: you have a web page open in a browser tab on Orca Mobile. Your phone drops the
connection for a moment — a lift, a cell handoff, switching between Wi-Fi and data — and comes back.
If the drop lands in the split second a new screenshot of the page is being decoded, that decode
never finishes and never reports back. Every screenshot after it goes into the same dead slot, so the
picture on your screen freezes on the last frame for the rest of that tab's life. The page is
still live on the other side; you just never see it change again, and the only way out is closing and
reopening the tab.

Now Orca notices that one stuck slot, replaces just it, and keeps the last good picture on screen
until a fresh one is ready — so there is no blank flash either.

User-facing before / after

Before After
Your phone reconnects while a browser screenshot is mid-decode The browser tab freezes on the last picture permanently. Scrolling and clicks still go through, but the view never updates again The stuck slot is replaced and live updates resume
Your phone reconnects with nothing in flight Worked fine Unchanged — Orca does nothing at all
While the replacement picture loads — The last good screenshot stays on screen; no blank flash
The connection moves between a relay and a direct link The same freeze was possible Handled the same way

When it bites

Orca Mobile users with a browser tab open, whenever the connection drops and returns — cellular
handoffs, Wi-Fi/data switches, backgrounding and reopening the app — and only when the drop happens
to land in the moment a screenshot is decoding.

What Changed

  • Track each mobile browser image layer's URI independently so a reconnect render cannot move an unready hidden frame onto the visible layer.
  • On an authenticated connected boundary, remount only an unresolved hidden decoder and fence late onLoad/onError callbacks by decoder epoch.
  • Keep healthy reconnects inert and retain the existing logical browser.screencast subscription.
  • Add deterministic pane, physical reconnect, and relay/direct migration coverage plus the mobile-browser.frame-continuity reliability gate.

Why

Current main already replays retained subscriptions after a physical socket reconnect and attaches them to the replacement session during logical relay/direct migration. The remaining freeze is in the React Native double buffer: if the hidden Image misses onLoad, every later frame continues updating that hidden layer while the last decoded layer remains visible forever.

This supersedes #14115:

  1. It leaves transport replay in the transport instead of restarting the pane's logical subscription.
  2. It repairs the wedged native decoder while preserving the last decoded frame per layer.
  3. It rejects stale callbacks from the discarded decoder and performs zero work when no decode is pending.
  4. Its acceptance test reproduces the frozen render surface directly, while separate controls prove physical and both logical migration directions already attach the stream exactly once.

The implementation commit credits @blade035 as co-author for the community contribution that informed this fix.

Linked Issue

Fixes #14274

Linear: STA-4128

Visual Proof

Blocked. The iOS simulator loaded this worktree's mobile app, paired to the isolated development runtime, opened the target worktree, and opened a live browser tab; however, the unsigned local runtime exposes no Relay path and there is no safely scoped emulator control for a physical socket drop while preserving the mounted pane. No screenshot is attached because it would not prove the transition, and Android/live-network validation remains uncollected.

Testing

  • I manually tested these changes locally — partial iOS smoke only; the target transition is blocked as described above

  • Automated tests added/updated

  • Pre-fix acceptance: failed at the hidden native Image identity assertion.

  • Candidate acceptance: passed.

  • Candidate with only the reconnect repair disabled: failed again at the same identity assertion.

  • cd mobile && pnpm vitest run --config vitest.config.ts src/browser/mobile-browser-frameless-stream.test.tsx src/transport/rpc-client-terminal-reconnect.test.ts src/transport/stable-logical-rpc-client.test.ts — 25 passed.

  • pnpm vitest run --config config/vitest.config.ts config/scripts/check-reliability-gates.test.mjs — 24 passed.

  • pnpm typecheck — passed.

  • node config/scripts/check-changed-code-quality.mjs — passed with 0 findings.

  • node config/scripts/check-react-doctor-changed.mjs — passed with 0 issues.

  • pnpm check:max-lines-ratchet and pnpm check:reliability-gates — passed.

  • oxfmt check and git diff --check — passed.

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • No wire payload, opcode, RPC, host publication, path handling, keyboard behavior, or process-liveness semantics changed.
  • Folder workspaces, SSH/WSL hosts, Git providers, and mixed client/host versions retain their existing behavior because the repair observes only the already-authenticated client state boundary.

Checklist

  • This PR is small and focused
  • I explained what changed and why (including ELI5)
  • Before/after screenshots or videos attached for UI changes — blocked for the target transition as documented above
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (N/A: no affected boundary)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass — scoped lint/typecheck/tests pass; full test/build left to CI

Co-authored-by: blade035 <blade035@hotmail.com>
@coderabbitai

coderabbitai Bot commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds the experimental mobile-browser.frame-continuity reliability gate. MobileBrowserPane now uses per-layer frame URIs and decoder epochs to reject stale image events, replace only pending decoders after reconnect, and render both keyed layers independently. Tests cover reconnect replay, transport migration, frame promotion, stale callbacks, subscription stability, and healthy connected-state boundaries. The reliability gate records validation commands, evidence, performance limits, promotion criteria, known gaps, and demotion rules.

Merge Risk: 🟡 Moderate · up to de318

A reconnect can still display a stale or unready mobile browser frame if an older image callback arrives after a newer frame replaces it, undermining the freeze fix. The migration test also does not verify that binary frames reach the replacement session, so the PR is not merge-ready until the callback fencing and targeted test assertions are addressed.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address the linked reconnect freeze, preserve transport and input behavior, and add coverage for physical and relay/direct migration paths.
Out of Scope Changes check ✅ Passed The production changes, tests, and reliability gate are directly related to mobile browser frame continuity after reconnect.
Docstring Coverage ✅ Passed Docstring check was indeterminate for this PR — some files could not be analyzed in time. Not blocking.
Title check ✅ Passed The title clearly and concisely describes the primary change: fixing mobile browser frames after reconnect. The linked issue identifier adds useful context.
Description check ✅ Passed The description is detailed and follows the required structure. It explains the user impact, implementation, rationale, linked issue, testing, limitations, and scope. It explicitly documents that visu…
Full details: Description check

Explanation

The description is detailed and follows the required structure. It explains the user impact, implementation, rationale, linked issue, testing, limitations, and scope. It explicitly documents that visual proof and full local lint, test, and build runs are unavailable or deferred to CI.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
config/reliability-gates.jsonc (1)

3-3: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Bump the file-level updatedAt to cover the newly added evidence date.

The file's top-level updatedAt is "2026-08-20" (Line 3), but the new mobile-browser.frame-continuity gate's evidenceRuns entry is dated "2026-08-23" (Line 3251), three days later than the declared update timestamp. If updatedAt is meant to reflect the most recent change to this manifest, it is now stale relative to content this same change introduces.

📅 Suggested fix
   "schemaVersion": 1,
-  "updatedAt": "2026-08-20",
+  "updatedAt": "2026-08-23",

Also applies to: 3251-3258


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 92d7099c-c131-462f-844a-5a477c55fdc7

📥 Commits

Reviewing files that changed from the base of the PR and between e3327c2 and de31813.

📒 Files selected for processing (5)
  • config/reliability-gates.jsonc
  • mobile/src/browser/MobileBrowserPane.tsx
  • mobile/src/browser/mobile-browser-frameless-stream.test.tsx
  • mobile/src/transport/rpc-client-terminal-reconnect.test.ts
  • mobile/src/transport/stable-logical-rpc-client.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review.

Comment thread mobile/src/browser/MobileBrowserPane.tsx
Comment thread mobile/src/transport/stable-logical-rpc-client.test.ts
@blade035

Copy link
Copy Markdown
Contributor

From the #14115 author — your decoder-wedge diagnosis also explains why the subscription-restart approach appeared to work: remounting the pane's logical subscription at the reconnect boundary implicitly remounts the hidden Image layer, so the wedged decoder gets replaced as a side effect. Same symptom relieved with a heavier hammer; the layer-epoch fencing targets the actual failure and preserves the last decoded frame. #14115 can be closed once this lands.

…stener

The shape-only assertion passed on a no-op callback, which is the dropped-frame
bug this migration path exists to fix. Also pin what a superseded frame's late
onLoad promotes.
# Conflicts:
#	mobile/src/browser/MobileBrowserPane.tsx
@blade035

Copy link
Copy Markdown
Contributor

Thanks a lot for the co-author credit 🙏 Glad my earlier attempt (#14115) could inform the official fix — happy to help verify the reconnect behavior on the mobile browser build if useful, I hit this flow daily.

@Jinwoo-H

Copy link
Copy Markdown
Contributor

Closing as superseded by #22392 (now on main), which fixes the same freeze at its root. The pane no longer re-points a still-decoding hidden layer, which is what lost the onLoad and sent every later frame into the dead slot. Each layer now tracks the source its Image holds, and a load only counts if it matches that source, so late callbacks can't flip the wrong frame. #22694 also restarts the stream on every return to the app. Thanks @BrennanKB5! Your analysis of the double-buffer freeze matched what we reproduced on an Android emulator. If you still see a frozen pane after a reconnect on current main, please reopen with the repro.

@Jinwoo-H Jinwoo-H closed this Sep 24, 2026
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.

[Bug]: Mobile browser pane freezes on the last frame after a relay/direct-socket transport reconnect

3 participants