* The window shows each layout records frame record by record, without reading its session back (dark, layout-owner build only)
A records frame no longer re-serializes the window's whole session and runs the
Loader. Panes' terminal bindings and live titles apply alone, to the tabs the
frame names; any other record builds only its own workspace's slices, with the
view and facts read from the store. Only a workspace the store does not show
from frames yet (startup, a reopened stream, a first visit, one sent whole)
takes the full build.
Tests install an apply check that, after every flush, compares the store with
the full build for the same cache and pre-flush store; the shared layout-apply
transport now sends main's records frames and fails a flush that differs.
* A frame of only tab rows shows without reading the window's session back or running the Loader (test, layout-owner build only)
* The account-restart test's fake binding runtime carries per-pane binding generations, so it passes with the switch on (test only)
* The account-restart test types its store writes, so its five old e2e type errors are gone (test only)
* A workspace build reads the editor selection and groups through the save's own writers, as the full build does (dark, layout-owner build only)
* A tab released from the window's hold shows the binding main wrote meanwhile (test, layout-owner build only)
* The frame-apply check still checks a flush's other workspaces when it builds one in full, and the shared transport keeps a test's own check (test only)
* The frame-apply check is proven to catch a mismatch beside a full build, and the shared transport restores only the check it installed (test only)
* Name the pane tree the window compares by what it holds: the tree without its panes' bindings or live titles (dark, layout-owner build only)
---------
Co-authored-by: E2E Test <e2e@test.local>
Guards the finding behind #31132: on a host with no window, a profile written only through
main's own commands (create, split, close, sleep, and a pane never started) reloads with the
layout-owner switch on and keeps every tab row, pane, binding, tab-bar entry and sleeping
agent. Main lists every tab it creates in the tab bar, so the load sweep never needs a
shutdown list to keep them.
Co-authored-by: E2E Test <e2e@test.local>
In the layout-owner build, main's load binds the pane again from its tab row while the
terminal is alive, so the replacement host publishes the pane ready and never pending.
The serve-restart spec waited for a pending frame that this build correctly never sends.
It now expects pending only in the current build, and asserts no pending frame in the
layout-owner build. A unit test pins the precondition on a windowless host: main's own
writes leave no shutdown list, and a real store load restores the binding.
Co-authored-by: E2E Test <e2e@test.local>
* feat(layout-owner): a layout-owner server advertises that it takes a paired desktop's layout intents (dark)
status.get adds layout.intents.v1 only in an ORCA_LAYOUT_OWNER=main build. It versions the
layout.subscribe frame shape and the layout.apply contract a paired desktop will rely on.
Tests pin the layout methods open to a paired desktop and closed to phones, and that an older
desktop still reaches every session.tabs mutation on such a server.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(layout-owner): read the paired-desktop frames and seed the server without casts
Co-Authored-By: Claude <noreply@anthropic.com>
* review: the capability comment says only what the code does, and tests pin each next check
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: E2E Test <e2e@test.local>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(sidebar): the phone reads projects and project groups from main's sidebar feed, installed with the rows
The phone built its project sections from a catalog the desktop window published through a
side channel (mobileRelay.projectSections). That copy changed on a timer after the event that told
the phone to re-read it, and a re-read asked for during a read in flight was dropped, so the phone
could keep an old grouping (a project under the wrong group, a group missing) indefinitely.
Main now publishes the catalog (repos with their group and order, project groups, projects and
host setups, every host's including paired servers') as a projectCatalog domain of the sidebar
feed, from its own store and its own reads of each paired server. The phone reads it from the same
install as the rows. mobileRelay.projectSections stays, answered from the same builder, for pages
that predate the domain; a page on a desktop without the domain still reads it, now re-reading
when an invalidation lands mid-read. The phone e2e list helper runs the page's real refresh
lifecycle instead of re-reading on every assertion, and covers a group move while a read is held.
* test(mobile): stand AppState in for the screen hooks' react-native mocks, and tolerate a world with no project rows in the parity catalog
* fix(sidebar): main files a server's copy of a grouped project under its group, and reads a server's catalog only on catalog events
- The window gives an ungrouped paired-server repo the group of an existing copy of the same project
(applyInheritedProjectGroups). That function now lives in src/shared and main's catalog builder
folds each server's repos through it, so the phone files the copy where the desktop does.
- A paired server's change watch now says whether the event can carry a catalog change. Only
ready, a replay and reposChanged mark the catalog stale; worktreesChanged (pins, unread, sort
stamps) reads rows only.
- The parity fixture now builds the server side from raw server answers, folded through the
window's mergeFetchedReposForHost and through main's own server read and builder, with a server
copy of a grouped project and a partial saved manual order. It fails without the inheritance.
* fix(sidebar): parse a paired server's catalog rows with a schema instead of reading keys reflectively
* fix(mobile): sync the feed ref after render and re-read queued project sections without an await loop
* fix(mobile): spell the AppState shape the refresh reads, so the e2e helper pulls in no React Native types
* test(sidebar): give the merged builder test's feed domains a project catalog
---------
Co-authored-by: E2E Test <e2e@test.local>
Follow-up to #30415. A watch event that superseded a pending overflow existence
probe left the tab's entry behind forever, and the dirty-tab disk-check request
map never removed anything. Superseding now deletes the entry, and closed tabs'
disk-check requests are dropped by the scan's existing open-tabs subscription.
The disk-check map uses a global sequence so a reopened tab can't reuse an old
read's number.
* ci: the manual switch-on leg runs the headless serve, paired-host and serve-restart specs
With H1's switch-on server template, these exercise main as the layout owner with no window.
Spec paths are exact: the changed-spec job greps each file, so prefixes only work for ssh-orcad-.
* ci: keep the Windows serve job out of the switch-on leg; it builds switch-off
* ci: the switch-on leg also runs the server-workspace tab composition and serve-restart binding specs
---------
Co-authored-by: E2E Test <e2e@test.local>
#30037 made the headless row builder emit customTitle on every terminal row; the Linux baseline
recorded before it (#27688) never listed the field, so the spec fails on main with either layout
switch.
Co-authored-by: E2E Test <e2e@test.local>
Main runs one pass at startup and each window load, with or without a window
(desktop, orca serve, orcad): every live local terminal no pane names is bound
to the pane its record names while main still holds it free, else shown in a new
tab bound to it. The window no longer asks main to adopt terminals; it only reads
that main holds them. The window's layout.apply loses its adopt field.
Main also derives activeWorktreeIdsOnShutdown, remoteSessionIdsByTabId and
activeConnectionIdsAtShutdown from its own layout, terminal records and SSH
connections, replacing the window's copies in every save and the quit checkpoint.
Co-authored-by: E2E Test <e2e@test.local>
* perf(editor): watch only active, recent and at-risk workspaces
Background workspaces no longer hold a whole-tree native file watch. The
active workspace, the 3 most recently visited ones, and any showing a
deleted/renamed tab keep theirs; every other workspace's subscription is
released (local, WSL, SSH via the relay, and runtime hosts alike).
When a dropped workspace is watched again, its tabs are verified against
disk through the existing paths: one overflow resync once the watch is in
place reloads clean tabs and refreshes git status, Files and markdown
lists; dirty tabs go through the restored-tab disk check, which shows the
changed-on-disk banner. Saves stay guarded by the host-side disk check, so
nothing is held while unwatched.
Also: watch requests are shared per directory and host; root events reach
every workspace on that root and SSH payloads carry their connection id;
late SSH and local/WSL capacity-retry installs send an overflow resync.
* fix(editor): catch up combined reviews, deletions, late installs and Files trees after a rewatch
- Overflow catch-up drops cached combined-diff bodies for the workspace and
re-reads every loaded section of a mounted review (snapshot-backed too).
- Clean edit tabs are probed on the owning host first; a file proven gone is
marked deleted and keeps its last content instead of reloading into an error.
- Dirty tabs are checked from the overflow itself, so the check runs after the
watch is installed, including late capacity/SSH installs.
- Local/WSL Files trees also take the renderer-synthesized catch-up.
- Deleted/renamed-tab pins are capped at 3 beyond the active and recent set.
* fix(editor): catch-up keeps unsaved combined-diff edits and drops stale existence answers
- Cached Changes views reset only clean loaded sections; dirty sections keep
their drafts and baselines (also for per-path invalidation).
- A mounted review re-reads loaded and in-flight sections on catch-up.
- Existence answers carry a per-tab generation; a newer probe or watch event
for the tab supersedes an older answer.
- A tab is marked deleted only on a per-path ENOENT, never on a whole-request
not-found or an old host's scalar fallback.
* fix(editor): re-check dirty tabs requested mid-read, prove runtime deletions, chunk existence probes
- A disk check requested while one is in flight is followed by one fresh read
(coalesced) instead of being answered by the older reply.
- The restored-tab check probes runtime-owned files on their host; a per-path
ENOENT marks the tab deleted. After a retry ceiling it stops, clears the
suspension and shows the changed-on-disk banner.
- Overflow existence probes are split into host-sized chunks, in order.
* fix(editor): a reload that finds the file gone keeps its content; stale absence answers re-read
- A tab that already read its file and now gets a path-specific ENOENT on reload
is marked deleted and keeps its last content, with or without an existence
probe, so older paired servers are covered.
- The restored-tab check applies its request generation after the absence
probe too, so an older answer cannot resolve a newer request.
Behind ORCA_LAYOUT_OWNER_IS_MAIN, a CLI or phone file open is applied by main as an
openEditorTab layout command, so a host with no window can open files. The window
reports each workspace's active group as an optional view fact on syncWindowGraph,
and shows main's tab once the frame that draws it arrives. Editor language
detection moves to shared so main can name a file's language.
Co-authored-by: E2E Test <e2e@test.local>
* fix(renderer): a twin repo id refuses instead of following the Active Server focus, and a listing for a repo with no row is this app's
findRepoOwner no longer lets focus pick between same-id repo rows: runtimeTargetForRepoOwner refuses a
twin, as resolveOwner does, and getRuntimeEnvironmentIdForRepo names no server for one. repoHostId
stamps and routes a repo with no row as this app (or a web client's only server), never the focused
server.
* fix(renderer): a fork names its source's host, and GitHub routing keeps its twin rule until it can refuse
* fix(renderer): a fork captures its source's server transport, and a GitHub edit on a twin refuses instead of using this computer
* fix(automations): a new-per-run workspace is created on the run's captured host
* fix(automations): pass only a captured run host, so a twin without one refuses
* test: stamp the server repo row through a store helper, prune the focus-read baseline
* feat(settings): one-list Remote Orca Servers page and bulk-manageable SSH Hosts
Remote Orca Servers: replace the three mode cards, Advanced fold, duplicate
managed/SSH-access lists and troubleshooting accordion with a single list
(one row per server: status, one action, overflow menu), an always-visible
'New projects open on' row, and Share / Cloud VM cards that open dialogs.
SSH Hosts: group by source, filter, checkbox + shift-range selection, one
confirm for bulk remove (skipping hosts with a managed server or
workspaces), bulk Test. 'Sync ~/.ssh/config' no longer clears removal
tombstones; 'Restore removed hosts (n)' is a separate confirmed action.
* fix(settings): address review findings on servers and SSH hosts redesign
- SSH filter stays clearable when the list shrinks under the threshold
- Cmd/Ctrl-A only selects hosts when pressed in the list itself
- group legacy no-source hosts with main's import predicate (now shared)
- capture the shift-range anchor before queuing the selection update
- blank server names de-duplicate instead of colliding
- Share dialog Done refreshes link count and resets name editing
- Install / SSH-route dialogs reload SSH hosts when opened
- default-host row shows the selector when the default is a WSL host
- managed 'Not running' no longer masks connecting/revoked/update states
- servers without a dropped transport can be re-checked
* test(e2e): WSL host spec matches repo.rm ending a removed project's shells
repo.rm now stops the project's terminals on the host (#29964), so the
'orphan left by older builds' setup can no longer be built; use a shell
only the host knows of instead. Resolve the project path so macOS
/private/var matches what the host reports.
* fix(settings): carry 'Orca on <host>' naming and paired-twice warning into the new server list
Ports #31099 and #31100 into the redesigned rows, install dialog and SSH
host dialogs.
* test(e2e): keep the WSL spec's project path as created
* A respawned terminal's pane rebinds in one write, and the window shows a binding-only frame without a rebuild (dark, layout-owner build only)
Main: when the window's spawn for a restored pane finds its process gone, the spawn that holds
the pane's reservation keeps the dead binding and its fresh bind swaps it (the restart swap), so
the pane is never published unbound: a same-id respawn publishes no frame, a new id one. A failed
spawn still retires the dead binding. Other callers and window-owned builds retire early as before.
Window: a frame that differs from the shown one only in panes' terminal bindings updates the
store's pane trees as a rebuild would, falling back to a rebuild for holds and shared terminals.
* Dead-owner respawn tests expect the layout-owner build's one swapped bind instead of the early retire
* Main publishes one records frame per layout operation, and the window resubscribes on a missed frame (dark, layout-owner build only)
Main: a durable mutation (a layout.apply, the renameTabs batch, a binding write, a repair) is
one operation. It names the partitions it wrote, and at its end the layout stream publishes
once: one `records` frame holding only the changed records (tab rows, workspace groups, a pane
tree per tab, a binding record per pane, editor files, browser tabs, removed workspaces), each
keyed by main's own ids. Writes no operation scopes (window saves) still publish when their
task ends. The snapshot and every records frame carry a stream epoch. Each pane's binding
carries `ptyGeneration`, which rises on every binding write in this process, so a same-id
respawn is one frame with one binding record. Readers beside the stream get the end of each
operation and the stream's own per-partition loaded model (`workspaceLayoutReads`), so they
need no second Loader pass, with or without a window.
Window: the cache applies records frames to its full layouts, so the existing flush and the
binding-only path are unchanged. A records frame of another epoch or out of sequence, a
malformed frame, or one the cache cannot apply resubscribes at once for a fresh snapshot.
Tests install main's model check, which applies each frame to what subscribers held and
compares it with a fresh projection; dev builds log a mismatch, release builds run none.
* E1 review fixes: keep a swapped binding, bound the window's resyncs, guard operation listeners (dark, layout-owner build only)
- A spawn whose swap bind persisted no longer retires that pane's binding when a later commit
step fails; a pane on an SSH host keeps the early retire, since its dead owner lives there.
- The window resubscribes at once at most three times before a frame applies cleanly, then takes
the loss path's backoff. A skipped frame of a newer type still advances its position.
- A throwing operation listener is logged; it no longer fails a committed mutation.
- Tests freeze the shared partition model a reader gets; the dev-build model check skips frames
that only change live titles. Generation and model-check comments say what is true.
* Keep the layout stream's backoff reset after a single resync; only a resync storm holds it
* A layout stream that recovers from a resync storm reopens on the normal delay again
* Move the replaced pane owner into its own module so the stable owner no longer imports its replacement (dark, layout-owner build only)
---------
Co-authored-by: E2E Test <e2e@test.local>
* fix(sidebar): a read Done yields to a pane still Working, re-tiered on any unread write (dark switch)
* fix(sidebar): re-tier through the merged producer's settled change
* fix(sidebar): key the shared list's order, lineage and retention by host identity (dark switch)
* fix(sidebar): repeat a nested project group's path in each host section (dark switch)
* fix(sidebar): hold Recent project order under the pointer on the desktop (dark switch)
* refactor(sidebar): keep the inline-lineage cast where it was
* fix(editor): follow-ups to disk-checked saves: repeatable stale reloads, Markdown whitespace, Undo of Reload
- A stale cached copy can reload again once the disk baseline moves or a mount sees fresh text.
- A clean Markdown tab keeps a trailing-whitespace-only draft across a reload of the same text.
- Undo of Reload re-reads the file's tabs, so typing in a tab that joined after the reload wins.
* fix(editor): Undo of Reload skips tabs that left the model; keep whitespace drafts only when disk is unchanged
* fix(sidebar): one agent phase history in the transition detector, so a compaction raises no unread and a title-started turn pushes its finish
The detector now reads hookless agents' titles as well as hook rows, so a pane
has one phase. The sort-stamps producer consumes the detector's classified moves
instead of rebuilding phases, and raises unread from the detector's news: a
session boundary's done still orders as a finish but is no news. A turn a title
started and the first hook row finished is now a moved finish, which the
headless phone push announces.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(sidebar): a pane leaves its old workspace before it is seeded in a new one
The detector emitted the seed before the leave, so the stamps producer moved
the pane into the new workspace and then deleted it there. Leave first, drop the
producer's own leave, and delete the now test-only nextAgentPaneState.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(sidebar): narrow the seeded workspace in the detector order test
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: E2E Test <e2e@test.local>
Co-authored-by: Claude <noreply@anthropic.com>
Every editor save carries the disk version its text came from and the host
refuses the write when disk moved on; only the banner's explicit Overwrite
writes without that check. Older hosts compare from the client first.
Co-authored-by: Claude <noreply@anthropic.com>
build-orcad-template.mjs takes --dark-test-template to let ORCA_LAYOUT_OWNER=main
through for test servers; the template and out/orcad carry the switch marker, and
packaging refuses a marked template. The e2e template steps build a switch-on server
when the workflow runs with layout_owner=main.
Co-authored-by: E2E Test <e2e@test.local>
A settings save that changes what the visibility filter reads (external/agent
defaults, workspace folder and its history, nesting) now moves the workspace
rows revision, whichever path wrote it. A paired-server answer to a fetch that
started before a contact gap keeps its rows but no longer marks them live.
Co-authored-by: E2E Test <e2e@test.local>
* fix(renderer): name both entries when a refusal comes from one server paired twice, and mark them in Settings
* fix(renderer): name twins only when they are the workspace's only publishers; generic text when a host is named
* fix(renderer): name twins only when they publish the workspace at one place
* fix(ssh): say where Orca runs instead of "managed server" in the UI
Users read "managed server" as hosting run by someone else, so they declined
moving their SSH hosts. Every user-facing string now says "Orca on <host>" or
"this host's Orca server", in all six locales; Settings > "Orca on your SSH
hosts" and the setup line say the code and files stay where they are and only
Orca's list of projects, tabs and terminals moves to the host. CLI command names,
logs and identifiers are unchanged.
Fixes#30880
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ssh): reassure only when something moves; name the host's label in its row
Review follow-ups: the reassurance shows only while Orca sets itself up or
moves a host, not on every reconnect; the converting line no longer repeats it;
the row names the SSH host's current label; the section description and the
Move anyway dialog stop repeating themselves; translate the Restart dialog's
buttons; drop "managed" from the remaining raw causes and the external
automation refusal.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(editor): a rename retargets only editors whose own owner is the renaming host
Each open tab is matched to the move by the owner it captured when it opened (its
server and SSH host), not by looking its worktree id up again, so a same-id twin's
editor on another host keeps its path and its unsaved draft.
* fix(editor): a server route that names no machine never strands the renaming host's own tab
* fix(editor): only an SSH host a tab names excludes it from a rename; a local route can be this app's view of one
* fix(editor): a rename's fallback host check reads both sides at once, never a stale pre-rename host
* fix(editor): a rename matches tabs by the rows' owners, never the Active Server focus
* fix(editor): a tab from another workspace moves only when that workspace is on the host the rename went to
* fix(editor): a rename's cross-workspace check uses the File Explorer's own host resolver on both sides
* test: shared switch-on fixtures so 71 layout-owner test files run with the switch on
- layoutApplyTestTransport answers layout.apply (accept, apply to main's model with its frame, pending, refuse)
- main's loaded layout as snapshot frames, shared from the frame-sync test
- tab-layout maps for the editor store harnesses; missing store methods in the main harnesses
- workspaceRepos/registerWorkspaceRepos so main's load keeps seeded layouts
- ORCA_LAYOUT_OWNER=main runs vitest with the switch on
- the 4 rollback-build tests stay switch-off, labelled
* test: keep switch-off assertions exact, settle only after layout commands, reset the transport per test
* test: parse the transport's command without Reflect.get; name the staged-session type
* test: pin the test transport in the layout.apply census; route the autosave fixture as unit support
---------
Co-authored-by: E2E Test <e2e@test.local>
* fix(mobile): resend lost Workspace option changes on reconnect, draw feed-owned row fields from the feed, retry a failed feed read
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(mobile): end a feed retry streak only on install, draw the feed's comment
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: E2E Test <e2e@test.local>
Co-authored-by: Claude <noreply@anthropic.com>
The append reset old-0's mtime to wall-clock time. Once real time passed the
busy fixtures' date (2026-10-20T10:00Z), old-0 sorted newest and was read by
the unfiltered first pass, failing the no-ordinary-read assertion.
Co-authored-by: E2E Test <e2e@test.local>
* fix(pairing): pairing a saved server again refreshes its entry instead of saving it twice
* fix(pairing): only Settings refreshes a saved server in place; other add paths refuse the duplicate
* fix(pairing): a same-key refresh keeps the server's workspaces and tabs; raw add refuses; match any endpoint key
* fix(pairing): refuse a code for a hidden saved server; one choke point publishes verified status only for the pairing still saved
* refactor(pairing): verified-pairing publish in its own module; an unreadable store skips the stamp instead of failing a saved pairing
* fix(pairing): match a host key however its Base64 is spelled
* fix(pairing): a refresh keeps the saved key spelling so the host fingerprint stays put
* fix(renderer): no route resolver reads the Active Server focus, so a local row never routes to the focused server
The worktree route's single-focused-runtime branch, the folder owner's focus fallback and the
worktree host default's focus fallback are gone. The client stamps every server row on arrival, so
an unstamped row is this machine's: terminal spawn and teardown, browser tabs, quick-open, chat and
path links now agree on that. resolveOwnerWithLegacyFallback folds into resolveOwner, and the
focus-free write route becomes the one operation route. Setup terminals keep their explicit
focused-runtime choice.
* fix(renderer): setup terminals keep their focused-runtime host, unstamped repos stay local for the composer, and fixtures stamp server rows
* chore: merge duplicate route imports, drop new fixture casts, prune focus-read baseline
* fix(cli): worktree create sends the host --project/--host selected
Codex review 6 #6: the CLI picked the right setup but sent only its repo id,
so a repo id set up on two hosts failed as selector_ambiguous; and
--project-host-setup ignored --host when setup ids repeated.
worktree.create gains optional repoHostId (older hosts drop it and keep the
old behaviour); the runtime narrows the repo selector to that host.
* refactor(runtime): the repo selector carries its host through every lookup
Review follow-up (Codex + Opus on #31066): a separate repoHostId was dropped
by the startup-agent lookup, the CLI's GitLab link check and the create
dedupe key. RuntimeRepoSelector now carries the host with the selector, so
showRepo/resolveRepoSelector and startup-agent resolution keep it; the
create dedupe key includes it; repo.show gains the same optional
repoHostId; the CLI spreads one CliRepoTarget into every repo lookup.
Handler-level tests cover worktree.create (plain, startupAgent,
startupDraft), agent.launch, repo.show and the dedupe key.
* test(runtime): drop a needless cast in the agent.launch host test; fold an import
* fix(ssh): a legacy relay restarted at the same socket never takes over an open tab
A pane's owner is now its relay endpoint plus the incarnation that relay listed. A
relay that restarts at the owner's socket and reuses pty-N under another incarnation
is never served for the tab, its exit and output never reach it, and its silence is
not read as the original shell's exit: the pane stays unverifiable.
* fix(ssh): keep an exit a reconnected relay reported before its listing bound the pane
* fix(ssh): a legacy relay's exit report counts when it arrives on the connection that listed the pane
* fix(ssh): a pane waiting on a replaced legacy relay keeps its connection
* fix(ssh): never hold or retake a replaced legacy relay's owner role for a pane it can't resolve
* fix(ssh): an epoch-scoped pane's exit reported during a reconnect counts without a listing row
* refactor(ssh): the owner ledger holds the connection that speaks for each legacy relay pane
The connection a listing verified stays bound to the owner until it closes, so its
exit reports count even after a later listing missed the pane.
* refactor(ssh): every legacy relay pane route asks the owner ledger; a replaced pane is never bound
Attach, input and stops go through liveRouteOf, which reaches only the connection the
ledger says speaks for the pane; exit and output reports use the same check. A pane
whose relay was replaced at its socket is never bound to the replacement, and a stop
owed to it is abandoned instead of being fenced against the stranger.
* fix(runtime): PR/MR worktree base refuses SSH projects named only by executionHostId
Codex review 6 #4: the guard read the legacy connectionId, so a row stamped
executionHostId 'ssh:<target>' without it ran git fetch on this machine.
* fix(runtime): one gate for local Git on a repo row; base resolution agrees with create
Review follow-up: a local row with a stale connectionId and runtime: rows now
refuse too (create already refused runtime: rows), through
requireRepoLocalGitRoute in repo-execution-host.ts.
* fix(runtime): keep base resolution for runtime-stamped rows this server owns
Codex follow-up: older servers keep a client-minted runtime:<id> stamp on
rows whose files are here, and base resolution worked for them before.
requireRepoLocalGitRoute reads the stored host (getStoredRepoExecutionHostId),
so they run locally again; SSH rows and any row with a connectionId still
refuse.
* fix(ssh): recover a tunnel port a ControlMaster still holds
A forward made through a ControlMaster leaves its local listener on the
master: the `-N` client exits at once and the master keeps the port. No
process of Orca's holds it then, so the orphan sweep finds no argv to
match and a managed tunnel's persisted port stays bound - every later
connect fails with `Local port 127.0.0.1:<port> is not available:
EADDRINUSE`, until the user finds and stops that master by hand.
Ask the master to drop just this forward (`ssh -O cancel -L`) when the
orphan sweep comes up empty, then rebind. OpenSSH refuses a spec no
master registered, so another forward on the port, and a port held by
anything but a master, are left alone - as are the master's own
sessions and its other forwards.
* fix(ssh): read a refused forward cancel off stderr, not the exit code
`ssh -O cancel` exits 0 whether or not a master held the spec, reporting
a refusal only as `master cancel forward request failed` on stderr. So a
spec no master registered read as a release that freed the port: the
recovery logged a cancel it never made and let the caller rebind a port
still held, turning a clear refusal into a second bind failure.
Treat a request failure on stderr as nothing released, so a port held by
anything but a matching master keeps reporting the original bind error.
* docs(ssh): document the port reservation contract and its recovery limits
State on each entry point what it returns and what it will not do: that
reservation only ever recovers a port already in use and only from what
the caller supplies, that the bind reports a busy port through the
error cause, and that a forward on port 0 carries no recovery at all
because the port it lands on was just free.
* fix(ssh): any managed-tunnel forward that fails to start falls back to the stdio bridge
* fix(ssh): skip the master cancel on Windows; note the forward's bridge fallback
* test(ssh): an abandoned forward's late exit uses a real close reason
* fix(ssh): cancel only Orca's forward on the master, and bridge a forward that never listens
Codex: -O cancel also cancels every forward the loaded ssh_config declares, so the
cancel now reads the control socket from ssh -G and sends the request with no config.
Opus: a forward still waiting on its own login never binds; that now takes the bridge.
* fix(ssh): pass the master's ControlPath through -S so a path with spaces stays whole
* fix(ssh): keep ssh's own exit reason when the forward also never listened
* test(ssh): the real sshd fixture waits for its processes before removing its temp dir
---------
Co-authored-by: zzzpyyy <167217540+zzzpyyy@users.noreply.github.com>
Co-authored-by: zhangpengyun <zhangpengyun@zpydeMacBook-Pro-2.local>
* fix(browser): an SSH host's capture is admitted at each capture and at its answer
Page captures, PDFs and every guest CDP answer are now checked against the host's
admission at the moment they run and again when they answer, so a page that moves
to a desktop-local document mid-command is never returned to the host.
* fix(browser): a host's input that landed still counts after a later revocation
* test(browser): a host input that landed resolves after a later revocation
* fix(runtime): project registration dedupes only against rows on this machine
Codex review 6 #7: create() matched paths across hosts, so creating a local
project at a path an SSH project used returned the SSH row and made no
directory. add() with no host had the same gap. One predicate,
runtimeRepoIsRegistrationCandidate, now backs add, create and clone.
* test(runtime): cover nested-SSH rows and clone reuse in host-qualified dedupe
Review follow-up (Opus P3s): runtime:<env> rows carrying a nested SSH
connectionId, add() for a runtime host, the clone controller, and a stricter
create() assertion.
* test(runtime): clone dedupe fixture commits without signing
* feat(ssh): offer a Node.js mirror when neither machine can download Node
* fix(ssh): put the mirror action beside Details on the host row
* fix(ssh): offer the mirror only where this computer's setting applies, and word main's error by mirror state
* test(ssh): type the refusal case
* test(ssh): one checked settings fixture for the mirror dialog
* fix(orcad): ship node-pty's winpty backend so Windows 1809 hosts can open terminals
node-pty selects winpty below Windows build 18309, but the Windows server
slot carried only the ConPTY addon, so the candidate's PTY self-test failed
on Windows 10 1809 / Server 2019 and the host was recorded as unsupported.
Reported by @lksr202 in #30906.
* test(orcad): smoke the winpty backend in the Windows prebuild slot
* fix(projects): a moved SSH host's hidden setups stay hidden, so its hosts list once (#30859)
Reported by @matheusrenov.
* fix(projects): hide a moved folder-only host's setup placeholders too
* test(projects): cover a WSL-moved setup; read the move journal once
* test(projects): type the setup catalog stubs instead of casting
A freshly restarted cell can be ready while the global load balancer still
answers 503 for it. The probe retried once after 2 s, so c10's 2026-10-20
roll failed after its drain and was left isolated. Retry wrapped source
5xx every 10 s for up to 2 minutes.
Co-authored-by: E2E Test <e2e@test.local>
* fix(editor): markdown preview reads its file with the source tab's grant and host
A preview of a terminal-opened file outside every workspace on a managed
server dropped the source tab's one-file read grant, so the editor refused
the read as an external local file. A client-local file in an SSH workspace
lost its readOnly flag the same way and was read from the SSH host instead.
Reported-by: @quzard
Fixes#31034
* fix(editor): a retargeted preview keeps its read authority after the source closes
* fix(editor): reload an open preview when its source gets a fresh grant
* Send a status batch's generated tab titles to main as one rename (dark, layout-owner build only)
* Share the window's as-today refusal rule with the title batch; cover a failed batch and a paused workspace
* Move the renameTabs request schema to its own module, under the file's line limit
---------
Co-authored-by: E2E Test <e2e@test.local>
* fix(updater): a live orcad's browser helper pauses for macOS desktop installs
orcad starts its browser helper from the installed Orca.app, so ShipIt waits
for it, and the installer refused it as an unregistered blocker with no
supported way out. orcad now registers the helper in the existing serve
install park: it stops the helper for the install and starts it again from
the new bundle once the hold ends. The refusal card for a helper an older
orcad still runs says what to do instead of "stop the server".
Reported-by: @routesmith
* fix(orcad): retry a failed helper restart, hold through a mid-swap bundle, catalog the guidance
* fix(orcad): a new park cancels a slow helper restart; reset backoff per install
* fix(relay): move director traffic in two steps so a deploy does not land on one instance
Cloud Run routes a new director revision's floor instances their first request
roughly 60-110 s after they start, while a traffic move reaches clients in
15-30 s. The one-step 100% move therefore put every assignment on the single
instance started with the revision, which sits at its own admission limit at
peak; the 2026-10-20 deploy returned ~550 assign 503s in two minutes that way.
With a floor above one, the deploy now moves 20% first, waits until Cloud Run's
request log shows every floor instance serving (six-minute deadline), then
moves the rest. A timeout or failure at that stage returns all traffic to the
previous serving revision, which still holds its warm floor. The staging deploy
identity gains log read access for the same check.
* fix(relay): bound the director spread log read, log the instances seen at timeout, and document split recovery
A hung gcloud log read could hold the 20/80 split past the six-minute deadline;
each read now times out after 30 s and counts as an unread poll. A timeout logs
the distinct instances seen, so a partial share packed onto fewer instances than
the floor shows up. The runbook covers restoring a split left by a cancelled job
and the missing selector-rollback tag after a timed-out deploy.