* 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
* 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
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>
* 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
* 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
* 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
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
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.
* 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
* 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
* 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
* 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.
* 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.
* 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.
- 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.
* 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.
* 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
* 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.
* 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>
* 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
* 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
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>
* 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
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
* 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>
* 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
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
* 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