Skip to content

Remote pane renders blank forever when the client starts while its paired runtime is unreachable #18495

Closed

Description

@luisgonzaleznf

Symptom

A terminal pane for a paired runtime environment renders completely blank — dark background, a single cursor block, no text — with no error banner and no retry. It stays blank indefinitely, including after the runtime becomes reachable again.

Observed live: a MacBook client paired to a Mac Mini runtime over Tailscale. The tunnel was down for ~10h after a macOS update left Tailscale stopped, and the client happened to start during the outage.

What made it confusing

The host side was healthy the entire time:

$ orca terminal show --environment "Mac Mini" --terminal term_ad84b983-...
connected: true
writable: true
preview: <real Claude Code TUI content>

orca terminal read --screen returned real content too. So the RPC pull path worked while the streamed pane showed nothing.

Also true, and each one a red herring in its own way:

  • Focusing the tab did nothing.
  • Resizing the pane (SIGWINCH) did nothing.
  • The pty stayed at ~53 columns throughout — a mounted xterm drives its pty's dimensions, so this is the tell that no client ever attached.
  • Creating a new pane worked fine, and recreating the session fixed it. The spawn path was never broken; only re-binding a pre-existing pane was.

Root cause

A client that starts while its runtime is unreachable attaches to already-open panes rather than creating them, so the failure lands in attach() rather than connect().

attach() never sets connected, so in handleRemoteTerminalError the recoverable branch calls scheduleResubscribeAfterTransportClose(), which returns immediately at its own guard:

if (destroyed || !connected || !handle) { return }

The return that follows skips connecting = false, emitRecoveryState() and surfaceErrorMessage(). getRecoveryState() therefore reports 'connecting', which the banner filter drops — so there is no error surface, no backoff, no parked retry, and no Reconnect button.

connect() handles the identical error class by arming scheduleConnectRetryAfterRecoverableFailure(). That asymmetry is the bug.

Two follow-on issues in the same path:

  1. attach() cancels recovery on entry, and its catch cancelled unconditionally, so a replay restarts the 250ms..30s ladder and the auto-recovery deadline never latches. connect() cancels only on the gone and fatal paths.
  2. useRemoteRuntimeRecoveryTriggers fires only on online and system-resume. Neither fires when a tailnet or VPN returns: navigator.onLine never went false (the LAN never dropped) and there is no power transition. So even a correctly parked pane waits for a manual Reconnect.

Repro

  1. Quit the client.
  2. Make the paired runtime unreachable (disconnect Tailscale/VPN).
  3. Start the client and let it restore a worktree that has remote panes.
  4. Restore reachability.

Expected: panes reattach, or at minimum show a Reconnect affordance.
Actual: tabs are present, panes are permanently blank, no error, no retry.

Impact

The ordering is ordinary — laptop boots, the client restores its session, the VPN connects a few seconds later. A short outage usually hides it; a long one makes it permanent. The failure is silent, and the recovery a user naturally reaches for (closing the pane) can take a live agent session with it.

Fix

PR incoming. Unit-testable without a live remote; verified red/green (expected 'connecting' to be 'backoff' before the fix).

