Commit Graph
72 Commits
Author SHA1 Message Date
Neil a0f1cb0ee2 refactor(git): group root modules by concrete domain owners (#41322)
* refactor(git): group root modules by concrete domain owners

* chore(git): reconcile retired-test metadata with owner cleanup
2026-11-11 18:02:15 -08:00
Neil 04158606b5 refactor(git): group command execution by concrete owners (#41316) 2026-11-11 17:05:03 -08:00
OrcaWinandOrcaWin 3c7d1e1675 test: remove 263 low-value cases across 20 suites (#41144)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-11 04:05:46 -08:00
Neil ec95c931f0 test(ephemeral-vm): make VM runtime store suites pass on Windows (#40093)
* test(ephemeral-vm): make VM runtime store suites pass on Windows

Drop the process.platform=linux stubs (they made secure writes fsync a
read-only handle, EPERM on Windows, and spawned recipes POSIX-style) and
remove temp dirs through one allSettled removeTree helper so Windows ACL
work cannot fail teardown with EBUSY.

* test: restore the rmSync import dropped by the merge from main
2026-11-10 17:58:12 -08:00
Jinwoo Hong 88880fca7a feat(orchestration): a paired Orca as a Run's coordinator on the server it placed work on (inert) (#40717)
* feat(orchestration): a paired Orca as a Run's coordinator on the server it placed work on (inert)

The server half of remote-owned workers. A paired Orca's call names one of its
Runs; the server admits it as a peer caller kind, writes that Run at frame
receipt, and holds it to the one access check. Its workers' mail and state
queue home under a sequence no reset reuses. Nothing sends the envelope until
the coordinator side ships, so this changes nothing today.

* fix(orchestration): reserve the peer address, keep conversation kinds apart, queue a correction once with its result

* fix(orchestration): hold back a corrected Task's change whatever its latest attempt; rotate pulls in a stable order per database

* chore(orchestration): typed field read, cast rationales, a wake test on fake timers

* fix(orchestration): a peer's receipts per conversation, its sends only into its Run, a page that fits every conversation, pending adopted Tasks keep their gate

* fix(orchestration): a peer's follow-up places where its worker ran

* fix(orchestration): a peer's current/new-child start gets the --on placement refusal
2026-11-10 14:56:02 -05:00
OrcaWinandOrcaWin a163b9fc5b test: remove low-value copy, inventory, and runner assertions (#40282)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-09 10:25:23 +00:00
NeilandJustas Brazauskas c3910146ee fix(vm-recipes): destroy a recipe VM when a phone deletes its workspace (#40205)
Deleting a recipe workspace from the phone removed only its checkout and
left the VM this desktop created for it running. Only the desktop window
ran the destroy.

The relaying desktop now tags the rows of workspaces whose VMs it created
with the environment's name, and says whether deleting will destroy it.
The phone's delete sheet names the environment, and its delete asks for
the destroy (`destroyHost: true`). The desktop then owns that removal:
- it runs as a workspace lifecycle delete
- the environment's Orca removes the checkout and must answer positively.
  An older Orca that answers on acceptance is polled until the workspace
  is gone.
- the VM is destroyed through the shared cleanup, behind any running wake
  or suspend, and only when no other workspace remains on it
- the result reports host.state truthfully: destroyed, kept, or
  left-running (no destroy step)

The environment's own error codes reach the phone unchanged, and a failed
removal shows its message on the phone instead of only a haptic.

Removals that don't ask for the destroy (older phones, the CLI, other
clients) keep today's behavior. No CLI API changes.

Based on #39235 by jutaz, a port of #28435.

Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com>
2026-11-09 01:03:23 -08:00
OrcaWinandOrcaWin 2093801982 test: remove low-value shared and native suites (#40172)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-09 01:02:16 -08:00
Neil 920a83889c refactor: move browser contracts and publication to defining owners (#40209)
* refactor: move atomic publication to its filesystem owner

* refactor: group shared browser definitions by feature

* refactor: bind proxy process and session contracts to defining owners

* refactor: consolidate direct session contract type imports

* refactor: retain atomic publication test timing under moved owner

* fix(benchmark): resolve historical tunnel framing source before loading
2026-11-09 00:45:45 -08:00
Neil 72935f9d6c fix(vm): resume a paired-server VM whose host went away on its own (#40199)
* fix(vm): resume a paired-server VM whose host went away on its own

* fix(vm): count only continuous failures as a lost paired server
2026-11-09 00:32:50 -08:00
Neil 6b7d5d6336 fix(remote-runtime): detect a paired host that stays connected but stops answering (#40104)
* fix(remote-runtime): detect a runtime that stays connected but stops answering

The shared control socket only checked ws pings, which a relay hop or a runtime
whose request path is stuck still answers, so a host could read as connected for
hours with no updates (#39785). Ready sockets now send a cheap status probe after
an idle interval; a missed deadline retires the socket so reconnect backoff runs,
and the mark stays in diagnostics until the runtime answers again. Asleep and
manually disconnected hosts are never probed.

orcad also starts a worker-thread watchdog that logs a blocked event loop and,
when run under a service manager, exits after five minutes so it is restarted
(#34410).

* fix(remote-runtime): clear the unresponsive mark on any post-ready frame and ignore late deadlines

Review follow-ups: a reconnected socket that streams frames proves the runtime
answers, so it clears the mark and lets backoff reset; the probe deadline now
sits above the ws liveness window so a dropped link is not blamed on the runtime;
a deadline that fires late after sleep is not counted; integration tests no longer
depend on wall-clock timing.

* fix(remote-runtime): gate the heartbeat on the shared retry hold and isolate the wedged-runtime test

* test(orcad): wait for stdio close before reading the watchdog child's stderr
2026-11-08 23:28:31 -08:00
OrcaWinandOrcaWin c0d3d2e0db test: remove 13 low-value suites and redundant cases (#40138)
* Remove low-value file, runtime and chat test cases

* test: remove low-value shared contract and timing suites

* test: remove leaking browser IPC mock suite

* test: isolate legacy relay bridge transport fixture

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-08 21:51:31 -08:00
Neil 39902f398b refactor: bind browser and remote runtime consumers to concrete owners (#40135)
* refactor: use concrete browser and input owners

* refactor: bind remote runtime callers to concrete transport owners

* Use checked subscription mock argument types

* Type browser lease mock with its subscription contract

* refactor: bind sleeping VM status probe test to socket owner
2026-11-08 21:41:28 -08:00
Neil 4652a35f87 fix(vm-recipes): stop dialing a sleeping workspace VM, and resume one whose host is gone (#40105)
A suspended recipe VM kept getting reconnect attempts: shared-control
socket retries, health probes and SSH logins. Providers that resume a VM
on a new SSH login or connection woke it again, so suspend saved nothing
(#36363).

While a VM's record is suspended (or its resume failed) and no wake is
under way, Orca now makes no background dial to it:
- SSH: connect and the reconnect ladder refuse that VM's own SSH host,
  which covers tunnels, managed-server checks, census and browser bridge.
- Shared control: no retry after close, and no socket open, including a
  retry timer armed before the sleep.
- Status probe: answers asleep without dialing.
A wake (opening the workspace, Resume) is pending before it dials, so the
wake's own connect passes. Afterwards the server's control socket and
status are reopened.

A VM recorded as up whose SSH host Orca gave up reaching (the reconnect
ladder used every attempt) may have been stopped or reclaimed by its
provider. Before, opening the workspace did nothing for it (#34089). Now
it runs the recipe's resume and reconnects. If that fails, the record is
left as it was, so normal reconnects go on.

Fixes #36363
Refs #34089
2026-11-08 20:40:19 -08:00
Kelvin Amoaba 48ccdd5212 fix(remote-runtime): keep node zlib out of browser bundles (#40124)
The renderer's crew follow reconnect scheduler imported the shared-control
socket state module, which reaches the Node-only sealed-frame reader and
its node:zlib codec. Vite dev throws on that import in the web client,
the desktop renderer's crew panel chunk and the component fixtures.
Move the reconnect timer function next to the scheduler, its only user.
2026-11-09 04:34:49 +00:00
Neil 3be3a9bd78 refactor: organize settings and account defining owners (#40070)
* refactor: place settings and account modules with defining owners

* refactor: reuse the repository recipe defining type
2026-11-08 19:43:10 -08:00
Neil a75d408aa6 fix(remote-runtime): keep a resumed VM's device pairing while its host key is unchanged (#39225)
* fix(remote-runtime): keep a resumed VM's device pairing while its host key is unchanged

* fix(remote-runtime): probe before dropping the transport, keep saved key spelling, leave SSH wakes as they were
2026-11-08 16:12:06 -08:00
Brennan Benson 8ffb618a5c Fork copied replies from their original chat (#39823)
* Keep inherited fork actions with the original chat

* Keep fork boundary tests aligned with inherited actions

* Keep inherited fork actions with their original chat

* Keep fork refusal tests within their typecheck projects

* Allow phone forks for supported agents beyond Claude and Codex

* Gate inherited tip forks on host original-chat support

* Require current host proof for inherited fork offers

* Bind inherited fork actions to the current host runtime

* Keep unsent browser forks definitive and offer accurate retry guidance

* Offer copied tip forks only at the fork point and name their source

* Offer forks on all completed copied replies

* Explain which original response can be forked
2026-11-08 16:06:30 -08:00
Neil c10881fa8c refactor(shared): organize execution and automation defining owners (#39776)
* refactor: import journal queue state from defining owners

* refactor(shared): organize execution and automation owners

* fix: retain type imports used by surviving test cases

* refactor: finish work item owners and refresh validation metadata

* test: infer reconnect status fixture type

* test(claude): resolve source files beside same-named directories

* test(runtime): group repository creation mock registrations
2026-11-08 06:46:09 -08:00
Neil 83f8e6340d refactor(shared): organize file operations and storage owners (#39746) 2026-11-07 22:58:35 -08:00
Neil 4e1d42ef7b perf(ssh): desktop and CLI read compressed frames from remote hosts (#39659)
* refactor(remote-runtime): share compressed frame inflate across client platforms

Move the length checks and decode of paired E2EE compressed frames into
the shared module behind a small gzip/UTF-8 codec, so the desktop can
read them with node zlib while mobile keeps its native/fflate path.

* perf(remote-runtime): read compressed frames from paired hosts on desktop and CLI

Desktop and CLI sockets to orcad/orca serve now advertise
transport.compressed-frames.v1, so a v2 host gzips large text replies and
terminal output before sealing them, as it already does for phones.

- Every Node client transport (one-shot requests, request connection,
  shared control, subscriptions) opens frames through one sealed-frame
  reader that inflates compressed frames on v2 channels, synchronously so
  frame order and v2 counters stay in arrival order.
- A compressed binary frame may claim up to the 8 MB binary outbound cap
  (terminal frames can exceed the 4 MB JSON cap), for phones too.
- ORCA_DISABLE_MOBILE_COMPRESSED_FRAMES=1 now also stops a desktop or CLI
  from asking for compressed frames.
- Relayed phones and the browser client are unchanged.

* refactor(remote-runtime): simplify the mobile inflate codec and clarify capability comment

* fix(remote-runtime): name why a sealed frame could not be read

Text-only sockets report an authentic binary payload as an unexpected
binary frame again, and tell a decrypt failure apart from a corrupt
compressed frame, instead of calling every failure undecryptable.
2026-11-07 18:27:58 -08:00
OrcaWinandOrcaWin 4243da77d5 test: remove 414 low-value tests (#39539)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-07 08:41:24 -08:00
OrcaWinandOrcaWin cecfa9865d test: remove 158 low-value tests (#39483)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-07 04:18:13 -08:00
OrcaWinandOrcaWin c8ed5c6be4 test: remove 295 low-value tests (#39480)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-07 04:03:51 -08:00
OrcaWinandOrcaWin a523f1d4c5 test: remove 32 low-value shared helper tests (#39475)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-07 03:20:35 -08:00
OrcaWinandOrcaWin 67c9d04ee4 test: remove 131 low-value terminal, session and transport tests (#39460)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-07 02:55:40 -08:00
Neil aed52aaa9b perf: stop host-status pings rebuilding title catalog and rereading server config (#39340)
* perf: stop host-status pings rebuilding the title catalog and rereading server config

Host-status updates walked every saved tab for title-refresh support before checking whether any host's support changed, and remote RPC/tunnel/worker checks reread, rehardened and reparsed the saved-server registry files on every call. Gate the title walk on an actual per-host capability flip, and reuse the merged registry read while a stat signature of its three files is unchanged.

* perf: invalidate registry read cache on in-process writes

Coarse-timestamp filesystems can reuse an inode within one tick, so a same-size rewrite could slip past the stat signature; writers in this process now bump a generation that is part of the signature.

* perf: key the registry read cache on file bytes, not stat metadata

A stat signature can miss a same-size rewrite on coarse-timestamp filesystems or a reused inode. Comparing the files' exact bytes (plus mode/ctime and the directory mode, which the uncached read re-hardens) means a hit can never serve data that differs from disk, from any writer.

* perf: only reuse a registry read once hardening holds and the path is unchanged

Reuse now requires the files' permissions to already be restricted (POSIX), so a transient chmod failure is retried on the next read as before; Windows hits keep the ACL verify-and-retry. Snapshots also record each path entry's identity, so a symlink re-pointed and restored mid-read cannot pin the value read through the other target.

* perf: build the cached registry value from the exact bytes it is keyed on

The value was computed by a second, independent read, so a directory or symlink swapped and restored around it could pin data the key's bytes never held. Each reader is split into read + parse-text, and on a miss the cache hardens, snapshots once, and parses that snapshot. Reuse also now requires the exact 0600/0700 modes the readers set, so an incomplete owner mode is repaired as before.

* perf: require the full hardened mode, including special bits, before reusing a registry read

* perf: keep Windows on the plain registry reader

Measured on a Windows host: a cache hit still has to re-verify ACLs, so it cost as much as the read it skipped (0.37 ms vs 0.33 ms). Windows now always uses the original reader, keeping its behavior byte-for-byte unchanged.
2026-11-07 09:56:47 +00:00
Neil e1379b34f3 feat(pairing): Add server and orca environment add exchange the link for their own token (#38948)
* feat(pairing): Add server and orca environment add exchange the link for their own token

After verifying the host, Add server (and repair) swap the access link's
token for one generated on this computer when the host advertises
pairing.ticket-exchange.v1, and save that instead. orca environment add does
the same with a short best-effort probe. Any failure saves the link as today;
the host keeps accepting it until the new token is used.

* test(pairing): Add server and CLI leave links unchanged for a released host

* fix(pairing): Add server and orca environment add sign in once after exchanging, and the CLI rejects a refused code

After exchanging a link, sign in once with the new token so the host
retires the link right away instead of at the first later use; a host
downgraded in between would not know the new token.

orca environment add no longer saves a pairing code the host refuses
(for example one another device already used); it asks for a new code.
2026-11-07 01:48:16 -08:00
OrcaWinandOrcaWin 95fadd01ba test: remove 321 low-value main and shared tests (#39429)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-07 01:37:39 -08:00
Neil 896c43564c fix(pairing): keep signed-in devices when the first-seen save fails; report revoked devices on downgraded latched hosts (#39337)
- Device registry: if saving a device's first sign-in fails, keep the sign-in in
  memory and retry the save every minute until it lands. Before, a restart after
  the pairing code's expiry treated the device as an unused, expired code and
  refused and removed it.
- Remote runtime client: when a host remembered as E2EE v2 refuses v2 and the
  legacy status check gets a sealed "unauthorized" reply, report that refusal
  instead of "update Orca on that host" with close code 4001, which managed
  orcad read as a different server on its port.
2026-11-07 00:21:24 -08:00
Neil b0a4b33908 feat(pairing): let clients exchange a pairing code for their own token (#38937)
* feat(pairing): let clients exchange a pairing code for their own token

New offers are marked as tickets. A client signed in with the pairing token
can call pairing.exchangeTicket with a token it generated; the host stores it
durably and keeps accepting the pairing token until the client first signs in
with the new one. That sign-in retires the pairing token and cuts sockets
still using it. Advertised as pairing.ticket-exchange.v1; clients that never
call it keep working exactly as before.

* fix(pairing): never retire a code other clients may already rely on

Exchange is offered only within 10 minutes of a ticket's first sign-in;
after that the code may be in use by several older clients and stays a
plain token. The first client to propose keeps its claim: a different
proposal is refused (that client keeps the code) instead of displacing it,
so two clients pairing at once can't lock out the first. Adds cross-version
tests against the pinned release: downgrade keeps exchanged and unexchanged
phones working, rows from an older host are never single-use, and the older
host doesn't advertise the exchange.
2026-11-06 23:57:05 -08:00
Kelvin Amoaba bd88cdeca8 perf(mobile): trim the page bundle back under its old budget (#39182)
* perf(mobile): trim the page bundle back under its old budget

* build(mobile): simplify the route entry anchors and zod table guard

* fix(mobile): import zod as a type in the RPC result reader
2026-11-07 03:28:04 +00:00
Neil 0efe201fbf feat(remote-runtime): use E2EE v2 for desktop and CLI connections to remote runtimes (#39218)
* feat(remote-runtime): use E2EE v2 for desktop and CLI connections to remote runtimes

Desktop connections to remote runtimes (orca serve, orcad, managed SSH,
browser bridge) and the orca CLI now use E2EE v2 framing for hosts that
support it, and fall back to the legacy framing for hosts that do not.

- One-shot requests try v2 first. A host that refuses the v2 hello
  (hosts from before v2) or refuses capabilities on v2 token auth gets
  one transparent legacy redial; nothing but the handshake is sent before
  authentication, so the redial is safe.
- After a v2 sign-in succeeds, the host's pinned key is remembered in the
  profile (shared by the app and the CLI). Every connection to that host,
  including long-lived sockets, then uses v2 and never falls back.
- Long-lived sockets keep legacy framing until a host has proven v2, so
  they never see an older host refuse a v2 hello.
- Hosts accept clientCapabilities on direct v2 token auth, so desktop and
  CLI clients keep their capability negotiation on v2. Relay token auth
  stays exact.

* feat(remote-runtime): recover a latched host that was downgraded, without a legacy downgrade path

A host this profile had signed in to over v2 was refused for good if it
was later downgraded below capability-carrying v2 token auth. Hosts now
advertise pairing.e2ee-v2-token-capabilities.v1. When a latched host
refuses v2, a one-shot request asks the host over legacy framing (keyed
to its pinned key, so only the real host can answer) for status.get:

- no capability: the host itself says it was downgraded; the latch is
  dropped and the request goes ahead over legacy;
- capability present, or no answer: the request fails with an error
  that says to update the host or check the network path. Only the
  read-only status check ever ran over legacy.

A refusal faked on the path cannot produce the host's answer, so it
cannot force legacy framing for a latched host.
2026-11-06 17:14:56 -08:00
047e113783 feat(cli): pair clients with a running Orca via orca pair (#36469)
* feat(cli): pair clients with a running Orca via `orca pair`

A headless Orca could only print a pairing offer at startup, so pairing a
phone with `orca serve service install` meant stopping the service. `orca
pair` asks the running runtime for an offer over the owner's local socket
through a new `pairing.createOffer` method (pairing-admin, never granted to
paired clients or phones). It reuses the desktop's offer logic, now shared
by the Settings IPC handlers: mobile offers stay LAN-only unless --relay,
and the first network pairing widens a loopback bind as the desktop does.

Also honor an explicit ORCA_SERVE_RUNTIME=orcad on packaged macOS. Electron
serve needs a desktop login and aborts in an SSH session with nobody logged
in; orcad is plain Node and serves there, at the cost of no in-app updater.

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

* refactor(cli): move the serve-restore and pair handler groups out of the manifest

handler-group-manifest.ts was already at the 300-line limit on main, so
adding the pair group failed lint. Group the two serve/pairing entries in
their own file, as jira, linear and reference already are.

* test(mobile): pin the desktop pairing IPC handlers' replies and runtime calls

Characterizes mobile:getPairingQR and mobile:getRuntimePairingUrl across
every branch (explicit and default address, LAN-only with no address,
Relay without an address, relay failures, this-computer reach, widening
and its failure, unavailable offers). The same file passes unchanged on
main and on this branch, so moving the offer logic into
pairing-offer-requests.ts did not change the desktop path.

* test(mobile): type the QR stub instead of casting it

---------

Co-authored-by: Office exitnote <h@Offices-MBP.lan>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>
2026-11-06 17:01:32 -08:00
Kelvin Amoaba ef3d9bbdc2 perf(mobile): seal and decode socket frames through native bindings (#39053)
* perf(mobile): seal and decode socket frames through native bindings

* refactor(mobile): decode base64 socket messages with the canonical primitive

* refactor(mobile): share the canonical base64 decoder with socket payloads
2026-11-06 18:11:41 +00:00
Neil 0314da7ed8 refactor(main): group RPC and orchestration modules by owner (#38813) 2026-11-06 08:21:33 -08:00
Neil 0f6448adf6 fix(remote): let Remove drop a managed server saved under two SSH aliases (#39048)
* fix(remote): let Remove drop a managed server saved under two SSH aliases

When one managed SSH server was saved twice, Settings said to remove one
entry, but Remove refused any pairing that has a managed deployment. When
another saved entry still manages the same server (same host key), Remove now
unlinks this entry through the managed forget path: it closes this entry's
tunnel and releases its SSH claims without contacting the host, so the server,
its projects and live terminals stay with the other entry. A managed server
with no other managing entry keeps the guard.

Fixes #38604

* fix(remote): recheck the managed twin inside the lifecycle queue

Codex review: a Remove queued behind another lifecycle action could forget
the last managed entry if the twin was removed meanwhile, and the twin match
compared raw Base64 text. Removal now goes through a forgetRedundant action
that re-finds the twin inside the target's lifecycle queue and unlinks in the
same tick, and matches host keys with the canonical matcher.

Refs #38604

* test: cover redundant managed removal after the dispatch suite was removed

Refs #38604
2026-11-06 06:53:14 -08:00
Neilandggbdpq a53c00219d fix(workspace): wake a sleeping recipe VM when a client opens its workspace (#39038)
A recipe VM workspace runs on its VM's own Orca server, so a phone, the CLI or
the desktop reaching it while the VM slept dialed a server that was not there
and failed. Only the host window's click ran the recipe's resume.

- The VM wake owner (WorkspaceSleepCoordinator) tracks the VM operations it
  was asked for and answers where the VM stands: a pending wake or a running
  suspend outranks the stored record.
- The host holds a request that opens the workspace (worktree.activate,
  defined once in shared code) until the VM is up: three minutes for the
  desktop and the CLI, and for a phone only as long as still lets the relay
  connect inside the phone's 30-second deadline. A VM still up keeps a sleep
  on its way from suspending it; a VM asleep or suspending wakes after what is
  running, joining a wake already pending. The open answers waking only when
  the hold runs out, wake_failed when the wake fails, and asleep when a Sleep
  asked for while it was held won; that open then wakes nothing more. Other
  requests answer waking or asleep instead of dialing nothing. A Sleep asked
  for during a wake keeps its place in the lifecycle queue and starts once the
  wake is done. A caller that gives up stops waiting and frees its queue
  place; the wake goes on.
- An open of a server the user disconnected is refused before anything wakes
  its VM, and cancelled if they disconnect while it waits. A wake carries the
  SSH host's user hold from when it was accepted and every Disconnect since,
  so its connect never lifts a hold; the VM still resumes and its new SSH
  address is saved for the user's own Connect.
- A wake re-pairs a paired VM's server. The request that waited for it is
  held to its caller's pairing as of before the wake (a stale caller is
  refused before it can wake anything), then to exactly the pairing revision
  the resume produced (including a re-pair its SSH reconnect made), whatever
  process now serves it; any other change still
  refuses it. A phone link still preparing for an open survives only the
  re-pair its own wake made; any other retirement, or a user Disconnect, ends
  it, and a link preparing for anything else is retired at once.
- The hold lives in the desktop's environment preparation, which its own calls
  and subscriptions, the phone relay and the CLI all go through. A phone open
  on a link to a server whose VM is not up gets a fresh link; the old link's
  requests are answered waking or asleep. Further opens join a link already
  waiting on the wake. A phone link's whole open (server preparation with any
  hold, grant sync and the socket) shares one deadline inside the phone's
  own, for every server.
- The CLI dials a VM directly, so `orca worktree activate` asks the desktop
  (runtimeEnvironment.wakeRecipeVm) to open the VM and reads the server's
  pairing again; the refusals (including runtime_manually_disconnected) keep
  their codes over RPC, and a host without recipe VMs answers that one RPC
  with method_not_found. Hosts that do this advertise
  host.recipe-vm-wake.v1.
- A workspace this host runs on a recipe VM over its SSH target wakes on this
  host's own activation, through the resume's explicit connect
  (wakeWorkspaceVm -> resumeVm -> connectRuntimeOwnedSshTarget). That connect
  runs whenever the VM is in SSH mode, even when its recipe had no resume step,
  since suspend tore down the transport.

Fixes #36923

Co-authored-by: ggbdpq <ggbdpq@gmail.com>
2026-11-06 06:51:09 -08:00
Neil 55697aec91 fix(cli): keep remote read-only commands working when environment usage can't be recorded (#39052)
* fix(cli): keep read-only commands working when environment usage can't be recorded

The CLI recorded each environment's last-used time with a strict store write,
so a profile whose directory refuses chmod (EPERM) failed commands whose reply
had already arrived. The desktop already skipped such failures in one path.
Move that best-effort recorder to shared code and route every desktop and CLI
usage write through it.

Fixes #38673

* test: assert the usage write failure, not call counts

Refs #38673

* test: skip the chmod EPERM case on Windows and drop a cast

Refs #38673
2026-11-06 05:53:10 -08:00
Neil 5159aff2e2 fix(ssh): say when a managed host now serves a different Orca server, and drop the Tailscale hint on SSH tunnels (#38745)
A saved pairing refused by this host's own server (it now publishes a different key, e.g. after
the SSH User changed) now says so, names the exact recovery (Forget the server, then Connect the
host), and stops retrying, instead of blaming another Orca on the port. Servers reached through
an SSH tunnel are their own host kind, so connection failures there no longer suggest Tailscale.

Refs #37766
2026-11-05 18:29:50 -08:00
NeilandBlue Thunder Somogyi 66695b1e9d Route federation ack and import over the shared control connection (#38735)
* fix(runtime-environments): route federation ack and import over shared control

The one-second federation sync pulls over the retained shared-control
connection but sent orchestration.federationAck and
orchestration.federationImport on a fresh one-shot socket, closed on the
main thread after every tick that carries progress. On a Mac whose
network content filter holds each TCP close for about a second, that
close froze keystroke relay for the whole app.

Allowlist the two relay mutations in shouldUseSharedControlEnvelope so
they take the same support-routed path as federationPull, including the
one-shot fallback for hosts without shared control. Both handlers are
synchronous and idempotent by request id and cursor. Long-running
mutations such as orchestration.workerStart stay one-shot, and
ORCHESTRATION_MUTATION_METHODS is unchanged.

* test(runtime-environments): pin federation relay transport to shared control

Cover the transport of one federation sync tick so a change that
reintroduces a per-tick one-shot socket fails a test:

- a pull, ack and import for one dispatch open no one-shot socket beyond
  the initial status.get when the host advertises shared control, and
  keep all three on one-shot sockets when it does not
- federationAck and federationImport fall back to one-shot with their
  envelope intact when shared control is unavailable
- shouldUseSharedControlEnvelope shares control for the relay reads and
  the two allowlisted mutations, and keeps orchestration.workerStart and
  other mutations on one-shot

* fix(orchestration): prove the worker server contract once per host incarnation

Every federation mutation without caller-proven contract first fetched
status.get to check the orchestration contract capability, then sent the
mutation as a separate call. The contract is now proven once per host
incarnation (runtime id) and each later mutation is pinned to that
incarnation through the existing expectedRuntimeId routing. A restarted host
answers runtime_environment_changed before any effect; the new incarnation is
proved and the mutation resent. A downgraded restart without the contract
receives no mutation, closing the window the old probe left between its check
and the send.

Fixes #37516

* fix(remote-runtime): honour request preparation on shared control

Federation import selects the prefix of its relay batch that fits the
host's inbound request bound through prepareRequest. That preparation was
only applied on one-shot sockets, so once import moved to shared control
its full batch was sent unprepared. Shared-control requests now serialize
through the same preparation step as one-shot requests, and every route an
import can take (shared or one-shot, pinned to a runtime or not) carries it.
The orchestration transport takes prepareRequest and expectedRuntimeId as
one send-options object.

Fixes #37516

---------

Co-authored-by: Blue Thunder Somogyi <bluethunder.somogyi@bastion.com>
2026-11-05 18:19:16 -08:00
Jinwoo Hong a9ba59c944 Nest workspaces under parents on other machines (#38740)
* feat(lineage): store cross-store parent links beside same-store lineage (inert)

A workspace's parent can live in another Orca store. Save that link in the
child's store under a new top-level key, crossStoreLineageByChildKey, so older
builds' same-store cleanup never reads it. Nothing writes records yet.

- Types, load normalization (malformed entries dropped one by one), section label.
- Store get/set/remove by child; a child keeps at most one parent record.
- Every maintenance path acts by the child only: deletion and missing-worktree
  pruning, repo removal and deregistered-repo residue, identity migration,
  profile project transfer, WSL move projection and UNC drive conversion, and
  the orcad migration dormant payload (export, validation, import, subtraction).

* fix(lineage): gate cross-store links on host capability; one parent per child

- Migration stage/commit carrying crossStoreLineage requires the new
  orcad.migration-cross-store-lineage.v1 capability; older hosts are refused
  before staging instead of silently dropping the links. Empty arrays are omitted.
- Payload validation and destination prepare reject a child that has both a
  cross-store and a same-store parent, through one shared conflict check.
- Saved entries stay on disk as written; the map is Record<string, unknown> and
  readers go through validCrossStoreLineage. Re-keying moves unreadable entries unchanged.
- Tests: literal expectations; drop touched mock-call and work-count assertions.

* test(lineage): drop clone and save work-count assertions from project group removal test

* feat(lineage): resolve cross-store parents into the sidebar parent map (inert)

- One resolver: a saved record nests its child under the single visible
  row whose store reports the parent's installationId and whose instance
  matches; copied installs, unknown stores and stale instances stay standalone.
- getSidebarLineageParentsByIdentity merges resolved cross-store parents with
  same-store edges before cycle breaking; a valid same-store edge wins.
- Workspace delete collects descendants only from the parent's own store.
- Store identity: paired stores from their verified status hostDescriptor;
  this store from the published descriptor, else host-installation.json.
- Nothing passes a view or writes records yet, so the sidebar is unchanged.

* fix(lineage): import WorkspaceKey from workspace-scope after main move

* fix(lineage): resolve sidebar parents once over complete rows; keep unowned rows' delete rule

- resolveSidebarLineageParentIdentities resolves same-store edges and cross-store
  records once over every visible store's unfiltered rows, then drops cycles.
  Ancestor injection and nesting reuse that map, so a filtered-out twin still
  makes a parent ambiguous.
- Removed, retired or refusing paired servers leave the view: their records stay
  saved and their children standalone. Offline servers keep their last-known id.
- A row with no owner is unknown, not ours: it never resolves, and delete only
  excludes descendants whose store is known and differs from the parent's.

* fix(lineage): refuse UNC rekey collisions and transfers giving a child two parents

The UNC drive conversion rekeys cross-store child keys through the shared
collision merge, so a UNC key and its drive key with different records throw
instead of one silently overwriting the other. Profile project transfer runs the
shared child-key conflict check before writing the target.

* feat(lineage): write, carry and nest cross-machine parents

- Capability worktree.lineage.crossMachine on the child's store; the CLI
  refuses before any request when the server lacks it.
- worktree.set / worktree.create accept crossStoreParent
  { installationId, workspaceKey, instanceId } and write only
  crossStoreLineageByChildKey (refused when it names this store, or the
  child has no identity). Remove Parent clears both.
- worktree.crossStoreParentRef: the parent's own store names it with its
  installationId; reparent refuses a parent whose chain already reaches
  another store. Same-store Set Parent refuses the same.
- worktree.ps rows and lineage lists carry the records; same-store lineage
  fields stay null so older readers show the child standalone.
- The desktop sidebar resolves parents over its own and paired stores'
  records (PR 2's resolver) at the visible-worktree and row-plan call sites.
- CLI: active/current or an inferred terminal parent against a server is
  sent as a cross-store parent.

* fix(lineage): read worktree rows through the record selector; mark the test runtime cast

* fix(lineage): hydrate every store, fence refreshes, honest CLI fallback

- Each host's catalog hydration fetches its lineage snapshot, so this
  desktop's identity and records load whichever store is focused.
- Cross-store records join the per-child request-start fence: a stale
  lineage list can't restore a removed record or override a newer one.
- A failed inferred cross-store parent sends noParent instead of
  forwarding the selector a server could match to a same-store twin.
- Inferred cross-store creates record env-workspace/inferred capture.
- The writer states its trust contract; the chain rule runs at the mint.

* feat(lineage): cross-machine parents on the phone, pickers, chat resume and labels

- Phone: the list nests a child under a parent on another machine, from the
  records rows already carry and the store identities its host verified
  (status.get descriptor; relayed servers' installationId on the host list).
- Set Parent popover, context menu and the composer's parent picker offer
  parents in other visible stores. The parent's own store names it
  (worktree.crossStoreParentRef); the window checks it against that store's
  verified installationId and the child store's capability before any write.
  Remove Parent and Open parent cover cross-machine links.
- Native chat resume ancestors follow resolved cross-machine parents.
- Host sections keep a nested child under its parent and keep its own
  machine label.
- Main sidebar feed resolves cross-store parents (rows carry records; desktop
  facts and paired-host facts carry identities); paired web windows read
  their host's installationId; reveal without an occurrence plan resolves
  them; `worktree create --agent` names a parent on another machine.

* test(lineage): type the mint test's parent row through unknown

* test(lineage): give the cross-machine tests' partial-row casts their SAFETY reasons

* fix(cli): worktree set names a parent on another machine

* feat(lineage): name the machine of parent candidates on another machine

* fix(lineage): Set Parent and Remove Parent act on the clicked row's own host

* fix(lineage): phone resolves parents over every row before the repo filter

* fix(lineage): chat resume nests by host-qualified identity across machines

* fix(lineage): local create refuses a same-install parent before checkout and shares the RPC recorder

* fix(lineage): name parent machines without a focus read; keep create's probes synchronous

* fix(lineage): chat resume keys offers by their own machine; only cross-machine parents leave a section

* fix(lineage): name an older parent server before asking it for a cross-machine ref

* fix(lineage): an app-picked parent on another machine records as manual

* fix(lineage): chat resume keeps base grouping; only resolved cross-store parents move a chat

* revert(lineage): drop chat resume from cross-machine parents; it groups by machine as before

* refactor(lineage): CLI builds cross-machine parents; explicit only

- Drop worktree.crossStoreParentRef and its reparent chain walk: the CLI
  builds the ref from this desktop's installationId (worktree.lineageList
  now reports it) and the parent row it reads. Cross-machine chains are
  allowed; same-store cycle validation and same-install refusal stay.
- No inferred cross-machine parent: remove crossStoreParentInferred and
  the fallback states. Against a server, this machine's terminal, cwd and
  env context is never sent; the CLI prints how to nest explicitly.

* refactor(lineage): build cross-machine parent refs from verified ids; pickers hide only loops; one desktop install id source

* refactor(lineage): refuse moving a linked workspace instead of carrying the link

Orcad migration and profile project transfer now refuse, before any staging or
write, when a workspace in the move has a cross-store parent record (raw entries,
readable or not). Remove the carry machinery: the dormant crossStoreLineage
field, its runtime capability and destination gate, import/subtract/claim code,
transfer carry and the two-parent conflict checker. Also drop the unused
removeCrossStoreWorkspaceLineage.

* docs(lineage): describe cross-store parent refs without the removed mint RPC

* test(lineage): drop the RPC call-list assertion from the cross-store ref test

* fix(lineage): refuse linked moves before any write and for shared-repo children

- Migration refusal passes the child's own host stamp to the scope matcher and
  refuses an unstamped key whose shared repo id the move includes, so such a
  child can't move while its link stays behind.
- Profile transfer checks the read-only source snapshot before pending-move
  recovery or storage migration; the payload check stays as the final guard.

* fix(lineage): refuse a linked profile move before the profile lookup can restore the index

* fix(lineage): startup hydration of other stores loads only cross-machine links

* fix(lineage): list the child's own machine first in the parent picker

* fix(lineage): keep profile argument errors ahead of the linked-move preflight

* fix(worktrees): let the first listing replace a restored server workspace stub

A restored active server workspace starts as a session stub with no head.
If git status fills in its branch before the first listing lands, the
listing merge took that as a concurrent branch switch and kept the stub,
dropping the catalog row (instance id, lineage, provenance). The workspace
then showed standalone until the next listing.

* fix(worktrees): accept a headless row's listing only when it confirms git status

A stale listing could otherwise roll back a branch switch git status saw on
an empty-head row (e.g. an SSH fallback row) during the first connected listing.

* feat(lineage): CLI nests a create on another machine under the caller workspace

- No parent flag and no --no-parent against a server: the caller's workspace
  on this machine (env, else cwd) is sent as an inferred cross-store parent,
  the way --parent-worktree active builds it. The host records it as
  inferred from environment or cwd.
- A server without worktree.lineage.crossMachine, or a caller workspace that
  can't be linked, still creates unnested and prints one note.
- An inferred parent falls away for folder targets, as same-machine folder
  creates record none.

* fix(lineage): CLI cross-machine inference reads the caller terminal and suppresses fallbacks

- The caller terminal's workspace joins env and cwd as an inferred parent,
  with same-machine precedence: env and terminal must agree, cwd only alone.
- A fallback (old server, unlinkable or unresolved caller, ambiguous cwd)
  prints one note and sends noParent, so the server's own create-time
  inference can't nest what the CLI reported as not nested.

* feat(sidebar): drag a workspace onto one on another machine to nest it

Dropping a workspace on a workspace from another machine now sets the
parent through the same cross-machine path as Set Parent: same eligibility
(loops, older servers), same refusal messages. Drop targets carry host
identity so the child's own copy is the one written.

* refactor(lineage): this machine's Orca resolves the caller parent for a create on another machine

- New read-only worktree.resolveCallerParent runs the same-machine create
  resolver over the caller's env, terminal and cwd context, so stale
  contexts are skipped and only validated parents can conflict, and returns
  the cross-store ref or a reason.
- The CLI's own precedence and terminal lookup are removed; it keeps the
  capability gate, one note with noParent on any fallback, and silence with
  no Orca context.

* feat(orchestration): remote worker-start --worktree new-child nests under the coordinator's workspace

worker-start --on <server> --worktree new-child creates the server's worktree
under the coordinator's workspace on this machine, using the same verified
cross-machine parent ref as the CLI's caller auto-nest. A server without
cross-machine parents still starts the worker, top-level, with a warning in
the receipt. Guide, CLI help and docs describe it.

* fix(lineage): resolveCallerParent reads this store's identity without creating it

- Uses the published host descriptor's installationId, else the existing
  host-installation.json; never creates or repairs it. Missing identity
  returns a reason, so the CLI creates unnested with its note.
- Drops the read-only wording: parent resolution does the same catalog
  upkeep a same-machine create does.

* fix(sidebar): validate every cross-machine parent before a multi-select drop writes

A drop of several workspaces builds every cross-machine parent ref first, so an
older server's refusal leaves the whole selection unchanged. Drops the touched
preview-timing test cases.

* refactor(orchestration): build the remote new-child parent ref with the shared caller resolver

Orchestration still picks the caller workspace; resolveCallerParentForAnotherStore
verifies it and builds the ref, and both read the installationId the same way.
Drops the copy and mock-call tests.

* fix(lineage): narrow the inferred source with a guard instead of casts

* fix(lineage): a composer parent picked on another machine nests normally once the destination is that machine

The retained host-qualified pick is classified against the current destination:
same host gives the ordinary parent fields, another store the cross-machine ref.
Also corrects two stale comments.

* perf(lineage): read only the requested child's cross-store entry

* perf(lineage): request each store's lineage list once at startup

* perf(lineage): cap optional caller-parent inference at 3 s and skip a timed-out local Orca

* perf(lineage): return cached sidebar parents when selector inputs are unchanged

* fix(lineage): snapshot selector inputs without a type assertion

* Route startup lineage and the sidebar lineage memo through their owners
2026-11-05 19:42:19 -05:00
Neil 791bbfbfde fix(remote-runtime): retire a shared-control socket that opens but never becomes ready (#38690)
The shared-control connection now bounds OPEN to ready with a 30s deadline
owned by its readiness timer. When it elapses, the generation closes with a
ready_timeout cause and reconnects through the existing backoff, which only
resets after 30s of stable readiness, so a slow host cannot cause a storm.

Fixes #37330
2026-11-05 14:49:54 -08:00
Brennan Benson 799b60b074 Keep orchestration messages tied to their actual agent identity (#37966)
* Show background completion mail as its command source

* Use minted sender handles in identity regression fixtures

* Verify command source labels in translated chat views

* Preserve command-only queued source regression coverage

* Use a minted terminal identity in host sender coverage

* Reuse command sender labels across unchanged message and queue reads

* Keep sender performance contract within the shared typecheck boundary

* Keep command reports in the background surface and preserve observed sender identity

* Use the shipped phone background report translation namespace

* Settle the prior dispatch before testing terminal reuse

* Keep live worker status tied to its recorded agent invocation

* Withdraw worker invocation proof when identity refresh fails

* Drop unproved worker associations at the failed refresh boundary

* Cover ordinary and remote sender rows during failed refresh

* Assert preserved session and peer content on failed sender reads

* Align sender navigation and relay tests with observed provenance

* Keep background report sources neutral and bound sender snapshot caches

* Construct readonly phone report fixture without mutation

* Keep callback-free federated transport assertions exact

* Keep orchestration sender identity separate from endpoint activity

* Validate mailbox receipt fixture shapes without casts

* Keep current Crew endpoint status separate from saved message activity

* Keep sender activity fixtures within lint contracts

* Avoid repeated serialization for fitting relay pages

* Use the valid workspace sidebar value in scale coverage

* Keep operation-count SQL expectation in sync with main
2026-11-05 11:00:15 -08:00
OrcaWinandOrcaWin 25cd81665e test: remove 675 low-value test declarations (#38129)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-04 04:51:57 -08:00
OrcaWinandOrcaWin 25994e0b94 test: remove 1460 low-value test declarations (#38082)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-04 00:25:44 -08:00
Neil 7bf0377072 refactor: place terminal and workspace contracts with concrete owners (#38061) 2026-11-03 22:49:34 -08:00
Neil d09519f0f1 refactor: group workspace and runtime contracts by defining owner (#38018) 2026-11-03 20:19:21 -08:00
Neil 926f8bad13 refactor: organize terminal and runtime contract owners (#38011) 2026-11-03 19:56:34 -08:00
Neil da96fadec3 refactor: organize source-control contract owners (#37990) 2026-11-03 19:12:25 -08:00