* perf(ssh): a web-mirror reveal reads its pane alongside the activation, not after it
* fix(ssh): an early pane read never erases an incarnation the activation learned
* fix(mobile): resume Agent Session History rows in an SSH workspace that can read them
* fix(mobile): keep transcript-referencing resumes on the transcript's own host
* fix(web): mount offline and reload when the paired host is unreachable at startup
* fix(web): treat a socket interrupted during startup as an unreachable host
* fix(web): tie the reload-on-reconnect to its startup pass
* fix(serve): give each Linux account its own Xvfb display instead of sharing :99
* fix(serve): require the spawned Xvfb to be alive before using its display
* feat(layout): add the dark layout.apply channel and route pane splits through main behind ORCA_LAYOUT_OWNER
A build made with ORCA_LAYOUT_OWNER=main registers layout.apply: main applies the
window's layout command to the stored partition through the shared applyLayoutCommand,
minting new ids. A user split then waits for main's answer and mounts the pane id main
minted. Every other build substitutes 'window', so neither branch is compiled in.
* fix(layout): write only the records a layout.apply command touched
A load-then-save rewrite also normalizes records the command never touched. The command's
effect is now the diff between the save before and after it, written onto the stored
partition record by record. A split whose main reply says a paired server holds the tab
splits as today.
* fix(layout): write a load-supplied record whole and check that patched partitions reload to the command's result
* feat(layout): close, rename and equalize panes through main behind ORCA_LAYOUT_OWNER
In a layout-owner build the window's pane close, title change and equalize go through
layout.apply and the window shows main's answer. A window pane close keeps the legacy
close's rules: the tab's last pane is refused, a pane restarted since main read the close is
left alone, every partition holding the tab closes it, and the tab's remote session id follows
the remaining pane. The rules sit behind a command field only layout.apply sets.
* fix(layout): close the tab when a racing pane close leaves the window one pane
* fix(layout): leave an unmounted tab alone when main refuses its last pane
* feat(layout): move panes and save divider ratios through main behind ORCA_LAYOUT_OWNER
Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125
* test(layout): a taken pane drop whose pane closed does nothing
Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125
* test(layout): pin the window's pane move to main's saved tree, and the drops main cannot take
Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125
* fix(layout): close a user's pane even if its terminal restarted, as today
Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125
* test(layout): pin the restarted-pane close directly, not through a made-up profile state
Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125
* fix(layout): leave a split to today's path only when the workspace's row names a paired server
The split switch point asked which host to route to with a helper that falls back to the focused
server, so a focused server could claim a workspace whose row names no owner. Use the same owner
test the tab mirror uses.
* test(layout): type the split owner mock without a cast
* test(layout): type the layout-owner command mocks with the real signature
* test(layout): type the gesture command mock with the real signature
* fix(layout): close the dark path's gaps: unmounted split panes, refused or failed splits and moves, ratios during a move, release guard
- A pane main saved but the window could not mount (source pane or manager gone during the await)
is closed again in main, so it does not come back on restart.
- A split main refuses because its copy lacks the workspace, tab or pane, or a failed call, splits
as today; a refused or failed move moves as today.
- Divider ratios committed while a move is in flight wait for the window's move, then go to main.
- The orcad bundle gets the ORCA_LAYOUT_OWNER define when the switch is on.
- Packaging and the orcad template refuse ORCA_LAYOUT_OWNER=main and any output a dark build marked.
- workspace-layout-record-patch.ts: no non-null assertions; plain comment for the map depth.
* test(layout): count a split pane as mounted from the split mock's own results
* fix(layout): act as today whenever main lacks the target or the call throws, not just for splits
- sendPaneCommandToLayoutOwner runs asToday for workspace/tab/pane_not_found and a thrown call, so
close, rename and equalize fall back like split and move; a handler that throws no longer runs the
fallback too.
- A close main refuses as its last pane, while this window still has others, closes as today.
- A move whose local step throws still releases held divider ratios.
- Drop the orcad output marker: nothing read it; the template build refuses the switch itself.
* test(layout): plain names for the last-pane fallback test and the marker comment
* docs(layout): say plainly what an indeterminate write can leave behind
* fix(layout): finish the main merge with this branch's versions of the layout-owner files
* test(build): copy the layout-owner build guard into the compiler-cache checkout fixture
* fix(layout): write a folded legacy row in its loaded form when a layout.apply command touches its claim
A legacy terminal row the load folds into a tab-bar tab stays stored as is. Closing the tab it
repeats, or giving it a tab-bar entry by moving it, let the next load resolve the claim the other
way: the row came back or took the other tab's terminal. Such a command now writes the row as
the load had it; every other record still stays as stored.
* feat(layout): accept tab moves, group splits and group sizes on layout.apply
Group sizes have no tab, so they go to the workspace's routed partition.
* refactor(layout): move the window's layout.apply sender out of the terminal pane folder
Tab groups send through it too; it now also acts as today when main lacks the group.
* feat(layout): reorder, move and split tab groups, and save group sizes, through main behind ORCA_LAYOUT_OWNER
A tab drop and Move Tab to Split wait for main's answer and then show main's placement, with main's
new group id; group divider sizes are drawn while dragging and saved by main when the drag ends.
Closing an empty group needs no command: main never holds an empty group.
* fix(layout): review fixes for tab moves through main
- Read a folded row's terminals with the loader's own claim rule, so rows sharing only an SSH
session are covered too, and drop the row's remote session entry with it.
- The record-patch test allows a folded row only in its loaded form.
- A tab drag ends at once; main's answer shows the move after.
- A lone group main moved whole is placed so only when this window's group is lone too.
* refactor(layout): adopt only a folded row's pane layout beside its row
A row's remote session entry is carried through load and save unchanged, so adopting it from the
load changed nothing (the earlier commit's message overstated it).
* feat(layout): record a window's terminal tab close through layout.apply
closeTabs can now record each terminal tab it closes, or does not find, as closed for the given
reason (pruned as a partition's records are), so a start that commits later is refused, as today's
close records it. layout.apply takes one tab's close with the window's reason: main forces it in
every holder partition, marks a user close's launches, stops no terminal (the window already did,
sparing any another tab shares), and leaves a workspace it lacks to today's close. A sibling the
load supplied that the close only renumbered is no longer written.
* feat(layout): send the window's terminal tab close to main behind ORCA_LAYOUT_OWNER
The window still closes the tab and stops its terminals itself; main records the close through
layout.apply instead of the close intent, which stays for paired servers and when main lacks the
workspace.
* fix(layout): review fixes for the tab close through main
- The window's close sends through a store-free sender, so the store no longer imports itself.
- Main marks a user close's launches by the terminal's own id.
- A recorded close prunes the whole partition's close records, as today, not one workspace's.
* fix(worktrees): drop sleeping-agent records of removed worktrees
A sleeping-agent record can outlive its tab. The bulk purge matched
records only by the pane key's tab, so such a record survived, was
persisted, and drew a sidebar row for the deleted worktree until the
runtime host connected. The single removal path also awaits after
terminal shutdown clears the records, leaving a window where a
provider-session event routed by worktree id writes one back.
Drop records (and their launch configs) by record.worktreeId in both
paths.
* test(worktrees): type the sleeping-record fixtures instead of casting
---------
Co-authored-by: Nawapat Buakoet <nawapat.b@covest.finance>
* fix(remote-runtime): probe sockets and reset backoff on wake, and keep late resubscribes revivable
Rebuilt from #23359 on current main. On OS resume or network return every
remote-runtime socket pings once and is replaced if it stays silent for 8 s,
the shared-control retry ladder restarts from its short delays, and a
resubscribe that fails or opens after the pane's recovery window latched is
parked so resume/online can revive it.
Co-authored-by: Meapri <104259736+Meapri@users.noreply.github.com>
* fix(remote-terminal): revive a latched remote pane when its host answers again
A pane whose ~3 min auto-recovery window ran out only came back on an OS
online/resume event or a Reconnect click. A Tailscale or VPN route that
returns fires neither, so the pane stayed dead over a working link even
though the host shell was alive. The environment's shared control socket
reaching ready again is now the revive signal: the pane advances its backoff
or replays its attach, exactly as Reconnect does.
Adds e2e repros for paired runtimes and managed SSH hosts: 30 s silent and
reset outages, and a silent outage longer than the recovery window.
* fix(remote-terminal): hold the control-ready reviver only while a pane is recovering
* fix(remote-runtime): re-probe a wake check whose deadline fired late, and trim the SSH outage spec
---------
Co-authored-by: Meapri <104259736+Meapri@users.noreply.github.com>
* fix(native-chat): refuse to start or send on a host that ignores Ask for approval
* fix(native-chat): hide Continue on a host held for its permission mode
* fix(native-chat): hold queued cards and goal turns on a held host; re-check on reconnect
* fix(native-chat): hold goal objectives and start retries on a held host; re-ask while held
* test(native-chat): give the remote Continue test a host that applies permission modes
* fix(settings): let an unfinished SSH access link be retried or cancelled
A failed link to a paired server keeps its durable pending operation so it
can resume, but the control only offered a new link with a fresh request
id, which main refuses, and no cancel. The pending state then blocked
environment rm and re-pairing with no way out. Show the unfinished
operation with Retry and Cancel, both sent under its saved request id, and
make the removal refusal say where to resolve it.
* test: match the pending SSH access refusal
The background wake a phone triggers on the desktop host resumed slept agent
records with no host PTY inventory check. In a worktree the desktop is not
looking at, a pane whose PTY the daemon still runs (e.g. after a renderer
restart) reads as unowned, so the wake forked a second claude --resume onto
the live transcript. Route it through the activation gate, which resumes only
records whose PTY the host inventory shows exited and declines when the
inventory cannot be read. (STA-3859)
* fix(status-bar): show usage from the host that owns the accounts
With an SSH-managed or paired Orca server owning the focused workspace's
accounts, the usage badges read that host's accounts snapshot instead of
this device's local poll. An older host that publishes no usage reads
"Usage unavailable", and loss of contact is an unverifiable verdict.
* fix(status-bar): keep remote usage fresh and honest across stream loss and owner switches
* fix(status-bar): never borrow the viewer's setup flags for a remote usage owner
* fix(status-bar): an outage stops a still-fetching remote usage slot from loading
* fix(status-bar): route usage by the focused workspace owner and fix a narrowing error
* fix(serve): drive hook titles and first-work rename without a window
Headless orca serve never opens a window, and both the synthetic agent titles
(Hermes readiness) and first-work branch/workspace rename were driven only from
the window's hook listener. Subscribe both to the hook store directly at
startup, and feed synthetic frames to the runtime tracker before the window
check, so they run in serve and desktop alike.
* fix(serve): narrow the hook event for the rename entry
* fix(serve): a new turn's working title reaches the tracker unseen; keep spinners across window reopen
A moved host's workspaces are owned by its managed server, so the Ports
panel showed the server's port scan and dropped the forwarding list and
Add button, though the SSH connection under the server's tunnel still
carries and restores this client's forwards. Show that host's forwards and
Add control above the server's scan. A forward listens on this machine,
so its rows open in the system browser here.
* fix(remote-files): open HTML, PDFs and images too large for one reply on a paired runtime
files.read stops at 512 KiB and files.readPreview at about 3 MB (one 4 MiB
reply, after base64), so larger files on a paired runtime failed to open.
Read them in chunks with the files.stat and files.readChunk methods hosts
already have, and refuse a save the host's 1 MiB frame cap would answer by
closing the connection.
Fixes#29306Fixes#26062
Co-authored-by: sotashimozono <shimozono-sota631@g.ecc.u-tokyo.ac.jp>
* fix(remote-files): keep a UTF-8 BOM when paging a large remote text file
* fix(remote-files): pin a paged read to one pairing and compare ctime
* fix(remote-files): pin the pairing before a remote read's first request
---------
Co-authored-by: sotashimozono <shimozono-sota631@g.ecc.u-tokyo.ac.jp>
* fix(orcad): log and survive recoverable process errors, stop cleanly on fatal ones
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(orcad): a fatal fault during an existing stop still exits as a failure
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(browser): the remote browser tunnel goes through the server's outbound proxy
A client-rendered browser tab on a paired server reaches sites through a
tunnel the server opens. The server dialed each destination with a bare TCP
connect, so a network that only allows traffic through a proxy refused it,
while the server's own browser (which honors the proxy) worked.
The native tunnel connector now picks the upstream proxy the same way the
server's browser sessions do: Orca's proxy setting first, else
HTTPS_PROXY/ALL_PROXY/HTTP_PROXY, each with its own bypass list, never for
loopback. HTTP(S) proxies are reached with CONNECT (with basic auth from the
URL), a no-auth SOCKS5 proxy through the existing SOCKS client; a proxy kind
it cannot speak fails the open instead of leaking around the proxy.
Fixes#29154
* fix(browser): match Chromium bypass semantics for Orca's proxy setting; keep HTTPS-proxy tunnels half-open; tolerate raw percent in proxy credentials
* fix(browser): full-glob bypass rules and trailing-dot loopback names stay off the tunnel proxy
* fix(browser): scheme-qualified and IPv6 CIDR bypass rules match like Chromium
* fix(browser): expanded IPv6 loopback from the SOCKS front end stays off the tunnel proxy
* test(browser): loopback spellings the SOCKS front end hands the tunnel stay direct
Remote panes cannot echo a keystroke before one round trip, so past ~150ms
RTT their own typing fell out of the fast paint lane. Each pane now keeps the
recent minimum input-to-output delay and adds it (clamped to 500ms) to the
redraw and synchronized-frame windows; local panes measure ~0 and keep the
existing windows.
Linear: STA-5314
* fix(daemon): a daemon update never hands a live old-daemon session id to the new daemon
Refs #26296
* fix(daemon): reconnect an older daemon through its inventory before refusing a spawn
* fix(daemon): reattach the current daemon's own session before consulting older daemons
* fix(daemon): reconnect the current daemon too before an older daemon can refuse its session
* fix(terminal): read a Windows ConPTY terminal as painted, not with PSReadLine's overwritten history prediction
* fix(terminal): bound the ConPTY rendered read, honor visible-only probes, and fill the line limit
* test(serialize): record the ConPTY capture's pre-existing wrap-pending cursor restore
* test(agent-hooks): guard that a slow last-status rename never stalls hook traffic
Regression guard for #22804 (STA-8446).
* test(agent-hooks): flush while the older rename is held so the stale-rename race is exercised
ORCA_TERMINAL_MEMORY_MAX and ORCA_TERMINAL_MEMORY_SWAP_MAX set MemoryMax and
MemorySwapMax on each terminal's systemd user scope. Off by default, applied only
after the probe scope shows the kernel enforcing them, and dropped (logged) where
the host cannot, so terminal scopes never turn off because of them.
Refs STA-3430
* fix(orcad): seal integration credentials on the headless server instead of storing them in plaintext
The Node SecretStore always reported encryption unavailable, so every
credential writer on orcad fell back to plaintext. orcad now seals with
AES-256-GCM using a host-owned master key kept in the OS keyring (macOS
Keychain, Linux Secret Service) when reachable at first start, else in a
0600 profile file bound to the machine ID. A key that cannot load fails
closed instead of downgrading. Existing plaintext credentials are sealed
on startup without loss.
* docs: describe how the headless server seals credentials and its limits
* fix(orcad): unique credential staging files, no keyring items for temp profiles, isolate startup test
* fix(orcad): keep the credential key with the credentials, one record per machine
* fix(orcad): never re-key over an unreadable record, serialize first-start key creation, refuse an unrestricted key file
* fix(orcad): verify a key record's Windows ACL and wait out a concurrent first start's keyring write
* fix(orcad): keep a keyring claim and fail closed when its store result is uncertain
* fix(credentials): restrict the staged file's Windows ACL before it replaces a credential
* fix(orcad): a temporary profile never creates a key in, or re-seals, the real credential home
* ci: baseline the two single-file credential ACL restrictions
* fix(credentials): stage Windows credentials born restricted and verify the key record asynchronously; document rollback
* test(bitbucket): inject the write failure into the Windows staging writer too
* fix(notifications): a reconnect replay no longer re-fires task-complete notifications
Carry the host's isReplay flag through the agentStatus:set IPC payload so the
renderer seeds completion tracking from replayed rows, as it already does for
the startup snapshot, instead of treating each cached done as a new turn.
* test: drop a type assertion from the replay notification test
A project set up here and on a paired server has repo rows on both hosts.
Local worktree rows are left unstamped, so their owner lookup fell to those
repo rows, found two hosts, and gave up; openFile then bound the tab to
whichever server was focused, which cannot find the workspace.
Listings stamp every paired-server row with its host, so an unstamped row
of such a project was listed here: route it to this machine.
Fixes#25467
* fix(ssh): hold the password challenge until the deferred key has been tried
When an agent is configured, the first attempt carries only unencrypted
explicit keys and leaves default-name (~/.ssh/id_*) and encrypted keys to the
no-agent retry. keyboard-interactive still sat at the end of that first
attempt's ladder, so a host that collects its password through
keyboard-interactive showed the Password: dialog before the deferred key that
authenticates had been tried. Since #15588 that prompt goes through the
password dialog and caches the answer, and cancelling it marks the attempt
cancelled, which also skips the key retry, so the connection fails instead of
falling back to the key.
An attempt that carries no key but defers some now skips keyboard-interactive
when the host lists it as a first-factor method next to publickey. When the
host does not list it, the challenge can only be a second factor after the
agent key partially succeeded, so it stays in the ladder. When the host does
not list publickey, the deferred key cannot help, so it stays too. Attempts
that carry a key keep the challenge: SshConnection never retries them with the
deferred keys, and the password retry reuses their config.
The deferral applies only when every deferred key parses as a private key that
needs no passphrase. With an encrypted one the retry would ask for its
passphrase instead, so skipping the challenge would only swap one dialog for
another, and cancelling the passphrase dialog fails the connection rather than
reaching the password flow. A file that is no usable private key (corrupted, a
public key under a private-key name, an unsupported type) makes ssh2's
connect() throw before the retry sends any auth request, which is not an auth
error, so the connection would fail without ever offering the challenge. Those
targets keep the origin/main ladder.
The cost: when the host rejects the deferred key, its password challenge now
arrives on the key retry, one failed handshake (plus a respawned ProxyCommand,
if configured) later than on origin/main.
Constraint: ssh2 1.17 hands an agent key's USERAUTH_FAILURE to the next agent key before recording partialSuccess, so after an agent partial success the auth handler sees the pre-agent method list with partialSuccess=false
Constraint: ssh2 parses config.privateKey before any agent auth can run, so default-name and encrypted keys stay deferred to the no-agent retry
Constraint: SshConnection runs the no-agent key retry only when the agent-first attempt carried no key
Rejected: drop keyboard-interactive from the deferring attempt's whole initial ladder | ssh2 hides an agent partial success, so publickey,keyboard-interactive hosts lose their second factor
Rejected: defer whenever any key is deferred | a mixed-key attempt gets no key retry, so a keyboard-interactive password host would never see the challenge
Rejected: defer for encrypted keys too | the Password dialog becomes a passphrase dialog, and cancelling it fails a connection that origin/main completes
Rejected: check the deferred keys only for encryption | a garbage or public key file aborts the retry with a non-auth error, failing a connection that origin/main completes
Rejected: run the no-agent retry even when no key is waiting on disk | adds the same extra handshake for agent users who have no key to try in between
Directive: keep deferKeyboardInteractive in step with the key-retry guard in SshConnection.attemptConnect
Confidence: medium
Scope-risk: narrow
Reversibility: clean
Tested: pnpm test src/main/ssh; real-socket fixtures for a first-factor challenge on a publickey,keyboard-interactive host, an encrypted default key, and an agent partial success followed by keyboard-interactive; unit tests for a garbage or public key file under a default key name; mutation checks (no deferral, no key check, encryption-only check, publickey guard removed, whole-ladder withholding, any-key-deferred condition) each fail a new test
Not-tested: the reworked revision against a live host
Related: #15588
* fix(ssh): keep an agent key's second-factor challenge when the host first offered both methods
* fix(ssh): defer the password challenge only for a single waiting key
* fix(ssh): read the deferred key only after the agent attempt reaches the challenge
---------
Co-authored-by: Sangrak Choi <kars@kargn.as>
A legacy project setup's id is its repo id, so a moved SSH host's
retained setup and the managed server's setup share one id. The Run Now
check took whichever came first and reported a host mismatch. Pick the
setup whose host matches the run context.
Every session the scanner read only partly (for example a Codex transcript on an
SSH host with a record over the 10 MiB cap) adds its own notice row above the
list, outside the list's scroll area. With dozens of them the panel filled with
notices and the sessions could not be reached. Notices now sit in one bounded
area that scrolls on its own.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(serve): reap orphaned processes when orca serve is PID 1
* fix(serve): keep AppImage, eval launches and exit status intact under the PID 1 reaper
* fix(browser): defer publication cleanup until native destruction completes
* test(browser): reproduce the serve tab-close SIGSEGV with a hidden page window
Headless serve backs each page with a hidden top-level BrowserWindow, whose
Aura view is what crashes; the <webview> guest the fixture used never reached
that path, so the test passed against the original code. With the serve shape
the test segfaults (fault 0x1268, RIP +0x55a8d7b) without the fix.
* test(runtime): wait for the deferred publication restore after paired create and close
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
linear-sdk.ts loads the SDK through createRequire, which the orcad bundle
cannot follow, and nothing staged the package beside it. Bundle the SDK
into node_modules/@linear/sdk/index.js at build time and list it as an
orcad artifact so templates and installs carry it.
Co-authored-by: thukabjj <31043997+thukabjj@users.noreply.github.com>
Orca keeps a Claude permission wait visible until it sees the exact tool it was
waiting on resume. A tool that fails reports PostToolUseFailure, whose tool
name and input Orca clears, so the resume check never matched and the pane kept
showing "needs permission" while Claude was already working. The exact
tool_use_id and owner now settle the wait for a failure as well.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ssh): flag a macOS host whose managed server outlived its launching SSH session
macOS Local Network privacy pins a nohup-launched orcad to the sshd-session
that started it; once that session ends, its terminals lose LAN access (#25934).
A macOS launch now records that session, a connect reports it when it proved
exited, and the host status offers a restart when no terminals are running.
* fix(ssh): retire the idle terminal daemon on a launch-session restart
* fix(ssh): keep the fence on an unsettled restart stop, and report a restart that can't hold its session
* fix(ssh): carry the launch session forward while the terminal daemon survives; never redial for the check
* chore(i18n): add the launch-session warning and restart strings to the English catalog
Every worktree switch on a paired desktop lists that worktree's session tabs.
session.tabs.list was pinned to the one-shot transport so snapshot recovery
keeps working while shared-control streams reconnect after a host restart, so
each switch opened, upgraded and E2EE-authenticated a brand-new WebSocket just
for that read: about three extra round trips per switch. Use the shared-control
socket when it is ready, and keep the one-shot path whenever it is not.
Refs #26785
* fix(remote-terminal): a headless host keeps a parked terminal at its viewer's grid
Parking a paired pane closes its stream, the PTY's last viewer. The floor then reclaimed the PTY to the host's geometry, which on a windowless orca serve/orcad host is the 120x40 spawn grid nobody sees. Every reveal then fetched a snapshot at 120x40 and reflowed it to the pane, and a TUI redrew twice. With no host pane to give the grid back to, the floor now keeps the last owner's grid; the parked owner's reserved seat resumes it without a resize.
* test(remote-terminal): a phone or another viewer still takes a parked terminal's grid on a headless host
* fix(remote-terminal): a phone leaving a parked headless terminal restores the parked grid
* fix(remote-terminal): keep the parked desktop grid as the restore baseline even while a phone fits
* test(remote-terminal): tie each PTY size probe to its own reply
Every time a paired desktop remounts a remote terminal pane (revealing a parked
worktree or tab), it re-sends the layout the host gave it. A headless orca serve
or orcad persisted the workspace session and republished the whole workspace's
tabs to every paired client for each of those identical writes. Compare the
edit with what the host already holds and skip both when nothing changed.
Moves terminal-layout-equality to src/shared so the host can reuse it.
Refs #26785
* fix(runtime): route a CLI close of a paired host's terminal to that host
orca terminal list on a desktop lists terminals a paired runtime runs, but
orca terminal close on one of those handles stopped the remote PTY id on this
machine, which cannot own it, and answered terminal_stop_unverifiable while
the process kept running on the host. closeTerminal and closeTerminalTab now
send the close to the owning runtime first and return its receipt; when that
host is unreachable the close fails and the terminal stays visible.
* fix(runtime): revalidate the pane after a paired-host close, and read the owner's stale-handle answer
Codex review: a pane rebound while the host call was pending had its new
occupant retired, and an owner that no longer knows the handle answers
terminal_handle_stale, which rejected instead of retiring the mirror.
* fix(runtime): refuse a pinned paired-host tab before asking its host to close it
Codex review: the renderer's pin guard runs at the local commit, after the host
had already stopped the process.