Activity

  1. tian2-ai commented on Sep 18, 2026

    @tian2-ai

    Adding a concrete, repeatable trigger for this, measured on a fleet that keeps hitting it.

    The race is with Orca's own auto-update restart

    macOS client paired to a macOS host over a tailnet. From the client's main.trace.ndjson:

    13:27:57  updater_update_downloaded (1.4.204)
    13:27:59  updater_quit_and_install_started
    13:28:07  main_process_lifecycle_started   <- client is back up; restores the
                                                  workspace and attaches remote panes
    13:28:56  host runtime startedAt           <- 49 seconds later
    

    The client relaunched itself from an auto-update and began attaching to already-open remote panes while the host runtime did not yet exist. That is exactly the window this issue describes — except nobody disconnected anything. The updater created the window.

    This matters because it is systematic rather than incidental: the two machines are on independent auto-update cadences (client was on 1.4.204, host on 1.4.205), so every update round is another chance to hit it, and the affected panes then stay blank permanently.

    Corroborating evidence, using this issue's own pty-width tell

    pane pty geometry reading
    blank pane 48 x 195 — the host's window width, unchanged for a day no client ever attached
    sibling pane, same workspace 40 x 101 — the client's width client attached normally

    The sibling pane had been created fresh after the client was already connected, so it went through connect() rather than attach(). Same workspace, same host, same client, opposite outcome — which lines up with the connect()/attach() asymmetry in the root cause above.

    Other notes from this instance

    If #18496 lands, its retry would presumably cover this case, since the runtime does become reachable ~49s later. A smaller independent mitigation: hold off attaching remote panes until the environment connection has settled after an updater-initiated relaunch, since that relaunch is a moment the app already knows about.

  2. added
    bugSomething isn't working
    os:macosIssues affecting macOS
    orca-remote-serverRemote Orca Server / orca serve / paired remote runtime
    on Sep 19, 2026
  3. added theissue type on Sep 19, 2026
  4. luisgonzaleznf commented on Sep 21, 2026

    @luisgonzaleznf
    ContributorAuthor

    Thank you — the updater trace is the part I didn't have, and it changes how this should be prioritised.

    My repro needed someone to disconnect a VPN by hand, which made the whole thing look incidental. Your timeline makes it structural:

    13:27:59  updater_quit_and_install_started
    13:28:07  client back up, attaching remote panes
    13:28:56  host runtime startedAt
    

    The app relaunches itself into exactly the window this issue describes, and the two machines are on independent update cadences, so there is no version pairing a user can choose that avoids it. Every update round is another roll.

    The geometry table settles the mechanism, too. 48x195 (host fit, unchanged for a day) on the blank pane against 40x101 (client fit) on a sibling in the same workspace is the connect()/attach() split reproduced on hardware I've never touched: the sibling was created after the client was already connected, so it went through connect() and got the retry; the blank one went through attach() and got the silent latch.

    Whether #18496 covers this

    I believe it does, and by a wider margin than I would have guessed before checking the numbers.

    The auto-recovery window is derived from the ladder rather than hand-tuned (REMOTE_RUNTIME_AUTO_RECOVERY_TIMEOUT_MS):

    ladder sum (250ms .. 30s)   =  60,750 ms
    + 8 attempts x 15s budget   = 120,000 ms
                                = 180,750 ms   (~3 min)
    

    Your 49s gap sits well inside that. With #18496 armed, the attach failure schedules a retry instead of latching silently, and the pane should reattach on its own within about a minute of the host coming up at 13:28:56 — worst case on the 4th or 5th rung, sooner if the attempts fail fast rather than burning the full 15s budget. The environment-reachable trigger is then a fast path, not the thing rescuing it. So this case should never reach a user-visible disconnected, let alone a permanent blank.

    Caveat, and it's the same one in the PR body: the unit tests prove the transport state machine, not the end-to-end render. I have not run the deliberate repro against a build carrying the patch, for either the VPN case or yours. If you can reproduce this across update rounds on a fleet, that is a far better test bed than my manual repro — happy to hand you a build to confirm before it merges.

    On deferring the attach

    Worth having, but I'd treat it as defence in depth rather than the fix. The retry is general: it covers every cause of the window, including the ones the app cannot see coming — a sleeping host, a slow tailnet, the VPN case I filed. Holding the attach until the connection settles only helps the one cause the app knows about in advance. If the retry lands and behaves as above, the updater case should already be invisible; if it isn't, that measurement is the argument for the deferral.

    One label note

    os:macos was applied here yesterday, but your report has Windows clients hitting this as well, and Codex TUIs alongside Claude Code. The defect is in the shared transport path — nothing in attach() is platform-specific. Probably worth dropping that label so it doesn't filter out of Windows triage.

    Agreed that #21208 is a different state and should stay separate — you've already made that case better than I could.

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

    bugSomething isn't workingorca-remote-serverRemote Orca Server / orca serve / paired remote runtimeos:macosIssues affecting macOS

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions