* fix(pty): an exit proof names its incarnation, and a failed discard keeps ownership
A same-id respawn could be retired by its predecessor's exit proof; finishPtyShutdown now refuses
a proof whose incarnation is not the record's, and retireRejectedPty is scoped to the rejected
spawn's incarnation. A spawn discarded after a failed save no longer drops ownership when its
shutdown failed: it reads unverifiable until a later stop reaches it.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(pty): a failed discard leaves a same-id replacement's verdict alone
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(pty): a lost discard reply does not overwrite an observed exit
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(pty): a failed discard tracks the PTY it could not stop; test the incarnation refusal
The discard runs before the spawn is published, so it records ownership and incarnation before
stopping; a failed stop now leaves a handle. Every exit proof states its incarnation.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(pty): track a spawn from its reply, so only an exit or a proven stop removes it
Ownership and incarnation are recorded at commit start instead of at publish, before the binding
save. The discard no longer re-records anything, so an exit observed during the save stays exited.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(pty): admit a same-id respawn despite a predecessor's verdict; retire an exit seen before commit on its fence
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(pty): admit a fresh spawn only past the runtime's incarnation-scoped start fence
Replaces the id-only verdict check, so a same-id respawn is tracked and an incarnation that exited
before its commit is not resurrected; registration no longer has to retire it.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(pty): a proof that names no incarnation still retires; tidy kill-time capture and adopted re-record
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(ssh): a network cut during a managed-server install fails promptly and says contact was lost
* fix(ssh): settle an SFTP transfer when its session dies, since ssh2 never answers the requests left
* fix(ssh): only a closed SFTP session reads as lost contact; a dead session also cancels the abandoned transfer
* test(ssh): put the mock's SAFETY note on the cast's own line
* fix(ssh): a channel open the host never answered also reads as lost contact
* fix(remote): keep a paired host's undelivered tab close across a reload
* fix(remote): show a tab again when its held close expires
* fix(remote): one choke point shows every held close the store drops
* fix(remote): keep held closes while the pairing is unknown; tidy dispositions
* fix(remote): save a held close for a long path-named editor tab
* fix(runtime): a project removal stops its workspaces' terminals together under one deadline; every teardown is classified by kind
* fix(runtime): a workspace the inventory finds mid-sweep is still stopped; the walk test sees const and top-level teardowns
No host ever read intent: 'gesture'. The client stops sending it, the params
schema strips it from older clients, and docs/reference records the rule that a
snapshot never carries intent.
* fix(projects): removing a project on this computer stops its sessions on the host, like repo.rm
* fix(runtime): a spent removal budget still asks every project terminal to stop; paired residue rows are no twins
* refactor(orcad): every snapshot state op fits the gateway exec budget in shell; the state lock sends its script once
* test(orcad): a restore that stops part way reports FAILED and reruns from the archive
Each stamp write made every client re-list the repo's worktrees; a pane whose
agent title flips while streaming turned that into hundreds of re-lists a
second. The write stays immediate; the notice is coalesced per workspace
(50 ms quiet window, 250 ms cap) through the existing keyed coalescer.
The pane-close consistency spec closed panes with a bare PaneManager.closePane, the
drawing-layer call that never asks main. In a layout-owner build that close reached main
only because killing the pane's terminal made main retire the pane; right after a reveal
the terminal has not attached yet, so main kept the pane. Every user close path already
sends the close to main first.
The helpers now close through the pane close request the menu and shortcut use, and every
scenario also checks that main's stored panes match the window's.
* test(e2e): mount-authority polls retry the host's "can't answer yet" terminal list
First-mount adoption briefly makes terminal.list refuse with
terminal_surface_ownership_unavailable. Thrown out of an expect.poll
callback, that ended the poll instead of retrying it, so the spec failed
whenever a slow runner's probe landed in that window.
* test(e2e): say the refused read returns undefined
* test(e2e): a phone lists the desktop's managed SSH workspace as the desktop sidebar does (switch on)
* 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>
* test(e2e): route the SSH phone feed list spec from its sources
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(pty): every PTY retirement names its exit proof; ratchet the unproven ones
* refactor(pty): count raw ownership drops, route an abandoned relay stop to unverifiable, honest proof labels
* fix(runtime): say why a new terminal on a host failed, with a retry
* fix(runtime): never offer a retry or say refused when the host's reply was lost
* fix(runtime): only a host error reply counts as refused; the wake flow keeps its own toast
* refactor(runtime): one 'couldn't confirm' notice carrying the reason, no failure classification
* fix(runtime): one create notice per workspace, so retried seeding can't stack toasts
* fix(ssh): name a noexec home instead of calling the host unsupported
* test(ssh): measure the stage exec probe in the host-op byte budget
* fix(ssh): the connect error names a noexec home too
* chore(i18n): add the noexec status line to the English catalog
* fix(ssh): only the shell's exec refusal reads as noexec; other probe failures reach the self-test
* fix(ssh): a stage the probe can't write skips the probe, so a full disk still reads as full
* fix(ssh): read the probe's exit 126 as noexec in any locale
* 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>
* 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
* 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
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>
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>
* 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.
* 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>
* 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.
* 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)
* 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
* 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>
* 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
* 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
* 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
* 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)
* 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.
* 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>
* 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.
- 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
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.
* 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
* 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
* 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>
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.
* 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.
* 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>
* 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.
* 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>
* 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>
* 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>
* 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.
* 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>
* 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
* 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