From 5cafefe726a28bc0052aed5255bfce1cd8fcfb42 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Wed, 7 Oct 2026 03:15:06 -0700 Subject: [PATCH] Phase 3: every SSH host runs a managed Orca server (orcad), replacing the relay (#24863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "revert: take the 26 Phase 3 (#16741 port) PRs back out of main (#24559)" This reverts commit 5f308bfa9c796f33e17c9e1cff180a2247450d0a. * 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 * 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) * 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) --------- Co-authored-by: m4air Co-authored-by: Claude Opus 5.5 (1M context) * 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 Co-authored-by: Claude Opus 5.5 (1M context) * 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 * 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 * 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) * refactor(ssh): import the T6-9 client's unsupported refusal instead of mirroring it --------- Co-authored-by: m4air Co-authored-by: Claude Opus 5.5 (1M context) * 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 * 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 * 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 * 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) * 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 Co-authored-by: Claude Opus 5.5 (1M context) * 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 * 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) * 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 Co-authored-by: Claude Opus 5.5 (1M context) * 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 * 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) * fix(settings): align managed-server form controls and name the section the setting reveals Co-Authored-By: Claude Opus 5.5 (1M context) * 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 Co-authored-by: Claude Opus 5.5 (1M context) * 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 * test(orcad): wait for killed terminal daemons to exit before removing their temp profiles (#24871) Co-authored-by: m4air * 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 * 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 * 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 * 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 Co-authored-by: m4air * ci(adhoc): build and ship the orcad template in adhoc macOS and Windows builds (#24969) Co-authored-by: m4air * 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 * 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 --------- Co-authored-by: m4air * 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 Co-authored-by: m4air * 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 Co-authored-by: m4air * 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 * 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 * 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 ... 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 Co-authored-by: m4air * 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 * 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 * 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 * 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 * 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 * 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 Co-authored-by: Claude * 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 * 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 * 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 * 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 * 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: (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 * 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 * 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 * 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 * 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 * 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 Co-authored-by: m4air * 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 * 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 * 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 * 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 * 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 * 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 * fix(ci): give the idle-exit spec its app build and route the convert harness to it Co-authored-by: m4air --------- Co-authored-by: m4air * 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 * 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 * 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 * refactor(ssh): one managed-tunnel ownership check and forward bookkeeping (#25316) Co-authored-by: m4air * 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 * 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 * 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 * 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 * 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 * 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 * 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