Skip to content

Phase 3.5: fix the top SSH/remote issues at the root cause - #26483

Merged
OrcaWin merged 27 commits into
mainfrom
OrcaWin/phase35
Oct 8, 2026
Merged

OrcaWin merged 27 commits into
mainfrom
OrcaWin/phase35

Conversation

@OrcaWin

@OrcaWin OrcaWin commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 146 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​6780 $\color{#cf222e}{\Huge{\mathbf{−}}}$​505 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​6275
Prod 301 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​4956 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1199 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​3757

ELI5

Phase 3.5 fixes the SSH and remote-server bugs that hurt the most people, at their root cause instead of one symptom at a time. We went through all 1,501 open issues and grouped them into 40 root-cause clusters rated P0 or P1. To keep this landable, the scope was cut to the 4 P0 clusters plus 9 high-reach "P0.5" clusters. The other clusters are parked for later phases. This PR lands the 14 fix PRs below. Twelve came from the triage. Two more (#26424 and #26433) fix problems the integrated real-host test found. Each was reviewed and merged on its own into the OrcaWin/phase35 integration branch, except the CI fix (#26335), which went straight to main.

What Changed

Each section gives the before and after as the user sees it, the mechanism that changed, and why that approach was picked.

1. One owner for a remote pane's reconnect (#26308)

  • Before: after a network hiccup, a terminal on a remote machine could spin in "connecting" forever, lose what you typed, or reload itself over and over. Two parts of the app both thought they owned reconnecting, so they raced.
  • After: the connection to the remote machine owns reconnecting, and the pane waits for it. If the host says the terminal is gone, the pane ends cleanly. Keys typed while the same terminal recovers are held and delivered once, in order. If the pane comes back on a different shell, they are dropped so they never run in the wrong shell.
  • Mechanism: the remote pane lookup now answers resolved | absent | unavailable instead of null, and every attach path reaches a final state. The connection reports ownsRecovery(), and the pane-level remount paths step aside while it does. The held keys reuse the existing pre-connect input buffer.
  • Why: each symptom came from one specific wrong exit path or from two owners of recovery. Fixing those paths directly was much smaller and safer than rewriting the transport's state flags. Only the host's own "not found" counts as the terminal ending; lost contact never does. Renderer-only, no wire change.

Fixes #21344
Fixes #25784
Fixes #20802
Fixes #21195
Fixes #20159
Fixes #20923
Fixes #18495

2. The orca command on an SSH host is a guest, not the owner (#26337)

  • Before: the orca command inside an SSH terminal had the owner's full power over the local app. From a sandboxed VM it could read and type into your local terminals, drive your other SSH hosts' terminals, and click and type on your Mac with orca computer.
  • After: by default it only sees and uses its own host's terminals, and a worker there can still report back to the coordinator that dispatched it. One per-host switch (Settings → SSH → host → Advanced) gives it more, but never desktop control, accounts, settings writes, skill installs, pairing, SSH connections, updates or network tunnels.
  • Mechanism: every RPC method now declares a permission (workspace, desktop-control, accounts-admin, settings-write, skills-admin, pairing-admin or host-admin), and the field is required, so a new method without one fails the typecheck. One check in the RPC dispatcher compares it with the caller (owner, phone, paired client, or SSH bridge) on every transport. The SSH bridge no longer gets the owner token: each invocation gets a short-lived credential bound to that SSH target, revoked when the command exits. Paired clients get an explicit allowlist.
  • Why: the root cause was not one method, it was that the bridge authenticated as the owner with no policy on that path. Per-method checks are easy to forget and cannot tell an SSH guest from the owner's own CLI. Nothing new crosses the wire.

Fixes #23835
Fixes #25259

3. Terminal daemon: clean launch environment, safe message handling, socket path that fits (#26306)

  • Before: on Linux and SSH hosts, four things could quietly kill every terminal: Chromium's DBUS_SESSION_BUS_ADDRESS=disabled: leaked into the daemon and every shell; the daemon used a systemd scope that dies ~10 s after the last SSH login when lingering is off; one malformed client message crashed the whole daemon; and a long data folder made the socket path too long, so terminal persistence was silently off.
  • After: the disabled: marker is dropped (a real address is kept). The scope is used only when lingering is on or Orca already runs inside the user manager; otherwise Orca logs daemon-scope-unavailable: linger off with the fix. A bad message closes only that one connection. A socket path that would overflow moves to a private, validated directory under /tmp.
  • Mechanism: one env helper used by every env builder; a loginctl linger check; small type guards on the hello and request envelope plus a .catch on dispatch; getDaemonSocketPath relocates only on overflow and validates both directory levels before any connect.
  • Why: each fix sits where the daemon is launched or talks to clients, so it covers every caller. Requiring linger outright would have removed restart protection from desktop users. Relocating only on overflow leaves every existing host on its current path. No wire change.

Fixes #25580
Fixes #21119
Fixes #24201
Fixes #17841
Fixes #17840

4. Remote scrollback survives reconnects (#26319)

  • Before: in a terminal on an SSH host or paired server, scrolling up often found nothing. A desktop client got only the current screen, and every repaint from the server (reconnect, reopening a tab, dropped output) first erased the client's own history.
  • After: desktop clients receive up to 1,000 rows of history when they subscribe, like phones already did. A repaint that carries no history repaints only the screen and keeps everything above it. A repaint that does carry history still replaces it, so nothing is duplicated.
  • Mechanism: the host sends every subscriber through the existing row ladder (1000 → 0) and adds an optional scrollbackRows field to the snapshot-start frame. The client leaves out the clear-scrollback sequence when that field is missing or 0.
  • Why: one new optional field, no new opcode, so mixed versions work in both directions. Sending full history on every overflow recovery would grow the frame that is trying to catch up, and would still erase anything older than 1,000 rows.

Fixes #14593
Fixes #20158
Fixes #6106

5. An SSH terminal no longer freezes after an abandoned catch-up (#26341)

  • Before: if Orca gave up on replaying missed output for an SSH terminal halfway (for example when a worktree was revealed during a reattach), the terminal froze for good. The remote program sat blocked on write() until Orca restarted.
  • After: the terminal keeps flowing; output already shown stays.
  • Mechanism: SSH output is flow-controlled with credit. A canceled delivery left a bookkeeping entry that every later "drawn" receipt tripped over, so credit was never returned. The output ledger now ignores settle and transfer calls for a delivery that is already closed.
  • Why: dropping the entries early would return credit for output not yet drawn. A no-op for a closed delivery keeps per-span accounting exact and covers every path that closes a delivery. Client-only, no wire change.

Fixes #24367

6. Removing a project only touches the host you picked (#26340)

  • Before: the same project can exist on two machines with one ID. Removing one copy sometimes looked the project up by ID alone and removed the other machine's copy, killing its terminals and purging its workspaces.
  • After: every removal names its host and matches that host's row exactly. If that host has no row, nothing is removed.
  • Mechanism: the host is now required in the removal type, so the compiler rejects any caller without one; the callers it found (cloud-VM cleanup, group deletion, the sidebar) were fixed. The saved catalog refuses an ID-only removal when two hosts share the ID.
  • Why: the root cause was the bare-ID fallback, not one caller. Making the host required closes the whole class instead of guarding callers one by one. No wire change.

Fixes #13071

7. orca serve loads SSH hosts, and Quit on a serve window only closes the window (#26348)

  • Before: orca serve never loaded its SSH hosts, so paired clients saw an empty list and ssh.connect failed. Quitting a desktop window on a machine running orca serve stopped the whole server and dropped every paired client.
  • After: a serve whose profile has SSH hosts runs on the host that loads them, and they work. Under serve, every Quit route (menu, Cmd-Q, tray, Dock, app switcher) closes the desktop windows only; signals, updates and shutdown still exit.
  • Mechanism: the Electron serve registers the SSH layer before it starts listening. Serve host selection reads the profile read-only and picks the Electron host when it has SSH targets (failing closed to Electron if it cannot tell). A macOS before-quit guard cancels unmarked quits; every process quit Orca starts is marked. Serve now reclaims SIGINT/SIGTERM/SIGHUP from Electron's native handler.
  • Why: porting the SSH stack to the headless server is a separate project (about 14 modules import Electron), so picking the host that already works fixes it today. Marking process quits is the only way to tell them apart from a user quit on macOS.

Fixes #15537
Fixes #25886

8. Agent session history works on headless server hosts (#26309)

  • Before: on a host running the headless Orca server, the agent session history panel was always empty with "The session scanner is missing from this Orca install."
  • After: it lists the host's Claude, Codex, Gemini, Cursor and OpenCode sessions.
  • Mechanism: the server build now builds every helper process from one list, and the scanner is on it and in the shipped-artifact manifest, so a missing file fails at startup instead of at first use. The "where is the helper" lookup now decides by "is there an app archive", in one shared helper, instead of "is it packaged". The build smoke-tests the scanner.
  • Why: running the scan inside the server would put large transcript parsing on the loop that serves terminals. Shipping the existing helper reuses its protocol unchanged.

Fixes #24105

9. ARM Linux servers stop dropping clients every 15 seconds (#26200)

  • Before: on some 64-bit ARM Linux machines (Raspberry Pi 4, OrangePi 5), every connected desktop and phone dropped and reconnected at each 15-second heartbeat.
  • After: connections stay up.
  • Mechanism: a pnpm patch to ws@8.22.0 writes an empty frame as one write (the header) instead of the header plus a zero-byte write, which fails with EFAULT on those kernels under Electron and kills the socket. The bytes on the wire are identical.
  • Why: fixing only Orca's own heartbeat would leave the library's automatic reply to older and third-party clients' empty pings broken. One fix at the single place that writes covers both.

Fixes #19876
Fixes #20673
Fixes #23398

10. The terminal tail stays cheap under redraw floods (#26316)

  • Before: a full-screen agent that redraws many times a second over SSH (Pi's ask_user panel) could pin the main process at 100% CPU and freeze Orca, and a restart replayed the output and froze it again.
  • After: updating the main process's short text copy of each terminal (used for previews, terminal read and agent-wait checks) costs about as much as the new output. The retained text is unchanged.
  • Mechanism: rows stay flat strings written one run at a time with cached trimmed text; rows over the line cap are dropped in batches; the redraw window uses the cursor's net reach rather than the sum of every cursor-up.
  • Why: making the model linear fixes every path that feeds it (SSH, local, restore). Clamping row width was rejected because it would change what terminal read returns. No wire change.

Fixes #11315

11. The SSH relay fallback runs Orca's own Node first, never a host npm install by default (#26303)

  • Before: when Orca could not run its managed server on an SSH host, the relay fallback used the host's own Node.js and compiled terminal support there with npm. That needs a compiler, internet, a POSIX login shell and working npm, and many hosts have none, so users saw "relay cannot find node-pty" and failed connects on offline or non-admin hosts.
  • After: the fallback first tries Orca's pinned Node with prebuilt add-ons (then a glibc 2.17 compat runtime, then a host Node 18+ with prebuilds, no npm). Wherever one runs, nothing is compiled on the host. If all three are refused, the connect does exactly what it did before, marked "unsupported configuration" on the host card. Plain SSH terminals plus SFTP are used only when that old path itself answers with a failure.
  • Mechanism: the default relay runtime is now pinned-node. A step down happens only on an answer from the host; lost transport stays retryable. Refusals the user can fix (missing library, security software, Windows application control) are remembered for 24 hours. NixOS's stub loader is classified so the connect steps on to the host's Nix Node.
  • Why: the rule is "no host does worse than before this PR". A table test checks it across 1,680 host combinations plus named review cases.

Fixes #20386
Fixes #22159
Fixes #23554
Fixes #24226

12. orca serve and orca open use the profile you chose (#26424)

  • Before: ORCA_USER_DATA_PATH=<folder> orca serve checked that folder to decide how to serve, then started Electron on the default profile. A packaged Orca reads only --user-data-dir. If your own Orca was open, the new server hit its single-instance lock, asked it to show its window, and quit. orca open had the same split, and on macOS it brought your running app to the front.
  • After: the chosen folder is the folder the server and app use. A serve that ends up on any other profile prints why and exits before it takes the instance lock, so it never contacts another Orca. On macOS, a non-default profile opens as a new instance; the default profile reopens exactly as before.
  • Mechanism: the CLI resolves the profile once, as an absolute path, and passes it to the serve selector, to orcad, and to Electron as --user-data-dir (dev builds also get ORCA_DEV_USER_DATA_PATH). It also passes ORCA_SERVE_USER_DATA_PATH, which Electron compares with its settled profile before the lock, then removes so terminals don't inherit it.
  • Why: pinning at launch fixes the actual split. The check before the lock is the safety net if a future launch path forgets to pin. Making Electron honor ORCA_USER_DATA_PATH on its own was rejected: Orca terminals pass that variable down, so a desktop app launched from one would take over another instance's profile.

Found by the Phase 3.5 integrated test (finding F4); there is no GitHub issue.

13. Open terminal tabs reattach after a managed server update (#26433)

  • Before: after a managed server update that restarts it while terminals are open (for example orca environment update --environment <host> --force), every open terminal tab showed "Remote runtime disconnected — Automatic retries stopped" and dropped typed keys. Reconnect, switching tabs and SSH disconnect/connect did not help; only quitting Orca did. The terminals on the host were fine the whole time.
  • After: open tabs show "Reconnecting to remote runtime" for a few seconds, then reattach to the same running terminals with their scrollback, and typed keys reach the shell.
  • Mechanism: the update gives the server a new pairing. Each tab used to pin the pairing it opened with and treat "pairing changed" as fatal. Now a tab moves to the new pairing only when the saved environments prove it is the same machine (same SSH host registration and the same server host key). It treats the refusal as lost contact and retries within its normal limit. Streams still on the old pairing are closed as recoverable drops instead of silently refusing to send. If the same environment id now points at a different machine (new host key), its terminal streams end before the new pairing is published, and tabs never retry on it. The renderer also ignores a 0×0 size hold from a server that just restarted, which used to shrink the visible pane to one cell.
  • Why: rotating the pairing is how a re-paired server is fenced, so terminal tabs now follow it the way other parts of the app already do, but only when it is proven to be the same machine. Lost contact never marks a terminal exited. Renderer-only, no wire change. This was not a Phase 3.5 regression: the pinning already existed in Phase 3.

Found by the Phase 3.5 integrated test (finding F2) and re-verified on a real host; there is no GitHub issue.

14. CI: apt off the Azure mirror (#26335)

  • Before: E2E jobs failed intermittently installing build tools, because apt kept fetching from a flaky mirror. The redirect only edited the source lists, but current Ubuntu runner images read mirrors from /etc/apt/apt-mirrors.txt.
  • After / mechanism: the step also rewrites that mirror list and retries the install once with --fix-missing.
  • Why: it fixes where the mirror comes from, not just adds retries. This one merged straight into main, so it adds no diff here.

Elegance pass (merged)

After the fixes landed, a cleanup pass made 22 verified refactors across the Phase 3.5 areas, for −407 net lines. None changes behavior: each was checked against the existing tests and reviewed clean. All five PRs are merged into the integration branch:

PR Area Added Deleted Net
#26474 SSH relay runtime ladder 245 456 −211
#26471 RPC caller permissions 158 153 +5
#26470 Remote pane binding 115 180 −65
#26465 Terminal tail 31 68 −37
#26478 Serve, daemon and misc. Also deletes the id-only repos:remove IPC and Store.removeProject, now that every caller removes a project by host 280 379 −99

Why

Most of these bugs had been patched symptom by symptom before and kept coming back. Each fix here targets the shared cause behind a cluster of issues, keeps the client and host compatible across versions (only optional fields, no new stream opcodes), and follows the SSH rule that losing contact is never treated as proof that a process ended.

Review loop. Every fix PR was reviewed by both Opus and Astra, looking only for P0 and P1 problems, and every PR was clean at its final head before merging into the integration branch. Some took several rounds: #26303 took 8 rounds to reach the "never worse than before" rule across every host type, and #26316 took 4.

Known limits (from the PR bodies):

Linked Issue

All Fixes lines are listed under each section above.

Visual Proof

N/A. The only UI changes are one switch under SSH host → Advanced (#26337) and the "unsupported configuration" notice on the SSH host card (#26303). Everything else is connection, process and packaging behavior, covered per PR by tests and the real-host runs described in each PR.

Testing

Integrated real-host test (2026-10-08): 8 of 9 scenarios passed. It used packaged macOS arm64 builds of the integration branch at three heads, against fresh Debian arm64 SSH containers, on both the managed-server and relay routes. Apps ran hidden with an isolated home and profiles.

# Scenario Result
1 Fresh SSH host becomes a managed server host Pass (12.6 s)
2 Three terminals through a killed connection and a 55 s network outage: no stuck panes, input typed after the drop is noticed delivered exactly once, full scrollback kept (#26308, #26319) Pass
3 Full-history redraw flood (50 MB + 600 repaint frames) on the managed and relay routes (#26316, #26341) Pass: no main or renderer stall over 250 ms. With #26316 the main-thread worst case fell from 82 ms to 5 ms, and the flood drained 3× faster
4 orca CLI inside an SSH terminal, opt-in off and on (#26337) Pass on the relay route
5 Two hosts with a same-id project: removing one keeps the other (#26340) Pass
6 Headless serve with saved SSH targets, paired client connects (#26348) Pass
7 Agent session history on a managed server host (#26309) Pass
8 Phase 3 smoke: SSH host card, Move to managed server, server update Card and Move pass. Update failed (F2): open tabs stayed disconnected until restart. Fixed in #26433 and re-verified on a real host: after a forced update the pane reattached in about 5 s, and text typed 15 s and 45 s later ran on the host
9 Relay route on a host with no Node and no compiler (#26303) Pass: ran on Orca's pinned Node, with zero npm, node-gyp or compiler calls

Other findings: F4 (orca serve started Electron on the default profile instead of the one it checked, and reached the user's running app) is fixed in #26424. F1 and F3 are parked; see Known limits.

Per-PR testing is in each linked PR. Each fix PR's new tests were confirmed to fail without its fix. The branch was synced with main in #26419 (all 66 CI checks green) and again in #26529, up to main at 8ae5264. Main's terminal redraw change (#26118) overlapped #26316: Phase 3.5's row model was kept, and main's new heap test drove one port (erase-to-end now copies a short kept prefix). pnpm tc, oxlint, check:code-quality:changed, audit:anti-slop and the affected suites pass.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

AI Disclosure

Review

Both Opus and Astra reviewed every fix PR (P0/P1 only); all were clean at their final heads.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Security: RPC calls are deny-by-default for every non-owner caller; the SSH guest credential lasts one command.
  • Cross-platform: linger and scope logic is Linux-only; socket relocation covers Linux and macOS; Windows hosts get rung A of the relay ladder and otherwise keep the old path; the native-quit guard is macOS-only.
  • Remote SSH and mixed versions: only optional fields cross the wire; older clients and hosts keep working.
  • Mobile: unchanged behavior; phones benefit from the ARM heartbeat fix and stable remote panes.
  • Performance: the terminal tail is linear; desktop subscribes send up to 512 KiB of history (the existing phone budget).

Checklist

  • This PR is small and focused (it is the Phase 3.5 integration branch; each fix was reviewed as its own PR)
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

Merge after the integrated real-host test and full CI are green.

OrcaWin and others added 20 commits October 7, 2026 13:42
…heartbeat (empty WebSocket write EFAULT) (#26200)

* fix(remote-runtime): never write an empty WebSocket payload on its own (arm64 Electron EFAULT)

Patch ws@8.22.0 Sender.sendFrame so a frame with an empty payload is written
as its header alone. Under Electron on arm64 kernels with 39-bit virtual
addresses, socket.write() of a zero-length buffer fails with EFAULT and kills
the connection, so every empty heartbeat ping, auto-pong and close dropped
the remote runtime with 1006. Wire bytes are unchanged.

* test(remote-runtime): run the ws empty-payload guard on real Node

Bun resolves 'ws' to its built-in client, so under the Bun test project the
guard exercised Bun's implementation instead of the patched package.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
…ists on orcad hosts (#26309)

orcad built every forked child except session-scanner-service-entry.js, so
aiVault.listSessions always failed on an orcad host. The resolver also skipped
the adjacent entry whenever isPackaged was true, so shipping the file alone
would not have been enough.

- build every ORCAD_CHILD_ENTRY_POINTS entry from one loop and add the scanner
- add it to ORCAD_ARTIFACTS so a deploy missing it fails integrity checks
- share one forked-child path resolver (asar layout vs flat app root) between
  the watcher and the scanner
- smoke-fork the built scanner during build-orcad under both runtimes

Fixes #24105

Co-authored-by: m4air <m4air@Mac.localdomain>
…ry (#26308)

A remote pane no longer latches in connecting on terminal_not_found, holds
keystrokes typed during same-terminal recovery instead of dropping them,
reports transport-owned retries so the pane is not remounted mid-retry, and
no longer infers absence from an inventory that predates the leaf.

Co-authored-by: m4air <m4air@Mac.localdomain>
…rror list (#26335)

Ubuntu 24.04 runner images resolve apt sources through /etc/apt/apt-mirrors.txt, so rewriting only the source lists left package downloads on azure.archive.ubuntu.com, which intermittently fails ('Ign:' on every package). Rewrite the mirror list too, and retry the install once with --fix-missing.

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
…ace (#26319)

Desktop subscribers of a remote terminal received screen-only snapshots,
and every pushed snapshot replay cleared the pane's scrollback with ESC[3J.
A reconnect, a remount, or a recovery after a dropped frame therefore wiped
history the host never resent.

- Host: desktop subscribe snapshots use the same scrollback-row ladder as
  mobile, and SnapshotStart now carries an optional scrollbackRows field.
- Client: a pushed snapshot clears scrollback only when the host says it
  carries history; screen-only images (scrollbackRows 0, or older hosts
  that omit the field) repaint the screen and keep the pane's history.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… path (#26306)

* fix(daemon): clean launch env, validate client frames, fit the socket path

- Drop Chromium's DBUS_SESSION_BUS_ADDRESS=disabled: marker from the daemon,
  its systemd-run scope launch and every PTY env (#25580, #21119).
- Use the durable user-manager scope only when that manager outlives the
  caller: lingering on, or the caller already runs inside it (#24201).
- Shape-check hello and request frames, and catch request rejections, so a
  malformed frame closes one connection instead of the daemon (#17841).
- Move the daemon socket to a short per-uid private dir when the data root
  would overflow sun_path (#17840).

* fix(daemon): validate a relocated socket dir before any connect, not just before bind

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
#26341)

A canceled recovery delivery released its source spans while the renderer
and model could still settle them; the stale projection then threw on every
later ACK, so the next delivery was never credited and the relay paused the
PTY for good. Settling or transferring a span of a closed delivery is now a
no-op.

Co-authored-by: m4air <m4air@Mac.localdomain>
…credential (#26337)

* fix(rpc): deny-by-default caller permissions and a scoped SSH bridge credential

Every RPC method now declares a required permission, and RpcDispatcher checks it
against the caller's scope on every transport (owner, mobile, paired runtime,
SSH bridge). The SSH relay bridge no longer spawns the client CLI with owner
authority: each invocation gets a short-lived credential bound to its SSH target,
revoked when the child exits. Without the new per-host opt-in, that credential
reaches only its own host's terminals; with it, workspace methods but never
desktop control, accounts, settings writes, skill install/share, pairing or host
admin. Paired runtime clients need a pairing-time grant (orca serve
--grant-desktop-control) for computer.* actions.

Fixes #23835
Fixes #25259
Refs #18269

* fix(rpc): let a bridged SSH worker report to its own coordinator; allowlist paired-runtime permissions

Without the per-host opt-in, orchestration send/ask/reply/check/inbox now pass when the caller is a
terminal on the bridged host, binding pane key, payload Dispatch, run and recipient to that caller's
own orchestration (recipient off-host only as the creator of its active Dispatch).

Paired runtime clients get an explicit standard permission set; pairing-admin and desktop-control
are grant-only.

* fix(rpc): resolve a bridged SSH caller's own Run and Dispatch mailboxes before admission

Mail routing stores a worker's follow-ups under dispatch:<id> and worker
questions under run:<id>, so comparing the original recipient to the caller's
terminal handle refused an SSH worker replying to its coordinator, an SSH
coordinator answering its own Run's question, and explicit sends to the
caller's own run:/dispatch: address. The bridge now resolves the exact
mailboxes the caller's live pane and process read (as check does) and admits
replies and recipients against those.

* refactor(rpc): parse SSH bridge selectors with a typed schema instead of Reflect.get

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
)

* fix(projects): remove a project only on the host the user picked

removeProject now requires a hostId and matches that host's row exactly.
The bare-id fallbacks (unique candidate, focused host) could resolve to
another host's live project and delete it. Orphaned SSH purge and group
deletion now pass each row's own host.

Fixes #13071

* fix(projects): group delete removes only the group's member rows

Same-id rows on other hosts outside the group were removed because membership
was rebuilt from bare ids after the cascade cleared it. Capture (id, host)
member pairs before deletion and remove only those.

* fix(projects): never let a stale catalog widen a project removal to every host

The renderer chose id-only `repos:remove` when its own catalog showed no
same-id row on another host. A stale catalog (another window, a paired
client, or a pending refresh) hid that row, and main then deleted every
host's registration and saved workspace metadata.

- The local catalog route now always calls `repos:removeForHost` with the
  owner row's host, for local, direct SSH, git and folder projects.
- `Store.removeProject` refuses an id that rows on more than one host share,
  so no id-only caller can delete another host's row.
- Setup rollbacks remove the just-created row by its host.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
…oses serve-hosted windows (#26348)

* fix(serve): load SSH targets on a headless serve; a user Quit only closes serve-hosted windows

- launchServeMode registers the SSH handler layer before RPC binds, so a windowless
  'orca serve' lists and connects the profile's SSH targets (#25886, #8489).
- Under serve, the app-menu Quit/Exit, tray Quit and renderer-recovery Quit close the desktop
  windows instead of quitting the process (#15537). Signals, supervisor stops, relaunches and
  update installs still quit normally; on macOS a signal reaches before-quit the same way
  Cmd-Q does, so the guard lives on the user commands, not in before-quit.

* fix(serve): SSH profiles serve on Electron; native macOS quits and frozen windows no longer trap or stop serve

- orca serve: a profile with saved SSH targets stays on the Electron host by default, since
  orcad has no SSH stack yet (#25886, #8489). ORCA_SERVE_RUNTIME=orcad still forces orcad.
- macOS serve: Dock > Quit, app-switcher Cmd-Q and other native terminates only close the
  desktop windows (#15537). Signals, supervisor stops, relaunches, update installs and
  logout/shutdown still quit.
- The serve signal listeners now reclaim SIGINT/SIGTERM/SIGHUP from Electron's native handler.
  Any signal that had a Node listener before app ready was left with Electron's handler, so
  these listeners never ran.
- A serve user Quit closing a frozen renderer's window arms the same 10s ack deadline as a
  desktop quit, and skips minimize-to-tray.

* fix(serve): stay on Electron when the profile's SSH targets cannot be read yet

A valid older-schema profile is rejected by the read-only probe, and the
selector took that as "no SSH", so a default serve before the first
migration started orcad with an empty SSH registry. Fail closed instead.

* refactor(window): type user-Quit close marks as a named window slice, not object

* test(serve): pass the desktop-control grant default in the headless SSH launch fixture

* test(serve): run the serve-selection SQLite fixtures in the Node runtime project

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
…edraw floods (#26316)

* fix(terminal): keep the main-process terminal tail linear under TUI redraw floods

Full-width in-place row rewrites rebuilt the row string per character
(O(width^2)), a long chunk past the line cap spliced the row array once
per newline (O(lines x cap)), and multi-frame chunks summed every
cursor-up so they fell back to the whole-tail path. Rows now hold
mutable cells joined once, capped rows are dropped in batches, and the
redraw window uses the net cursor reach.

Fixes #11315

* fix(terminal): do not rebuild a redraw row the cursor only revisits

Rows stayed cell arrays once touched and re-joined on every newline, so a
cursor revisiting one wide row thousands of times cost O(width) per
newline. Runs now splice into the row string, a row switches to cells
only after 32 edits between snapshots, and cells are joined once and
dropped.

* fix(terminal): keep one cached row per chunk so unchanged redraw revisits cost O(1)

A row now keeps its cells, right-trimmed snapshot and content end across
newlines and only invalidates them on a real content change; identical
writes leave it clean. Adds a work-accounting matrix over restore, the
append path and live onPtyData, plus a per-character row oracle fuzz.

* fix(terminal): return redraw rows to flat strings with cached snapshots

Cells made every changed snapshot a full-width join, which was far slower
than the old flat-string splice. Rows are flat strings again, written one
run at a time; each keeps a cached right-trimmed snapshot and content end
that only a real change invalidates, so identical rewrites and cursor-only
revisits stay O(1). A wall-time test now times every review probe against
a vendored copy of the old row model on restore, append and onPtyData.

* test(terminal): read runtime leaf and PTY tail through typed members

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
…host npm install (#26303)

* fix(ssh): relay fallback runs the pinned Node ladder by default, never a host npm install

A host whose connect falls back to the relay route now gets the A/B/C ladder
(Orca's uploaded Node and prebuilt addons) unless it opts into Host Node. A
refused rung C steps to rung D with an actionable reason instead of compiling
native deps on the host.

* fix(ssh): mark the opt-in Host Node relay runtime as unsupported on the SSH host card

The relay fallback now defaults to Orca's pinned Node ladder; the host-npm path is
reachable only by an explicit per-host Runtime or ORCA_SSH_REMOTE_RUNTIME override.
When a relay actually launches on it, main publishes hostNodeRuntime on the
connection state (optional, admitted field-by-field for paired clients) and the
SSH host card shows an unsupported-configuration warning.

* fix(ssh): keep the relay runtime ladder honest about what it cannot verify

- NixOS's stub loader refusing Orca's generic Linux Node (exit 127, 'Could not
  start dynamically linked executable') is classified wrong_libc, so the ladder
  steps on to the host Node instead of failing the connect at rung A.
- In plain SSH mode the shell provider answers absent only for shells it saw
  exit; a PTY id it never minted (one an earlier relay may still run) is
  unverifiable, never exited.
- The rung D message offers Host Node only when the host's own Node refused the
  prebuilt addons, and calls it an unsupported configuration; a download,
  unresolved-host or Windows refusal gets no Host Node advice.

* fix(ssh): keep the host-Node relay where the ladder can't judge the host

A client without Orca's runtime artifacts (a dev, e2e or source build with no
orcad template, or an offline client) refused rungs A and C with
artifacts_unavailable, and the connect settled on rung D. That reason describes
the client, so it now lands on the host-Node relay it had before the ladder,
marked unsupported, and persists no host decision. A proved noexec still lands
on D.

Windows hosts have no rung B or C yet, so any rung A refusal there keeps the
host-Node relay too. The rung D message is worded from the host OS: Windows
gets no noexec-mount wording and is offered Host Node.

* fix(ssh): a host-Node fallback with no Node settles rung D, and mutable refusals expire

The host-Node relay the ladder chooses in place of D (a client artifact gap,
or a Windows rung A refusal) hard-failed the connect when the host had no
Node: the resolver threw a plain Error, which plain SSH mode does not admit.
The fallback now proves host Node with a strict probe first; an answered
"no Node" settles D with a classified, host-OS worded reason, and an
unanswered probe stays a retryable failure. A gap no longer rewrites D to
host Node once rung C proved host_node_missing.

Mutable pinned-runtime refusals (missing_lib, security_software, and noexec
on Windows) are no longer persisted and replay from memory for ten minutes
only, so a repaired host gets rung A back. The in-memory refusal cache moves
to ssh-relay-pinned-refusal-cache.ts.

* fix(ssh): no host does worse than the pre-ladder host-Node default

Restructure the relay runtime ladder around one invariant: A, B and C are tried
first, and any refusal past them falls back to exactly the pre-ladder default,
the host-Node relay marked unsupported. That covers an unidentified libc, a
client artifact gap, Windows, mutable refusals and a C N-API floor the npm build
would not have hit. Rung D is reached only when that fallback proves the host
has no usable Node, or a proved noexec home would defeat it too.

Strict host-Node resolution now treats only a command that ran and exited as an
answer, so a refused channel (MaxSessions), session limit or timeout stays a
retryable failure instead of a 'no Node' verdict. The launch reuses the Node the
fallback probe found.

Mutable refusals (missing_lib, security_software, Windows noexec) persist again
with their proof time and replay for 24 hours, so a host that still blocks
Orca's runtime is not re-uploaded to on every connect, while a repaired one gets
rung A back. A table-driven test asserts the invariant per host class.

* fix(ssh): every answered rung failure reaches the host-Node default, and D only past it

Two shortcuts skipped the pre-ladder default. An answered but unclassified
install failure (tar ENOSPC, an extract or hash marker, an unverified Windows
promote, a self-test that failed outside every refusal class) aborted the
connect on every attempt; it now steps down as a non-persisted install_failed.
Only unanswered failures (abort, timeout, channel-open, session limit, an
unconfirmed termination, SFTP connection loss, ssh exit 255) stay retryable,
and nothing steps down once a relay launch was issued.

A proved noexec no longer jumps to D: exec denial can be per binary (AppArmor
allowing /usr/bin/node), so it skips B and C, which load from the same tree,
and reaches the host-Node fallback. D now follows only that fallback answering
with a failure, so its outcome is a superset of the default's by construction.

The never-worse test is now the cross-product of every A, B, C and host-Node
answer on Linux and Windows, plus named review cases.

* fix(ssh): system-ssh upload failures and a broken cached runtime step down before launch

System-ssh directory upload and system sftp threw plain Errors that dropped the
host's exit status, so a remote tar ENOSPC or a Windows sftp permission refusal
aborted the connect instead of stepping down. The remote side now throws
SystemSshCommandExitError (ssh exit 255, signals and lost-connection
diagnostics stay retryable), and a local tar failure is still blamed on the
client rather than the remote tar that saw its truncated stream.

The warm runtime check only tested that the verified executable existed, so a
cached runtime that the host's exec policy or a removed library had since broken
was launched and hung on every reconnect. The same command now also runs it and
reports a refusal the way promotion does, so it steps down before any launch,
with no extra round trip.

* fix(ssh): keep the remote answer when local tar only failed writing to it

When the host's tar exits early (ENOSPC, a refusal), BSD tar on macOS exits 1
with 'Write error' and no signal, so the upload rethrew the local error and
dropped the typed remote exit, aborting the connect. The remote answer now wins
whenever local tar finished, was signalled, failed after ssh closed, or reported
a write or pipe error; a local tar that failed on its own input first is still
the client's fault.

* test(ssh): hostile-host cells allow read-only version probes, never installs; probe each Node once

The noexec-home cell failed on 'npm --version': under the never-worse ladder a
proved noexec reaches the strict host-Node fallback, whose probe asks npm its
version before settling D. The cells now judge install and compile calls (npm
install/ci, node-gyp, cc, make) and allow '<tool> --version' / '-v'.

The probe ran twice because the login shell named the Node the path probe had
already rejected. Candidate checks are now memoized per resolution, for the
host-Node fallback and rung C alike.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
# Conflicts:
#	config/scripts/build-orcad.mjs
#	config/scripts/orcad-entry-build.mjs
#	config/scripts/vitest-sqlite-runtime-files.mjs
#	docs/reference/headless-linux-server.md
#	src/main/runtime/rpc/methods/structured-agent-session.ts
…er before the instance lock (#26424)

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
…er update re-pairs the host (#26433)

* fix(remote-runtime): a remote pane rebinds after a server update re-pairs its host

A pane captured the host's pairing revision once, when it mounted. A forced
managed-server update re-pairs the host under the same environment id, so every
later request from that pane was refused as stale, and the refusal counted as
fatal: the pane stopped retrying and Reconnect replayed the same stale revision.
Each request now carries the current revision, a pairing refusal re-reads the
catalog before the retry, and the pane treats it like lost contact.

* fix(remote-runtime): close a stream left on the old pairing and ignore a 0x0 grid hold

A stream that reopened before main re-paired stayed open while sendFrame refused
its stale revision, so typed keys vanished. A revision change now closes it as
recoverable so the pane rebinds on the new pairing. A just-restarted server also
published a 0x0 hold that shrank the visible pane to one cell; holds with no grid
are ignored.

* fix(remote-runtime): a pane follows only its own machine's re-pair

A re-pair under the same environment id to a different host key published its
revision before the catalog retired the old tabs, and the pane recovered at
the new revision, so its next call was authenticated as the replacement peer.
The catalog now classifies each re-pair before publishing revisions: a
replaced or removed machine's terminal streams end for good first, and a pane
only advances its pairing along a proven same-host rotation (same registration
and host key). An unresolved identity holds the pane until it is proven.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
@OrcaWin
OrcaWin requested a review from brennanb2025 as a code owner October 8, 2026 07:12
@OrcaWin
OrcaWin deployed to adhoc-mac-build October 8, 2026 08:03 — with GitHub Actions Active
@OrcaWin
OrcaWin deployed to adhoc-mac-build October 8, 2026 08:15 — with GitHub Actions Active
OrcaWin and others added 2 commits October 8, 2026 01:29
* refactor(rpc-permissions): drop never-read incarnation and invocation id from bridge credentials

* refactor(rpc-permissions): define runtime device grants once and reuse bridge scope fixtures

* refactor(rpc-permissions): decide terminal host membership in one predicate and share the opt-in hint

* refactor(rpc-permissions): parse bridge selectors once per call

* refactor(rpc-permissions): admit and bind unopted bridge methods from one table

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
* refactor(remote-pane-binding): keep the pane's pairing revision in one slot

* refactor(remote-pane-binding): share one hold decision across the input entry points

* refactor(remote-pane-binding): define the persisted-handle adopt once in attachPane

* refactor(remote-pane-binding): build the pushed-snapshot meta once

* refactor(remote-pane-binding): drop the test-only peerReplacedEnvironmentIds wrapper

* test(remote-pane-binding): share the input and subscribe frame readers via stream fixtures

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
@coderabbitai

coderabbitai Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

OrcaWin and others added 4 commits October 8, 2026 01:57
* refactor(relay-ladder): put the host-Node fallback in the ladder before D

The Auto ladder now lists legacy as the rung before D, so the step function
is one rule plus the next rung. Deletes nextRelayRuntimeStep, the hostOs
step context and the mutable hostNodeFallback flag (now a constructor-time
laddered field). Windows still ends at legacy: B and C refuse there before
any host I/O.

* refactor(relay-ladder): one rung-D reason and message over reachable states

D is reached only from the host-Node fallback, so its refusal is
host_node_missing or install_failed. Merges the reason and message
functions, drops three message branches that could not run, and folds
noexecRemembered/noexecProved into one noexec field.

* test(relay-ladder): one never-worse cross-product with a remembered axis

Drops the named cases (each matches a cross-product outcome), the transport
case that only checked the model against itself, and the third case the rank
check already implies. Adds rung B's noexec answer and per-rung remembered
flags for A and B, the only rungs that replay a cached refusal.

* refactor(ssh): one answered-miss rethrow for the node resolution probes

Four catch blocks repeated the same session-limit, abort and strict checks.
They now call rethrowUnlessAnsweredMiss and keep their own fallthrough.
Also drops a stale 'Caches nothing' comment.

* refactor(ssh): share the host-answered system-ssh exit rule; flatten the upload throw cascade

isHostAnsweredSystemSshExit owns the 255/null transport rule for both the
upload pipeline and isAnsweredHostFailure. settleUploadPipeline drops a
branch that could not run: a failed tar is judged once, then ssh, then pipe.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
* refactor(ssh-pty): build obligation transitions behind one closed-delivery guard

* refactor(build-orcad): table-drive the shipped worker smokes

* refactor(daemon): pass scope probe seams as one deps object and share cgroup line parsing

* refactor(repos): route every project removal through removeProjectForHost

* refactor(build-orcad): type the worker smoke table for the type-aware gate

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: m4air <m4air@Mac.localdomain>
* refactor(terminal-tail): share mulberry32 and compare whole results in redraw equivalence tests

* refactor(terminal-tail): keep one splice ceiling at the onPtyData entry point

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
# Conflicts:
#	src/main/runtime/terminal-tail-redraw-buffer.ts
#	src/main/runtime/terminal-tail-row-retention.test.ts
…erases (#26547)

Each erase that keeps at least half the row skipped the copy, so a chain
of them left nested slices pinning the original wide row. Rows now track
an upper bound on the string they slice and copy once the cumulative
shrink passes half.

Co-authored-by: m4air <m4air@Mac.localdomain>
@OrcaWin
OrcaWin merged commit e3639ef into main Oct 8, 2026
114 of 115 checks passed
Meapri added a commit to Meapri/orca that referenced this pull request Oct 9, 2026
Re-stacks every topic onto upstream 40716d7 (after stablyai#26483 Phase 3.5 and
stablyai#26847 Phase 3.7) through the server sync; tree is exactly
integrate/fork-2026-10-09-3. First parent is the previous main-update commit,
which descends from main, so main still moves by fast-forward.

Fork-Topic: sync-automation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
YgorLeal added a commit to YgorLeal/orca that referenced this pull request Oct 16, 2026
…ring a split leaf

waitForHostSessionHandle retired a pending split leaf on a single
session.tabs.list that showed its siblings but not the leaf. That listing
can be a split the host is still publishing, or another sibling's stale
in-flight listing joined after the one the transport waited out, so a
live terminal was reported as "Remote terminal was closed." (stablyai#20917).

- A retiredTerminalSurfaces proof for the leaf retires on the first
  listing, checked before the empty-tab republish guard, so a tab whose
  last leaf was closed no longer waits out the 15s window.
- Without a proof, a sibling-only listing must be confirmed by a second
  one; a live sighting in between resets it.

stablyai#26483 already moved this loop to listings that start after the current
in-flight request; this closes the remaining single-observation inference.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment