Commit Graph
15138 Commits
Author SHA1 Message Date
Jinwoo HongandClaude 4bc3971b98 fix(sidebar): key folder workspace rows by host so the phone matches the desktop (dark switch) (#30721)
* fix(sidebar): key folder workspace rows by host in the shared builder so the phone matches the desktop

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

* test(sidebar): pin the SSH folder row key encoding

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 08:26:12 -04:00
Neil a9011d083c fix(projects): refuse to file a folder under a project its own GitHub remote contradicts (#30717)
* fix(projects): refuse to set a folder up under a project its own GitHub remote contradicts

Setting an existing folder up for a project stamped the project's identity
as the folder's upstream even when the folder's own remote named another
GitHub repository, so the folder was silently relabelled into the wrong
project (fabricated upstream, wrong projectId on its setup and workspaces).

Fixes #30663

* test: type the folder-remote spec store without casts

* fix(projects): judge a registered folder by its current remotes; ignore host aliases

* test: cover the desktop re-read of an already-registered folder
2026-10-19 05:10:00 -07:00
Neil f22aecf1ed fix(runtime): one teardown policy per kind; removal closes host browser pages everywhere, headless sleep stops terminals (#30715)
* fix(runtime): one owned-session teardown policy; removal closes host browser pages everywhere, headless sleep stops terminals

A declared table says what each entry point (worktree, folder and project removal,
sleep, disconnect) does to each owned kind (PTY, native chat, host browser page,
launch record), so a new kind is a compile error until decided. Removal over desktop
IPC and repo.rm now close the host's browser pages and launch records like the
runtime removal; worktree.sleep with no renderer sleeps the host's terminals instead
of doing nothing. Sleep->chat is declared pending #28445; disconnect stops nothing.

* fix(runtime): project removal finds headless pages and failed launches without a client page; stub the new runtime method

* fix(runtime): headless sleep rejects an unproven stop; read headless page owners through the bridge, not Electron
2026-10-19 05:02:21 -07:00
Kelvin Amoaba 162e07cf97 fix(browser): refuse agent clicks while the user is mid-IME composition (#30358) 2026-10-19 11:45:29 +00:00
yi111andYi-111-a ec0c1f115a fix(github): accept SSH certificate org remotes (org-<id>@github.com) (#30659)
GitHub orgs using an SSH certificate authority get an SCP remote whose user is
org-<id> rather than git. The three GitHub remote parsers only matched a
literal git@ user, so those repos were classified not-github and lost PR,
issue and check support.

Co-authored-by: Yi-111-a <yi-111-a@users.noreply.github.com>
2026-10-19 04:43:32 -07:00
Stefan StefanovandClaude Sonnet 5.5 346f6028a3 fix(git): stage and discard files in WSL repos opened by UNC path (#30693)
The Git runner sends a command into WSL when the working directory is a
\wsl.localhost UNC path, even if no WSL distro is set in the options.
literalPathspec only looked at the options, so Windows-style backslashes
in file paths reached Git inside WSL unconverted and stage, unstage and
discard failed to match the file.

literalPathspec now also checks the worktree path, and every caller
passes it.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 04:43:08 -07:00
Neil 25e53f4b0c fix(security): put every trust decision on a host-owned token surface (#30710)
* fix(security): put every trust dialog on a host-owned token surface

Plugin themes write CSS tokens on the document root, and dialogs portal to
body, so any trust surface without plugin-security-chrome inherits them; a
theme could hide what is being approved. The orca.yaml hook trust dialog,
notebook run confirmation, plugin install, remove and rollback dialogs were
missing it. DialogContent gains surface="security", which applies the
host-owned tokens, and a scrollable prop for the plugin dialogs' existing
viewport-height scrolling; every trust dialog uses the surface and a test
lists them. The style guide says when to use each.

* fix(security): keep inline trust decisions on the host-owned surface

The agent command approval card (Allow/Deny) and the browser certificate
override sit outside a dialog, so plugin theme tokens still reached them.
A SecuritySurface primitive puts its child on the same host-owned token set
as DialogContent surface="security"; both use it, and the security surface
test now lists inline trust surfaces as well as dialogs.
2026-10-19 04:21:16 -07:00
NeilandClaude e72d8f926a fix(ssh): say the host's disk is full when a managed-server install runs out of space (#30679)
* fix(ssh): say the host's disk is full when a managed-server install write fails for lack of space

OpenSSH's sftp-server reports ENOSPC and EDQUOT as the generic SSH_FX_FAILURE, so a deploy onto a
full home failed with the bare detail "Failure" under "Try connecting again". After a failed
install write, probe the host's free space where the write was going; when it cannot hold the
write, fail with an error that names the filesystem, how much is free and how much is needed.
Lost or aborted transfers and Windows hosts keep their original error.

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

* fix(ssh): diagnose only SFTP's generic failure on an exhausted filesystem

Codex review: a partial upload may spend the room the rest needed, so compare against an empty
filesystem, not the bundle size, and keep explicit refusals such as permission denied.

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

* refactor(ssh): reuse formatKb and size an upload the way uploadDirectory walks it

Opus review: the shared formatKb already exists, and following symlinks inside the tree could
recurse; uploadDirectory follows the root and skips links below it.

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

* test(ssh): measure the free-space probe in the host-op byte budget

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 04:20:09 -07:00
Neil 92ed331184 refactor(plugins): extract the shared content-pack registry and list store (#30697)
* plugins: extract the verified artifact-pack registry from language packs

Pure refactor. Verify, bounded read, parse and per-plugin error handling
move into PluginArtifactPackRegistry; language packs become one instance,
so later data families reuse it instead of copying the file.

* plugins: reconcile content families from one list

Pure refactor. Reconcile and error reporting walk one family list in the
same order as before, so a new family is one entry.

* plugins: one line per family for declared artifacts and contribution defaults

Pure refactor. Declared artifact paths are a list of per-family rows, and
the contributes default comes from the schema (prefault) instead of a
second copy of the field list; the existing defaults test pins the result.

* plugins: extract the renderer plugin content list store

Pure refactor. The fetch generation, startup join window and content-change
subscription move into createPluginContentListStore; language packs become
one instance, and store fields are renamed to entries/fetchEntries.
2026-10-19 04:01:12 -07:00
Neil 148944c020 refactor: de-barrel remote-runtime-client, runtime-terminal-stream and runtime-terminal-inspection (D2, D3) (#30684)
* refactor: de-barrel remote-runtime-client and runtime-terminal-stream (D2)

importers name the defining modules; the two vi.mock factories that named a re-export move to
the defining module (pty-id spy) or drop the dead key (RemoteRuntimeClientError)

* refactor(renderer): de-barrel runtime-terminal-inspection (D3)

importers name runtime-terminal-input-recording, runtime-terminal-verified-input and
shared/remote-runtime-pty-id; 22 vi.mock factories move the re-exported keys to those modules
(pty-id keeps its other exports via importOriginal)

* test: drop the duplicated verified-input mock (Opus P3)
2026-10-19 03:58:26 -07:00
Jinwoo Hong 7cc2859e90 fix(relay-ops): let same-cap rolls run while another cell is reserve; refuse a reserve roll target by name (#30707)
* fix(relay-ops): let same-cap rolls run while another cell is reserve; refuse a reserve roll target by name

The director refuses cell-status for a step-5 reserve cell with a 404
{error: cell_in_reserve_mode}. The same-cap headroom check read it for
every general cell, so any roll failed while c7 was reserve. A reserve
cell that is not being rolled now counts as no free slots (its seats are
not in the database) and is listed with reserve: true. A reserve roll
target, in the headroom check, the same-cap job's first cell-status read
and the candidate tool, now fails with the design's procedure: flip it to
admitMode=db first, roll, then flip it back. Any other 404 still fails.

* fix(relay-ops): pin the headroom gate's imports as evidence code; name both cells in a two-cell reserve refusal
2026-10-19 06:57:06 -04:00
NeilandClaude 41fdd7e6d8 fix(ssh): keep SFTP uploads pipelined so a high-latency link doesn't crawl (#30704)
* fix(ssh): keep SFTP uploads pipelined so a high-latency link doesn't crawl

ssh2's WriteStream acks each chunk before writing the next, so uploadFile moved one 64 KiB chunk
per round trip: about 200 KB/s on a 300 ms link, which made the ~90 MB managed-server deploy (Node
runtime plus orcad bundle) take over seven minutes. A 2 MiB write buffer lets ssh2's _writev keep
that many bytes of WRITE requests in flight, as fastPut's default does.

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

* test(ssh): type the fake session's stream options with ssh2's WriteStreamOptions

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 03:48:35 -07:00
Neil cffcef7be0 fix(projects): setup edits and removals name the host, so the twin on another host is never touched (#30664)
* fix(projects): setup edits and removals name the host, so a twin on another host is never touched

Repo-backed project host setups take the repo id, so a project on two hosts has
two setups with one id. Update and delete took the first match by id, so editing
or removing the SSH copy could rename or remove the local one. The args gain an
optional hostId (older peers strip it and keep the first match).

* fix(projects): tolerate only the legacy runtime stamp when a named host misses an independent setup

* fix(projects): refuse a twin edit on a server that would act on the wrong twin; read legacy runtime stamps as local

* fix(cli): refuse setup --host on a server that would ignore it

* refactor(projects): exact host match only; name the host on the wire only for twins on one route

* fix(projects): a named host the catalog lacks refuses; settings remove stays by id

* refactor(projects): name hosts on the wire only for this computer's catalog; paired servers keep the by-id request
2026-10-19 03:47:23 -07:00
Neil ec89fa6099 feat(orcad-migration): host applies a signed editor-drafts blob inside the commit (S10-H) (#30665)
* feat(orcad-migration): host applies a signed editor-drafts blob inside the commit

* fix(orcad-migration): consume received blobs only after the commit is on disk
2026-10-19 03:46:41 -07:00
Jinwoo Hong 218b9a737b The layout drift log names the values that differ (dark, layout-owner build only) (#30678)
* The layout drift log names the values that differ

* Keep the drift log's value copies type-checked and lint-clean

* Give each side of each logged change its own size allowance

* Name the drift log's id length cap apart from other modules' caps

* The drift log names the terminal bound to each pane of a changed tab

* Pin the terminal id length rule in the drift log tests
2026-10-19 06:41:38 -04:00
Jinwoo Hong 9def13dde2 ci: run the layout-owner switch-on specs as an advisory PR check (dark, layout-owner build only) (#30582)
* ci: run the layout-owner switch-on specs as an advisory PR job

Build the e2e app with ORCA_LAYOUT_OWNER=main through the existing e2e.yml
(new layout_owner input, read only by the app and orcad builds), run the
layout oracle, its positive control and the layout-owner acceptance specs
with the save-divergence log on, and upload a summary by kind and spec.
Not in the PR aggregate's needs.

* ci: summarize divergence logs without a test report, and drop file-path ids from kinds

* ci: switch-on job adds the stream-loss spec and the managed-SSH Docker lane, drops diff-word-wrap (fails switch off), and names each looped test in the summary

* test(e2e): editor-model-host-isolation opens both files through the store's open action, so a layout-owner build's main holds the tabs

* ci: the divergence summary skips a cut-short test report instead of failing

* ci: re-run PR checks on a merge ref that includes the main typecheck repair (#30680)
2026-10-19 06:41:26 -04:00
Jinwoo Hong 1cf7472d1e feat(relay): batch a memory cell's resume-confirmed audit rows (#30655)
* feat(relay): add the per-cell phonePath switch, effective only in reserve mode

Step 6 of the relay redesign keys every phone-path change on this switch. The cell
reports the effective value in runtime status, and the flag tool refuses phonePath=memory
unless the cell is reserve, and a reserve flip that would turn phones over unnamed.

* feat(relay): resume paired phones from one device read on memory cells

With phonePath=memory in reserve mode, a phone resume is admitted from one device
read and the cell's own seat, and takes no activity leases. A refused device closes
4401, a failed read 4503, and only a missing device row falls through to the invite
path. A phone whose host has no seat gets one assignment read: 4409 when the row
names another cell or the read fails, otherwise 4404.

* feat(relay): keep a memory resume's basis in the cell and confirm it from one read

A phone resumed on the memory path keeps its connection basis in the cell, not in
relay_connection_bases. Its confirm rereads the device row once, renews it only when
less than 29 days remain, and always answers a current-version confirm renewed:true
with the expiry the database holds. Confirms route by where their basis lives, so a
flip between accept and confirm completes either way. Retries answer from a 10-min
in-memory result map.

* test(relay): roll a memory phone cell back with confirms in flight

Flipping back by admitMode=db, phonePath=db or a dead-man trip leaves in-flight
confirms to finish on their memory basis, writes no phone lease and leaks none, and
evacuation still refuses the cell while Postgres records it as reserve.

* feat(relay): batch a memory cell's resume-confirmed audit rows

Memory confirms buffer their audit rows and the cell writes them as one multi-row
insert every 5 s or 500 rows, idempotent by a per-event id. The buffer holds at most
5,000 rows and drops a row older than 10 minutes rather than retrying it, counting
and logging both kinds of drop.

* fix(relay-ops): store phonePath=db as no key, so old images keep taking writes

An explicit phonePath=db left in the switch file would make the flag tool refuse every
later write after an image rollback, including a watcher's admitMode=db flip back.

* fix(relay): log a failed no-seat read, recheck the switch after the device read

A failed assignment read for a phone with no seat now logs a rate-limited line before
its 4409. A switch that turned off during the device read sends the phone down today's
path. resolveResume reuses the one-read lookup, so the expiry and grace rules have one copy.

* fix(relay): log a failed memory renewal; test flips, replays and basis cleanup

A renewal write that fails still answers the stored expiry, but now logs a rate-limited
line so a persistently failing update is visible before phones age toward re-pairing.

* test(relay): assert a rolled-back memory splice issues no statement when it closes

* fix(relay): flush resume audit early only on crossing the batch size, drop the shutdown flush

A failing database no longer takes a 500-row insert per confirm; retries wait for the
5 s timer. The shutdown flush raced the pool closing, so it is gone and the at most 5 s
loss is documented. A stopped buffer no longer re-arms its timer.
2026-10-19 06:37:35 -04:00
Jinwoo HongandClaude 78bad24d33 test(e2e): nine sidebar specs create workspaces and links through main so they pass with the switch on (#30667)
* test(e2e): create reveal-spec workspaces through main so they pass with the sidebar switch on

The agent-send picker and host-section reveal specs injected fake repos and
workspaces into the window store. With ORCA_SIDEBAR_INPUTS=main the sidebar
lists main's rows, so those workspaces never appeared. The SSH host is now an
unreachable target seeded into the stopped profile, and the paired-server host
is a real headless orca serve host; pins, lineage and notes go through main.

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

* test(e2e): type the seeded hosts as execution host ids

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

* test(e2e): poll the reveal specs' page setup instead of awaiting it across CDP

An evaluate that awaits a preload promise sometimes fails with "Resulting
promise was garbage collected" in switch-on runs. evaluatePolled runs the work
in the page, polls its outcome, and still fails on a rejected or failed write.

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

* test(e2e): parent reorder spec creates its workspaces and links through main

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

* test(e2e): lineage specs create their workspaces and links through main

seedLineageScenario names and orders the two seeded workspaces and links them
with assignWorktreeParent. Extra families and surrounding rows come from a
fresh repo with real worktrees; pins and stale instances are main writes.

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

* test(e2e): default-branch and downward-drag specs create their workspaces through main

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

* test(e2e): reveal and active-delete specs create workspaces through main

The 60-row reveal, the folder-workspace reveal and both active-delete tests
now use real workspaces and folder workspaces added through main, ordered with
metadata writes. The deletes go through the real removal path, so the row
removal sampler waits by time rather than a frame count.

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

* test(e2e): address review on the switch-on spec follow-up

writeWorkspaceMeta now fails unless main lists the written values; the sleep
test's parent is a real worktree instead of a store-faked one; the legacy
inline-lineage case stays window-only and is skipped with the switch on;
the parent-reorder collapse key is built from the row's host.

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

* test(e2e): read-back reports an unlisted workspace and compares values structurally

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

* test(e2e): wait for the added repo before showing its worktrees

updateRepo returns false without writing when the store does not list the repo
yet. On CI the add's own refresh superseded the helper's fetch, so the visibility
write was skipped and every discovered worktree stayed hidden. The helper now
waits until the store lists the repo and fails if the write is refused.

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

* test(e2e): name the repo wait in its failure message

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

* test(e2e): size the setup poll budget to the number of main writes

evaluatePolled waited a fixed 10 s, sized for one write. Linking 53 children takes
about 8 s locally and 36 folder workspaces about 4.5 s, so CI ran past it ("page
work never settled"). Bulk setup now passes a budget of 10 s plus 500 ms per write.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 06:29:17 -04:00
Jinwoo Hong b8f7ece200 feat(relay): keep a memory resume's basis in the cell and confirm it from one read (#30651)
* feat(relay): add the per-cell phonePath switch, effective only in reserve mode

Step 6 of the relay redesign keys every phone-path change on this switch. The cell
reports the effective value in runtime status, and the flag tool refuses phonePath=memory
unless the cell is reserve, and a reserve flip that would turn phones over unnamed.

* feat(relay): resume paired phones from one device read on memory cells

With phonePath=memory in reserve mode, a phone resume is admitted from one device
read and the cell's own seat, and takes no activity leases. A refused device closes
4401, a failed read 4503, and only a missing device row falls through to the invite
path. A phone whose host has no seat gets one assignment read: 4409 when the row
names another cell or the read fails, otherwise 4404.

* feat(relay): keep a memory resume's basis in the cell and confirm it from one read

A phone resumed on the memory path keeps its connection basis in the cell, not in
relay_connection_bases. Its confirm rereads the device row once, renews it only when
less than 29 days remain, and always answers a current-version confirm renewed:true
with the expiry the database holds. Confirms route by where their basis lives, so a
flip between accept and confirm completes either way. Retries answer from a 10-min
in-memory result map.

* fix(relay-ops): store phonePath=db as no key, so old images keep taking writes

An explicit phonePath=db left in the switch file would make the flag tool refuse every
later write after an image rollback, including a watcher's admitMode=db flip back.

* fix(relay): log a failed no-seat read, recheck the switch after the device read

A failed assignment read for a phone with no seat now logs a rate-limited line before
its 4409. A switch that turned off during the device read sends the phone down today's
path. resolveResume reuses the one-read lookup, so the expiry and grace rules have one copy.

* fix(relay): log a failed memory renewal; test flips, replays and basis cleanup

A renewal write that fails still answers the stored expiry, but now logs a rate-limited
line so a persistently failing update is visible before phones age toward re-pairing.
2026-10-19 06:27:13 -04:00
Neil e590699e7e refactor: round-3 pass-1 follow-ups (#30699)
- one repoRowOwnerStamp for the SSH and runtime session placeholder builders
- lineage refresh resolves the transport host once; pin that a server reply never appends a copy
- markdown-document-paths tests live next to the shared module
2026-10-19 03:27:09 -07:00
Neil aa739f3395 refactor(renderer): one FileTypeIcon component for file icons (#30695)
Pure refactor. Twelve rows, trees and menus called getFileTypeIcon(path)
and then rendered or createElement-ed the result themselves; they now
render <FileTypeIcon filePath />, so a change to where file icons come from
lands in one component. The tab and chat pill keep getFileTypeIcon because
they hand an icon component to another component.
2026-10-19 03:21:46 -07:00
Neil 224d126133 refactor(composer): split run-target rows and worktree-creation steps into named pieces (#30703)
* refactor(composer): build run-target rows from per-section builders

* refactor(composer): move run-target row actions into their own module

* refactor(composer): render run-target rows in RunTargetRowList

* refactor(worktree-create): dispatch remote-target preparation and share the provisioning log

* refactor(worktree-create): extract the pending create's tab strip

* refactor(sidebar): split the ephemeral VM resume out of worktree activation

* refactor(composer): give the run-target submenu rows an explicit row type
2026-10-19 03:13:56 -07:00
Neil 4f425a2861 fix(cli): refuse server names --environment cannot tell apart, and say how to pick one of twin projects (#30676)
* fix(cli): refuse a paired server name --environment cannot tell apart, and say how to pick one of twin projects

* fix(cli): point an ambiguous --worktree at its host-qualified identity

* fix(cli): keep the ambiguity listing on the paired server the command chose

* fix(cli): compare saved server names the way --environment does, and route cross-host twin projects to --project --host
2026-10-19 03:12:30 -07:00
Jinwoo HongandClaude b50929956f feat(relay-ops): watch a memory phone cell and turn its phone path off on a breach (#30662)
* feat(relay): add the per-cell phonePath switch, effective only in reserve mode

Step 6 of the relay redesign keys every phone-path change on this switch. The cell
reports the effective value in runtime status, and the flag tool refuses phonePath=memory
unless the cell is reserve, and a reserve flip that would turn phones over unnamed.

* feat(relay-ops): watch a memory phone cell and turn its phone path off on a breach

The reserve watch now also judges five phone rules while the switch file says
phonePath=memory, from the cell's own runtime metrics against the 10 minutes before
flipped-at: 4401s, confirm failures, attach-deadline 4404s and accept database errors
over their lines for 3 minutes, and completed accepts under half of background. A
phone-rule trip writes phonePath=db only; any other trip still flips the cell back
whole. Resume audit drops page after 5 minutes and flip nothing.

* fix(relay-ops): store phonePath=db as no key, so old images keep taking writes

An explicit phonePath=db left in the switch file would make the flag tool refuse every
later write after an image rollback, including a watcher's admitMode=db flip back.

* fix(relay-ops): keep phone watch rules off traffic noise, and keep watching after a phone trip

Completed accepts are no longer judged. Rate lines use the background mean plus
max(margin, share, 4 Poisson deviations), with a margin of 3 for attach deadlines. A
phone trip writes phonePath=db mid-watch and the reserve rules go on; both flip backs
remove the phonePath key so a cell rolled back to an image without it takes the write.
The phone background starts where phonePath went memory, and a background broken by a
restart refuses the watch. Audit drops page only while rising; failing renewals page too.

* test(relay-ops): replay the phone rules on 26 recorded hours of c7 and c29

Per-minute counts from the cells' runtime metrics (read-only, 10-18/19) replace the
synthetic quiet window. c29's bursty auth failures tripped the attach-deadline rule at a
margin of 3 in 29% of 300-minute watches, so its margin is now 5 like the others.

* fix(relay): judge the phone rules as a share of attempts, retry a failed phonePath write

The count rule false-tripped busy c28 (33/121 five-hour watches). The share rule (over half of
the minute's attempts, 0.25 over the background, capped at 0.6) trips none of the recorded c7,
c28 and c29 watches and catches two refusals per accept within 3 minutes. A failed mid-watch
phonePath=db write now keeps watching, retries each sample and pages after 3 failures; break
glass also drops phonePath.

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

* docs(relay): cite the replayed c28 count-rule false trips

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

* fix(relay): judge phone confirm failures against completed accepts

Each completed accept makes one confirm, so failures/(failures + accepts) could never pass half
and a total confirm outage never tripped. Confirm failures are now a share of accepts.

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

* test(relay): note why the confirm replay tests only trips

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 06:12:03 -04:00
Neil 90236c3bde refactor(browser): one page gate and one owner for a host command's hold (#30652)
The revocation gate, the input cancel list and the hold's lifecycle each lived in two places.
Sinks now call one gate, held input is recorded at the CDP send (and the one native key path),
and runWithRemoteCliPageAdmission owns retire-on-revoke, release and error conversion.
2026-10-19 03:11:54 -07:00
Jinwoo Hong 1a41a23419 feat(relay): resume paired phones from one device read on memory cells (#30649)
* feat(relay): add the per-cell phonePath switch, effective only in reserve mode

Step 6 of the relay redesign keys every phone-path change on this switch. The cell
reports the effective value in runtime status, and the flag tool refuses phonePath=memory
unless the cell is reserve, and a reserve flip that would turn phones over unnamed.

* feat(relay): resume paired phones from one device read on memory cells

With phonePath=memory in reserve mode, a phone resume is admitted from one device
read and the cell's own seat, and takes no activity leases. A refused device closes
4401, a failed read 4503, and only a missing device row falls through to the invite
path. A phone whose host has no seat gets one assignment read: 4409 when the row
names another cell or the read fails, otherwise 4404.

* fix(relay-ops): store phonePath=db as no key, so old images keep taking writes

An explicit phonePath=db left in the switch file would make the flag tool refuse every
later write after an image rollback, including a watcher's admitMode=db flip back.

* fix(relay): log a failed no-seat read, recheck the switch after the device read

A failed assignment read for a phone with no seat now logs a rate-limited line before
its 4409. A switch that turned off during the device read sends the phone down today's
path. resolveResume reuses the one-read lookup, so the expiry and grace rules have one copy.
2026-10-19 06:06:50 -04:00
NeilandClaude 9f37fcdf70 fix(persistence): a commit that runs out of disk no longer stops all later saves (#30694)
* fix(persistence): a commit that runs out of disk no longer retires the profile-state writer

A WAL COMMIT on a full disk fails with SQLITE_FULL after SQLite rolls the transaction back. It was
reported as indeterminate, which retires the writer, so every later write failed until a restart:
on a managed SSH host, a disk that filled once left orcad unable to open a terminal even after the
space was freed. SQLITE_FULL fails a frame write before the commit point, so it is now a known
failure; every other failed commit stays indeterminate.

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

* docs(persistence): say why SQLITE_FULL on COMMIT is safe to treat as not committed

Opus review: the claim holds because powersafe overwrite (on by default) means no padding frames
follow the WAL commit frame.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 02:56:37 -07:00
Jinwoo Hong cb0b152409 test(relay): census every activity-lease reader for phones without leases (#30625)
* test(relay): census every activity-lease reader for phones without leases

Step 6 lets a reserve cell accept phones without writing confirmation or
splice leases. This ratchet classifies every reader of the lease table by why
that is safe, and pins the guards that keep completion and idle rehome off a
reserve cell whose host holds no source lease.

* test(relay): scan subdirectories and USING reads, and ban a bare lease table name

The ratchet now walks every production file under src, counts USING as a read, and
fails on a bare table-name value, which could reach an interpolated FROM it cannot see.
2026-10-19 05:54:32 -04:00
OrcaWinandm4air 83c768d1b4 fix(crash-reporting): Windows native crash reports say what the fault touched and which code ran (#30691)
* fix(crash-reporting): record what a native access violation touched and the bytes that ran

Windows renderer AV reports carried only the faulting module offset. Two
recent ones (Orca.exe+0x23d3f88, +0x21674c2) map in official Electron
symbols to stores into memory the preceding instructions had just written,
which an intact binary on a healthy CPU cannot fault on. Without the access
kind, fault address and executed bytes, an engine bug, a corrupted code page
and unstable hardware look identical.

Read them from the Crashpad dump: AV access kind and target, the target's
distance from the faulting thread's stack pointer (x64), and 16 bytes at
the instruction pointer from the captured memory list.

* fix(crash-reporting): capture code bytes only inside a loaded module

They exist to diff against the shipped binary; bytes from JIT or other
anonymous memory add nothing and could carry page contents.

* fix(crash-reporting): capture code bytes only from Windows dumps

On macOS and Linux Crashpad records the touched data address as the exception
address, so bytes read there would be module data, not the faulting code.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-19 02:49:44 -07:00
OrcaWinandm4air 00d1545a65 fix(diagnostics): crash bundles always keep the terminal daemon's newest log lines (#30690)
* fix(diagnostics): reserve a share of the crash bundle for the daemon log

Trace spans (git.exec, renderer_memory) fill the 4 MiB bundle cap on busy
hosts, and the daemon log was read only after them, so 11 of 13 recent
Windows renderer-OOM bundles carried no daemon lines at all: no session
counts, no daemon deaths, no process-growth level.

Collect the daemon log first with a 1 MiB budget; whatever it leaves
unused still goes to trace spans.

* fix(diagnostics): return unused bundle space to the daemon log

The 1 MiB share is now a floor, not a cap: after spans are collected the daemon
log takes whatever they left, so a chatty daemon beside quiet traces keeps up to
the whole bundle as before.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-19 02:47:39 -07:00
OrcaWinandm4air da6d823ea0 fix(crash-reporting): renderer crash reports say whether the window was hung or blank (#30692)
* crash-reporting: say whether the main window was hung or blank when its renderer died

Windows killed/exit-1 renderer reports read the same whether Task Manager, an
EDR, or Orca ended the process. Field bundles show several of them follow
minutes of renderer silence or a document that never loaded after an update,
which points at users ending a frozen Orca. Record Electron's
unresponsive/responsive transitions as durable breadcrumbs and attach
rendererUnresponsiveForMs, rendererDocumentLoaded, rendererDocumentAgeMs and the
window's visible/minimized/hidden + focus state to the renderer crash details.

* fix(crash-reporting): a failed load reads as a blank window, and a crash ends the hang

Electron fires did-finish-load after did-fail-load, and a failed load never
commits, so track from did-start-navigation and flag main-frame load failures.
Clear the hang at renderer death so the replacement renderer's responsive is not
logged as the dead one recovering.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-19 02:45:42 -07:00
Jinwoo Hong a12c592f75 feat(relay): add the per-cell phonePath switch, effective only in reserve mode (#30629)
* feat(relay): add the per-cell phonePath switch, effective only in reserve mode

Step 6 of the relay redesign keys every phone-path change on this switch. The cell
reports the effective value in runtime status, and the flag tool refuses phonePath=memory
unless the cell is reserve, and a reserve flip that would turn phones over unnamed.

* fix(relay-ops): store phonePath=db as no key, so old images keep taking writes

An explicit phonePath=db left in the switch file would make the flag tool refuse every
later write after an image rollback, including a watcher's admitMode=db flip back.
2026-10-19 05:38:03 -04:00
NeilandClaude Opus 5.5 912cb06398 fix(managed-server): repair main typecheck after the byte-budget and snapshot-room PRs crossed (#30680)
* fix(managed-server): import newestStateMtimeCommand from its new module

#30635 and #30618 merged in parallel: #30618 moved newestStateMtimeCommand
to orcad-state-newest-mtime.ts while #30635's byte-budget test imported it from
orcad-state-snapshot.ts, breaking typecheck on main.

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

* test(managed-server): measure the snapshot-space probe and record #30618's capture growth

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:29:45 -07:00
Jinwoo Hong 0e1d0ad4a8 A CLI split that times out leaves no pane (dark, layout-owner build only) (#30563)
* fix(layout): a pane main closed stays closed when its terminal starts late (dark, layout-owner build only)

* fix(layout): main takes back a CLI split pane whose terminal does not start within 10 s (dark, layout-owner build only)

* fix(layout): the timeout take-back keeps a pane whose terminal started or is starting (dark, layout-owner build only)

* fix(layout): one take-back call keeps the layout-writer count

* fix(layout): a split take-back is decided in the write queue, and a spawn for a taken-back pane starts nothing (dark, layout-owner build only)

* test(layout): a spawn for a taken-back pane is refused before the provider spawns

* test: typed store fake in the closed-pane spawn test, no casts
2026-10-19 05:21:06 -04:00
Jinwoo Hong 9294b0ae9c fix(sidebar): the window raises the unread flag exactly when its host does not (dark switch) (#30638)
* fix(sidebar): the window raises the unread flag exactly when its host does not

With ORCA_SIDEBAR_INPUTS=main the host raises the workspace unread flag. A host
now advertises workspace-unread.host-raises.v1 in its status; a switch-on window
raises the flag itself only for workspaces whose paired server lacks it, waiting
for that server's status first. The desktop advertises the same capability to the
hosts it pairs with, and a host leaves raising to an older desktop (one that hosts
client pages without the capability) while it is connected.

* fix(sidebar): every desktop socket advertises the unread capability; window answers from cached status

The browser host lease and network tunnel build their capabilities on the shared
remote base, not the Electron list, so a current desktop's lease looked like an
older desktop and silenced the host. The capability now lives in the shared base.
The window reads the cached status first and probes only before the first one.

* test(sidebar): pin the cached-status path and both older desktop socket shapes

* test(e2e): narrow the server row's host before Mark as read
2026-10-19 05:14:00 -04:00
Jinwoo HongandClaude 0c3807e73b test(e2e): reveal specs create workspaces through main so they pass with the sidebar switch on (#30642)
* test(e2e): create reveal-spec workspaces through main so they pass with the sidebar switch on

The agent-send picker and host-section reveal specs injected fake repos and
workspaces into the window store. With ORCA_SIDEBAR_INPUTS=main the sidebar
lists main's rows, so those workspaces never appeared. The SSH host is now an
unreachable target seeded into the stopped profile, and the paired-server host
is a real headless orca serve host; pins, lineage and notes go through main.

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

* test(e2e): type the seeded hosts as execution host ids

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

* test(e2e): poll the reveal specs' page setup instead of awaiting it across CDP

An evaluate that awaits a preload promise sometimes fails with "Resulting
promise was garbage collected" in switch-on runs. evaluatePolled runs the work
in the page, polls its outcome, and still fails on a rejected or failed write.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 05:10:40 -04:00
Jinwoo Hong 7a1b5336a8 fix(relay-ops): judge a reserve cell's connections against its configured hard cap (#30668)
A reserve cell's cell-status read is refused, so it carried no
connection_hard_cap and the evaluator fell back to the 500 default:
run 39225932592 froze on reserve c7 at 731 connections under its
1000 cap. Read connection_hard_cap from the durable Terraform cells
relay-ops already parses and emit it for reserve cells.
2026-10-19 05:07:20 -04:00
Brennan Benson 77195a1266 fix(session-history): cached results keep every session the scan found for a workspace (#30586)
A repeat open or tab revisit of Session History's Workspace and Project
tabs dropped to 250 sessions, losing older matching sessions (341 -> 250
in QA). Every cache reuse re-cut the scan's result with a shared newest-N
counter for in-scope rows, which the scan's own per-agent budgets don't
match. The cut now bounds only out-of-scope rows, so a scan's result
passes through unchanged, and scoped requests reuse only a scan of their
own depth so lowering the depth rescans.

Tracked in STA-11167.
2026-10-19 02:03:13 -07:00
NeilandClaude d600658b84 fix(ssh): closing a bridged relay tab stops its shell, even across a restart (#30587)
* fix(ssh): stop a closed bridged relay tab's shell from every close route, across restarts

The renderer's tab-close kill tombstoned a bridged lease before the bridge's
own close ran, so the old relay's shell kept running. Route it through the
bridge like the runtime kill. Persist a pending bridged stop on the lease's
pendingKill so a restart before reconnecting still sends it, and list such
leases for the stop without serving them as panes.

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

* fix(ssh): end a closed bridged tab's lease when its stop can't be proven

Clearing the stop left the lease bridgeable, so a process that took the
recycled relay id was served under the closed tab. Retire it as any closed
tab's lease is.

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

* fix(ssh): keep a closed bridged tab closed across restarts even when its stop can't be fenced

An unfenced legacy stop now gets one try and ends the lease, a repeated close
keeps the first record's age, and the Docker bridge spec covers a close while
disconnected followed by a restart.

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

* fix(ssh): re-list after abandoning a bridged stop so the closed pane drops

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

* fix(ssh): route CLI and worktree stops of a bridged tab through the bridge, and keep a repeated close fenced to the first identity

* fix(ssh): refuse sleeping a bridged pane, settle bridged stop-and-wait on the relay's exit proof, and never attach a pane closed mid-attach

* fix(ssh): settle a bridged stop-and-wait on the exit proof alone, for every overlapping waiter

* fix(ssh): keep an unfenced bridged stop's pane tracked for its trailing exit report, never serving it

* fix(ssh): keep awaiting a bridged stop's exit after a failed reply, never name a retired pane to the relay CLI, and require a real empty listing in e2e

* fix(ssh): tell the runtime a bridged terminal exited once its stop-and-wait is proven

* fix(ssh): keep an unfenced bridged stop's exit observable after its reply fails

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 01:59:23 -07:00
NeilandClaude 79b7d702be test(managed-server): 6 KB wire-size budget for every POSIX host-op command (#30635)
* test(managed-server): byte budget for every POSIX host-op command

Builds each exported POSIX host-op builder at realistic maximal arguments and
asserts the raw command stays under 6 KB, under the 8,000-byte compression
threshold. Ops already over budget are listed with ceilings that may only
shrink until they move onto the host script (round 4 R4-10).

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

* test(managed-server): budget the wrapped, fence-guarded command on Linux and macOS

Measures what a gateway sees on the plain fallback (fence guard + uncompressed
POSIX wrapper), takes the larger of Linux and macOS, launches the SSH way (nohup),
and also discovers the top-level src/main/ssh builders. Exports three builders
that are sent as their own exec request so they can be measured.

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

* test(managed-server): count the stdout fence; scan ssh transport subfolders

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 01:55:29 -07:00
Neil f6e9e649f4 fix(projects): a project added from the CLI, web or a paired desktop through a symlink or subfolder shows its workspaces (#30657)
* fix(projects): a project added from a paired client or the CLI through a symlink or subfolder shows its workspaces

* fix(git): keep a literal backslash in a POSIX checkout root
2026-10-19 01:54:59 -07:00
Neil 94c858cda6 fix(ssh): a cancelled or revoked host's browser command stops on the desktop (#30630)
* fix(ssh): a cancelled or revoked host's browser command stops on the desktop

orcad now tells a cancel-capable desktop when the CLI gave up on a forwarded browser command, and a running host command on the desktop checks revocation before every side-effecting step (each helper CDP message and each direct input dispatch).

* fix(browser): keep local commands out of a host's scope, gate each helper on its proxy

* fix(browser): hold a host-driven page at its input sinks instead of per call site

* test(browser): checked mock types in the host revocation suites

* fix(browser): refuse only a host's own traffic, never the desktop's page commands

* fix(browser): a host's proxy never keeps its scope, and a revoked click still lets go of the button

* fix(browser): a revoked command may release only what it pressed, on every input path

* fix(browser): refuse every revoked step, and let the desktop release what a revoked command left held

* fix(browser): retire a revoked command's helper daemon before letting go of the page; release held input with a lease, synced pointer state and the focus guard

* fix(browser): deferred proxy input keeps its hold, releases count once they land, and a revoked helper is retired at once with its routes kept

* test(browser): keep the mock page cast on its SAFETY line
2026-10-19 01:54:54 -07:00
Neil a85c17650f refactor(process-host): let runProcess callers observe stderr as it arrives (#30628)
An optional onStderr hook receives every raw chunk, so long commands can
stream progress while still getting the bounded result. Capture,
truncation and termination are unchanged; existing callers pass no hook.
2026-10-19 01:54:26 -07:00
Neil fa0bd69099 refactor(ssh): derive a host-key policy from the target owner (#30627)
* refactor(ssh): derive a host-key policy from the target owner

The verifier and decision took an isEphemeralRuntimeTarget boolean.
They now take a HostKeyPolicy derived from the owner by one function,
and the owner shape gets a named SshTargetOwner type. No behavior
change: on-demand runtime VMs map to the ephemeral policy, everything
else to standard, and every existing decision test passes unchanged in
substance.

* docs(ssh): separate owned-target hiding from the recipe-VM id prefix

Hidden-ness is decided by owner in main; the runtime-ssh- prefix also
carries per-provision semantics (fresh filesystem, VM cleanup), so only
on-demand-runtime owners may use it. Comments only.

* refactor(ssh): one id predicate for every Orca-owned SSH target

Surfaces that hide owned targets or must never dial them from the
renderer (startup re-dial, reconnect hints, automation host catalog,
status bar, host registry, state broadcast) now ask isOwnedSshTargetId,
which also covers the machine- prefix. isRuntimeOwnedSshTargetId keeps
its narrower recipe-VM meaning (address and filesystem replaced every
provision), so name retirement and VM cleanup are unchanged. No target
uses machine- on main yet, so behavior is identical; tests pin both.
2026-10-19 01:54:22 -07:00
Jinwoo Hong c18cb91dce fix(relay-ops): let the incident monitor sample a reserve-mode cell (#30661)
The director's cell-status refuses a step-5 reserve cell with a 404
{error: cell_in_reserve_mode}, so every monitor sample failed while any
cell was reserve. Classify that exact refusal as reserve-not-probed, skip
the refused migration reads, and judge the cell on probes, logs and the
selector instead. Any other 404 still fails the sample.
2026-10-19 04:52:53 -04:00
Neil 7ce58ec47e refactor(terminal): one PTY attribution for teardown, close and identity; delete the deprecated owner resolver (#30626)
* refactor(terminal): one PTY attribution for teardown, close and identity; delete the deprecated resolver pair

- strict ssh: route: a proven direct listing beats a same-id server proxy; other contradictions refuse
- terminal spawn's git branch takes the strict route when a host is named (rivals refuse)
- resolveTerminalPtyOwner + isUnattributedTwinWorktree replace resolveRuntimeOwnerForWorktree in
  shutdown, pane and agent hibernation, and PTY identity; close logs unroutable runtime ids
- delete getRuntimeEnvironmentIdForWorktree/resolveRuntimeOwnerForWorktree (legacy branch moved
  into the adapter), the alias golden test, the importer ratchet, its baseline and pr.yml step
- hasHostEvidence reads the owner indexes instead of scanning repos and groups

* fix(terminal): close and identity read spawn's attribution for unscoped remote ids (Opus P2)

- an unattributed twin's unscoped remote: id is skipped and logged on close, never closed locally
- setup terminals: identity and close follow spawn's runtime; raw-id teardown stays local
- drop the last stale getRuntimeEnvironmentIdForWorktree mock (tests/e2e unit suite)

* refactor(terminal): one ownership read in resolveTerminalPtyOwner

* test(terminal): pin the close-path twin skip and the two-proxy direct listing (Opus P3)

* test(terminal): keep the close-provider owner fixture's full union type (Codex P1)
2026-10-19 01:50:57 -07:00
NeilandClaude a451f472c3 fix(ssh): make room for the pre-activation snapshot on remote hosts, and say why a capture failed (#30618)
* fix(ssh): make room for the pre-activation snapshot, and say why a capture failed

Every update left its own full state copy behind, and a failed capture its partial archive, so a
small or quota-limited home filled until no update could snapshot. The capture now prunes every
pre-* snapshot except the one the activation record names, removes its own partial archive on
failure, and prints tar's error. Before stopping the running server, the activation checks that
the snapshot fits, so a host without room keeps its server.

Refs #30491

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

* fix(ssh): the room refusal says what is free apart from what a prune reclaims

It also stops claiming the recorded server is running, and a test pins that the record's
snapshot survives both the room check and the prune.

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

* fix(ssh): measure snapshot room before the last terminal recount, within a deadline

A slow scan after the recount could let a terminal opened meanwhile be stopped. The scan now runs
first, gives up after 12 s per du (UNMEASURED reads as unknown), and a lost probe channel leaves
the room unknown rather than failing the update. The newest-mtime probe moves to its own module
to keep the snapshot module under its line limit.

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

* fix(ssh): pick timeout's syntax for the room probe, and pin its failure paths

BusyBox before 1.30 takes `timeout -t SECS`; plain `timeout 12` disabled the probe there.
Tests cover a timeout on the old-snapshot scan and a lost fence from the probe.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 01:35:18 -07:00
Neil 699b9a0637 feat(files): guarded file writes that refuse to overwrite a changed file (#30605)
* feat(files): guarded file writes that refuse to overwrite a changed file

Adds an optional disk precondition to file writes: the writer compares the file
on disk with an expected signature inside the write, serialized per file (path
then inode), and refuses with a typed changed/missing result instead of writing.
Remote hosts serve it as files.writeIfUnchanged behind a capability; older
hosts get a client-side compare that fails closed, pinned to the probed runtime
and connection. Undo of a delete and the MCP starter config use it to write only
into a missing or identical file. Groundwork for guarding editor saves.

* fix(files): pin guarded writes on the sending socket; keep refused undo; narrow not-found and pinning

* fix(files): pin a guarded save to one pairing; keep only refused undo steps

* fix(files): ENOENT by code only; resolve symlink chains fully; pin lost-reply recovery to the save's pairing
2026-10-19 01:27:53 -07:00
Jinwoo Hong cf13e0cf9d fix(sidebar): phone feed list leftovers before the flip (dark) (#30631)
* fix(sidebar): phone feed list leftovers before the flip (dark)

Bump the feed's semantics version for P11's desktop facts; mark switch-on phone page bundles so packaging and orcad staging refuse them; show the page's last-listed rows before the first feed read; draw the host's pending creations; pass the listed host's own label into host sections instead of the running device's platform.

* fix(sidebar): review fixes for the phone feed leftovers (dark)

A dark orcad may stage the dark page (only a marker of an off switch is refused); the bundle tree writer leaves the marker; the pre-feed list keeps the view's filters; a creation names its project only outside that project's header and survives search when its project matches; the feed view and row labels read one resolved desktop platform.

* fix(sidebar): second review fixes for the phone feed leftovers (dark)

Test the off-switch marker check across switches; resolve the desktop platform once for rows and headers; a section search narrows keeps only matching rows.
2026-10-19 04:27:09 -04:00
da58ee0875 Session History: the Workspace tab stops re-reading every transcript to find its sessions (#30469)
* perf(session-history): remember each transcripts folder so Workspace scans stop re-reading files

* refactor(session-history): keep the scope cwd table in its own module

* test(session-history): type the hoisted read log without an assertion

* test(session-history): type the collected session folders

* chore(wsl-move): list the parse cache scope_cwds table

* Keep scoped session selection current with cache authority

* Consolidate scope cache imports in regression test

* Run scope cache SQLite regression tests under Node

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: Orca Worker <orca-worker@localhost>
2026-10-19 01:25:58 -07:00