Commit Graph
224 Commits
Author SHA1 Message Date
Neil 00a82505c4 test: speed terminal comparisons and retry policy checks (#26396) 2026-10-07 21:01:48 -07:00
5cafefe726 Phase 3: every SSH host runs a managed Orca server (orcad), replacing the relay (#24863)
* Revert "revert: take the 26 Phase 3 (#16741 port) PRs back out of main (#24559)"

This reverts commit 5f308bfa9c.

* feat(orcad): Windows remote primitives for managed orcad hosts (W1) (#24525)

* feat(orcad): Windows remote primitives for managed orcad hosts (W1)

* refactor(orcad): run Windows host ops as node.exe with plain argv, no PowerShell hop

* fix(orcad): refuse secret-shaped names on the breakaway launcher's --env

* fix(orcad): name the secret env guard for its role

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(orcad): stage and commit a dormant migration catalog on the managed server (#16741 T6-9) (#24521)

* feat(orcad): stage and commit a dormant migration catalog on the managed server (#16741 T6-9)

The destination half of a catalog migration: an orcad stages a T6-7 manifest
(repositories, project groups, folder workspaces, dormant session, client,
automation and worktree metadata, retired names, scrollback snapshots) with
exclusive claims, then commits it with a receipt so a retried commit returns
the same receipt and never imports twice. Served as orcad.migration.* runtime
RPC behind the orcad.migration-catalog.v1 capability; the client refuses a
host without it or with method-not-found, and any other failure is left for
the caller to recheck. Dormant only: no live PTY projection. Inert on the
desktop until T6-10.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(rpc): catalog the orcad.migration params in the shared contract; name the catalog-import install target

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

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

* feat(ssh): journal and fence an SSH host for dormant migration, gated on proven terminal exit (#16741 T8-c1+c2) (#24522)

A migration from a relay-hosted SSH target into a managed orcad now starts with
a journal in its own sidecar directory, then the target's managed-owner fence,
then a profile flush, before any remote call. A fence with no journal is
unverifiable and never released; a journal whose fence is gone is stale and
grants nothing; an unreadable journal fails closed. The fence requires every
terminal the target ever leased to be proven exited, checked before the fence
(with the relay's process list) and again under it. Same-owner claims now need
the durable record that explains them. Inert until T8-c4/T6-10.

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

* feat(orcad): run orcad itself on Windows hosts (W2) (#24529)

* feat(orcad): run orcad itself on Windows hosts (W2)

* test(orcad): load the ConPTY smoke's addon from out/orcad so the temp slot can be removed

* test(orcad): skip the foreign-uid lock case when running as root

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): a connected but unused SSH host previews as movable (#24609)

The untransferred-dependency census counted activeConnectionIdsAtShutdown naming the
target as workspace-session state. The renderer rewrites that list on every connection
change, so merely connecting to an empty host blocked the move. It is a reconnect hint;
the remote work it can stand for is counted on its own. The empty-target claim check
likewise ignores global-field copies inside the host's session partition.

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(ssh): stage, commit and abort a dormant migration against its destination (#16741 T8-c3) (#24523)

* feat(ssh): stage, commit and abort a dormant migration against its destination (#16741 T8-c3)

The coordinator re-checks before every stage and commit that the fenced source
still exports the journaled manifest, carries no untransferable state and
started no terminal. A lost answer is re-read from the destination's catalog
state; only a committed read whose receipt matches the journal advances it,
and the journal is on disk before anything returns. Abort releases the fence
only on proof the destination holds nothing, or on an unsupported destination
before anything was staged, and never once the destination committed. Codes
against T6-9's catalog client through an injected interface. Inert until
T8-c4/T6-10.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(ssh): import the T6-9 client's unsupported refusal instead of mirroring it

---------

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

* feat(orcad): deploy, activate and roll back orcad on Windows SSH hosts (W3) (#24563)

* feat(orcad): deploy, activate and roll back orcad on Windows SSH hosts (W3)

* test(orcad): exhaustive op switch in the Windows lifecycle fake

* test(ssh): narrow the Windows host-cell descriptor by lane before building a relay cell

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(serve): run orca serve on the local orcad slot behind ORCA_SERVE_RUNTIME=orcad (T6-11) (#24608)

* feat(serve): run orca serve on the local orcad slot behind ORCA_SERVE_RUNTIME=orcad (T6-11)

* fix(serve): keep orcad selection app-side and wait out Windows temp cleanup

* refactor(orcad): move the data-root privacy check out of the instance lock

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* test(serve): prove D7 and the profile lock across a real Electron/orcad serve switch (#24619)

* test(serve): prove D7 and the profile lock across a real Electron/orcad serve switch

* ci(e2e): install ripgrep for the serve mode-switch job's window-manager wait

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(ssh): convert an SSH host with Orca state into a managed server through a journaled migration (#16741 T8-c4) (#24562)

* feat(ssh): convert an SSH host with Orca state into a managed server through a journaled migration (#16741 T8-c4)

The conversion entry resumes or takes the fence, deploys and pairs the
managed server into it, marks the server as migrated, then stages and commits
the dormant catalog. Every step is keyed by the journal, so a repeat after a
crash, deferral or lost reply resumes the same migration. Status reports an
unfinished migration, and rollback is refused while one runs or when the
rollback snapshot predates the migrated catalog. Inert until T6-10.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* style: oxfmt the c4 conversion and maintenance files

* fix(ssh): name the fake migration destination's type so declarations stay portable

---------

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

* feat(orcad): decommission, managed stop and GC on Windows SSH hosts (W4) (#24570)

* feat(orcad): decommission, managed stop and GC on Windows SSH hosts (W4)

* fix(orcad): accept a managed stop request whose lock path is spelled with Windows client separators

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(ssh): retire a migrated SSH host's source state only after a proven commit (#16741 T8-c5) (#24565)

* feat(ssh): retire a migrated SSH host's source state only after a proven commit (#16741 T8-c5)

Once the journal records destination-committed, the source profile drops the
manifest's repositories, folder workspaces and unreferenced project groups,
its dormant session, automation, client and worktree state, and the leases
the fence proved exited. The profile flushes, the retirement is verified,
the journal moves to source-retired and compacts once the server matches.
A retry after any crash repeats idempotent work. The fenced target stays: it
carries the managed server's tunnel. Conversion now ends retired. Inert until
T6-10.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(orcad): retirement drops the migrated host from the reconnect hint

The census no longer treats activeConnectionIdsAtShutdown as untransferable (#24609), so
retirement must remove the target from it; otherwise a restart dials a host that is now a
managed server.

* style: oxfmt the c5 conversion file

---------

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

* feat(orcad): convert Windows relay-hosted SSH targets to managed orcad (W5 part 1) (#24579)

* feat(orcad): convert Windows relay-hosted SSH targets to managed orcad (W5 part 1)

* test(orcad): start the Windows lane's exec spy after the relay gate prelude restores its own

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(settings): managed servers and "Move to managed server", behind an experimental setting (#16741 T6-6 + T6-10 UI) (#24590)

* feat(settings): managed servers and "Move to managed server", behind an experimental setting (#16741 T6-6 + T6-10 UI)

Adds a Managed servers section under Remote servers (deploy an empty server,
status with deferred-update and migration states, update, rollback, recover,
stop and cancel-stop, and SSH access for paired servers), and a Move to managed
server action on connected macOS and Linux SSH hosts with a preflight summary,
a terminals-closed confirmation and a resumable progress view. Main wires the
conversion to the relay's process list, the direct session and the T6-9 catalog
client. Everything is hidden until the new experimental setting is turned on,
and Windows SSH hosts are never offered. Merges the T6-9 branch (#24521) until
it lands on the integration branch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(settings): align managed-server form controls and name the section the setting reveals

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(orcad): ask the relay with an absolute deadline via the W5 terminal-gate lister

The conversion wiring passed a relative 10 s as listProcesses' deadlineMs, which the
provider reads as an absolute time, so every relay inventory timed out after 1 ms and
the terminal gate could never prove exit. Adopt #24579's lister verbatim so the stacks
merge cleanly.

* feat(settings): name blocking saved state in plain, localized words

The move preview listed internal dependency ids such as workspace-session; each kind
now has its own catalog entry.

* feat(settings): offer managed servers and the move on Windows SSH hosts

W1-W5 are on the integration branch, so a Windows relay-hosted host can deploy, convert
and retire like a POSIX one. The move still waits for a connected relay that reported
its platform.

---------

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

* test(ci): list the serve mode-switch e2e job in the release-cut permissions matrix (#24865)

* test(ci): list the serve mode-switch e2e job in the release-cut permissions matrix

* test(ci): expect the orcad Windows host cells in the SSH Windows hosts workflow

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* test(orcad): wait for killed terminal daemons to exit before removing their temp profiles (#24871)

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(updater): read rollout kill switches from the update-campaign payload, all inactive (#24867)

The nudge request Orca already polls may now carry an optional versioned rollout block
naming the Node runtime flips. A typed reader resolves each flip with kill-switch, version
range and install-id-bucketed percent semantics, falling back to the baked value (every flip
inactive) when the block is absent, invalid or never read. No consumer reads it yet.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(telemetry): report Windows security-software refusals and unverifiable or failed runtime checks (#24866)

ssh_remote_runtime_resolved dropped the self-test's security_software refusal to 'none' and
sent nothing when a self-test was unverifiable or failed, because those attempts throw before
a rung settles. Add the refusal value, self_test 'unverifiable', and an outcome field
(resolved | unverifiable | failed) deduplicated per host and outcome per session.

Co-authored-by: m4air <m4air@Mac.localdomain>

* refactor(settings): move WorktreeVisibilityDefaults into its own module (#24884)

global-settings-types.ts sits at the 300-line max-lines ceiling; merging main's two new
agent-state-rules settings with experimentalManagedServers put it at 301. The worktree
visibility defaults type moves next to the other visibility types and is re-exported so its
38 importers are unchanged.

Co-authored-by: m4air <m4air@Mac.localdomain>

* refactor(watcher): move the supervisor's child, terminating child and canary into a child slot (#24887)

* refactor(watcher): move the supervisor's child, terminating child and canary into a child slot

* fix(watcher,runtime): take the child slot's child type from the shared wrapper, and stub main's title-display clear in the projection test

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>

* ci(adhoc): build and ship the orcad template in adhoc macOS and Windows builds (#24969)

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>

* feat(orcad): bound the desktop slot cache and prune proven-stopped orcad versions after each managed deploy (#24973)

* feat(orcad): bound the desktop slot cache and prune proven-stopped orcad versions after each managed deploy

* fix(orcad): keep the in-use slot plus the two most recent others, and prove same-version reuse survives eviction

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>

* Phase 3: managed orcad on every SSH connect, with downgrade-safe fence (#24975, #24979)

* feat(orcad): fence managed hosts outside owner and keep converted projects for downgrades

Shipped builds hide any SSH target with an owner, so a downgrade made a converted host and its
projects vanish. The managed fence moves to a new orcadFence field (older builds keep but ignore
it), phase-3 owner fences migrate on load, and managed hosts stay visible but refuse a direct
relay. Conversion now stops at destination-committed with sourceRetainedAt; source retirement
waits on the baked-off orcad-source-retirement rollout flag. This build hides retained rows, and
a start that finds an older build changed them marks the host sourceChangedAt (relay, needs a
new move) instead of merging a second manifest.

* feat(ssh): every SSH host runs managed orcad, decided on each connect (#24979)

* feat(settings): managed servers are no longer experimental

Remove experimentalManagedServers and its gates; the Managed servers section always shows.
Loading drops a stored value, which an older build reads back as its own default (off).

* feat(ssh): every SSH host runs managed orcad, decided on each connect

Before a relay is started, the connect decides the host's server: a converted host connects
through its tunnel (retiring a retained source once orcad-source-retirement is on), an empty
host deploys orcad, and a host with Orca state converts through the journaled migration. A host
with live or unproven relay terminals keeps the relay this session and converts later. A
refusal for any other reason keeps the relay and names the blocker. A host orcad can't run on
(unsupported target, no template, native preflight, runtime self-test) releases any claim or
fence it took, records why with this app version, and keeps the pinned-relay ladder.

Progress and the decision ride an optional SshConnectionState.managedServer field (dropped by
older clients' admission). SSH Hosts shows each host's server status; the manual move dialog
is removed.

* fix(ssh): always await the connect's server decision, after the provider authority rotates

The decision now runs after the old session and transport are torn down and the authority has
rotated synchronously, so concurrent connects still join one attempt; a shutdown that began
during the decision wins over the rotation. The IPC tests use an async double, no sync branch.

* test(renderer): the IPC events store double carries its SSH connection states

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>

* test(ssh): prove connect-time conversion to managed orcad on real Linux and Windows hosts (#24981)

* test(ssh): prove connect-time conversion to managed orcad on real Linux and Windows hosts, and the downgrade view

* fix(e2e): read the SSH host's session partition, provision the convert cell's account, and keep rollout file overrides out of packaged builds

* test(e2e): report the full relay state when the convert poll times out

* test(e2e): require the relay to list no terminals before the converting connect

* test(e2e): require no running-terminal lease before the converting connect

* test(e2e): report the host's terminal leases when the converting connect keeps the relay

* test(e2e): end the relay era with no relay shell left to respawn

* test(e2e): settle before the converting connect and name the tabs a blocking shell belongs to

* test(e2e): use the exited relay tab as the session tab, since any mounted tab starts a shell

* test(e2e): carry an editor tab through the conversion instead of a terminal tab

* test(e2e): log the conversion census inputs before the converting connect

* fix(orcad): log which state blocked a refused conversion

* test(e2e): log both session partitions before the converting connect

* fix(orcad): a source partition's copy of focus on another host no longer blocks conversion

* fix(ssh): an ssh2 forward on port 0 reports the port it bound, so managed tunnels pair

* test(e2e): give the conversion its full budget again

* test(e2e): report the migration journal phase when the conversion stalls

* test(e2e): report the connect's own result and main's state when the conversion stalls

* fix(ssh): a converted host's managed state reaches the renderer instead of staying on 'converting'

* test(e2e): print a failed server call's response

* test(e2e): give server calls the budget a fresh server's first inventory needs

* test(e2e): read the converted worktree's tabs with a scoped session.tabs.list

* test(e2e): log the converted worktree's tabs instead of asserting them, pending the server-side fix

* test(e2e): prove retirement by the dropped source rows; the journal compacts away after it

* test(e2e): drop the conversion diagnostics now the cell passes

* test(e2e): fail a hung disconnect or connect with main's state instead of the whole budget

* test(e2e): convert an upgraded relay-era profile's host on its first connect, on Docker and Windows

* test(e2e): seed the relay-era target the way addTarget registers it

* fix(ssh): a stale ssh2 forward drops a late connection instead of crashing main on 'Not connected'

* fix(orcad): the active-slot readiness probe reads Windows hosts through the host script

* ci(ssh-windows): let only the convert cell's account open the SSH local forward its managed server needs

* test(e2e): match server paths in their JSON-escaped form, for Windows backslashes

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(orcad): move what an older build added to a converted host, or keep the server's version (#24980)

* feat(orcad): move what an older build added to a converted host, or keep the server's version

A host an older build changed stays on the relay with two actions. 'Move the new projects' runs a
fresh journaled conversion of only what the host's earlier migrations didn't move: the source is
viewed with those migrations retired from it, so nothing that overlaps the server is merged, and
a row the server already holds fails the whole move at stage, before any commit. Its journal
supersedes the chain head; retained-source checks compare against its baseline, and retirement
retires every manifest in the chain only after the newest committed. 'Keep the server's version'
records the current source as the baseline and returns the host to its managed server.

* test(ipc): the runtime environment handler contract lists the delta-move channels

* test(native-chat): snapshot the journal directory after the attach's restart-offer lock is released

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): converted hosts keep their editor tabs, forwarding-refusing hosts stay on the relay, listAll settles (#25099)

* fix(orcad): publish the headless graph so session.tabs.listAll settles instead of hanging

* fix(ssh): a system SSH forward on port 0 picks a free port first and reports it

* fix(runtime): a headless host lists and closes the editor tabs its session holds, so migrated editors reach clients

* fix(ssh): keep a host that refuses TCP forwarding on the relay, and release a conversion it stranded

* test(e2e): assert the migrated editor tab and a settled listAll, and keep a forwarding-refusing host on the relay

* fix: restore the journal import after rebase, type the probe's failure code, and update headless-graph test seams

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): client focus and 'local'-stamped tabs no longer refuse a host's move to its managed server (#25110)

* fix(orcad): client focus and 'local'-stamped tabs no longer refuse a host's move to its managed server

* fix(orcad): a v1.4.218 profile focused on the SSH worktree converts, and a refusal names what blocks it

The debounced session writer never patched activeWorkspaceKey or activeWorkspaceExecutionHostId,
so the first focus stayed on disk. The all-dependency census also counted global focus copies in
every non-source partition.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(ssh): offer to move a host whose open terminals keep it on the relay (#25097)

A host with any open SSH terminal never converted: the connect gate keeps the relay while relay
terminals are live, and an open tab respawns them on every connect. The first such connect per
host per app version now marks the relay status with offerMove (recorded as
managedServerMoveOffered), which toasts "Move <host> ... Its N open terminals will restart." The
SSH Hosts status line keeps a "Move to managed server" action while terminals are live.

Confirming runs ssh:moveToManagedServer: stop the host's relay terminals through the extracted
ssh:terminateSessions path, re-run the connect gate's terminal census, refuse on anything but
exited, then reconnect so the connect-time decision runs the journaled conversion.

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(orcad): reach managed orcad over an SSH stdio bridge where sshd refuses port forwarding (#25120)

* feat(orcad): reach managed orcad over an SSH stdio bridge where sshd refuses port forwarding

Hosts with AllowTcpForwarding no were kept on the relay. The managed tunnel now
probes forwarding each time it starts and, on refusal, serves the same local port
through a second provider: each accepted socket opens one SSH exec channel running
a small bridge on the host's pinned Node, which dials orcad's loopback port.
POSIX hosts run it with node -e; Windows hosts run it as the content-addressed
host script's stdio-bridge op with base64 line framing. Bridges are capped at 8
per connection under sshd's MaxSessions default, and a lost channel only drops its
socket. Only a host where even the bridge cannot run keeps the relay, recorded as
ssh_tunnel_unavailable; the older tcp_forwarding_refused record is retried.

* test(e2e): prove the stdio bridge by refused forwarding plus a working call

* test(e2e): connect the refused-forwarding host without the relay-only repo step

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(ssh): report each connect's server decision, and show orcad.log's last lines when setup fails (#25118)

* telemetry: one enum-only event per connect decision (outcome, reason, tunnel transport, host platform, duration), plus conversion start/commit/fail, deploy failures, and the per-host move offer and its result
* errors: deploy, launch and rollback failures carry the last 40 redacted lines of the host's orcad.log, read over SSH (Windows through the node.exe host script)
* SSH Hosts: a deferred or failed setup offers its reason, log tail included, under Details

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(startup): Windows never crashes resolving userData when roaming AppData is unavailable (#25113)

* fix(startup): pin Windows appData and userData before anything resolves them

A Windows session without a loaded profile (e.g. orca serve over SSH) can fail the
roaming AppData known-folder lookup. Electron 43 then falls through to Chromium's
userData provider and crashes natively. Resolve appData first (falling back to
APPDATA, then USERPROFILE\AppData\Roaming), and set userData explicitly so
Electron's provider never runs.

* test(startup): remove the AppData fixture through the retrying helper

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): keep a terminal the previous Orca version's relay still runs instead of replacing it (#25124)

After an app update the new relay answers "not found" for a PTY the previous build's relay still
runs, because the old relay refuses this build's handshake. The client read that as absence: it
expired the lease and the pane cold-restored into an empty shell while the user's shell kept
running, unreachable. Each deploy now takes a census of this target's older relay endpoints; while
one is live or unverifiable, a not-found reattach keeps the lease and the pane binding, and the
pane says the terminal is still running under the previous Orca version. A detached lease also
keeps blocking managed-server conversion until that terminal exits.

The cross-version harness now extracts src/relay, and a new test drives v1.4.218's relay socket and
grace lifecycle with this build's endpoint probe: the probe leaves no grace deadline and reads the
old relay as live work.

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(orcad): update a managed server on connect when it runs an older build and is idle (#25122)

* feat(orcad): update a managed server on connect when it runs an older build and is idle

A connect to a managed SSH host now runs the Managed servers update when the host's
orcad differs from this app's bundled build, the template carries the host's target,
and the update planner finds no live or uncounted terminals. A rejected candidate is
restored through the activation journal; the reason is recorded per app version so
later connects don't retry it. A host a newer Orca activated is never downgraded:
the activation record now names the app version behind each build, and an explicit
rollback holds the build it left.

* test(e2e): connect without a racing disconnect after relaunch, and report each attempt

On launch the app already reaches the managed server through its tunnel; a disconnect racing that
restore cancelled the connect that runs the update.

* feat(orcad): run the update check when the launch restores a managed server's tunnel

An auto-restored host may never see an SSH connect, so its server would never update. The tunnel
restore now runs the same check, once per server per session and off the caller's path, through
the shared update-check module; a server mid-migration is left alone.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): retry the pinned Node download through transient network failures (#25154)

A Chromium network change (net::ERR_NETWORK_CHANGED) during the pinned Node
download failed the deploy and sent the host back to the relay. The archive
download now retries up to three more times, after 1s, 3s and 9s, on dropped
connections, timeouts, stalls and retryable HTTP statuses, removing the partial
file first; checksum mismatches, other HTTP errors and cancels stay final. The
transient-error classifier moves from the speech download to
src/main/network/transient-download-error.ts so both share it.

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(serve): run orca serve on orcad by default, with ORCA_SERVE_RUNTIME=electron as the opt-out (#24972)

* feat(serve): run orca serve on orcad by default, with ORCA_SERVE_RUNTIME=electron as the opt-out

* test(serve): read Electron serve's pretty-printed readiness in the CLI mode-switch e2e

* feat(serve): gate D7 on Windows and serve on orcad there by default

* test(serve): take the profile lock in the CLI mode-switch e2e, and keep Windows profile logs on failure

* test(serve): tell Electron and orcad serve apart by readiness health, and trace Windows startup

* ci(e2e): dump Electron's native log and stack on the Windows serve mode-switch job

* fix(serve): keep Windows on Electron serve until it can adopt orcad's daemon

The Windows D7 job shows Electron serve exiting before its window when it relaunches
onto a terminal daemon orcad forked. Restore the win32 fallback and skip that case
there as a known gap; the follow-up PR fixes it and re-flips Windows.

* fix(serve): let ORCA_SERVE_RUNTIME=orcad opt in on Windows while Electron stays the default

* test(serve): skip the Windows D7 cases where orcad forks the daemon, and stop cleanup hiding a failed relaunch

Test 3 hits the same Windows gap as test 2: orcad forks its own daemon there, and Electron
crashes at startup beside it. A failed relaunch also made dispose close the old, already
closed app, whose throw replaced the launch error.

* test(serve): run every Windows D7 case, with the isolated home's AppData in place

Electron 43 crashes natively (0xFFFF7003) when it resolves userData and Windows cannot find
roaming AppData. The e2e home isolation points USERPROFILE at a fresh folder with no AppData,
so later launches hit that. The harness now creates it, every D7 case runs on Windows again,
and a new case proves Electron serve starts beside another profile's live orcad daemon.

* test(serve): retry removing a Windows e2e profile while a killed daemon releases it

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(ssh): resume a terminal the previous Orca version's relay still runs, through that relay's own bridge (#25170)

* feat(ssh): resume a terminal the previous Orca version's relay still runs, through that relay's own bridge

After an app update the previous relay keeps the user's shells alive but refuses this build's
handshake. Its own relay.js --connect, run from its own version directory, presents its own bundle
hash, so on POSIX hosts the client now reaches it that way: a pane whose reattach the current relay
held for an older relay opens a route through the old bridge, takes the PTY owner role without
output flow control, reattaches the PTY with its replay, and routes every later operation on that
id to the old relay. When the last pane a route serves exits, the route hangs up and the old relay's
own idle grace retires it. Windows hosts, relocated short sockets and unreachable bridges keep the
held-pane behaviour.

The cross-version harness now builds v1.4.218's relay from its tagged sources and runs it as the
real detached daemon: a shipped client leaves a shell in it, this build resumes the pane through the
old bridge, types into it, sees its output, and watches the old relay exit on its own after the
shell does.

* fix(ssh): read the legacy relay router through its instance

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): a host re-upgraded after a downgrade can move what the older build added (#25179)

The delta view left the moved projects' session state in place whenever it held anything
unmovable, so it then counted against the delta. Relay PTY bindings, shutdown markers and the
relay consumer's recovery record blocked every move although the terminal gate already proves
those terminals exited before any move commits; the move now drops them instead.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(worktree-ps): report a lost-contact host's terminals as unverifiable, not live:0 pty:no (#25167)

During a network drop to a relay-served SSH host, every PTY on it reads as an
unconfirmed exit, so worktree ps skipped them and printed live:0 pty:no for a
terminal that was still running. The listing now counts terminals whose liveness
verdict is unverifiable into a new optional unverifiableTerminalCount, and the CLI
prints live:unverifiable pty:unverifiable (JSON carries the same word) instead of
zero. A host-confirmed exit still reads as zero.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(terminal): a held pane shows no client OS or shell, and the boundary doc says POSIX hosts resume it (#25194)

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): tunnel to the port managed orcad bound and verify it is ours (#25182)

When another Orca already listens on 6768, orcad binds a different port. The
tunnel kept forwarding to 6768, reached the other runtime, was rejected with
4001, and the connect still reported a managed server.

The tunnel now reads orcad's bound port from its active slot's readiness
(falling back to the persisted port for slots without one) and, after the
forward is up, proves the server answering is the paired runtime. On a
mismatch it re-reads the port once and fails with orcad_identity_mismatch
instead of reporting managed.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): a converted host shows only its managed server's rows, and its old editor tabs load there (#25193)

Retained relay-era project groups and the per-host SSH catalog now hide like repos and folders.
On the managed transition the renderer reloads server names, groups, folders and worktrees, then
drops the host's relay-era rows a local refresh would keep. A restored tab with no host stamp in
a workspace now owned by a managed server takes that server as owner, in place.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>

* fix(orcad): ship the port-scan worker so managed servers detect workspace ports (#25197)

orcad resolves port-scan-command-worker-entry.js beside orcad.js, but the orcad
build never emitted it and ORCAD_ARTIFACTS never listed it, so every managed
server logged 'probe worker unavailable' and had no port detection. The build now
emits it with the other children, and the artifact list carries it, so it is
uploaded, hashed and covered by the template contract.

A new test bundles orcad and its children and fails when the bundle names a
worker or child entry the slot does not ship. Two existing gaps it found,
session-scanner-service-entry.js and wsl-transcript-fs-process-entry.js, are
listed as known and may only shrink.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): keep retrying when a reconnect loses the socket before the SSH banner (#25195)

After a network outage, a port forwarder or NAT can accept the TCP
connection and then close it before the server sends its banner. ssh2
reports that as 'Connection lost before handshake' with no errno, so the
reconnect ladder classified it as permanent and published 'error' with
no retry scheduled. Treat it as recoverable on the bounded ladder only;
the initial connect keeps its narrow classifier.

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(serve): serve on orcad by default on Windows too (#25162)

D7 now runs every case on Windows (orcad-serve-mode-switch-windows), including Electron
serve adopting a daemon orcad forked, so Windows no longer needs the Electron default.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): the terminal gate asks the relays before treating a detached terminal as running (#25200)

* fix(orcad): the terminal gate asks the relays before treating a detached terminal as running, and retiring a host drops its relay recovery record

* fix(ssh): earlier-relay census gaps and an unanswered relay stay unverifiable; asking the terminal gate changes nothing

- the gate is read-only; the conversion and delta move retire proven detached leases themselves
- an expired lease also needs every earlier-build relay to answer before it reads as exited
- a failed, input-less or truncated census marks its older relays unverifiable, so reattach holds
- a legacy relay route closes only when no attach or listing still awaits it
- a disposed session forgets the census it started

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(migration): recover interrupted conversions and delta moves, and trim dead migration code (#25226)

* refactor(migration): drop the unused full dependency census

* refactor(migration): one table for the routed UI fields a migration carries

* refactor(migration): share the session focus field list and fix stale claim comments

* fix(session): keep a fenced SSH host's source session partition intact through renderer saves

The renderer cannot see a fenced host's repos or folders, so hydration drops their
worktree rows; a save that still routes any row to ssh:<target> (a folder workspace
key stays valid) rewrote that partition without them. That changes the migration's
source manifest and loses the session a downgraded build reads back. Main now
ignores renderer writes to a fenced, unchanged host's partition.

* fix(migration): resume or back out unfinished conversions and delta moves, serialize delta moves, clean stale journals

- On connect, a registered server whose conversion never committed resumes the commit; a failure aborts it through the destination, unregisters the server and releases the fence.
- An interrupted delta move keeps its journal and gets its changed mark back; the next move resumes it from the journaled manifest or aborts it when the source has moved on.
- Delta moves run under the target lifecycle queue and re-check the head, so two concurrent moves cannot write two journal heads.
- Delta checks re-read the live source instead of a frozen copy.
- Stale journals from a stopped or never-registered server are cleared.
- Retiring a chain goes newest first and compacts only at the end.
- The journal schema tolerates fields a newer build adds.

* fix(migration): hide source rows only after commit, and retire what an older build added to a moved project

- Source rows and the renderer session guard share one rule: hidden once the server committed, shown while a migration is still in flight.
- A delta move a crash interrupted gets its changed mark back on the next start.
- Retirement removes worktree metadata and automations by moved-project scope, so an older build's additions no longer fail the leftover check forever; that check ignores rows of other migrations in the chain.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): survive a slow first daemon start and close review gaps (#25213)

- Give the terminal daemon 30 s to start on Windows and retry the spawn once before
  falling back, so a cold first deploy is not refused as daemonless.
- Say in the activation refusal that orcad.log holds the daemon's error and that the
  next connect retries.
- Managed stop completion now waits out daemon retirement plus the shutdown deadline.
- Publish the instance lock atomically and reclaim an abandoned torn lock.
- Fix the stop listener closing before it was defined on an already-present request.
- `orca serve`'s cache prune keeps the slot a running local orcad uses.
- A timed-out daemon retirement reopens admission once it is refused; a retirement that
  may have reached the daemon stays fenced.
- A headless host keeps an editor tab's unsaved draft unless the close is forced.
- Unverifiable terminals are attributed by the host's PTY record, like live ones.
- An explicit --user-data-dir is no longer overridden on Windows.
- Read Windows daemon process identity from the process table, not PowerShell.
- Remove the unread data-event incarnationId and daemon health runtime fields, share
  one process-alive and error-code check, and fold the websocket limits file back.

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(cli): stop, update, roll back and recover managed Orca servers from the CLI (#25201)

* feat(cli): stop, update, roll back and recover managed Orca servers from the CLI

orca environment status|update|rollback|recover|stop|cancel-stop call the same managed-server
actions as Settings > Managed servers, over new managedServer.* runtime RPC methods. The desktop
main process registers those actions; the runtime advertises managedServer.v1 only then, and the
CLI refuses on a runtime without it or one that answers method_not_found. stop requires --yes.

* fix(ipc): keep a missing managed-server selector a rejection, not a synchronous throw

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): Move to managed server converts the host it is offered for (#25196)

The move stops the relay terminals and passes the terminal census; with #25179 the conversion no
longer refuses on the stopped terminal's saved tab, layout leaf and pane incarnation. A new test
drives one live relay terminal through Move to a conversion whose manifest carries the tab without
its relay PTY, so it spawns a fresh shell on orcad.

ssh:terminateSessions also no longer records a not-found shutdown as terminated while an older
Orca build's relay may still run the PTY (#25124): that terminal is reported unverifiable and its
lease kept, so the move refuses instead of converting over a running shell.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): deploy the glibc 2.17 runtime on old-glibc hosts, and fall back to the pinned relay (#25180)

* fix(orcad): deploy the glibc 2.17 runtime on old-glibc hosts, and fall back to the pinned relay

Managed orcad picked its runtime from the host's libc flavour alone, so a CentOS 7 host
(glibc 2.17) got the default linux-x64-glibc Node, whose self-test fails there. The deploy
now picks the runtime by glibc the same way the relay ladder picks rung A or B, and the
host-side slot checks accept the compat target it ships.

When orcad still can't run, the host is recorded as such and the relay connect that
follows now runs the pinned-runtime ladder instead of defaulting to the host-Node relay,
so a host with no Node lands on rung B rather than failing.

The hostile-host lane deploys managed orcad on a fresh CentOS 7 host and asserts it runs on
the compat runtime with no default runtime uploaded.

* test(ssh): CentOS 7 cell asserts the compat runtime pick, then the refusal and relay rung B fallback

The compat template still ships the base @parcel/watcher binary, which needs GLIBCXX_3.4.20; CentOS 7's
libstdc++ stops at 3.4.19, so the candidate's preflight refuses. The cell now pins that end-to-end
behaviour: compat runtime picked and uploaded alone, refusal classified native_preflight, and the relay
that follows settles on rung B with no runtime setting.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* test(serve): prove D7 on an installed Windows app whose daemon runs from the relocated daemon-host (#24976)

* test(serve): prove D7 on an installed Windows app whose daemon runs from the relocated daemon-host

* test(serve): read the pre-switch scrollback best-effort and wait for it after reattach

* test(serve): opt the packaged Windows serve switch into orcad explicitly

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(migration): a retirement that fails after deleting rows resumes instead of reading as changed (#25234)

The chain head records sourceRetiringAt before any row is retired; the startup change check and the chain's own comparison skip a head that carries it.

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(orcad): managed orcad idles out after 15 minutes and is started again whenever it is down (#25121)

* feat(orcad): a managed orcad stops after 15 idle minutes and starts again on the next connect

A client-launched orcad now exits, like the relay, once no client, terminal,
working agent, staged migration or activation fence has been seen for 15
minutes. The exit is the normal graceful shutdown, which leaves the terminal
daemon running; the daemon retires only if it proves itself empty. A record in
the data root tells the next start (and its readiness health) that the stop
was an idle one rather than a crash.

On connect and after host resume, a fresh tunnel whose server does not answer
starts the activated slot under the activation fence, but only on a proven
exit, so a stopped server reads as not running rather than a failure.

* fix(orcad): keep orcad-entry under max-lines; idle e2e connects without a relay repo

* fix(orcad): deploy and rollback launches carry the managed idle-exit fence

The candidate launch in activation and the rollback launch built their
own launch spec without the activation root, so a freshly deployed orcad
never enabled idle exit; only the wake path did. The field is now required
on every launch spec, so the type system covers each launch site.

* feat(ssh): start a stopped managed orcad on connect, on restore and after resume

Once orcad stopped (idle, kill or host reboot), a connect still resolved
managed over a forward to a dead port and every call failed. Every connect
now checks the server behind its tunnel, as does a call through a restored
environment; a server proven stopped is started from its activated slot
under the activation fence, adopting a surviving daemon and its terminals.
The status line shows the start, and a start that fails keeps the host
managed with the reason and orcad.log's tail, never as a terminal verdict.

* fix(ssh): reuse a serving verdict only on the same SSH transport, for 5s

A reconnect right after a reboot was answered from the previous
transport's cached verdict, so the stopped server was never started.

* fix(ssh): key the serving verdict on the tunnel's remote port too

* test(ssh): a stopped server starts before the update counts its terminals

* feat(ssh): check serving at the bound port, and follow a restarted orcad to a new one

The serving check uses the port the tunnel forwards to (the one orcad bound).
A restart that binds a different port drops the forward and rebuilds it at
the new port, within the same ensure or on the explicit connect check. The
tunnel manager class moves to its own file to stay under max-lines.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a connect with no leases asks the host's relay endpoints before converting (#25223)

* fix(ssh): a connect with no leases and no relay session asks the host's relay endpoints before converting

* refactor(ssh): one isLiveSshPtyLease for every lease-liveness check

* fix(ssh): the host relay census asks a relay its PTYs before reading it as live work

An accepting relay whose holders or children the probe could not read (no lsof, an unrecognised
service child) read as live work, so a relay-era host that had exited its last shell never
converted. The relay's own bridge answers pty.listProcesses without the owner role.

* fix(ssh): the host relay census runs each relay's probe and bridge on the runtime it runs on

Pinned-ladder hosts often have no Node on PATH, so a PATH Node read every relay as unverifiable.
Each daemon's own argv names its pinned runtime, or the host Node a legacy relay started with.

* fix(ssh): an older relay's bridge runs on that relay's own runtime

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(orcad): build @parcel/watcher into the glibc 2.17 compat slot, so CentOS 7 runs managed orcad (#25199)

The compat target swapped in only node-pty, so it shipped the base target's upstream
watcher.node, which needs GLIBCXX_3.4.20; CentOS 7 stops at 3.4.19. orcad's preflight
refused it, and relay rung B lost file watching without saying so.

The compat slot now compiles @parcel/watcher from the package's own sources against the
pinned headers with the C++ runtime static, like node-pty. The slot gates (glibc 2.17
symbol floor, no shared libstdc++, N-API 8) and the smoke load cover it, the template
stages it into the compat target, and both orcad and relay rung B pick it up from there.

COMPAT_SLOT_ADDONS names a compat slot's own addons: a compat slot missing one fails
--require-slots, and a compat template target that would ship any native file without a
compat build fails the template build. The CentOS 7 cell expects an activated managed
server again, and every launched relay cell loads its watcher directly.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(phase3): close e2e routing holes, localize managed-server outcomes, trim harness and CI (#25206)

* fix(phase3): close e2e routing holes, localize managed-server outcomes, trim harness and CI

- Route the auto-convert spec into needs_build and route the new orcad/serve
  e2e helpers to the specs that use them, with routing tests.
- Run the auto-convert Docker lane only when routed; fold the missing-AppData
  check into the Windows mode-switch job and drop crash-hunt diagnostic env.
- Delta-move dialog keys its preview on the target id and cannot close or
  resubmit mid-move; Resume has an in-flight guard.
- Managed-server toasts and status lines show localized messages instead of
  raw codes or main-process English; add singular and zero-count wording.
- Settings style fixes (quiet Cancel, section header, labelled fields,
  progress labels); delete dead i18n keys and the unused previewConversion.
- Harness: kill the serve child on readiness timeout, share the isolated
  profile and spawn-until-ready helpers, hooks and a condition wait in the
  auto-convert spec, shared cross-version exec helper.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(phase3): localize the refusal toast, share serve readiness and liveness helpers, correct D7 docs

- The refusal toast no longer shows main's English blocker detail; the SSH
  Hosts status line keeps it under Details. A missing terminal count is no
  longer defaulted to 0.
- startOrcadServe uses spawnUntilReady, so a readiness timeout kills orcad;
  one isPidAlive replaces the spec's copy and the lock-holder loop.
- The port-6768 auto-convert test uses the shared hooks.
- The docs say the Windows D7 job checks rather than gates.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ci): give the idle-exit spec its app build and route the convert harness to it

Co-authored-by: m4air <m4air@Mac.localdomain>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* ci(adhoc): pin every job of an adhoc build to the commit resolved at dispatch (#25311)

Each job read the requested branch name and checked out whatever it pointed at when that
job started. A push mid-run mixed commits: in run 37234548638 the glibc217 slot lane
built 2ddea8736e, then #25199 merged, and desktop_template checked out 70948d597f, whose
merge step requires the compat watcher that lane never built.

A first resolve-ref job (no secrets) resolves the branch, tag or full SHA once, and every
job checks out that commit. The mac job still vets it for reachability before signing;
the requested name stays the concurrency key and the release label.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(terminal): report a confirmed relay PTY exit as exited and give a cold Windows PTY probe more time (#25304)

A worktree terminal close returned as soon as the relay confirmed the stop, but the
exit frame reaches the runtime record only after the SSH output intake drains, so the
verdict read straight after saw a still-connected PTY and answered unverifiable. The
stop now waits, bounded by its deadline or 10 s, for that record.

The bundled runtime's PTY probe gives the first Windows spawn 15 s and retries once
after a timeout only; spawn errors and non-zero exits still fail at once.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(relay,orcad): keep new wire messages forward compatible, drop unshipped relay.reset, fix shutdown and serve gaps (#25298)

* fix(orcad-migration): keep migration wire forward compatible with newer peers

* docs(pty): record why pty.resumeClient negotiates by method-not-found

* fix(relay): remove the unused relay.reset method and keep a deferred shutdown serving

* refactor: drop dead hold API, release gate and migration pass-throughs; fix serve and delegation gaps

* fix(lint): keep reopen hooks within file size limits

* revert type dedupe in orcad-incumbent-recovery to avoid a parallel conflict

* fix(lint): name stop-reply request fields for their role

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* refactor(ssh): one managed-tunnel ownership check and forward bookkeeping (#25316)

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a stuck managed server can recover, cancels finish their change, unknown stays unknown; delete unused reset/drain code (#25227)

* refactor(ssh): delete the unused connection reset and drain machinery

* fix(ssh): surface a stuck managed server, finish activations past their first change, and fall back from Windows launch refusals

- A rejected build that changed profile state now refuses as orcad_recovery_changed_state
  (unverifiable) with a Recover path that restores the snapshot once the operator accepts;
  an interrupted activation is no longer reported as a quiet update deferral.
- Activation and rollback drop the abort signal after their first journaled change.
- Windows launch refusals and unsafe command lines send the host back to the relay.
- Fence refusals keep unverifiable blockers unverifiable; the POSIX liveness probe reads
  kill errors in the C locale and treats permission errors as unknown.
- A host with no relay fallback surfaces its real connect error; disconnect and removal
  clear the setting-up status, and a cancelled decision's progress is dropped.
- Remove tcp_forwarding_refused leftovers, dedupe incumbent stop/restore, exec-or-empty,
  census client, errorMessage and the blocking-blocker predicate; split activation and
  snapshot files under 300 lines.

* fix(ssh): one import per module in the rollback transition

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(migration): committed reads trust the receipt, conversions survive UI churn, journals are cached, and round-2 low items (#25327)

* fix(ssh): keep a newer build's fence and note fields, and validate a fence beside a legacy owner

Normalization now validates known fields but passes unknown ones through on orcadFence and the managed-server notes. A legacy managed owner next to a malformed fence falls back to the owner's environment id instead of keeping the malformed fence.

* fix(migration): delete a retired migration's scrollback files once retirement is durable

Retirement dropped the moved terminals' scrollback refs from session state but kept the files forever. After the journal records source-retired, the files the manifest names are deleted, except refs any session partition or pending export still names. A crash before the delete repeats it on the next retirement pass.

* fix(orcad): a committed migration reads as committed from its receipt, survives receipt eviction, and abandoned stages expire

- Committed-state reads trusted only a byte-identical copy of every moved row and snapshot, so a live server that had been used could never confirm its own commit. The receipt alone now proves it; full equality stays inside the commit.
- A receipt that ages out of the 64-entry list keeps a compact record, so the commit never reads as absent.
- A stage no client returns to within a week stops holding the server awake and is dropped at the next stage.

* fix(migration): freeze a host's session from the fence on, ignore UI churn in the frozen-source check, and cache parsed journals

- Renderer writes to a fenced host's ssh: partition are skipped from fence time, not only after commit, so tab work mid-conversion cannot change the source.
- The frozen-source check leaves out workspace session and client routing state, which the UI rewrites as the user works (including through the local partition and UI state); the server gets them as journaled.
- Parsed journals are cached per directory, keyed by each file's inode, size and mtime and dropped on every write and remove, so list and session calls stop re-parsing every manifest.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): an older relay a census cannot rule out stays held, and a reconnect resumes held PTYs through it (#25331)

- A census that does not know the host platform is unverifiable, never "no older relay". On Windows
  it now probes each older version directory's pipe for the target, the way relay GC does, so a
  live older Windows relay keeps its PTYs held instead of respawning their panes; those endpoints
  are held, never bridged.
- On reconnect, a PTY the current relay disowned while an older relay holds it is reattached through
  that relay's own bridge, with its runtime restored and its replay forwarded. When no route serves
  it, it is left for recovery like an exhausted reattach.
- A superseded or disposed deploy no longer starts the census, so it cannot replace the current
  attempt's.
- A route stops holding an unserved PTY that exits.
- Drop the unused censusPreviousRelays export.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(phase3): round-2 ui-infra review fixes (#25325)

- An unverifiable move refusal with no count says so instead of "0 terminals".
- One move per host: the dialog stays mounted while open, joins a run already
  in flight on remount, and the toast shares the same guard.
- A managed server's start, wake or update no longer reloads every host's
  catalog; only a new environment for the host loads, scoped to that host
  and the local catalog.
- A failed host-partition session write is no longer acknowledged as written,
  so the writer re-queues those fields.
- The deploy picker lists only hosts with no managed server or pending move.
- Harness: orcad and the released relay daemon are stopped when startup fails.
- Windows host CI: the convert cell always runs last and fails on a failed
  native switch or build; ssh-host-server unit tests no longer trigger it.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(relay,cli): never report an unreachable terminal as exited; keep PTYs through a deferred shutdown (#25328)

* fix(relay,cli): never read an unreachable relay or terminal as exited; keep PTYs on a deferred shutdown

* fix: stop an unrecorded breakaway child, route orcad serve through the spawn chokepoint, and drop review-flagged leftovers

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): reclaim a crashed desktop lock, close the stop-cancel race, drop dead daemon and RPC code (#25338)

* fix(orcad): reclaim a crashed desktop lock, close the stop-cancel race, drop dead daemon and RPC code

- A desktop reclaims a stale desktop lock record (Electron's lock proves it), and a real
  orcad hold shows a dialog instead of exiting silently.
- A failing quit handler no longer skips closing observability.
- Completion withdraws its request when a cancel lands mid-write; the listener removes a
  cancelled leftover.
- An idle stop's clean record is retracted when the shutdown fails or overruns.
- The managed-stop request tolerates unknown fields from a newer client.
- Shared error-code checks, one win32 coverage rule, no redundant isAlive filters.
- Remove the recovery-only daemon provider, requirePinnedWsPort/strictPort and the
  superseded orcad.migration.importCatalog RPC.

* fix(startup): keep main-process-preflight under the line cap

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): fail fast on a held fence, restore untouched rollbacks, atomic Windows host script, honest tunnel ensure (#25332)

* fix(ssh): fail fast on a held activation fence, restore a rollback the target never touched, stage the Windows host script atomically

- withOrcadActivationLock no longer waits up to 15 minutes inside the target lifecycle: a held
  fence answers at once (orcad_activation_recovery_required); deploy probes it before uploading.
- A rejected rollback target that left the restored snapshot untouched puts the newer build back
  unasked; only a real change waits for the operator. Crash recovery applies the same rule.
- The Windows host script is written only when missing, through a partial file and a rename; a
  bridge exit without a sentinel is unverifiable unless the shell could not find the command.
- The managed tunnel's ensure() throws when superseded, and a caller arriving after close()
  builds a fresh run instead of joining the doomed one.
- An unparseable stop answer after a stop was sent keeps the fence (new 'unconfirmed' outcome).
- stopRemote starts over instead of reporting live when another run settled the journal.
- Releasing an unreachable setup stops the orcad it activated and clears active on proven exit.
- A failed startup is torn down quietly so its error stays published; port-forward listeners
  keep an error handler; a linked SSH access connect is cancelled when its window closes.
- Shared errorMessage, one SFTP transfer helper, getConnectGeneration only, test-cell names.

* fix(ssh): stage the Windows host script through the pinned node.exe, which cmd.exe and PowerShell both run

* fix(ssh): host-script staging runs as host-script ops; a joiner of an overtaken tunnel run builds its own

- The presence check and the install are ops in the uploaded host script (script-present, and
  script-install run from the partial upload itself), invoked as node.exe <script> <op> <args>
  like every other Windows host op: no inline code.
- ensure() callers that joined an in-flight run no longer inherit its 'superseded' end; they
  start a fresh run, which also covers a close() in between.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a connect re-checks unverifiable relay terminals once its relay session can answer (#25385)

The server decision runs before any relay session exists, so a terminal this desktop left
detached could never be asked about and read unverifiable for as long as it ran. On Windows no
endpoint census can fill that gap. Once the session is up, the relay that holds the PTY answers
and a terminal it still runs is reported live.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(settings): a managed server whose status never loaded reads unknown, not "Not running" (#25389)

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(relay,runtime-env,cli): keep a deferred relay's AI Vault and skill uploads, make managed re-pair crash-safe, show unverifiable in worktree ps text (#25393)

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): keep a surviving daemon's slot through the cache prune, and drop an idle record a signal stop took over (#25387)

- The slot prune also protects any slot a live terminal daemon's PID record points into, and
  evicts nothing while a daemon record is unreadable.
- The shutdown trigger reports whether it took ownership; an idle stop that another source
  took over discards its clean-idle record.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): edit a managed host's connection, show rows no migration owns, drop the unused quit-drain predicate (#25403)

- SSH settings may edit a managed host's connection fields; its fence and generation are never written from the renderer, and its tunnel is closed so the next use redials. Removing it is refused with a pointer to Stop under Managed servers, which stops and removes the server; the renderer no longer ends the host's terminals before that refusal.
- A fenced host with no journal (an empty host's deploy, or one whose journal compacted after retirement) no longer hides its rows: no committed move owns them, so projects an older build added stay visible. An unreadable journal still hides them.
- The quit drain's mayDetach predicate and disconnectAll's filter had no production caller and are removed.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): input to a terminal an older relay holds but no route serves is refused, not dropped (#25404)

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): refresh a changed host's status once its delta move or keep-server choice lands (#25416)

The status line kept saying the host was changed on an older Orca until a manual reconnect. Main now publishes the host as managed by its server after a successful move or keep, with a disconnected state when the move released the relay session.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a stopped managed server stops showing on its SSH host without a reconnect (#25409)

* fix(ssh): a stopped managed server stops showing on its SSH host without a reconnect

On unlink, forget the host's managed-server decision and republish its connection state. The
republish also drops the host's cached worktree scans, so worktree.ps stops naming the removed
server.

* fix(runtime-environments): a removed server's workspace session partition goes with it

Host-scoped listings enumerate session partitions as known hosts, so worktree ps kept naming a
stopped or removed server as an omitted, unselectable host. Unlinking or removing a server now
drops its runtime:<id> partition.

* test: give the removal-storage fake store casts their SAFETY rationale

* fix(runtime-environments): drop orphaned runtime workspace sessions at startup

A crash between unlinking a server and dropping its session, or a build that unlinked before the
drop existed, left a runtime:<id> session listings name as an unselectable host. Startup now drops
runtime sessions whose server is not in the environment store; it never touches local or ssh
sessions, and skips entirely when that store is missing or unreadable.

* test(migration): retirement re-aims focus without creating the destination's session

Locks in the ordering the startup reconcile relies on: a runtime:<id> session is never written
before that server is registered.

* docs(runtime-environments): name the ordering invariant the startup reconcile relies on

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a connect whose relays prove its leases ended retires them, so the next connect converts (#25405)

Every connect decides before a relay session exists, so a detached or expired lease reads
unverifiable there. The post-session re-check proved such leases ended but left them in place, so
the host stayed unverifiable on every connect and each respawned pane added another lease.

Co-authored-by: m4air <m4air@Mac.localdomain>

* ci(ssh-windows): check out preload and renderer for the orcad-convert e2e build

Main's #25359 narrowed this workflow's checkout to the server and test trees. Phase 3's
orcad-convert cell builds the full e2e app with electron-vite, which also needs src/preload
and src/renderer, so the x64 inbox cell failed with UNRESOLVED_ENTRY.

* test(orcad): model a really converted host in the v1.4.218 downgrade wire test

#25403 shows a fenced host's rows when no migration journal explains the fence. The downgrade
test fenced the host without a journal, so this build showed the retained project it is meant
to hide. Write the destination-committed cutover journal a real conversion leaves.

* fix(ssh): a briefly held fence waits and is retried, not recorded as a failed update; terminate keeps held leases (#25420)

- The activation fence is retried for a few seconds. A fence still held answers
  orcad_activation_fence_busy (a waiting deferral, never an update failure) unless a journal or
  a lock past its stale age shows an interrupted run, which stays recovery-required.
- acquireInstallLock reports Busy only when a holder answered; a lock command that only ever
  failed surfaces its own error.
- Terminate detaches instead of disposing when any PTY was unverifiable, so the final teardown
  never bulk-marks a lease an older relay may hold as terminated.
- A wake whose connection dropped while holding the fence releases that fence on this client's
  next wake (no journal, slot proven exited), so a relaunch-then-connect is not left fenced.
- The orcad e2e reconnect helper surfaces a connect's error text instead of a JSON parse error.

Co-authored-by: m4air <m4air@Mac.localdomain>

* ci(ssh-windows): check out all of src for the orcad-convert cell

Its e2e global setup also compiles the bundled CLI from src/cli, which the narrowed checkout
left out.

* fix(orcad): idle stop — drop the record when a signal stop wins, and read the activation lock, not its root (#25464)

* fix(orcad): drop the idle-stop record when a signal stop finishes first

Every stop's cleanup now discards the record unless the idle trigger owns the stop, so a
takeover that exits before the idle request runs no longer leaves a false idle stop.

* fix(orcad): idle check reads the activation lock, not the transaction root

An interrupted acquire can leave the root without a lock; the client already treats that as
unfenced, and managed orcad now does too instead of never idling out.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): main refuses a managed server host's removal before ending its terminals (#25465)

The remove flow skipped ending terminals for a managed host only when the renderer's cached target list already showed the fence, so a fence that landed after the list loaded still lost the host's terminals before main refused the removal. The removal's terminate call now carries forRemoval, and main refuses it for a managed host with the Stop… message before touching anything; the renderer no longer decides.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(terminal): closing an SSH workspace counts a host-confirmed terminal exit as stopped (#25479)

* fix(terminal): a workspace close counts an SSH terminal's confirmed exit as stopped

The close's verdict treated any SSH terminal whose record was still present as
unconfirmed, even when that record held a host-confirmed exit. SSH records outlive
their exit, so every successful close of a relay terminal answered unverifiable.
Also, a relay reattach that finishes registering after the stop no longer revives an
incarnation whose exit is already recorded.

* test(terminal): cover a reattached relay exit confirmed without an incarnation

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(cli,ssh): managed-server actions outwait their own deadlines; cap the unverifiable serving detail (#25473)

orca environment update/rollback/recover/stop/cancel-stop waited the 60 s RPC default while the
desktop runs the whole action inline, so a slow host printed a timeout failure for an action that
kept going. They now wait 20 minutes, and a timeout says the action may still be running and points
at `orca environment status` instead of reporting failure. status keeps the default.

The retained managed-server state now clamps serving.detail to the same byte limit as its sibling
detail fields.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): keep the previous orcad.log on Windows across a restart (#25481)

The Windows breakaway launcher's addon recreates orcad.log on every start, while POSIX
appends, so a crash's log was gone once orcad restarted. The orcad launch now asks the
launcher to move the last run's log to orcad.log.1 first, capped to its last 1 MiB.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(sidebar): a stopped managed server leaves no empty project group behind (#25488)

The removed-runtime purge dropped the server's repos, setups and worktree rows but kept the project
groups and folder workspaces the renderer fetched from it, so an empty heading stayed in the
sidebar. It now drops those runtime-stamped rows too.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): run automations on orcad and keep a managed host up while they can fire (#25475)

* fix(orcad): run automations on orcad and keep a managed host up while they can fire

orcad never built an AutomationService, so with orca serve defaulting to orcad scheduled
runs never dispatched and Run now threw runtime_unavailable. The headless service setup
moves out of Electron startup into automations/runtime-automation-service.ts; orcad
installs, binds, starts and stops it, and managed idle exit counts an enabled schedule or
an unsettled run as busy.

* docs(orcad): list automations among the idle-exit conditions

* fix(automations): precheck reads the SSH manager from its registry, keeping electron out of orcad

The precheck imported getSshConnectionManager through ipc/ssh, which pulled 32 electron
modules and node:sqlite into the orcad bundle and failed build:orcad.

* test(orcad): stub the automation wiring in the push-startup runtime harness

That harness stubs OrcaRuntimeService without an automation surface, so starting the real
service threw setAutomationService is not a function and timed out the next test.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): Move to managed server survives a relay that hangs up on its last terminal exit (#25487)

BUG-15: with two live terminals, one served through an older build's relay, Move failed with
"Failed to terminate SSH host sessions: …: Multiplexer disposed" although both shells died. The
old relay reports the exit before the shutdown reply; that exit closes the route (its last served
PTY), and the disposed mux rejected the shutdown still awaiting its reply.

- The legacy relay route settles a shutdown whose PTY exit it already observed.
- Move no longer aborts on a failed stop: the terminal census (what the conversion trusts) decides.
  Exited closes the relay session and converts; live or unverifiable refuses and republishes the
  relay status, so the stale terminal count is replaced.
- The move dialog offers Try again after a refusal or failure.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): closing a terminal a previous Orca relay runs stops it there and confirms the exit (#25471)

A stop on a PTY an older relay runs reached the current relay whenever no route served it at that
moment (after a reconnect with the pane unmounted, or a shell no pane ever resumed), and the current
relay answers a stop for an id it never minted as done, so the PTY read stopped while its shell
kept running. A stop on a served PTY failed instead: its exit arrived before the stop's reply,
closing the route under the pending request.

- A served PTY stops on its route; a PTY no route serves is stopped through a short-lived route to
  the older relay that lists it, which hangs up once that PTY exits.
- When an older relay may hold the PTY but cannot be asked (incomplete census, Windows pipe, a
  bridge that will not open), or its bridge drops mid-stop, the stop is unverifiable, never reported
  done; terminate keeps the lease.
- A route stays open until its in-flight requests settle.
- The provider's exit stream includes the exits older relays report, so a stop observes the PTY it
  stopped exit on the relay that ran it.

The cross-version harness runs what terminal close --all runs per PTY against a real v1.4.218 relay,
for a pane resumed this connection and for a shell no pane resumed, and sees the old shell exit there
and the old relay retire on its own grace.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a live run's journal reads busy, a wake releases only its own fence, a cancelled re-check never reports connected (#25470)

* fix(ssh): a live run's journal reads busy, a wake releases only its own fence, a cancelled re-check never reports connected

- A held fence asks for Recover only when its lock is stale or a journal has no fence over it;
  a journal under a fresh fence is a run still working and reads orcad_activation_fence_busy.
- A wake writes an owner token into the fence it takes and later releases only a fence carrying
  that token; observing the fence gone forgets it.
- The connect re-checks ownership after the relay-terminal re-check, and the re-check itself
  neither records a decision nor retires leases for a cancelled attempt.

* fix(ssh): a tunnel caller that joined a run a disconnect cancelled builds its own

The launch-time restore's tunnel run connects over SSH; a disconnect then cancels that connect.
An explicit connect that had joined the run inherited its SshConnectAttemptCancelledError and
failed (seen as the idle-exit e2e's 'connect threw: ... cancelled'). A joiner now builds once
anew after any end of the joined run except an auth failure, which it shares rather than prompt again.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(runtime-env): a late reply from a replaced pairing never overwrites the re-paired device identity (#25490)

* fix(runtime-env): a reply from a replaced pairing never overwrites the re-paired device identity

* fix(types): narrow identity fields in markEnvironmentUsed

* fix(ssh): managed tunnel proves its server by runtime id; SSH access linking keeps the strict device check

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(migration): retained scrollback survives a closed tab, local acknowledgements stop blocking, close-intent retirement retries, mobile selections merge per workspace (#25508)

- A transfer reads a retained snapshot straight from storage once its tab closes, and releasing the retention deletes a ref no session names; the frozen-source check leaves the snapshot list to the journaled manifest.
- Only acknowledgements on panes the source host owns count toward the ui-routing blocker.
- Retiring close intents treats an absent source with an identical destination entry as already done.
- Importing a device's mobile selections keeps its selections for other workspaces.

Co-authored-by: m4air <m4air@Mac.localdomain>

* test(ssh): an expired lease an older relay still lists is never retired (#25449)

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(migration): keep each host's state its own, count only saved commits, and never wedge connect on a partial session (#25516)

* fix(migration): a host-qualified owner key belongs only to its own host

Owner matching stripped a key's host qualifier before matching its repo id, so converting host A claimed, moved and on retirement removed host B's session state when the two hosts share a repo id, and destination-qualified focus written by retarget read as leftover source state, failing retirement with orcad_migration_source_ui_routing_reappeared. A qualified key now matches only its own host, and an unqualified key in another host's session partition belongs to that host.

* fix(migration): a partially written session partition never wedges connect, and a marker two partitions agree on stops blocking the move

Real-host BUG-14: the renderer's per-host snapshot leaves out maps a host has no rows in, and main stored host partitions exactly as sent, so a runtime partition lacked tabsByWorktree and the dormant-state collector threw on every connect. Main now fills the required maps on every host-partition write, the migration collectors tolerate a partition persisted without them, and an unreadable session blocks the move instead of failing connect.

The same profile's workspace-session blocker was a false positive: the local and host partitions both carried defaultTerminalTabsApplied for the moved worktree with the same value, and the fragment merge refused any shared worktree key. It now refuses only when the partitions disagree.

* fix(migration): only a flushed commit acknowledgement moves a migration to committed

A committed state read may come from a receipt the server holds in memory but failed to flush. A retry took that read as proof, journaled destination-committed and went on to retire the source, so a later server restart could lose the catalog on both sides. A committed read in stage and in abort is now confirmed through the idempotent commit(), which flushes before it answers; a failure leaves the journal and the fence where they were.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): relay shells no lease here knows count as live, Move stops them, and Windows asks every relay pipe (#25518)

* fix(ssh): a relay shell no lease here knows counts as live, and terminate stops it

A CLI-created terminal has no lease, so with an attached lease the gate answered from leases alone,
a live decision was never re-counted once the relay could answer, and terminate stopped only the
shells it held leases or panes for. The relay's own listing is the authority on what runs.

* fix(ssh): a Windows connect asks every relay version's pipe for its PTYs before converting

Windows pipes cannot be listed, so the connect-time census answered 'unenumerable' and a shell no
lease here knew let the host convert under it. Each version directory's pipe for this target is
derived from its path, so the census probes them all, current included, and asks a live one
through its own bridge; a live pipe it cannot ask is unverifiable.

* fix(ssh): the terminal gate counts what earlier relays still run, leased or not

After an app update a shell a respawn superseded on its tab keeps running on the previous relay
with no live lease here, so a decision counted only the leased shells and Move could not see it.

* fix(ssh): a Windows relay folder with a live pipe it cannot ask stays unverifiable

Each pipe is probed and asked on its own, so one that answered with no PTYs can no longer stand in
for a live sibling the census could not reach.

* fix(ssh): terminate also stops shells only an earlier relay lists

provider.shutdown routes a held id to the older relay that runs it, so the terminate set now takes
listPreviousRelayPtyIds too; one it cannot reach is reported unverifiable as before.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a refused Move to managed server reconnects the host on its relay (#25543)

Stopping the terminals closes the relay session, so a move the census refused (another desktop's
terminals, or an older relay it can't rule out) left the host and its workspaces disconnected
until a manual Connect. The refusal now reconnects the host; the connect-time decision reads the
same census and keeps the relay. A reconnect that converts after all reports the move; a
reconnect that fails still reports the refusal.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(relay): an agent exec ends on its child's exit, not on pipes a background process still holds (#25544)

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): stop the automation scheduler first when a managed stop is dispatched (#25548)

A dispatch could otherwise race the daemon retirement census or write a run record that a
rollback restore then silently discards.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): count a degraded daemon's in-process terminals in the terminal census (#25545)

In degraded mode fresh terminals run on the local fallback inside orcad, but the census read
only daemon adapters, so an update or stop saw 0 live sessions and killed running agents.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): a retained source whose drafts or settings an older build changed is never hidden as unchanged or retired (#25549)

* fix(orcad): a retained source whose drafts or settings an older build changed is never hidden as unchanged or retired

The retained-source fingerprint covered only repo, folder and group identity, so an unsaved draft
edited on an older build read unchanged: the host kept serving the server's older draft and
retirement deleted the newer one. Retention now also records a versioned fingerprint of the source's
drafts and user-authored names and settings; a mismatch marks the host changed, and a journal
without one is never retired automatically.

* fix(orcad): a retained source's automations are part of its state fingerprint

An older build can edit an automation the source keeps; retirement would delete that edit as if
the server held it.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a rollback restores the older snapshot only behind orcad's terminal barrier (#25551)

The rollback's census is taken while orcad still admits work, so a terminal or automation that
starts before the stop had its state wiped while its PTY survived. The incumbent is now stopped
through its managed stop with idle-daemon retirement: orcad closes terminal admission on every
daemon generation, counts live sessions under that fence, and retires the daemon only when none
exist. Only 'retired' lets the older snapshot replace state; live, unverifiable or a missing
answer refuses and relaunches the incumbent on its untouched state. A build without managed stop
is refused before anything changes.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): failed managed setup leaves 'connecting', edited managed host redials, idled-out server starts before its census (#25474)

* fix(ssh): a failed managed setup leaves 'connecting', an edited managed host redials, an idled-out server starts before its census

- doConnect publishes the error and clears the 'setting up' status when the managed-server
  decision throws for a still-current attempt; a cancelled one still reports cancellation.
- Editing a managed host's connection fields closes its tunnel and disconnects its transport,
  serialized with the target's lifecycle, so the next use dials the edited target.
- The terminal census starts a server that idled out behind a forward still up, so Stop, Update,
  Rollback and status no longer refuse with 'census unavailable' on every retry.

* fix(ssh): a fenced failed setup publishes its cause, never-launched slots are collectable, a reused PID is not orcad

- doConnect publishes the relay decision's setup failure (and clears 'setting up') when a failed
  managed setup kept the host fenced, instead of throwing a bare 'serves a managed server'.
- The liveness probe answers NEVER_LAUNCHED for a slot with no process record and no readiness
  file; GC removes such a slot, and every other reader still reads it as UNKNOWN.
- On POSIX a PID whose command line does not run the slot's orcad.js reads DEAD, so a stopped
  orcad behind a reused PID is woken instead of reported serving or unverifiable.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a legacy relay route a new pane started serving stays open when a pending stop settles (#25542)

A served PTY's exit that lands before its stop's reply defers the route's hang-up until the stop
settles. A second pane the same older relay holds could start serving through the route in that
window, and the deferred hang-up then closed it anyway, sending that pane's input and stops to the
current relay. Serving a pane now cancels the deferred hang-up, and a settling request hangs up only
a route that serves nothing.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(migration): the journal holds a migration's scrollback until commit or abort, across failed uploads and restarts (#25550)

Retention was scoped to one transfer call, and its release in finally deleted a closed tab's
snapshot after an interrupted upload, so every retry failed with source_snapshot_changed.
Inline buffers had no file for the retained read at all. Retention now follows the cutover
journal: held from the journaled export through staging, rebuilt at startup, released on
commit or a removed journal. Inline bytes are written to their ref while held.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>

* fix(migration): a repo id two hosts share never lets legacy keys cross hosts, and a dangling identity alias stops blocking the move (#25558)

- Repo ids are not unique across hosts: the same id may be registered on two SSH hosts. Stores that are not session partitions (worktree metadata, automations, lineage, client state, sparse presets, retired names) can hold legacy keys with no host qualifier, so an id both hosts register said nothing about whose a key was. The scope now records such shared ids; an unqualified key or bare repo id for one only matches with its row's own host evidence (worktree metadata's hostId, an automation's ssh target), so another host's rows are never moved, counted or retired. An automation's target generation now matches only alongside its target id.
- An identity alias whose identities hold no metadata (worktreeMetaByIdentity lost them, as on the B4 profile) is nothing to move rather than a worktree-metadata blocker; retirement drops it.

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(cli): orca environment recover --accept-changed-state --yes restores over changed state (#25597)

Recover refused when a rejected build changed profile state, and its refusal told the user to run
Recover, which the CLI could not do. --accept-changed-state (confirmed with --yes) maps to the same
acceptChangedState the Managed servers settings pass, and the refusal now names the flags.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): refuse an update that would end a degraded host's in-process terminals (#25598)

The census now reports inProcessSessions separately. Those terminals run inside orcad and
end with any restart, so planOrcadUpdate defers with a non-forceable
orcad_update_ends_in_process_terminals instead of claiming they survive on the daemon.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(migration): a delta move protects what it imported from rolling back across it (#25599)

A delta move never advanced the server's migration mark, so rolling back an update taken before the delta was admitted and dropped the delta's projects. The delta now records the mark before any commit can land, resumed commits included; the mark keeps the latest migration and never moves back; and the rollback gate also counts every journal into the server, so deltas finished before this change stay protected.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a missing relay inventory never proves terminals exited, and the Windows census covers every desktop's relays (#25611)

- The migration terminal gate returned `exited` whenever this desktop held no unresolved lease, even
  when the current relay or the earlier relays could not be asked. A failed or incomplete inventory
  is now `unverifiable` regardless of local leases. With no relay session at all the gate asks for a
  host census (`needsHostCensus`) instead of reading the silence as exit; the connect, conversion and
  delta move pass that census in, and the connect hands its own census result to the conversion it
  starts. A census that cannot list endpoints (`unenumerable`) is unverifiable too.
- The Windows connect-time census derived pipe names from this desktop's target id only, so another
  desktop's relay on the same account was never probed. It now lists every `orca-relay-*` pipe on
  the machine and maps each to the relay instance that owns it through that instance's credential
  file or active-pipe marker, asking each with its own credential. A pipe no version directory
  accounts for is unverifiable unless the host proves it another account's (or gone), and an
  inventory that could not be read is unverifiable.

Co-authored-by: m4air <m4air@Mac.localdomain>

* refactor: drop the unshipped pty.resumeClient relay method and unused SSH provider unregister guards (#25595)

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a connect still deciding its server holds the raw 'connected', closes a transport its cancelled decision opened, and an edit keeps a relay host's session (#25641)

- handleSshConnectionStateChange holds a raw 'connected' while a connect is in flight even before
  any relay session exists (published as 'connecting'), so the census, deploy or conversion that
  dials the pool no longer reports the host up with no providers.
- priorConnection is captured before the server decision; a connect cancelled after the decision
  closes a transport the decision opened, unless a newer connect is using it.
- Editing a fenced host an older build changed (it runs on the relay directly) no longer
  disconnects its transport; only a host reached through its managed server redials.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): a retained source is unchanged only against a pre-commit baseline of everything a user wrote (#25602)

The state fingerprint read drafts, automations and workspace metadata from what a move could
carry, so a session a move refuses hid an older build's draft edit, and retention hashed the source
after the commit, so a crash before retention blessed whatever an older build changed in between.
The baseline is now written with the fence, before any commit is possible, from the source read
directly; a session that cannot be read, or a journal without that baseline, is unverified.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): a delta move refuses a source that changed while it checked terminals (#25691)

The plan and manifest are taken before the terminal check and session release are awaited, but the
journal took its baseline after them, so a draft typed in between became the baseline while the
server received the older one, and retirement deleted the newer draft. The baseline now comes from
the plan's own snapshot, and a source that changed since it refuses the move.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(migration): a save landing mid-retirement no longer defers retirement (#25692)

Retirement removed the source rows, then awaited the profile flush, then checked nothing came back. A session save that landed during that flush re-added a source-owned row, the check failed, and the journal stayed committed until a later connect. Retirement is idempotent, so it now runs one more pass before deferring; a row back after that is reported with the partition and owner key it reappeared under.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(automations): a headless run never reads completed when its agent never ran (#25700)

* fix(automations): a headless run never reads completed when its agent never ran

orcad (and Electron serve) finished a dispatched run on a satisfied
tui-idle wait, and a ready shell prompt satisfies it: a run whose agent is
not installed read 'completed' within seconds. Like the desktop runner,
completion now needs the agent's own status for the run's pane after
dispatch; without it the run fails after the agent-start window with the
reason, instead of claiming completion.

* fix(automations): keep idle-means-done for agents without status; fail only a refused command

Not every automation agent reports status on orcad (no hooks on the host, no
recognised title), so requiring it would fail their runs. A run completes on
the agent's own status, fails when the shell refused the agent's command
(bash, zsh, dash, fish, PowerShell, cmd), and otherwise keeps the old
idle-means-done rule after the agent-start window.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(migration): the retained-source fingerprint sees all worktree metadata retirement owns (#25694)

* fix(migration): the retained-source fingerprint sees all worktree metadata retirement owns

The state view read only legacy worktreeMeta keys and attributed them without meta.hostId, so
identity-backed metadata and unqualified rows a shared repository id leaves to hostId were missing
from the fingerprint: an older build's edit read as unchanged and retirement deleted it. The view
now uses the same attribution as export and retirement, and metadata that claims the source host
but cannot be attributed leaves the source unverified. The fingerprint version moves to v2.

* fix(migration): the retained-source fingerprint skips automations on a repo id another host shares

The state view matched automations by scope.repoIds, which ignores sharedRepoIds, so host A's
fingerprint included host B's automation on a shared repository id; editing it marked A changed and
routed it back to the relay. The view now uses the move's automationTouchesScope.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): a failed readiness read no longer kills a healthy candidate; log unsettled activations (BUG-17a) (#25701)

* fix(orcad): a failed readiness read no longer fails a candidate's launch, and log every unsettled activation

BUG-17: the candidate went ready and the client SIGTERMed it ~1 s later through its reject path,
yet that readiness passes the gate, so the launch itself failed: one readiness-wait exec that
errored failed the launch outright. Retry such reads until the readiness deadline; an
unconfirmed termination still fails at once. The update's outcome never reached the app log,
so every update or rollback that does not go through now logs its code and reason.

* fix(orcad): the host-side readiness wait ends on a wall-clock deadline

On a loaded host each poll's reads outlasted its sleep, so the step-counted loop ran past
the client's 30 s exec timeout. That timeout failed the launch, the reject path SIGTERMed a
candidate still starting, and orcad, which defers a stop until startup completes, published
readiness and then exited (BUG-17).

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(migration): stop a converted host's stale tabs landing in local, and retry retirement only on an exact replay (#25712)

* fix(migration): retirement re-removes only an exact replay of moved session state

#25692's second pass re-ran retirement on any row that reappeared during the flush, which could delete a tab or draft written after the move. Retirement now records the session rows it removes before it runs; a row that reappears is removed again only when it is byte-identical to one of those (in any partition). A new or changed row defers retirement and stays.

* fix(ssh): a converted host's leftover session rows stay in its own partition, never local

After conversion the renderer drops the SSH host's projects and worktrees but keeps their session rows. With no catalog owner left, the next save routed those rows to the local partition, where retirement read them as moved source state reappearing and deferred. Converting now pins each dropped worktree's session key to the host's partition; main's fence guard keeps that partition frozen, so the stale rows are never written.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(test): restore the codexProviderHandle import main's #25078 dropped again

#25722 restored it, then #25078 removed it, so pnpm tc fails on main's tip.

* chore(sync): keep main's own cloud and mobile files byte-identical to main

Earlier syncs added lint-only brace and template fixes to these main-owned files; reverting
them keeps #24863's diff against main free of files Phase 3 does not own.

* fix(terminal): a remote pane's reattach error no longer shows this client's OS and shell (#25693)

* fix(terminal): a remote pane's reattach error no longer shows this client's OS and shell

The error toast appended the client's environment for every non-SSH error. It now shows it only
when the pane's known execution host is this client; an SSH, managed or not-yet-known host omits it.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(terminal): type the pane-host fixture as runtime owner state

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>

* fix(orcad): a retained activation fence is ownerless, so recovery can always take it over (BUG-17) (#25698)

* fix(orcad): a retained activation fence is ownerless, so recovery can always take it over

BUG-17: a recovery that took a stale fence over, failed and retained it left a fresh lock, so
every later recovery read it as still fresh and the host could never be recovered. A run that
keeps the fence once it is done now backdates the lock; one whose remote command may still be
running keeps it fresh.

Also run the in-process terminal deferral before the forced protocol check, so a degraded host
whose daemon is empty names its in-process terminals instead of an unreported protocol.

* test(orcad): the CLI's accepting recover takes over a fence a refused recover just retained

The fake host now answers a stale-only takeover busy while a recovery's own takeover is
fresh, which reproduces BUG-17's 'still fresh' loop without the ownerless mark.

* test(orcad): keep the fake host's fence acquisition void where callers expect it

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a cancelled connect closes only the transport its own decision opened and nothing newer adopted (#25696)

* fix(ssh): a cancelled connect closes only the transport its own decision opened and nothing newer adopted

#25641's cleanup took any transport that differed from the pre-decision one as the cancelled
decision's, guarded only by connectInFlight. A replacement connect that completed (and left
connectInFlight) then had its live transport disconnected by the stale attempt.

The pool now attributes a transport it opens inside a connect's server decision to that attempt
(AsyncLocalStorage), and the latest user adopts it: a connect that connects or publishes a managed
route, or a managed tunnel that records a forward. A cancelled attempt closes the transport only
when it opened it, nothing newer adopted it, and no current replacement is in flight.

* fix(ssh): a still-current connect whose server decision fails closes the transport that decision dialed

The decision's own failure (a throw, or a fenced relay refusal) published 'error' but left the
transport it dialed open, so getPublicSshState read 'connected' and a later auto-reconnect
broadcast a plain 'connected' with no relay. Both branches now close exactly the decision-owned
transport through abandonDecisionTransport, which treats the attempt's own in-flight entry as
no newer owner while that attempt is still current.

* test(ssh): name the stand-in transport type

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): Windows readiness identifies itself by one PID when the process-table snapshot times out (#25723)

* fix(orcad): Windows readiness identifies itself by one PID when the process-table snapshot times out

A loaded Windows runner timed the whole-table snapshot out during the bundled runtime's
readiness preflight, so the candidate failed to start and activation rejected it.

* fix(orcad): fall back to the one-PID query only for a slow process table, never an unreadable one

An unreadable table (EDR-hooked snapshot, restricted token) must still fail qualification.
The table now rejects slowness with a typed WindowsProcessTableTimeoutError, and only that
falls back. Review by win-serve.

* build(cli): list the process-table timeout error in the CLI project's file list

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a connected conversion proves the host idle with the account-wide census, not this target's lists (#25697)

* fix(ssh): a connected conversion proves the host idle with the account-wide census, not this target's lists

The migration terminal gate asked the host-wide census only when no relay session existed. With a
session, it trusted this target's relay listing and its earlier-relay census, both of which name
only this target's instances, and returned `exited` when they were empty, so another desktop's live
shell on the same account, under a different target id, let the host convert under it.

`exited` now always needs a complete host-wide census: this target's lists can prove `live`, but
empty lists only pass the question to the census, and a gate given none answers `unverifiable`
with `needsHostCensus`. The connect-time refinement passes the census too, so a connect retires its
leases only when no relay on the account holds work. The Windows host lane now runs a second
desktop's relay with a live shell and expects the connected gate to read the host live.

* test(ssh): a connected relay's empty lists still ask the account-wide census

* refactor(ssh): drop the gate's unread needsHostCensus flag; its unverifiable reason says why

* test(ssh): the delta snapshot fixtures give their account-wide census

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a reconnected transport starts managed orcad itself instead of inheriting a dropped start (#25689)

* fix(ssh): a reconnected transport runs its own orcad start instead of inheriting a dropped one

The serving check deduplicated in-flight starts by environment only. When a
connect dropped mid-start (as when the launch-time auto-connect is replaced
by a reconnect), the caller on the new transport joined the start bound to
the dead one, got its failure, and reported the host managed with no server
running. In-flight checks now join only on the same connection, connect
generation and port, and a wake's own fence token is cleared only by that
wake.

* fix(ssh): a reconnected wake releases the fence its dropped wake held at any point

The flake's real verdict was 'fenced': the dropped launch-time wake held the
activation fence, and the reconnected wake could not prove it its own. A
wake now claims its token before its first remote step, an absent owner
record under a held token is still its own, and a reconnected wake waits for
this client's dropped wake to settle before reading the fence.

* fix(ssh): release only a fence carrying this process's own wake token

A fence with no owner file could be another client's fresh one. Releasing it
now requires the owner token this process wrote; the token is claimed before
the write so a drop after it still proves ownership.

* fix(ssh): type the wake's fenced fallback

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* test(ssh): stop the exec-stdin test double from failing on EPIPE (#25739)

The truncation test's fake exec channel forwarded the local shell's
EPIPE (or 'Cannot call end after a stream was destroyed') as a channel
error. Whether that error or the shell's exit code won depended on
scheduling, so the test failed under full-suite load. ssh2 silently
drops writes once the remote stops reading; the double now does the
same, and a 1 MB payload makes the early-stop path deterministic.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): Move lets the reconnect's decision run its census, and a census outside a connect holds 'connected' and closes its own transport (#25735)

- Move to managed server no longer runs a separate host census after tearing the relay down,
  which dialed the pool with no connect in flight and broadcast a raw 'connected' with no
  session or providers. It reconnects, and reads the decision the reconnect's census recorded.
  A relay a failed stop left up is detached (leases kept), not disposed, before the reconnect.
- The CLI and delta-move census (censusHostRelayTerminalsFor) runs outside a connect under its
  own owner: the raw 'connected' it causes is held, and a transport it opened that nothing
  adopted is closed afterwards. Reusing a pooled transport inside a scope now adopts it.
- Drop the now-unused publishRelayTerminalsStatus.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(test): drop the restored codexProviderHandle import now that main restored it

* refactor(migration): keep a converted host's source rows instead of retiring them automatically (#25768)

Automatic source retirement leaves Phase 3: nothing deletes a converted host's retained rows on connect, delta move, keep-server's-version or restart. They stay hidden and are removed only by stopping the server, removing the host or uninstalling. Change detection goes back to the catalog-identity fingerprint, so an older build's edits inside an already-moved project stay preserved in the retained rows without marking the host changed. The copy-only helpers the delta view uses are renamed to subtract, and the converted-host session pin now also overrides a boot primary of local.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(automations): a headless run is watched past tui-idle timeouts by the one run observer (#25733)

* fix(automations): a headless run is watched past tui-idle timeouts by the one run observer

The headless dispatcher awaited a single tui-idle wait, which rejects after
its 5-minute default, so a healthy agent working longer was published as
dispatch_failed and never observed again. The dispatcher now hands the run
to its completion watcher, whose runtime observer already re-arms wait
timeouts, honours cancellation and bounds total observation; the agent
status and missing-command checks fold into that observer, and the separate
completion loop is gone.

* fix(automations): an already-idle pane completes when the start window passes

Real-host: a stub that exited before the window left an idle shell with no
agent status, and the observer re-armed a tui-idle wait that never resolves
for an already-idle shell, so it timed out instead of completing. The
observer now keeps judging the pane while its output is unchanged, and only
waits again once the pane changes.

* fix(automations): resolve a watched headless run by its launch handle first

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(runtime-env): a re-paired managed server's subscribers recover without a reload (#25752)

* fix(runtime-env): a re-paired managed server's subscribers recover without a reload

The renderer kept the pairing revision it last read, so after an on-connect update re-paired a
managed server every subscribe and request was refused as 'pairing changed' until a reload. The
first refusal now re-reads the environment catalog, so revision-keyed subscriptions resubscribe
and requests carry the new pairing. A managed server re-pairing for the same host registration is
the same peer, so its workspaces and tabs are no longer purged as a replaced environment.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(runtime-env): a re-paired managed server is the same machine only when its host proves the same identity

Same SSH target registration is not proof: a reinstalled host or a target now pointing elsewhere
keeps it. The runtime id the pairing handshake verifies must be known and unchanged; otherwise the
re-pair retires the environment as before. A proven runtime id change also counts as replaced.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(runtime-env): decide a managed re-pair's same machine by the host's proven key, not its runtime id

The runtime id is minted per process start, so every orcad restart would read as a new host.
The host's E2EE public key persists in its own profile across updates and its pairing handshake
proves it; main now lists a digest of it, and the renderer keeps a re-paired managed server only
when that digest is known and unchanged under the same SSH target registration.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(runtime-env): defer a managed re-pair's same-machine decision until the host key is known

A re-read that lands before the new pairing's host key is listed no longer purges: the decision
waits for a catalog that carries the key and retires only if it differs. Adds the update-flow
store test: same registration and key keeps workspaces and tabs, including a re-read that runs
before the key is known.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(runtime-env): watch for a pairing refusal on a side branch so requests settle on the same tick

Chaining .catch onto every subscribe and request delayed each success by a microtask, which let a
StrictMode cleanup run before a client-event subscription resolved, so its unsubscribe landed late.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>

* refactor: consolidate pane-ownership, migration-catalog, activation-launch and parser helpers (#25738)

* refactor(orcad): one launch-and-judge helper for activation and rollback

* refactor(orcad-migration): one copy each of the destination projections, selectNewRows, assertSameValue, compareKeys and slotLiveness

* refactor(orcad-migration): one string-list validator and one uniqueness check, error codes passed in

* refactor: one shared pane-ownership and terminal-layout module for migration, profile transfer and split layout

* fix(orcad-migration): row-identity helpers in a leaf module (no import cycle); key order in its own module

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): reopen a managed tunnel to a host another desktop restarted (#25800)

The tunnel's identity check pinned the saved runtime id, which orcad mints per process. A host
updated or woken by another desktop, or restarted while this one was away, failed every reconnect
with orcad_identity_mismatch, and nothing could refresh the id because that needs the tunnel. The
E2EE handshake with the pinned host key and our accepted token now prove the server; the first
authenticated status reply records the new id. A different host is still refused.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>

* fix(orcad): create the readiness file owner-only so its pairing token is not world-readable (#25809)

orcadLaunchCommand truncated .orcad-readiness before setting umask 077, so under a
login umask of 022 the file that receives the pairing offer (with a runtime-scope
device token) came out 0644. umask 077 now runs first, the readiness file is
chmod 600 after the truncate (a redirect keeps an earlier build's 0644), the pid
and log files are tightened too, and the slot dir and ~/.orca-remote are chmod
700 so files earlier builds left readable are no longer reachable. The state
snapshot capture also sets its umask before creating the snapshot directory.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(terminal): explain a pane whose saved session another host connection owns (#25814)

terminal_pane_owner_host_mismatch reached the user raw, with an issue link. It now reads as a
plain explanation with the open-a-new-terminal action, like the reattach failure.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>

* test(topology): allow phase3's headless editor-tab retirement in main's boundary ratchet (#25823)

Main's #25329 added the ratchet; phase3's mobile-session-editor-projection.ts writes the host's
own session through setWorkspaceSessionForWorktree, the same way the listed headless
mobile-session tab writers do.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(automations): a headless host closes finished run terminals, keeping the newest few (#25831)

* fix(automations): a headless host closes finished run terminals, keeping the newest few

The desktop closes a run's terminal when the run completes; orcad had no
renderer to do it, so hourly automations left a shell and PTY per run open
forever (28 after ~6h on a real host). The headless service now closes a
finished run's terminal after a 10-minute grace, keeps the newest three per
automation viewable, and never touches a run that has not finished.

* fix(automations): never close a run terminal a client typed into or is viewing

Mirrors the desktop's take-over rule on headless hosts: a finished run's
terminal stays open when any client drove input to it since spawn, is
attached to or viewing it, or when this process cannot tell (it adopted
the PTY rather than spawned it).

* test(runtime): register a viewer through the public subscribe API

* fix(automations): close only completed runs' own panes

A failed run can still hold a live agent (blocked on a prompt, past the
watch window, or after an observer error), so like the desktop only a
completed run's terminal is closed. And only the run's own pane closes, so
a pane a user split into the same tab survives.

* fix(automations): close a run pane only while it still holds the run's PTY

The use check read run.terminalPtyId, but the close hit whatever PTY now
occupies the run's pane. Restart-exited-pane and the Codex account-switch
restart put a new PTY there, so a terminal a user was using could be killed.
The close now resolves the pane's current PTY and closes only when it is the
run's own; otherwise it closes nothing and only clears the run's terminal.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): a snapshot restore past the exec timeout keeps its fence instead of racing a second restore (#25811)

* fix(orcad): a snapshot restore past the exec timeout keeps its fence instead of racing a second restore

A capture, restore or clear ran under the generic 30s exec timeout. On ssh2 the timeout closes the
channel and reads as a confirmed failure, but sshd leaves a pty-less command running, so rollback
ran its rescue restore and recover orphaned the fence for a second one, both in the same stage.

- State mutations run through execOrcadStateMutation: no abort, a client wait past the host's
  deadline, and any closed channel or busy/deadline answer is unconfirmed, so the fence stays fresh.
- POSIX hosts wrap each one in `timeout -s KILL` (where present) and a pid-checked lock dir under
  ~/.orca-remote; the Windows host script takes the same lock.

* fix(orcad): a running state mutation keeps the activation fence fresh

The fence goes stale by its lock dir's mtime after 20 minutes, and a capture, restore or clear can
now run up to 15 under it, so a rollback's rescue capture plus restore could outlast the window and
let a recovery steal the fence from a live run. While a mutation runs, the host now touches the
fence every 60s (POSIX: a background beat that stops with its shell; Windows: an interval in the
host script, whose mutations are now async so the timer runs). A dead process stops refreshing, so
stale takeover still recovers it.

* fix(orcad): the state-mutation fence heartbeat never refreshes a wake's fence

A wake writes .orca-wake-owner into the fence dir and lets its fence age toward takeover; the
heartbeat now skips a fence that holds that token, and only ever changes the dir's mtime.

* fix(orcad): a state mutation releases its host lock before answering, and names its holder by pid and start time

On Windows answer() exits in the stdout write callback, so an op that answered before its first
await (MISSING, EMPTY, FAILED) exited before the wrapper's finally and leaked the lock; a reused
pid then read as alive and every later capture, restore and clear answered busy. Ops now return
their token and the wrapper answers after releasing the lock. The holder is pid plus creation time
(the slot's process-tree addon); one that cannot be identified is stale once its lock misses five
heartbeats. POSIX gets the same heartbeat-age check for a reused pid.

* fix(orcad): a state mutation's host lock is owned by its whole process group

The lock named only the shell's pid, so a shell killed while its rm or tar ran let the next
mutation take the lock and race that child. Each mutation now runs in its own process group
(setsid, or perl setpgrp on macOS), with timeout inside it so a deadline KILL reaches the children
too. The lock records the group, and is taken over only once no member is alive; a host that can
start no group records none, and its lock is never taken over. Windows ops run in-process, with no
children to outlive the holder.

* fix(orcad): record a state mutation's process group without ps -p, and never hold a groupless lock forever

BusyBox ps has no -p, so Alpine hosts recorded no group and their lock read busy forever after a
timeout kill, reboot or OOM. The group now comes from /proc/<pid>/stat (read after the comm field's
last paren), with ps -o pgid= -p as the fallback. A lock that still names no group is taken over
once its pid is dead and its heartbeat has missed three beats.

* fix(orcad): only proof of exit frees a state-mutation lock

A Windows holder whose creation time could not be read was taken over after five quiet minutes
though its pid was alive, so a suspended clear could resume and delete freshly restored profiles.
Both platforms now free the lock only on proof of exit: a dead pid, a different creation time, or
(POSIX) a group with no live member. A live holder of unknown identity stays busy until it exits.
The owner record is written exclusively, so a run that resumes after a takeover backs off.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): never offer Move for terminals another Orca desktop or session runs (#25815)

On a host where another desktop held a live relay shell, this desktop read relay_terminals_live
with offerMove, and its copy ("Its N open terminals will restart") implied they were its own. The
census already attributes them: terminals counted only by the host-wide census, with no lease or
listing of this target naming one, run under another target or session. That verdict now carries
elsewhere / terminalsElsewhere; no move is offered (no toast, no status-line action) and the status
line says the terminals belong to another Orca desktop or session.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(automations): an update releases finished, unused automation shells before counting terminals (#25844)

Hosts with schedules kept completed run shells (the newest three, and any not
yet past their grace), which counted as running terminals and deferred every
on-connect update with orcad_update_terminals_running. The update and
rollback census now ask the server to close completed automation run
terminals no client used, with no grace or keep rule, and count after the
daemon drops them. Used, unknown, failed and running ones still count.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): every activation fence holder carries a generation token its steps and release must match (#25834)

* fix(orcad): clear a bare stale activation fence instead of asking for Recover, and report a restarting update from status

BUG-21: a wake cut short leaves a stale fence with no journal. Every update then answered
'Recover it first' while Recover answered 'none'. The fence-hold check now takes such a fence
over and drops it, and the update retries once; Recover is asked for only over a journal.
The CLI also treats a connection closed by the server's own restart during update or rollback
as expected and reports what status shows once the runtime answers.

* fix(cli): type the reconnect status response explicitly

* fix(ssh): a wake's fence carries its owner token from the moment the lock exists

The idle-exit e2e still read 'fenced' on reconnect: the launch-time wake's
lock landed on the host but its connection dropped before the client saw OK,
so the wake body never ran and never wrote its owner token, leaving a fence
nothing could prove. The token is now claimed before the lock and written by
the same command that creates it, and a wake registers itself before any
remote step so a reconnected wake waits for it instead of racing it.

* fix(orcad): every activation fence holder carries a generation token its steps and release must still match

Astra pass 8: a holder suspended past the stale window resumed, kept acting, and its
unconditional release deleted the successor's fence and recovery journal mid-update. Every
holder (activation, rollback, stop, recover, wake) now writes a token into the lock it creates or
takes over. Each remote step it issues checks that token on the host, in the same command on
POSIX and inside the host script for Windows host ops; release is conditional on the token and
moves the lock aside instead of removing the root. A superseded holder aborts with
OrcadFenceLostError and its release is a no-op.

* fix(orcad): state mutations check the fence token before their lock, and refresh only a fence they still own

On POSIX the fence guard runs outermost in serializedStateMutationCommand, before the mutation
lock and the work, and the heartbeat touches the fence only while the token is still this run's.
The Windows host script records the --fence token and refreshFence compares it. A fence-lost
answer from a state mutation is a refusal, never a FAILED fallback. One owner-file constant
replaces the wake-owner copies.

* refactor(orcad): a state mutation's heartbeat touches the fence directory it checked ownership of (review)

* fix(orcad): a release moves the journal and lock aside and keeps only its own generation's

Astra pass 9: a release that passed its token check and stalled before deleting could, once a
takeover and a successor came and went, delete the successor's journal and lock. The journal is
now stamped with the writing run's fence token (a recovery takeover re-stamps the journal it
adopts), and the release renames the journal and the lock aside, deletes each only if it carries
this run's token, and otherwise moves it straight back.

* test(orcad): a successor restore stays busy beside a paused clear on the Windows host script

* fix(orcad): classify a lost fence from the step's exit and stdout, never the error message

The real exec error quotes the command, and every fenced command carries the
guard's marker text, so any failed or timed-out fenced step read as a lost
fence and dropped its unconfirmed flag. execCommand now attaches exitCode and
stdout to its exit error; execOrcadRemote rethrows unconfirmed terminations
before any reclassification.

* fix(orcad): a wake keeps its fence token until the fence is released

A disconnect fails a wake's next step without the unconfirmed flag, and the
fence release then fails over the dead connection. Forgetting the token on
that error left a fence the reconnected wake could not prove its own, so it
reported the host as held by an update.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* test(recovery): keep Phase 3's recovery-lifetime test on main's legacy-worker ports

Main added a required hasRequestedReleases port and now skips persist when a pass resolves
nothing, so the test mocks the new port and holds the pass at workspace resolution instead.

* fix(ssh): say "1 terminal" when another Orca desktop runs one on the host (#25853)

The terminalsElsewhere status line had no plural forms, so B9 read "while 1 terminals another
Orca desktop … are running". It gains _one/_other entries like the other terminal-count strings
on that line and in the move offer, which were already pluralized.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(automations): close failed and exited runs' terminals once the shell is proven alone (#25859)

* fix(automations): close failed and exited runs' terminals once the shell is proven alone

Failed (command not found, timeout) and forever-dispatched runs kept one
shell per run, unbounded and counted by the update gate. Their terminals now
close like completed ones (unused, past the grace, outside the newest few)
but only on fresh execution-host proof that the spawned shell is alone at
its prompt; a live or unprovable agent keeps its terminal. A still-dispatched
run closed this way is marked failed. Dead terminals no longer take one of
the newest-three keep slots. The update drain follows the same rules.

* fix(automations): prove a run shell alone from the process table, not the daemon's ownership flag

On a real daemon session the daemon's confirmShellForeground stays false
after a plain 'command not found' and after an agent that exited, because its
ownership flag only turns 'shell' after a full-screen command; failed runs
would never have closed. The proof now also reads the host's process table:
on POSIX the PTY's root shell must own the terminal foreground group with
nothing stopped under it, on Windows the host's job-based child census must
be empty. Anything unobservable still keeps the terminal.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* test(ssh): extensive orca CLI matrix on Windows hosts (#25114)

* test(ssh): extensive orca CLI matrix on Windows hosts

Adds three dispatch-only app cells to the ssh-windows-hosts lane that drive the
e2e build and the bundled orca CLI against the provisioned Win32-OpenSSH host:
empty-host deploy/terminal/reconnect/orcad-restart/decommission, seeded
relay-era conversion, and an open relay terminal keeping the host on the relay.

* test(ssh): pin the relay-kept cell's runtime; keep cleanup from masking failures

* test(ssh): run decommission before the orcad restart in the managed cell

* test(ssh): decommission through orca environment stop; accept an unverifiable relay close

* test(ssh): require a confirmed relay close; app cells must run last

* test(ssh): log and accept either relay-kept census reason; keep app-cell test results

* test(ssh): relay-kept requires a live census and its status line again

* test(ssh): match the pluralized relay-kept status line

* test(ssh): orcad restart proves a new process, terminal adoption, and a kill-then-connect relaunch

* test(ssh): restart kills only the orcad server, not its terminal daemon; wait for a released profile

* test(ssh): collect orcad.log.1 so a restarted orcad's previous run is kept

* test(ssh): the managed cell proves a workspace listener is detected and attributed

* test(ssh): start the port listener without $, so a PowerShell terminal doesn't expand it

* test(ssh): the port check proves Windows command-line attribution; retry a dropped version read

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* refactor(runtime): move run-terminal client-use and shell-alone checks out of the branch-cleanup runtime (#25876)

* refactor(runtime): move run-terminal client-use and shell-alone checks out of the branch-cleanup runtime

orca-runtime-preserved-branch-cleanup.ts had grown past max-lines (303) with
the headless run-terminal helpers. Their logic now lives in
run-terminal-client-use.ts and the runtime keeps one-line delegators, with
behavior unchanged.

* fix(ci): the runtime Electron ratchet bundles its entry points once, not 2.5k times

check-runtime-electron-ratchet bundled ~2,532 entry points each in full (format cjs, no
splitting), so esbuild held thousands of copies of the runtime graph: about 2.2GB RSS and 11s per
run, twice per test file. It was in flight in every unit shard that died with "The runner has
received a shutdown signal" (#25815 5/5 twice, #25876 2/5 twice). With esm + splitting the shared
modules land in one chunk: same metafile, about 200MB and 2s.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* test(ssh): wait for the busy relay's child before probing it (#25916)

The fake relay's spawn is not visible to pgrep at READY on Linux under Bun, so
the probe could count zero children. The sibling cases already wait.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a quit that aborts an upload whose read already ended no longer crashes main (BUG-23) (#25922)

Quitting while an on-connect orcad update was uploading its bundle aborted the connection's
teardown signal. sftp-upload's abort handler destroyed the local read stream with the signal's
reason, but once that read had ended, 'finished' had already removed its listeners, so the
stream emitted an unhandled 'error': [main_uncaught_exception] AbortError: This operation was
aborted. Electron's error dialog then blocked the main thread and the app never exited.

The read stream now always has a no-op error listener; the transfer's outcome still comes from
'finished'. Both the bare upload and the connection-level teardown abort are covered.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): a launch reads readiness at least once; fake hosts match the capture's tar flag, not any -cf (#25918)

A random fence token contains `-cf` about 1 time in 125, and the fake hosts
read any command containing it as a snapshot capture, so a rollback's restore
answered CAPTURED and the rollback never launched. Separately, a client
descheduled between computing the readiness deadline and checking it skipped
every read and failed a ready launch.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): a fence this desktop's exited process left is cleared without the 20-minute wait (#25941)

* fix(orcad): a fence this desktop's exited process left is cleared without the 20-minute wait

The client records the fence tokens its processes hold beside the profile. On
a later launch, a POSIX fence carrying a token from a process that has exited,
quiet for three heartbeats and with no live state mutation, is backdated so
the existing stale rules clear it or hand it to Recover at once. Another
desktop's fence, a live holder's, or one with a mutation still running keeps
the normal stale window.

* fix(orcad): held fence tokens are best effort, pinned to this machine and boot, and pruned after a day

A token-file write that fails no longer breaks a fence operation; an entry
recorded on another machine sharing the profile, or before a reboot, never
proves its holder exited; entries older than 24 hours are dropped. Tests cover
a journal kept for Recover, a successor freshened back after a racing backdate,
a Windows host, and the record across release, supersession, busy and a lost
connection.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): an install lock this desktop's exited process left mid-upload is taken over without the 20-minute wait (#25991)

A quit during the bundle upload leaves the version dir's install lock, not the
activation fence. The lock now carries this desktop's token, recorded in the
held-token store, and is forgotten only once its removal is confirmed. On a
later attempt, before each stale check, a POSIX lock whose token belongs to an
exited process of this machine and boot, quiet for three minutes, is backdated
so the existing stale takeover claims it at once. The fence path now shares
the same helper.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): a desktop that met another desktop's update fence clears its note once the host answers (#25995)

The serving note "holds this host" and a fence-busy update deferral stayed until a reconnect,
minutes after the other desktop's update finished. The connect now rechecks serving and the
update every 45s while the fence holds, and publishes the first answer without it. A recorded
deferral is dropped once the host runs its candidate or a newer release.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>

* test(ci): run Phase 3's SQLite-backed tests in the Node runtime project

Main's #25967/#25998 boundary requires every test that opens real SQLite to be listed. This adds
Phase 3's eleven orcad and SSH migration tests, plus main's own agent-launch-instant-tab test
(#25430), which main's tip also leaves unlisted.

* fix(ci): keep Electron probes out of the node-server suites again (#26046)

The runner excluded *.electron.test.ts with a CLI --exclude, but main's switch to Vitest inline
projects (#25967) gave each project its own exclude list, which overrides the CLI one. The
directory selectors then pulled profile-state-writer-stall.electron.test.ts into the glibc-floor
and musl orcad-template jobs, which have no xvfb. Resolve the exact files with vitest list and
drop Electron and cross-runtime ones before running.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): keep the SSH host card quiet while its managed server is healthy (#26072)

The card showed "Runs a managed Orca server" under every healthy host. A managed server is the default, so the status line now appears only for setup progress, updates, the relay, or failures.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): Move to managed server keeps the host's terminal tabs (#26077)

* fix(ssh): Move to managed server keeps the host's terminal tabs

Move stops the relay shells; their exits read as a user exit and closed
the tabs before the conversion copied them to the server. Suppress those
exits for the move, restart stopped shells on the relay when the host
stays, re-home the open workspace onto the server, and report stopped
shells to the runtime so terminal list stops calling them connected.

* fix(ssh): mark Move's relay stops in main's intentional-stop register

The renderer-only exit suppression left main retiring the stopped tab from
the saved SSH session before the conversion copied it, left other viewers
unprotected, and swallowed real exits for the whole request. Register
exactly the shells the move stops, from just before each shutdown, as a
'replaced' stop with their incarnation; main keeps the surface and labels
the exit for every viewer, while a confirmed death stays 'exited'. Move
now returns the shells it stopped, and a host that stays on the relay
restarts only those tabs, discarding any buffered exit first.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(hosts): show an SSH host and its managed Orca server as one host (#26076)

* fix(hosts): show an SSH host and its managed Orca server as one host

Phase 3 registers the Orca server it deploys over SSH as its own runtime environment, so every
host list built from the execution-host registry listed the machine twice under the same name.
The registry now folds the pair into one row named after the SSH host. The row routes to the
server, since a managed host has no relay, unless main reports the host back on its relay; the
other id stays as an alias so selections and renames saved under it still resolve. A retired id
that workspaces still point at keeps its own row, and servers no configured SSH host deployed
(manual pairings, orphans) are untouched.

* fix(hosts): keep both ids of a merged SSH host and dedupe only in pickers

Deleting the merged-away id from the registry broke every consumer that matches hosts by exact
id: Add Project fell back to local after a connect, the composer lost ready projects and drafts
(and could swap in an unrelated local project), and a host scope hid folder-only workspaces.

The registry now keeps both entries and marks the pair (aliasHostIds on the row pickers show,
mergedIntoHostId on the other). Pickers show one row per machine, and a choice of that row
expands to both ids: sidebar host scope, jump palette filter, notification toggles, run-target
and repository host offers. Add Project resolves a saved SSH id to its server row and blocks the
actions while that server comes up instead of choosing local. The composer's resolver now fails
closed when a named draft repo isn't actionable rather than picking another project.

* fix(hosts): widen saved host scopes, both-way palette aliases, guard Add Project host

- A sidebar or agents host scope saved by an older build (or before a route flip) can hold one id
  of a merged SSH host; a background gate widens it to both ids so exact-id filters match either
  owner.
- The palette host filter now resolves a saved id to both owners whichever id it names.
- Add Project's create and clone refuse to run while the chosen host is unresolved, and their
  submit buttons stay disabled, instead of falling through to this computer.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(sync): reconcile main's ratchet bundling and cold-serve hydrate with phase3

The Electron-import ratchet keeps main's single-stdin bundle (cjs); the
auto-merge had also kept phase3's esm splitting, which broke main's
import-graph test. Editor tabs now follow the windowless full-seed rule
from #26022, so a cold serve restart lists persisted editors too.

* fix(ssh): reclaim this desktop's own exited lock on Windows hosts too (#26087)

* fix(ssh): reclaim this desktop's own exited lock on Windows hosts too

The relaunch after a quit mid-update now frees the activation fence and the
version-dir install lock on a Windows SSH host the same way it does on POSIX,
instead of waiting out the 20-minute stale window. The host script ages the
lock only when its token belongs to a desktop process proven exited, it has
been quiet for three heartbeats, and (for the fence) no state mutation is
live, where a mutation holder counts as gone only by pid plus creation time.

* fix(ssh): take an exited holder's lock only through the steal arbitration

Review found the reclaim backdated the lock by path after checking it, so a
live successor that replaced the lock in between could be aged and then
stolen, and an interrupted or failed restore left it aged for good.

The exited-holder check is now read-only. The steal command itself accepts
the proven token and, inside its steal claim and identity recheck, also takes
a lock whose owner file still names that token and that has been quiet for
three heartbeats. Nothing is written to a lock before the steal owns it.
POSIX uses the same path.

* fix(ssh): never take an exited holder's fence while a state mutation can start

Review round 2 found the fence's live-mutation guard ran only in the read-only
proof, so a mutation admitted after the proof, or one whose first heartbeat
landed after the steal sampled the fence's age, kept running under a fence
the steal had replaced.

For the fence, the steal now takes the state-mutation lock inside its claim
(mkdir on POSIX, the exclusive owner.json on Windows) and holds it until the
takeover is done; it refuses when any mutation lock exists. Holding it, it
rereads the owner and only then re-samples the fence identity. A mutation now
rechecks its fence token right after it takes the mutation lock and stops with
the fence-lost marker if it changed. The Windows proof also falls back to the
stale window when its command line would not fit cmd.exe.

* fix(ssh): record the exited-owner steal as a real mutation-lock holder

Review round 3 found the POSIX steal held the state-mutation lock as an empty
directory, which a mutation reclaims after a minute without any liveness
check; a steal stalled that long lost its exclusion and could replace the
fence under a running mutation.

The steal now writes its pid (and group, under the same rule) with the
mutation's own noclobber owner writer, so only proof of its exit frees the
lock, and it removes the lock only while the lock still names it. On
Windows the owner record is moved into place whole, so it never exists
empty, and is removed only while it still names the steal's pid.

* refactor(ssh): keep the relay lock commands off the orcad host-script graph

The mutation-lock owner writers moved into a leaf module, so the relay's
install-lock commands no longer import orcad-state-snapshot and, through it,
the Windows host script, orcad-instance-lock and the daemon process query.
Those modules evaluate imports at load time that existing suites mock
partially. No behavior change.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-10-07 03:15:06 -07:00
Kelvin Amoaba 8f424512b3 feat(native-chat): suggest files on @ and name the triggers (#26017)
Shift+Enter and right-click no longer pick a row in the / menu either.
2026-10-06 23:29:20 -07:00
Jinwoo HongandClaude 72b84118e0 test(e2e): terminal layout parity check against main (#25681)
* test(e2e): add terminal layout parity check for topology refactor PRs

Runs fixed terminal-layout journeys in the real app on two builds, captures
the renderer topology and the saved workspace session, normalizes volatile
values and fails on any difference not declared for a named bug.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): record quit exit status and report paths main does not reproduce

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): invoke pnpm correctly under corepack and silence the typeless-module warning

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): accept pnpm's forwarded -- in the layout parity runner

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): close parity panes through the user's chord and treat absent maps as empty

Driving PaneManager.closePane directly left main to learn of the close from the
PTY exit, which raced quit; the keyboard path commits the close in main first.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): build parity checkouts before running, forward -g, and add a drag-out scenario

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-06 18:26:49 -04:00
Neil 40f03199f8 fix(native-chat): fingerprint the consumed transcript boundary (#25965) 2026-10-06 14:11:17 -07:00
Neilandsotashimozono 7e52bed4e0 fix(editor): recover PDF previews after failed refreshes
Recover PDF previews after failed refreshes, clear previous pages on changed contents, and preserve scroll and zoom while releasing superseded resources.

Co-authored-by: sotashimozono <shimozono-sota631@g.ecc.u-tokyo.ac.jp>
2026-10-06 13:37:10 -07:00
Jinwoo Hong 2f001fe880 fix(remote-browser): host client browser pages again when a paired host comes back after an outage (#25827)
* test(remote-browser): reproduce a client-hosted browser tab left unavailable after a network outage

A paired desktop that loses its host for longer than the browser-host
lease grace (25s reset cut, or a 100s silent cut the host's heartbeat
reaps) tears down its client-hosted pages and never hosts them again,
leaving 'Client-hosted browser unavailable'. Red on main.

Extends the freezable TCP proxy with a full silent or reset link cut.

* fix(remote-browser): host client browser pages again when a paired host comes back after an outage

When the network between a paired desktop and its Orca host dropped for longer
than the browser-host lease grace, the desktop tore down its client-hosted
pages and never offered to host them again, so the tab stayed on 'Client-hosted
browser unavailable' until the app was relaunched.

Regaining contact with a runtime now re-claims browser hosting the same way a
runtime restart already did: if this desktop still has client-placed pages for
that host, it starts its browser client host again and the host's existing
recovery re-places them, reloading at their last URL. The notice now says the
host is offline (and that the page reloads when it reconnects), names another
desktop only when the placement belongs to one, and otherwise says the page is
not available here right now.

* fix(remote-browser): claim offline only for a host that can reconnect, and re-host from the notice

The unavailable notice said the host was offline for any null status, which
also covers a retired or refused pairing and a host whose runtime did not
answer. It now uses the host connection state and excludes revoked verdicts.

When the control link returns while the old browser host is still inside its
lease reconnect, the contact-edge ensure is a no-op; if that reconnect then
fails, nothing re-hosts. The notice now re-claims hosting on mount while the
host is reachable, which is exactly that stranded state.

Plain regained contact no longer queues a second preparation behind one
already in flight; only a runtime restart does.

* fix(remote-browser): re-prepare browser hosting on every regained contact

A preparation still in flight when contact returns started while the host
was unreachable, so it is the one most likely to fail. Coalescing the
contact edge into it dropped the only trigger that could succeed and left
the page unavailable. Always queue one follow-up preparation instead.
2026-10-06 16:02:29 -04:00
Jinwoo Hong e2ccf52dc3 fix(remote-runtime): a paired terminal accepts input again after its host app relaunches (#25736)
* test(remote-runtime): reproduce dead input after a paired host relaunch

A relaunched desktop host accepts RPC before its renderer publishes a window
graph. During that gap session.tabs.list answers with an unpublished empty
graph (publicationEpoch "none", snapshotVersion 0, tabs []). The client's
reconnect inventory treats that as removal and retires the pane
(retireRemoteTerminalId(-1)): the pane goes to "ended", the reconnect overlay
disappears, and keystrokes never reach the surviving daemon PTY.

Both tests are red on main by design; they are the repro for the fix.

- e2e: holds the relaunched host's runtime:syncWindowGraph so the client's
  reconnect deterministically meets the unpublished host (4/4 red).
- unit: transport-level repro of the same retirement (red in ~2s).
- restart helper gains a beforeFirstWindow hook; LaunchOptions moves to its
  own module to stay under the max-lines budget.

* fix(remote-runtime): a paired terminal survives its host app relaunching

After the host app quit and relaunched (daemon still running), a paired
client's terminal cleared its reconnect overlay and then ignored all input.
The relaunched host answers session.tabs.list before its renderer publishes,
with a synthesized empty frame. A paired client sees that frame through its
navigation projection as epoch "none:client-navigation", which the shared
"does this frame answer for the worktree" check did not recognise, so the
pane read it as removal and retired itself.

- hostSnapshotAffirmsWorktreeContents treats the client-projected placeholder
  as no answer at any version (the projection adds its navigation revision).
- The reconnect inventory keeps polling on such a frame instead of retiring;
  a published frame lacking the surface still retires the pane.
- Pushed frames that affirm nothing no longer report the surface absent.
- When the bounded inventory wait ends without evidence, the first published
  snapshot carrying the same handle now reattaches the pane, rather than
  waiting out the ~3 minute auto-recovery deadline.

Client-only: the host already labels the frame, old hosts send the same
frame, and the wire is unchanged. The e2e helper drops its type assertions.

* refactor(remote-runtime): simplify the host-relaunch reconnect fix

- One epoch rule: the placeholder epoch, bare or with the shared
  client-navigation suffix, is no answer. Hosts only ever send it at
  version 0, so the version check is dropped.
- Unpublished frames are dropped where pushed snapshots enter, instead of
  a nullable per-subscriber update.
- A published same-handle snapshot fires the parked retry through
  retryNow(), which now also fires a retry parked while still
  'recovering' (nothing is in flight then). This replaces state-reading
  wiring in the listener and lets online/resume fire it too.
- The Windows e2e runs the fixture through PowerShell's call operator.

* test(remote-runtime): prove the e2e hold beat the relaunched host's first publish

The spec now asks the held host for its tab list and requires the
unpublished placeholder before releasing, so a hold installed too late
fails instead of passing against unfixed code. The helper keeps its gate
in a typed global instead of Reflect lookups.

* fix(remote-runtime): keep a fenced handle's parked retry revivable

A retry parked for a handle that needs a replacement was consumed by an
early external trigger and then refused by the epoch's snapshot-wait
guard, leaving nothing to revive the pane. An external trigger now takes
over that snapshot wait as a fresh attempt, and a republished fenced
handle no longer fires the retry. Also refreshes comments that still
paired the placeholder epoch with version 0.

* fix(remote-runtime): a host answers an empty worktree for real once it has published

The host sent its unpublished placeholder for any worktree it had no
entry for, including one its window had never opened, long after
startup. With the placeholder now read as "ask me later", a paired client
opening such a worktree never got its first terminal.

The host now sends the placeholder only until its graph first publishes.
Afterwards a worktree with no tabs gets a real empty answer, and a client
that was told "ask me later" during startup is sent that answer once on
publication. Against an older host the client withholds the automatic
first terminal for such a worktree, which the user can still create.

A host push now fires only a parked retry, never cutting a scheduled
backoff short.

* chore(reliability-gates): reference the empty-worktree tests in gate commands

* test(remote-runtime): find the never-opened worktree by repo id on every platform
2026-10-06 00:47:27 -04:00
Kelvin Amoabaandmmarabel 1168e0f8c8 fix(ssh): let a placed worktree seed while its host is in conflict (#23213)
One host tab on a folder this client lacks marked every worktree on the host unverifiable, so reopening an emptied one never got a terminal.

Fixes #22015

Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>
2026-10-05 16:22:17 -07:00
Neil 6c07570040 fix(cursor): keep usage cookies on the selected account (#25243)
Keep Cursor quota requests tied to the selected account by omitting ambient Electron session credentials. Preserve explicit account cookies and redirect handling.

Credit: Li-Sanze for the original credential-mode fix in #23626, carried through #24575; jjongsta and chengjiaxiao for the reports. Native HTTP/HTTPS cookie isolation, mutation controls and proxy behavior were verified before merging. This does not claim to resolve the separate initial-authentication failure in #23612.
2026-10-04 14:47:05 -07:00
NeilandNeil 130b2ef425 feat(documents): open CSV and TSV files from the OS
Extend existing OS document associations and delivery to CSV/TSV, preserving restoration and authorization.

Co-authored-by: Neil <neil@stably.ai>
2026-10-03 00:51:13 -07:00
Neil b332742b89 Speed up large Markdown Find and render oversized tables (#24948)
* Speed up large Markdown Find and render oversized tables

* Poll table preview geometry outside hidden renderers

* Preserve Markdown navigation through refresh and tab restoration

* Confirm Markdown restoration when refreshed content is ready

* Trace table refresh positions and update Unicode search reference

* Recognize queued measurement scrolls before restoring Markdown anchors

* Rebuild Markdown Find ranges after renderer components change
2026-10-03 00:39:44 -07:00
Kelvin Amoaba 3fba1952c8 perf(tab-bar): a change to one tab no longer re-renders every tab (#24261)
With many tabs open, a change to any one tab (a retitle, an agent finishing, a tab switch, a git status write, or a browser tab update on SSH and web clients) re-rendered every tab in the strip, so the strip stuttered. Each tab is now a memoized row that re-renders only when its own values change, with stable handlers, a stable drag id list and stable drag sensor options. Editor tabs get their own git status, and mirrored browser tabs keep their page-id list while the ids don't change.

Part of #24241: opening, closing or reordering a tab still re-renders every tab once.
2026-10-02 16:47:05 -07:00
Neil d9fbb4eecf Keep SSH typing replies inside narrow split terminal panes (#24682) 2026-10-02 02:39:34 -07:00
b49abdb1f4 fix: recover renderer launch failures in the running app (#24250)
* fix(recovery): back off a launch-failed renderer instead of tripping the crash breaker

A renderer that the OS refused to spawn (macOS exit 1003 = LAUNCH_RESULT_FAILURE; field
cause: per-user process limit, posix_spawn EAGAIN) burned the 3-reload crash-loop budget
in ~750ms and raised a "graphics driver" prompt, while the condition lasted minutes.

- launch-failed retries in place on a 250ms..60s backoff (~2 min), outside the breaker;
  a loaded document resets it. Other crash reasons keep the breaker.
- Each launch failure records renderer_launch_failed_probe {spawnError} from a cheap
  spawn probe, so bundles name EAGAIN/EACCES/ENOENT directly.
- The exhausted prompt says the process limit was hit (probe EAGAIN), drops the
  graphics-driver wording, keeps Try Again as default, and offers no Restart:
  app.relaunch also needs a free process slot and silently fails without one.

* fix(recovery): skip the launch probe on Windows and probe the prompt once

- Re-check quitting after the prompt's probe; don't re-probe on Copy Commands.
- recordRendererLaunchFailureProbe never rejects (breadcrumb write guarded).
- Windows: no spawn probe; a child per failed launch is the per-operation burst EDR scores.

* test: cover quitting and duplicate renderer launch failures

* test: use typed access in PTY delay regression fixture

* fix: scope extended launch retries to POSIX hosts

* test: cover launch probe behavior on native Windows

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-02 01:50:08 -07:00
Neil 53930a161b Keep SSH typing replies visible during background pressure (#24629)
* test: align source-control fixtures with current store contracts

* Bound E2E package setup and retain cancelled-job traces

* Remove empty passing sentinels from opt-in socket tests

* Make SSH typing pressure fixture readiness and replies observable
2026-10-02 01:21:11 -07:00
Neil 3f37fcc423 test: align source-control fixtures with current store contracts (#24571) 2026-10-02 00:21:38 -07:00
Neil ba9af21d75 test: classify terminal driver input with the current PTY contract (#24560) 2026-10-01 23:56:49 -07:00
Neil c9a9b8d109 test: restore delayed PTY writes in large-paste coverage (#24556) 2026-10-01 23:44:39 -07:00
Neil b666d07117 test: update worktree setup and enforce cleanup results (#24552) 2026-10-01 23:38:52 -07:00
Neil 1fbfb13e0f test: isolate seeded Git repositories per Playwright worker (#24550) 2026-10-01 23:31:24 -07:00
Neil 02790c53e8 Fix Codex status after Ctrl+C copy and side-chat navigation (#24339)
* fix: preserve Codex status on ambiguous Ctrl+C input

* fix: confirm Codex turn cancellations from host rollout records

* fix: keep ephemeral side hooks separate from the Codex main turn

* perf: watch active Codex rollouts and skip unrelated records

* fix: retain confirmed Codex cancellation across late relay events
2026-10-01 17:33:58 -07:00
Brennan Benson 8c1670f2c4 test(e2e): fake Codex answers the --no-daemon --help probe without a spawn (#24440)
* test(e2e): fake Codex answers the --no-daemon --help probe without a spawn

Since #23933 Orca runs `codex --help` before a path-named Codex launch. The
fakes logged it as an agent spawn and held the probe for its 5 s timeout.
Move the app-server refusal and --help answer into one shared
FAKE_CODEX_LAUNCH_PROBES_SOURCE used by every fake Codex.

* test(e2e): stop asserting the dispatch capability column a current worker no longer has

#23994 stopped minting the per-dispatch capability, so capability_hash is null.
The --help spawn failure used to stop this test before it got here.
2026-10-01 16:57:15 -07:00
Jinwoo Hong 9afd1101ff fix(orchestration): stop minting and printing the dispatch capability (#23994)
* fix(orchestration): authorize worker reports without the dispatch capability

Worker lifecycle reports and questions no longer depend on the per-dispatch
capability token that lives only in the agent's conversation. The host now:

- ignores capability_hash/capability_revoked_at for authorization on every
  row and checks the exact worker process instead (ask gains that check);
- refuses a report whose calling terminal is provably another orchestration
  party (a Run coordinator or another Dispatch's worker), treating env that
  names no live pane here as absent;
- applies one worker-state rule locally and remotely: a stop in flight
  refuses, while stop_unknown and start_unknown accept and settle.

Minting and printing the flag are unchanged, so an older host and older
preambles keep working.

* fix(orchestration): stop minting the dispatch capability

Dispatches no longer mint a per-Dispatch token, and preambles, the bundled
skill guide and the ask resume hint stop printing --dispatch-capability. The
consumer-generation bump and delivery fence that minting carried stay, now
as setDispatchConsumer. Readers that inferred meaning from capability_hash
read what they meant instead: worker-show's injected stage comes from the
attached consumer, and a failed start copies custody identity only when no
authority was ever attached. The CLI keeps accepting and forwarding the
flag for older hosts.

Cancelling a Task is recorded as failed with a reason; task-list now shows
that reason and the guide and task-update notes document the recipe.

* fix(orchestration): name the fenced party without implying which Dispatch it owns

* refactor(orchestration): one worker report rule, fence only a different party

- One module owns the unproven/settleable worker states and the refusal rule; local send records it, ask and remote throw it. A stale process is worker_identity_changed on every path.
- The caller fence passes the worker's own terminal when its --from handle went stale.
- Document the shared-tmux-server limit; drop the dead dispatch_capability_invalid rejection member; tests assert dispatch state, not the capability column.

* refactor(orchestration): drop setDispatchConsumer and the dead capability retention

- dispatch --inject no longer re-points the row createDispatchContext just wrote; worker-show reports every worker-less Dispatch as context_only, since Orca keeps no record of the paste. Tests re-point through a fixture.
- failWorkerStart always records when the lifecycle closed; nothing authorizes on it.
- Restore the ask resume hint's echo of a passed --dispatch-capability: an old host checks it before --resume.
- Move the cancellation convention to #23983.

* test(orchestration): drop capability-era assertions other tests already cover

* refactor(orchestration): drop the host-side capability field and no-op test fixtures

- RpcRequest and the SSH bridge stop carrying orchestrationCapability; the CLI's wire field stays for older hosts.
- Fixtures pass identity to createRootDispatch instead of re-pointing to the same values; drop absence checks for a flag that can no longer be produced.

* test(orchestration): cover a current process whose terminal moved to another pane

* chore(orchestration): finish the capability cleanup in test stubs and skill wording

* test(orchestration): drop needless response casts; mark the db stub cast safe
2026-09-30 13:26:06 -04:00
Jinwoo Hong c3183a4556 test(e2e): let the completed-worker fake Codex answer the --help probe (#24033)
#23900 probes codex --help before each launch; the fake counted it as a worker spawn, breaking two specs.
2026-09-30 02:33:12 -04:00
Brennan Benson 21d4ae9448 feat(agents): pre-trust the folder wherever Orca starts an agent (#23744)
* feat(claude): pre-trust worktrees Orca creates

Claude Code asks "Do you trust this folder?" on first launch in any folder it
has not seen, which blocks unattended launches in worktrees Orca itself made.
Orca now records where a worktree's content came from when it creates it, and
before each Claude launch writes Claude's own folder-trust entry for that
worktree's root (never the main checkout) when the new setting is on and the
content is the user's repository. Forks, bare commits, folder workspaces and
external checkouts keep Claude's prompt. The write takes Claude's lock, never
creates or breaks the file, runs on the SSH host itself, and is revoked when
the worktree is removed or the setting is turned off.

Launches that already pass --dangerously-skip-permissions also skip the trust
prompt for that one process only, via CLAUDE_CODE_SANDBOXED=1 on the command.

* fix(claude): parse the relay trust request with a schema and ship its search keys

* fix(claude): never write a WSL guest's trust into the Windows config

A WSL worktree's Claude reads the guest's own config. Two paths still wrote
its trust into the Windows host's ~/.claude.json instead: the Claude auth prep's
fallback (runtime 'wsl' but the host config dir, when the WSL home cannot be
resolved), which wrote a Linux-path key the removal revoke can never delete;
and the Agent Teams leader, which passes no auth or distro and wrote a UNC key.
Require the guest's own config dir, and treat any WSL worktree path as guest-only.

* revert(claude): drop the skip-permissions trust shortcut

Pre-trust stays limited to worktrees Orca creates from the user's own
repository. The per-launch CLAUDE_CODE_SANDBOXED prefix skipped Claude's
trust question in every folder for launches carrying the skip-permissions
flag (Orca's default Claude args), including the user's own folders and fork
PR worktrees, and the setting could not turn it off. Remove the prefix, the
inherited-variable strip that existed only for it, and the Agent Teams
leader-to-teammate propagation; restore the tests that pinned the prefixed
launch string.

* fix(claude): revoke SSH trust in the config file the grant used

At spawn the relay resolves Claude's config from the launch env, which carries
a CLAUDE_CONFIG_DIR set in Orca's Claude default env. claudeTrust.converge had
only the relay's own process env, so removing the worktree or turning the
setting off revoked in the default file and the grant outlived the worktree.
Send the config-file keys with the request, as the local revoke already uses.

* i18n(settings): translate the Claude worktree trust setting

* fix(settings): say Claude trust applies when Orca starts Claude

The description said Claude skips its trust prompt in any worktree Orca
created. Trust is written only when Orca itself starts Claude there, so a
`claude` typed by hand in a fresh worktree still asks. Say that, and bring
the es/fr/ja/ko/zh translations in line with the new text.

* fix(worktrees): treat a base on an Orca-added fork remote as fork content

A worktree based on a named ref was always stamped as the repository's own
content, so picking the fork remote Orca adds for a pull request (or a local
branch tracking it) as the base made a fork's code eligible for Claude trust.
At create time, read the repo's `remote.<name>.orca-created` markers and each
branch's tracked remote in one `git config` call; a base on such a remote is
stamped as a fork's content, and a read failure is not vouched for. Remotes
the user added, such as `upstream`, stay first-party.

* perf(claude): revoke worktree trust once per config file, not per worktree

Turning "Trust worktrees Orca creates for Claude" off read and parsed the
whole Claude config once per Orca worktree on the main process. Group the
revocations by config file locally and by SSH connection, and make
claudeTrust.converge take a batch of requests.

* feat(settings): one agent-wide "trust the folder" setting in Settings > Agents

Replace the Claude-only worktree trust toggle with a single setting,
agentWorkspaceTrustEnabled (on by default; unreleased, so no migration).
The row says what it does for every agent: agents Orca starts skip their
"trust this folder?" prompt in that worktree or folder, turning it off
stops new trust while existing trust stays, and while it is off unattended
launches (orchestration workers, automations, the phone) stop at the
agent's trust question until someone answers.

Translations for es/fr/ja/ko/zh. Also restores the `awaitingUnnamed` chat
catalog keys an earlier merge of main dropped from this branch.

* feat(agent-trust): pre-trust the workspace for every preset agent at PTY spawn

Every Orca-started agent PTY passes through one of the two spawn builders
with its declared launchAgent, which survives setup-script wrapping. The
builders now call one hook that, for a fresh launch (never a reattach or
restored pane) with the setting on, applies the agent's trust preset to
the worktree, folder workspace or main checkout it starts in.

- One dispatcher, applyAgentWorkspaceTrust(preset, workspacePath, launch
  context), carries what a writer needs: the final spawn env, the Claude
  managed-account auth prep, the WSL distro and the SSH connection.
- Claude joins the presets on both the claude and claude-agent-teams
  entries. Its writer stays grant-only in claude-folder-trust-file.ts:
  the file Claude reads (CLAUDE_CONFIG_DIR / custom-OAuth suffix / legacy
  .config.json / a WSL guest's own file), Claude's <file>.lock never
  broken and taken only when a write is due, atomic temp+rename keeping
  mode and symlinks, never creating the file, NFC + realpath keys.
- SSH Claude launches forward the optional claudeFolderTrust spawn field
  so the relay grants with its own spawn env; old relays ignore it and
  Claude asks. Other presets keep the SFTP writer. A WSL launch never
  writes the Windows home: non-Claude presets skip it, Claude writes the
  guest's file or nothing.
- Codex keeps the 20 s deadline its shared config lane needs; every other
  preset gets 1.5 s. A miss means the agent asks; trust bookkeeping never
  fails or blocks a launch.

Removes the Claude-only machinery this replaces: the eligibility/host/
lifecycle/spawn modules, the persisted creation content-origin field and
its classification, revoke-on-removal, the setting-off sweep, the
claudeTrust.converge relay method and the Agent Teams leader special case
(the leader pane now spawns through the hook with the claude preset).
The agent config types move to tui-agent-config-types.ts so the config
table stays under the line budget.

* refactor(agent-trust): delete the pre-spawn trust writes the spawn hook replaces

The spawn hook is now the only owner of agent folder trust, so remove
every other writer:

- the agentTrust:markTrusted IPC channel, its preload bridge and types,
  and all renderer callers (agent-trust-preflight and its callers in the
  background session, work-item direct launch, session continuation,
  worktree creation, folder workspace composer and session fork);
- the main pre-spawn sites: the createdWithAgent preflight in
  worktree-remote.ts, markLocalWorktreeTrusted/markRemoteWorktreeTrusted
  and the runtime's markWorkspaceTrustedForAgent family with the
  markTrusted ports of the runtime create flows;
- Codex's own launch-prep and resume-prep trust writes.

Each of those launches reaches a spawn builder with launchAgent set, so
the hook covers it. This also fixes a live gap: the worktree-remote.ts
copy of the preset switch omitted Antigravity, so an agy agent started
from a desktop worktree create still asked; the single dispatcher covers
it. Trust is also written on the host the PTY actually spawns on, which
removes the #11163 class of writing the wrong host's config.

* test(agent-trust): type the spawn-builder trust fixtures and prove the spawn waits for trust

The builder test passed untyped args (a string launchAgent) and cast its deps,
which failed tc:node. It now builds both spawn states from a fully typed deps
fixture and a typed restored pane, with no casts.

Adds a case that holds the trust write pending and checks the builder does not
finish until it settles, the ordering the deleted renderer and launch-prep
tests used to cover.

* fix(agent-trust): give SSH trust writes the 20 s deadline again

The dispatcher gave every non-Codex preset a 1.5 s budget, including the SSH
writers for Cursor, Copilot and Qoder, which make several round trips over the
link. Before this PR those writes had 20 s (desktop) or no limit (runtime), so
on a slow link an unattended SSH worker would now stop at the agent's trust
question. SSH writes get the 20 s deadline back; local non-Codex writers keep
the short budget, and Codex keeps 20 s.

The relay's Claude grant keeps the short budget: it writes the relay host's own
disk and does not cross the link.

* fix(agent-trust): never pre-trust a home folder or a filesystem root

A folder workspace can be the user's home folder or a disk root. Claude and
Copilot let a trusted folder cover every folder under it, so pre-trusting one
of those would silently trust everything on the machine for those agents.

One check, isHomeOrFilesystemRoot, now refuses them for every preset: the
dispatcher checks roots and this machine's homes (including the spawn env's
HOME and a cached WSL guest home), the SSH writer checks the remote home it
already resolves, and the relay checks its own home. The agent then asks, as
it would without Orca.

* refactor(agent-trust): drop the Codex launch plumbing that only carried trust

The spawn hook replaced the trust writes in Codex launch prep and resume prep,
which left the fields that fed them unread: CodexHomeLaunchContext.workspacePath
and .launchAgent, the resume prep's workspacePath, and the structured Codex
launch input's workspacePath (plus the extra target lookup that produced it).
Remove them and their plumbing; unavailableManagedHomePath stays.

Also removes test stubs of runtime trust methods this PR deleted, whose
not-called assertions could no longer fail, and two comments that still
described the old trust preflight.

* chore(reliability-gates): point the trust gate at the spawn-time trust tests

The agent-session trust gate still listed three test files this PR deleted
(the renderer preflight, the Codex launch-prep deadline and the e2e trust
completion suites), so check:reliability-gates, which runs in PR CI and in
pnpm lint, failed on missing files. Its invariant also described the deleted
IPC handler and pre-spawn writers.

The gate now covers what replaced them: the spawn builders holding the spawn
until trust settles, the fresh-launch and setting gates, the per-preset
deadlines, and the home and root refusal.

* fix(agent-trust): skip the relay Claude grant for a WSL shell

On a Windows SSH host whose pane shell is wsl.exe, Claude runs inside the WSL
guest and reads the guest's config. The relay still granted trust in the
Windows host's own .claude.json, writing the Windows home for a WSL launch,
which the local path never does. The relay now skips the grant there, so that
Claude asks, as a local WSL launch does when Orca cannot reach the guest file.

* perf(agent-trust): only agent launches wait on the trust hook

Both spawn builders awaited the trust hook on every spawn, including plain
shells, reattaches and agents without a preset. Awaiting even a resolved
promise adds microtask ticks ahead of the pane-spawn reservation check, and
this handler already keeps non-Codex spawns off an await because an extra tick
reorders those reservation races.

The hook now returns null when there is nothing to write, and the builders
await only a real trust write.

* test(agent-trust): keep the home and root cases off any real Claude config

The home and root cases ran the real Claude writer with the test process's
env, so a regression in the guard would have written trust for the home
folder and / into whatever Claude config that env named. They now point
CLAUDE_CONFIG_DIR at a folder that does not exist, and the writer never
creates a config.

* test(runtime): drop needless casts from the launch-host test

The renamed launch-host test kept three `as never` casts on launch options
that already match launchAgentTerminal's parameter type. The changed-lines
casting gate reads the renamed file as new and failed on them.

* test(agent-trust): type the Claude grant mock with the real writer's signature

The mock took an unknown target, so installing the real writer as its
implementation would not typecheck under strict function types.

* fix(codex): drop the launch context the trust move left unread in local spawn env

* fix(agent-trust): queue Claude grants per config file so a launch burst keeps them all

Concurrent grants in one process retried Claude's file lock in lockstep, so each
retry round admitted about one winner. Starting 12 Claude agents at once left 6
of them at the trust question with nothing logged. Grants for one config file now
queue in-process; only Claude's own writes contend for the lock. The relay shares
the writer, so bursts of SSH launches are covered too.

* fix(agent-trust): never pre-trust a folder above a home either

The guard refused only an exact home or a filesystem root. A folder workspace at
/Users, /home or C:\Users was still pre-trusted, and Claude walks up parent folders
for a non-git folder, so every non-git folder in the user's home became trusted.
The guard now also refuses any folder that contains a home, on every host, and is
renamed to say what it decides.

* perf(agent-trust): skip the SSH round trips for Antigravity, which has no remote writer

Every Antigravity launch over SSH now reaches the remote trust writer, which
resolved the remote home and realpath'd the workspace over the link before
writing nothing (the known remote gap). That delayed each launch by two SSH round
trips, and up to the 20 s deadline on a stalled link. It now returns first.

* fix(settings): keep the hidden folder trust row out of web-client settings search

The paired web client hides the host-only "Trust the folder" row, but settings
search still listed it, so searching "trust" opened the Agents pane with no
matching row. Its search entry is now filtered the same way as Agent Awake.

* fix(agent-trust): a failing breadth guard skips trust instead of failing the spawn

* docs(qoder): New Tab now pre-trusts through the agent-wide spawn hook

* fix(agent-trust): never pre-trust a home reached through a symlink

Every trust writer stores the workspace's resolved path, but the breadth
guard compared only the path as given. A folder workspace that is a
symlink to the home folder (or a real home picked while HOME names a
symlinked one, as on distros that link /home to /var/home) passed the
guard, and Claude, Copilot and Cursor then trusted the home itself.

The local dispatcher and the relay now compare given and resolved forms
of both the workspace and each home. The SSH writer resolves the remote
home alongside the workspace, in parallel, so it adds no round trip.
Local non-Claude WSL launches still skip before any filesystem call.

* fix(relay): a failing breadth guard skips Claude trust instead of failing the SSH spawn

The relay ran its home/root guard and homedir() before its catch, so a
throw there rejected the relay's terminal spawn. Same fix as the main
dispatcher's: the whole grant, guard included, is best-effort.

* fix(agent-trust): guard the path each writer stores, not the path Orca was asked to trust

The breadth guard checked the launch's workspace while each writer stored a
transformed path, so every new transformation opened a hole. Codex stores a
linked worktree's main checkout: with a git repo rooted at the home, a Codex
launch in one of its worktrees wrote trust for the whole home.

One relay-safe host module now computes the stored path (Codex's main-checkout
hop, then given and resolved forms of it and of each home), refuses a root, a
home or a folder above one, and only then writes. Main uses it for local and
WSL launches and the relay for Claude. An unknown home writes nothing, and the
WSL home cache is keyed case-insensitively by distro.

* fix(ssh): the relay writes every preset's trust on the SSH host itself

Codex, Cursor, Copilot and Qoder trust over SSH was written from the desktop
over SFTP: four or five round trips per launch, so it needed a 20 s deadline
that outlasted the 8 s draft paste, the 10 s phone wait and the 15 s web-client
create. It also skipped Claude's atomic rename, ignored CODEX_HOME, and stored
the worktree where local Codex stores the main checkout.

The unreleased `claudeFolderTrust` spawn field becomes `agentWorkspaceTrust`,
sent for every preset. The relay derives the preset from the `launchAgent` it
already receives and runs the same host writer main uses, on its own disk,
within 1.5 s and with no extra round trip. Antigravity still returns early on
the relay (its writer is unverified on SSH hosts), a WSL shell still skips,
and any throw means the agent asks.

Deleted: the SFTP preset writer, the remote Qoder writer, the SSH deadline
clause and the desktop-side SSH root pre-check.

* test(e2e): keep CLAUDE_CONFIG_DIR out of isolated Electron launches

The spawn hook now writes Claude folder trust into the config
CLAUDE_CONFIG_DIR names, so an e2e run started from a shell that sets it
could add trust entries to the developer's real Claude config. Also drops
a stale comment that still named Codex launch prep as the trust owner.

* fix(agent-trust): guard Claude's resolve() form of the workspace too

Claude's writer stores both resolve(path) and the realpath. The breadth
guard compared only the given path and its realpath, so a workspace
path that does not exist and climbs back with `..` (for example
<home>/missing/..) passed the guard while Claude stored a key for the
home itself. The guard now also compares resolve(path), so it sees
every form a writer stores.

* test(relay): pty.spawn writes agent trust before the agent's process starts

Nothing exercised the relay handler's call into the trust writer, so
removing that call, or no longer awaiting it, left every suite green
while SSH launches silently stopped pre-trusting. The new case holds the
trust call pending and checks the spawn waits for it, and that the call
gets the request, the declared agent and the final spawn env.

The reliability gate lists the new suite and records the resolve() form
the breadth guard now compares.

* fix(agent-trust): refuse a home only for agents that inherit trust from it

The home and root refusal applied to every preset, so Codex, Cursor and
Antigravity started asking in a home folder workspace, where they did not
before. Only Claude, Copilot and Qoder let trust on a folder cover the
folders below it; Codex matches its start folder or that folder's repo
root, Antigravity the exact folder, and Cursor itself never inherits from
a home, a folder above one or a shallow path. The refusal now reads a
per-preset table in the host module, so the local and relay writers share
the rule.

* fix(agent-trust): trust Codex at the folder it starts in, as before

Before this PR, Codex launch prep trusted the spawn's start folder. The
spawn hook trusted only the workspace root and skipped terminals with no
workspace, so Codex began asking in a floating terminal and in a subfolder
of a non-git folder workspace: its lookup checks the start folder, then
that folder's repo root, and a plain folder above it is neither. The hook
now passes the resolved start folder for presets marked as keyed by it
(Codex only), falling back to the workspace root.

* fix(agent-trust): pre-trust a structured Codex chat's folder, as before

Before this PR, creating a structured (native) Codex chat pre-wrote Codex
trust for its folder through launch preparation. The PR removed that write
and routed trust through the PTY spawn builders, which a structured chat
never passes. Codex's app-server trusts the folder itself only when the
chat's permissions can write it, so a read-only chat started running
untrusted and ignored the project's .codex config. Creating the chat now
calls the same dispatcher, behind the same setting, before launch prep.

* fix(settings): plainer folder trust setting text
2026-09-29 16:03:28 -07:00
Brennan Benson 2807332735 refactor(shared): bring constants.ts back under the max-lines limit (#23923)
* refactor(shared): move onboarding, notification and terminal platform defaults out of constants.ts

* chore(lint): keep the shapedSidebar naming exemption on the file that now holds it

* chore(i18n): regenerate the runtime catalog so it covers main's shipped keys
2026-09-29 14:38:47 -07:00
Neil e87772b3a4 test: retire a dormant worker through host-owned status (#23686)
* test(e2e): await renderer recovery after worker exit

* test(e2e): publish worker recovery through authenticated hooks

* test: keep retired background worker dormant before activation
2026-09-28 14:34:49 -07:00
Jinwoo Hong 9b46c3f0f2 fix(terminal): let unselected Cmd+C reach apps that own their selection (#23597)
* fix(terminal): let unselected Cmd+C reach apps that negotiated kitty keyboard

On macOS Orca swallowed an unselected Cmd+C as a no-op copy, so a full-screen
TUI like Codex, which captures the mouse and keeps its highlight out of xterm's
selection, never received its own copy chord.

- isAppOwnedCopyChord (xterm-bypass-policy) is the one rule: macOS, no xterm
  selection, and non-zero kitty flags from the pane's mirror. The pane's xterm
  bypass and the dashboard popout's key handler both use it.
- A selection copy stays claimed through its repeats and release, including
  custom copy bindings, so kitty event reporting cannot leak them to the PTY.
- Plain shells keep sending nothing; Linux and Windows are unchanged.
- The e2e kitty helpers move to helpers/terminal-kitty-keyboard.ts so the
  shortcut spec stays under the line limit.

* fix(terminal): popout copy ownership reads xterm's selection like the pane

A highlight of blank cells trims to empty text but is still a selection, so the
popout must not hand that Cmd+C to a kitty app while the pane withholds it.

* test(terminal): keep the held-copy binding test beside the copy dispatch tests

The shortcut-policy suite is at its line limit.

* test(terminal): stub a blank-cell selection without widening the preview harness type

* style(terminal): tighten the app-owned copy comment and read the popout selection after the early return
2026-09-28 15:49:22 -04:00
Brennan Benson 6c56c0a3dc fix(browser): a failed SSH route keeps its card while the host redials (#23465)
* fix(browser): a failed SSH route keeps its card while the host redials

A browser route that already failed swapped its "SSH connection unavailable"
card for "Connecting" on every dial of its host, and re-ran prepare once per
dial cycle, so the card flickered and its buttons detached mid-click. Only a
route that is still preparing now waits on a dialing host; a failed route
keeps its card until the host actually connects, which re-derives it.

Retry and Try anyway land on preparing together with the new attempt, so a
press while the host dials waits for the connect instead of starting a
prepare the effect immediately cancels.

The escape-hatch e2e now makes the host truly unreachable before the
disconnect; it passed before only because the card stayed latched over a
host the terminal had already reconnected.

* fix(browser): an unrouted SSH route waits for a dialing host too

Only a failed or ready route is exempt from the host wait; a route that just
became routed (or still shows another target's page) has no answer for this
target, so it must not start a prepare that its own preparing write cancels.

The escape-hatch spec now reads the settled failure from the renderer store:
main's ssh:getState drops the entry on disconnect and on a failed connect, so
its status is null there and never matches the failure pattern.
2026-09-27 17:57:21 -07:00
Jinjing 29847641ab test(e2e): fix failures and improve stability (#23480)
- Narrow toolbar width and set window minimums for consistent testing
- Add node_modules symlink to fixture for ESM import resolution
- Exercise manual paging and fix button selector
- Preserve repo filters in reveal workflow
- Adjust timing strategy and increase test timeout
2026-09-27 16:17:06 -07:00
OrcaWinandOrca Worker 4cafa50ec0 fix(windows): reuse shared PowerShell literal quoting at every hand-rolled escaper (#23083)
Co-authored-by: Orca Worker <orca-worker@localhost>
2026-09-25 23:13:31 -07:00
OrcaWin 82412dab8b Persist profile state in SQLite with background writes (#22612)
Migrate profile state to SQLite and move writes and backups into a background worker. Acknowledge terminal, SSH and automation changes only after durable saves. Preserve JSON import, recovery, rollback and compatibility exports.

Validate migration, worker failures, maintenance, cross-profile moves and terminal lifetime races with unit, integration and end-to-end coverage.
2026-09-25 22:47:33 -07:00
7d4413b3d7 fix(pdf): keep the search counter in sync with selected matches (#22420)
* fix: update the PDF search counter

Co-authored-by: BM Cho <bm1016bm@gmail.com>
Co-authored-by: makoto-developer <72484465+makoto-developer@users.noreply.github.com>

* test(pdf): respect explicitly headful launch mode

* test(pdf): wait for rendered folder search highlights

* test(pdf): wait for rendered text before initial search

---------

Co-authored-by: BM Cho <bm1016bm@gmail.com>
Co-authored-by: makoto-developer <72484465+makoto-developer@users.noreply.github.com>
2026-09-25 20:50:24 -07:00
Brennan Benson 80e0bee23b fix(floating-workspace): keep agent launches from moving the main window's tab (#22603)
* fix(floating-workspace): keep agent launches from moving the main window's tab

Launching an agent from the floating workspace's "+" menu switched the main
window off whatever chat or editor tab it was showing and onto its terminals.

The main window's selection is supposed to move only for the worktree it is
showing: browser and editor tab creation, splits, moves and drops all check
`activeWorktreeId === worktreeId` before touching it. Two places did not:

- `launchAgentInNewTab` called `setActiveTabType('terminal')` without a
  worktree, which targets the active worktree whatever worktree the launch
  landed in.
- terminal `createTab` wrote the global `activeTabId` for a tab in any
  worktree.

Both now follow the store rule. The launch still selects its tab within its
own worktree, which is what the floating panel renders.

The floating titlebar button had side-stepped this with an `activate: false`
opt-out plus manual selection. That opt-out had no other caller and is removed;
the button now launches and focuses like every other entry point.

* fix(tabs): scope the remaining launch surface writes to the launch's worktree

Three more launch paths create a terminal tab and then call
`setActiveTabType('terminal')` without a worktree, which targets whatever
worktree is active when the call runs rather than the one the tab landed in:

- the paired-host agent launch, after the host's asynchronous create
- Session History resume, which can target a worktree the user is not viewing
  and activates it only afterwards
- sleeping-agent resume, which the activation gate runs after asynchronous
  readiness checks, by which time the user may have moved to another worktree

Each now names its worktree, like the local agent launch. The new tab still
lands selected when the user switches to that worktree.

* test(tabs): pin the paired-host launch scope in its existing web-runtime test

* test(tabs): type the left-worktree resume fixture instead of casting it

* refactor(tabs): require the worktree that setActiveTabType applies to

`setActiveTabType(type, worktreeId?)` quietly fell back to the active
worktree when the caller left the worktree out. A caller acting on a tab in
another worktree (the floating workspace, a background launch, a reveal that
lands after an async step) therefore retyped whatever the main window was
showing. The launch paths fixed earlier in this branch were instances of that;
54 other callers still relied on the fallback.

The worktree is now a required argument (nullable only for the no-active-
worktree case), so every caller states which worktree it means and a new
unscoped call fails to compile. Each call site passes the worktree of the tab
it acts on; where that is by construction the active worktree (shortcuts,
palette, tab strip), the result is unchanged. `activateTabAndFocusPane`
resolves the tab's owning worktree the same way `setActiveTab` does.

End-to-end helpers that drive the store directly pass the active worktree,
which keeps their previous behaviour.

* fix(floating-workspace): let the floating New Terminal activate its own tab

The floating "+" New Terminal created its tab with `activate: false` and then
selected it with `activateTab`, because creating an active tab used to write
the main window's selected tab even for another worktree. `createTab` now
activates a tab only within its own worktree's group unless that worktree is
the one on screen, so the workaround is no longer needed.

Creating the tab active also moves the floating workspace's remembered tab to
the new one; before, it stayed on the previously selected floating tab, which
auto-acknowledge reads to decide which floating agent the user is looking at.

* refactor(floating-workspace): route every floating New Terminal through one creator

The floating "+" New Terminal had stopped deferring activation, but Cmd+T with the
floating panel focused still went through a separate creator that created the tab
inactive and activated it by hand, which left the floating workspace's remembered tab
on the previous tab. Both now call createFloatingWorkspaceTerminalTab, which creates
the tab active in its own group and focuses it.

* docs(tabs): say why an unowned tab id keeps the on-screen worktree scope
2026-09-24 10:54:43 -07:00
Jinwoo Hong dfff3915c4 fix(browser): scope back/forward/reload/zoom/grab shortcuts to the originating split (#22340)
* fix(browser): scope back/forward/reload/zoom/grab shortcuts to the originating split

With two browser panes visible in a split, Back, Forward, Reload, Hard
Reload, page zoom and Focus Address Bar fired in every visible pane. Main
forwarded these guest chords without the page id, and each split's active
pane subscribed. The renderer-side listeners for the same chords were also
window-wide per pane, so a key pressed in the toolbar (or in a terminal in
another split) reached every active browser pane.

Guest-forwarded chords now carry the originating browserPageId; preload
admits only well-formed payloads and each pane ignores ids that aren't its
own. Toolbar-path listeners use the same focused-split scope Find already
uses. The streamed remote pane's history chord moves onto that scoped hook.

Cmd/Ctrl+C grab (STA-3319) gets the same scope and no longer arms while a
text selection exists outside the browser pane, so copying from the native
chat transcript works again.

* refactor(browser): simplify split shortcut scoping per review

Drop the preload payload admission (main and preload ship together), fold
the three inline scope checks into browserChromeShortcutOwnsEvent, and
replace the outside-overlay selection check with a plain live-selection
rule so Cmd+C copies from surfaces that do not move split focus.

* refactor(browser): share one zoom command type and tidy shortcut comments

BrowserPageZoomEventDetail and BrowserPageZoomCommand were the same shape;
keep one in shared/browser-page-zoom.ts and route guest and local zoom
through a single handler.

* refactor(browser): narrow the zoom event with instanceof instead of a cast

* test(e2e): pin split-scoped browser shortcuts

Two browser splits (and a terminal beside a browser) now prove that Back,
Forward, Reload, Hard Reload, page zoom, Focus Address Bar, and the element
grab chord act only on the split that sent them, from both the guest page and
the browser toolbar. A native chat selection proves Cmd/Ctrl+C copies instead
of arming grab. Split fixtures move to a shared helper so both specs reuse them.
2026-09-22 20:14:19 -04:00
Neilandstevelliu ea02d90704 fix(omp): answer startup Kitty queries before renderer handoff (#20620)
* fix(omp): answer startup Kitty queries before renderer handoff

Forward actual renderer capability through local and remote spawn. Preserve source ranges and following keyboard mode pushes, and retain independent ConPTY color authority.

Refs #17081. Secondary review: #17082.

Co-authored-by: stevelliu <stevelliu@tencent.com>

* test(omp): cover fragmented keyboard modes and ConPTY handoff

* fix: preserve keyboard startup intent without terminal colors

* fix: negotiate keyboard support for host-authoritative agent launches

* fix: keep terminal creation within line budget

* fix(omp): negotiate keyboard support for paired web launches

* test: remove obsolete message type import after main integration

* fix: validate paired launch results and retry incomplete SSH test snapshots

* fix(omp): negotiate keyboard support for background paired launches

---------

Co-authored-by: stevelliu <stevelliu@tencent.com>
2026-09-19 01:29:16 -07:00
Neil 2a53293b11 test(e2e): stop a spec's parking-delay override from leaking into the rest of its worker (#21571)
* test(e2e): scope the parking-delay override to the spec that needs it

A Playwright worker imports many spec files into one Node process, and the app
fixtures launch Electron with a spread of that process's env. The split-
orientation spec set ORCA_E2E_TERMINAL_PARKING_DELAY_MS at module scope, so the
2s override outlived the file and reconfigured every app launched by every spec
that followed it in the same worker — measured directly: a probe spec sees a
30000ms cold-park delay on its own and 2000ms when that spec runs first.

Module scope is the part that cannot be undone. A write inside a test body can
save and restore, as four other specs here do; a write at import time runs
before any hook exists to restore it. test.use({ orcaAppExtraEnv }) reaches the
app launch without touching the worker every other spec shares.

The ratchet holds the module-scope writer count at zero.

* test(e2e): re-apply screen-reader mode while reading the accessibility tree

Separate from the env leak above, and unproven against the CI failure it
resembles: this is robustness, not a diagnosed fix.

screenReaderMode is an option on the xterm instance and the accessibility tree
belongs to that instance's DOM. The SSH cold-activation spec set it once,
imperatively, then waited on the node. A pane that parks and remounts, or
rebinds after a reconnect, comes back as a new instance with the option off, so
the one-shot mutation stops producing the node the wait is waiting for and the
wait reports "element(s) not found" rather than a content mismatch.

The helper re-applies the option inside the poll and returns null when the node
is absent, so a replaced instance is retried instead of being fatal. Five other
specs still use the one-shot pattern and are left alone.
2026-09-18 23:25:52 -07:00
Neil 2038376d8e fix(terminal): a park must not discard the only copy of a remote pane's scrollback (#21285)
* fix(terminal): keep a client copy of a parked remote pane's scrollback

A remote-runtime pty's bytes never transit the client's main process, so the pane's
xterm buffer is the only client-side copy. The ordinary cold-park unmounted that pane
without capturing it, licensed by TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY — a static
build string that says nothing about whether the host retained this pty's buffer. On
reveal, a host that answers 'no-serializable-buffer' (or stays silent past the request
timeout) collapses to a null snapshot and the pane paints blank: tabs and splits survive,
the scrollback is gone.

Capture before every park, not only the retention-budget force-park, so the reveal has a
copy to replay when the host cannot answer. An unverifiable host answer is not proof the
pane was empty; keep the buffer, never discard it.

Adds ORCA_E2E_FORCE_REMOTE_TERMINAL_SNAPSHOT_UNAVAILABLE so an e2e can reproduce the
host-retains-nothing state, mirroring the existing forced-truncation lever.

* test(terminal): prove a parked remote pane survives a host that answers nothing

The oracle is a token the test types into the terminal before the park and the fixture
echoes back. Nothing replays stdin, so a respawned command cannot reproduce that line —
only the pre-park buffer can. An earlier argv marker passed vacuously for exactly that
reason.

The control ('host retains the buffer') is insensitive to the fix and fails if the harness
never parks, never reveals, or never echoed the token, so the regression case cannot be
green for a harness reason.

* refactor(terminal): validate the paired host terminal RPC shape instead of casting it

The merge-commit consistent-type-assertions gate flags every new `as`. Two were fixture
shapes that a type annotation states directly, and the third hid an unchecked RPC payload —
readCreatedTerminalTab now fails with the shape named rather than surfacing later as an
undefined surface id.

* fix(terminal): let a park capture survive an unhydrated repo catalog

Reading state.repos unguarded threw out of the cold-park effect whenever the catalog was
absent, which would break parking itself. Capture is best-effort evidence; an empty catalog
also fails open in shouldPreserveTerminalScrollbackBuffers, the safe direction for a park.

* docs(terminal): pin why the two unhydrated-catalog fallbacks point opposite ways

shouldPreserveTerminalScrollbackBuffers fails open toward 'remote' because a worktree wrongly
judged local parks with no copy at all. worktree-runtime-owner.ts resolves the same unhydrated
catalog to 'local', which is safe there and would be data loss here. A reader pattern-matching
'fail open' across the two gets one of them backwards.

* fix(terminal): keep a parked pane's scrollback across a reconnect merge

The direct-SSH pull replaces a replaced tab's layout wholesale, and a park capture does not
bump tab.generation — so a just-parked tab is not in locallyPreservedTabIds and the only
client-side copy of its remote scrollback went with the layout it replaced. That is the same
data loss this branch already fixes, one layer down, and it is the layer that decides whether
the fix survives the app update the user actually performed.

Carry the client's leaf-keyed scrollback into the host's layout, filtered to the host's own
root leaves. Structure stays the host's verbatim, so a split it added while we were away still
wins and a leaf it retired still drops its bytes. Local wins a conflict: neither copy is then
the only one, but remote-wins would overwrite the tail captured since the last upload and
propagate that backwards on the next replace-session patch.

Not a generation bump: the pane key is `${tab.id}-${tab.generation}`, so bumping would remount
the pane and destroy the very buffer the capture just serialized, lift the recovery-storm
ledger ceiling, and let a stale local ptyId win through preserveNewerLocalTerminalFields.

* fix(terminal): carry a parked pane's scrollback through the mirrored-layout rebuild

Found in review of this PR by rc-ssh-remoting. chooseRemoteTerminalLayout rebuilds a
mirrored tab's layout from the host's picture and never carried buffersByLeafId or
scrollbackRefsByLeafId forward, though it already receives existingLayout. The host
publishes no scrollback of its own, so ANY session-inventory frame landing between park and
reveal dropped the only client-side copy: the rebuild is bufferless, terminalLayoutEqual
compares buffers so the write is not bailed out, and apply-terminal-records assigns it
wholesale.

Measured before the fix: 336 bytes captured at park, 0 after one forced frame, blank pane on
reveal. After: 411 bytes survive the frame and the reveal repaints.

The e2e passed either way because no frame happened to land in its window, so it was not
covering the destroying event. It now forces one inside the park -> reveal window and asserts
the capture survives it.

An identical fix was written and reverted earlier in this branch as 'no measurable effect' —
that measurement ran on a harness deleting the client profile between launches, so nothing
downstream of persistence could register. It was never actually tested.

* feat(session): add a local-only home for ordinary-park scrollback

localOnlyScrollbackByTabId is a top-level session field, tabId -> leafId -> buffer, that never
rides the remote projection: exportRemoteWorkspaceSession is an explicit allowlist of named
top-level fields, so a new one is omitted for free, whereas anything added to
TerminalLayoutSnapshot is copied whole. It is also outside the two records the mirrored-tab apply
rewrites, so a host inventory frame cannot wipe it.

Registered in every exhaustive session registry ('tabKeyed'), hydrated and scoped like the layout
map, dropped with its tab on close/removal/purge/repo removal/mirrored retirement, copied on profile
transfer, emitted by the incremental patch builder, and capped by pruneLocalTerminalScrollbackBuffers
alongside the shared home — with a per-home test so an uncapped path cannot go unnoticed.

Known ceiling, not widened here: the field routes through the partition router that falls back to
'local' when the repo catalog is unknown at write time (#21295).

* fix(terminal): keep ordinary-park scrollback off the upload, and read both homes through one resolver

The ordinary cold park fires on every workspace hide. Its capture now splits: structure (root,
ptyIds, titles) stays in the shared layout, bytes go to localOnlyScrollbackByTabId. Force-park,
hibernate, sleep and shutdown keep writing buffersByLeafId, because that copy is what a second
desktop cold-restores from; a shared capture clears the local copy so the two homes never hold two
versions of one leaf.

resolveLeafScrollbackBuffers is the only read across the two homes (local wins a conflict: it is
the later write by construction). restoreTerminalPaneLayout no longer reads buffersByLeafId
directly, the capture's merge prior comes from the resolver, and the post-replay release covers
both homes.

Measured with the projection at 20 tabs x 2 panes at the per-leaf cap: the shared-layout shape
exports ~22 MiB per replace-session; the local-only shape exports the bufferless baseline.

* test(sync): pin that the mirrored rebuild carries the client scrollback refs

The carry-through added in f210dece83 keeps scrollbackRefsByLeafId for leaves the host still
names (a ref is the only pointer to a local scrollback file), so the rebuilt layout equals the
stored one and the write bails. The old assertion expected the refs to be dropped and has been red
on this branch since that commit.

* test(e2e): assert where a park's bytes land, and re-point the inventory-frame check at the force-park

Once ordinary parks stop writing buffersByLeafId, the existing survivedInventoryFrame assertion
passes trivially — there is nothing in the layout to wipe. The ordinary scenarios now assert the
store-level upload contract (bytes in the local-only home, shared home empty) and that the local
home is out of a host frame's reach; a third scenario reaches a force-park (host without paired
parking, client retention limit 1) and asserts the shared capture survives the forced frame, which
is where the mirrored-layout carry-through is load-bearing.

* test(e2e): leave the force-park reveal out of the assertion instead of matching it loosely

expect.any(Boolean) reads as a check but cannot fail. The force-park reveal is served by the host
tail on a host without paired parking, so it is logged for diagnosis and the assertion carries the
three fields that are deterministic on this topology.
2026-09-18 19:49:38 -07:00
09073086a8 feat(terminal): inline images via @xterm/addon-image (perf-first) (#19512)
* feat(terminal): inline images via @xterm/addon-image, perf-first

Add opt-in inline terminal images (SIXEL, iTerm2 IIP, Kitty graphics)
through @xterm/addon-image, designed to keep idle terminals unaffected.

Performance:
- The addon (base64-inlined wasm decoders + protocol handlers) loads off
  the boot critical path via a deferred loader that mirrors the WebGL
  addon: primed after first paint only when the setting is on, read back
  synchronously at attach, with a 3-attempt cap so a transient failure
  never disables images for the session and a missing chunk never
  refetches per pane. renderer-boot-graph guards against eager import.
- enableSizeReports:false so the addon never sets windowOptions and
  double-answers Orca's own CSI 14t/16t responder.
- Perf-tuned decode/storage limits (storageLimit, sixel/iip/kitty size
  caps) in one place.

Correctness:
- Orca's DA1 handler wins over the addon's (last-registered-first), and
  the default DA1 response never advertised Sixel (;4), so DA1-detecting
  tools (chafa, img2sixel, viu, timg) never emitted it. The winning
  handler now appends ;4 while the setting is on, resolved per query so a
  live toggle changes the next DA1; idempotent against the ConPTY
  response that already lists it.
- ORCA_IMAGE_PROTOCOL=kitty is exported to spawned shells (local, daemon,
  relay/SSH) and forwarded across the WSL boundary, so image-capable
  agents can pick an encoder. Unknown image sequences are swallowed by
  xterm when the addon is detached, so this never garbles output.
- Settings toggle (default on) gates rendering and DA1 advertisement.

Cross-checked against community PRs #7775, #11706, and #19201 at the end;
credited below.

Co-authored-by: s546126 <s546126@users.noreply.github.com>
Co-authored-by: XRX193 <XRX193@users.noreply.github.com>
Co-authored-by: lmsh7 <lmsh7@users.noreply.github.com>

* fix(terminal): bound inline image memory and classify Kitty replies

* fix(terminal): bound image decode and release image resources on cleanup

* fix(terminal): address image addon review feedback

* test(terminal): stub setPaneInlineImagesEnabled in appearance manager fakes

* fix(terminal): evict unplaced kitty payloads before displayed images

Byte-budget eviction dropped the oldest transmitted blob regardless of
placement, so a new upload could erase a visible image while abandoned
blobs still held budget. Unplaced payloads now go first and displayed
ones only when that is not enough. The incoming image is always stored,
so an oversized one overshoots the cap by one payload instead of being
dropped after the protocol already acked OK.

* fix(terminal): gate DA1 Sixel on real addon attachment; claim SSH image spec in CI

- DA1 advertised Sixel from the setting alone, so a pane whose lazy addon
  chunk was still loading (or had failed all three attempts) told
  feature-detecting tools to emit DCS that nothing could render. Track the
  attached decoder per terminal and require it before setting the ;4 bit.
- tests/e2e/terminal-inline-images-ssh.spec.ts was Docker-gated but claimed
  by no lane runner, so pr-e2e-gate-contract failed and the spec would have
  self-skipped green forever.
- Reject non-positive PNG IHDR dimensions before decode: they are parsed with
  signed shifts, so a dimension >= 0x80000000 came back negative and slipped
  past the pixel-limit comparison.
- One resolveTerminalInlineImagesEnabled() for the default-on setting; the
  four call sites mixed '?? true' with '!== false', which disagree on null.
- One readInlineImageResources() walk of the addon internals instead of two
  copies that could drift against the patched dependency.
- Isolate the deferred-attach drain per pane; make the zoom-invariance and
  backing-storage e2e assertions fail when the feature is dead.

* refactor(terminal): one lazy xterm addon loader for webgl and image

terminal-image-addon-loader was a structural clone of the webgl one — same
memo, attempt cap, and .then(ok,err)-clears-memo recovery. Both now wrap
createLazyXtermAddonLoader; each keeps its literal import() specifier so the
bundler still splits the chunk (verified against a fresh build: addon-image
stays out of the boot graph).

* refactor(terminal): name openTerminal's addon flags; pin image addon limits

Two adjacent optional booleans could be swapped without a type error once
inline images added the second one.

* docs(terminal): state the real per-pane image ceiling; drop test ordering dependency

storageLimit:32 reads like the pane's budget but keys three pools — decoded
pixels, retained encoded Kitty blobs, and pending WASM decoders — so the worst
case is ~98 MB per pane with no cross-pane governor. Say so at the constant.

pane-inline-images.test.ts's deferred case needed to run first; it now takes a
fresh module instead, and the rest prime in beforeAll. Verified by running the
file with that test moved last.

* fix(terminal): satisfy rebased static analysis gate

* fix(terminal): complete casting gate cleanup

* fix(terminal): recover failed image addon loads

* fix(terminal): bound image decoder allocations

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: s546126 <s546126@users.noreply.github.com>
Co-authored-by: XRX193 <XRX193@users.noreply.github.com>
Co-authored-by: lmsh7 <lmsh7@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
2026-09-18 16:32:49 -07:00
Neil 5c8540948d test(e2e): name the paired-client quit that preserves the profile (#21300)
Quit-without-deleting is closeElectronAppForE2E + cleanupE2EDaemons — dispose's first two
steps without removeProfile. The composition is correct today but undiscoverable, and
getting it wrong is silent and expensive in both directions.

dispose() + reuseUserDataDir yields a FIRST RUN on an empty profile, so every persistence
assertion after it reads empty and is indistinguishable from data loss. That produced a
phantom data-loss report, live in two write-ups before a diagnostic listing zero session
FILES (rather than zero buffers) contradicted it.

Reaching for a bare app.close() to skip the deletion hangs instead: it lacks the timeout
and force-kill fallback that closeElectronAppForE2E wraps around it, and burned a ten
minute test deadline producing no reading at all.

Test infrastructure only; no production code. Unblocks restart-persistence coverage for
the paired topology.
2026-09-17 21:21:14 -07:00
Jinjing 852ee907ee fix(e2e): stabilize flaky E2E tests against timing races (#20900)
* fix(e2e): stabilize flaky E2E tests against timing races

- Paired terminal: use stable cold activation assertion instead of
  racy one-shot read; background tabs park eagerly.
- Native chat: scope hydration assertions to transcript subtree to
  avoid false positives from UI chrome (worktree rows, tab titles).
- Onboarding: inject verified status snapshot with max sequence to
  prevent hydration from downgrading host health during skip-to-
  project-setup.
- Paired web: encode host health faults in snapshots with high
  sequence so real hydrations cannot outbid injected state.
- Quick open: clear prior tooltips and increase hover timeouts to
  handle streaming result remounting.
- Terminal attention: pass 'terminal-bell' to unread marker to match
  production contract (reads marker value, not presence).

* fix one last test
2026-09-16 15:27:35 -07:00
Neil 231e805b1e fix(lint): enable anti-slop/no-shape-in-symbol-names (#20785)
Flip `anti-slop/no-shape-in-symbol-names` from "off" to "error" and clear
every violation under src, config, tests and mobile.

What the rule bans
------------------
The case-insensitive substring "shape" in any JS/TS identifier: variables,
functions, parameters, types, type parameters, class members, private names,
object-literal keys and JSX identifiers. The one exemption is a statically
accessed member read owned by another value (`zodObject.shape` is fine), so
third-party APIs stay readable without a suppression.

"Shape" names a value's structure rather than its domain role. `UserShape`,
`validateArgShape` and `errorShape` all tell you the symbol is "an object
with some fields" -- which is already what a type says -- while saying
nothing about what the value is for or who owns it. The rule forces the
name to carry the domain instead.

Violations fixed
----------------
689 violations across 109 files at baseline (verified by re-running the
audit against the pre-change tree with the rule set to "error").

Fix pattern
-----------
Rename for the domain role, not the structure:

  -type FieldShape = 'list' | 'map' | 'whole'
  -const FIELD_SHAPES = { ... } satisfies Record<keyof Observation, FieldShape>
  +type FieldEncoding = 'list' | 'map' | 'whole'
  +const FIELD_ENCODINGS = { ... } satisfies Record<keyof Observation, FieldEncoding>

  -function assertGitPushTargetShape(target: unknown): void
  +function assertValidGitPushTarget(target: unknown): void

  -function describeReadDirPathShape(p: string): ReadDirPathKind
  +function classifyReadDirPath(p: string): ReadDirPathKind

Predicates became statements about the value (`isDeltaShapedProviderFrameKind`
-> `isDeltaProviderFrameKind`, `isDeleteShapedDiscardEntry` ->
`discardDeletesEntryFile`, `isSkillsCliAgentKeyShaped` ->
`isUsableSkillsCliAgentKey`). Type aliases dropped the suffix where the
remaining name was already unambiguous (`GhGraphqlErrorShape` ->
`GhGraphqlError`).

No wire-visible name was renamed: no IPC or RPC channel, stream opcode,
request/response param, persisted field, or i18n key. The `--shape=symlink|copy`
CLI flag read by .github/workflows/skill-update-roundtrip.yml is unchanged --
only the local variable holding it was renamed.

Exemptions
----------
They are file-scoped entries in config/oxlint-anti-slop.json, not inline
`oxlint-disable` comments. An inline directive naming an anti-slop rule reads
back as an UNUSED directive under the root lint scan, which does not load this
plugin -- the changed-code quality gate counts that warning, so the comment form
cannot be used for a rule that lives only in this config.

* src/renderer/src/components/browser-pane/annotate/**:
  in the screenshot annotator a "shape" is the drawn geometry -- pen, arrow,
  rect, ellipse, highlight. That is a genuine domain noun, and it pervades
  every symbol in the module.
* repo-icon.tsx, repo-header-project-actions.tsx, mobile MobileRepoIcon.tsx:
  lucide exports the icon component as `Shapes`. The name is theirs, and the
  matching REPO_LUCIDE_ICONS key is the persisted icon name shared with the
  desktop picker -- renaming it would orphan saved repo icons.
* src/shared/onboarding-state-types.ts, src/shared/constants.ts:
  `shapedSidebar` is a persisted onboarding-checklist field and a telemetry
  enum member; renaming it would orphan saved state.
* src/shared/rpc-contract/rpc-send-params.ts: matching zod's own literal `shape`
  property is what selects the ZodObject branch of the conditional type.

No exemption was added merely to avoid a rename. Eight symbols initially
suppressed as "a cross-module refactor outside this change" were proven to have
zero non-TypeScript references repo-wide and renamed instead.

Zod's `ZodRawShape` needed no exemption at all: `Readonly<Record<string,
z.ZodType>>` is its definition, so repo-update-params.ts and
ui-update-value-tolerance-params.ts spell it out instead. Likewise
telemetry-event-classification.ts now reads `.shape` through an `in` narrowing,
which also retires two pre-existing type assertions; three more assertions the
rename had dragged onto changed lines (two `JSON.parse` sites, one node:sqlite
row read) became annotations and an explicit row mapping.

Verified
--------
* Audit reports zero violations; confirmed the rule genuinely fires by
  planting a probe violation.
* node config/scripts/run-typecheck-projects-in-parallel.mjs exits 0.
* Vitest over src/shared, src/main/github/project-view, the annotate module,
  the repo-icon components and the Chromium SameSite electron spec: all green.
* All 66 removed "shape" identifiers grepped repo-wide across every file type;
  none survive.
* node config/scripts/generate-rpc-params-catalog.mjs --check exits 0.
* node --check on every changed .mjs; oxfmt clean on all changed files.
* `pnpm run check:code-quality:changed` reports 0 findings.

Not machine-verified: the 3 mobile/ files (its Vitest run cannot resolve
`expo/tsconfig.base.json` in this worktree), and the WSL- and Playwright-gated
specs. All are rename- or comment-only hunks, read in full.
2026-09-15 02:00:27 -07:00
Neil 7b53b5abd1 test: replace fixed UI waits with observable readiness (#20369) 2026-09-12 01:19:07 -07:00
Jinwoo Hong 729491597f feat(desktop): measure relay regions and reconnect after idle cutover (#20106) 2026-09-11 11:43:48 -07:00
20c56249d5 fix(terminal): keep a deliberately slept workspace cold until it is woken (#20075)
* fix(terminal): keep a deliberately slept workspace cold until it is woken

Sleeping a workspace kills its PTYs but keeps its panes mounted and keeps each
tab's session id as a wake hint. Any later remount of those panes (recovery,
parking, portals) reattached that dead id, and the daemon's create-or-attach
spawned a fresh shell, so slept workspaces revived on their own (#10205).

The existing sleep-intent marker now outlives teardown and gates the deferred
connect itself, so both the reattach and fresh-spawn arms stay cold. It is
released by activating the workspace, by any PTY binding to one of its tabs
(CLI, automation, client wake), and by purge. A queued startup still connects.

Reproduces the community root cause from gatsby74 in #13343; the regression
e2e remounts a slept hidden pane and fails on main.

Co-authored-by: gatsby74 <gatsby74@users.noreply.github.com>

Co-authored-by: mmarabel <mmarabel@users.noreply.github.com>

* fix(terminal): let a slept pane wait for its wake instead of latching cold

A pane whose connect ran while its workspace was slept used to mark itself
connected and stop; nothing re-armed it, so a wake that produced a live PTY
before the user clicked (CLI create, background agent resume, split panes)
left panes stranded. The connect now waits on the sleep marker and resumes
when the marker clears, and a torn-down pane drops its listener.

Tabs created with a live PTY clear the marker too, the sleep flow marks each
workspace only when its own teardown starts, and purge forgets the marker
without waking anything.

* fix(terminal): wake a waiting pane once, in its remounted generation

Activation clears the sleep marker after the set() that bumps dead tabs'
generations, and the waiting pane only resumes its connect when its tab
generation is still current. Otherwise the stale pane and its remounted
successor both reattached the same session id on a deliberate wake.

* fix(terminal): resolve the waiting pane's tab by either id and re-arm after wake

The wake listener looked the tab up by the pane's render id, which can be a
unified id whose terminal tab lives under entityId, so the generation check
declined forever for those panes. Mount, fresh spawn, and the wake listener now
share one live resolver. The wait flag resets when the listener fires so a
second sleep can hold the pane again, listener dispatch is guarded, folder
activation clears after its own set(), and the sleep flow re-asserts the marker
after each teardown while releasing a workspace the user activated meanwhile.

* fix(terminal): ignore PTY binds that land inside the sleep teardown window

A spawn resolving while shutdown was still awaiting the host bound a PTY and
cleared the marker, waking every waiting pane mid-sleep; re-marking afterwards
could not un-connect them. The sleep flow now scopes each teardown so binds in
that window are not wakes. The e2e asserts a deliberate wake yields exactly one
PTY, and the dispose test proves the listener is gone.

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: mmarabel <mmarabel@users.noreply.github.com>
2026-09-11 01:45:57 -07:00
fb9ba4b681 fix(editor): make markdown images inline so a paragraph stays schema-valid (#19746)
* fix(editor): make markdown images inline so a paragraph stays schema-valid

Image was registered as a block node while paragraph is content:'inline*',
but the markdown pipeline nests an inline image as a paragraph child.
Schema.nodeFromJSON does not validate content, so the editor built a
schema-invalid document that rendered fine and threw on the first step
that reassembled the paragraph - i.e. on the user's next keystroke.

Report 0e46c048 (1.4.198, macOS): RangeError "Invalid content for node
paragraph" from checkContent via Node.replace, tearing down the
editor.rich-markdown boundary.

Register Image as inline and override paragraph's parseMarkdown so a lone
image is not hoisted out of its paragraph. Also fixes the same crash class
reachable through details/summary. Markdown output is byte-identical.

* fix(editor): keep a fenced code block intact when an image is inserted into it

Making the image node inline meant it could no longer be fitted into
codeBlock (content:'text*', marks:''), so inserting one with the cursor
inside a fence made ProseMirror close the block at the insertion point:
the remaining code escaped as plain prose and the language attribute was
lost, and autosave wrote that markdown to the user's file. The pre-fix
block image split the fence into two intact blocks instead.

Resolve the insert content against the target position: when an inline
image cannot be fitted where the caret sits, wrap it in a paragraph so
ProseMirror splits the block and both halves keep their ``` fencing and
language. Prose insertion is unchanged. Every production insert path now
shares that resolution - the toolbar picker, the slash command and the
clipboard-screenshot paste through insertRichMarkdownImageFromPath, plus
the GitHub/GitLab composer's image-URL insert - each with a regression
test.

Also guard the unchecked cast of Paragraph.config.parseMarkdown: a Tiptap
upgrade that drops the field would otherwise turn every paragraph parse
into a TypeError and take the whole editor down, instead of degrading to
parseInline.

Four of the new round-trip cases asserted only on getMarkdown(), which
walks the document without running NodeType.checkContent and so emits
byte-identical output from a schema-invalid document - they passed on the
pre-fix code. roundTripMarkdown now runs doc.check(), the list-item and
table-cell case performs a real edit, and the standalone-image case types
beside the image. All twelve cases now fail on the merge-base.

Adds an Electron e2e spec driving the real renderer: a paragraph image and
a toggle-summary image each survive a keystroke, and Bold over a selection
spanning the image keeps it.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Neil <neil@stably.ai>
2026-09-10 17:42:50 -07:00
Jinjing 73d0521410 Replace the sidebar create dropdown with two direct action buttons (#19653) 2026-09-09 10:42:18 -07:00
Jinwoo Hong 12f53da542 Remove settled-worker automatic resume and hibernation fences (#19544)
* Remove settled-worker automatic resume and hibernation fences

* test: retirement rollback case follows the no-fence policy

Case 4 seeded and asserted automaticResumeBlockedBy, which this branch
deletes. A rolled-back settled worker is now an ordinary done record that
wake clears as passive evidence, same as any finished agent pane.

* chore(i18n): regenerate the runtime-required catalog for the contrast floor strings

* test(orchestration): give the stopping-worker guard fixtures a Run
2026-09-08 05:14:59 -04:00
Jinjing c056c6f9ac Unify sidebar create actions into single dropdown menu (#19375)
* Unify sidebar create actions into a single dropdown menu

- Combine "New workspace" and "Add project" under a unified "Create" button
- Remove layout logic that split these actions based on sidebar width
- Normalize "Add Project" to "Add project" (lowercase) throughout the UI

* Use null instead of 'Unassigned' for unassigned shortcut labels

Add formatOptionalPrimaryShortcutLabel that returns null when a
shortcut is unassigned, enabling simpler conditional rendering in
dropdown menus. Remove associated translation strings.
2026-09-07 19:24:34 -07:00