Commit Graph
1787 Commits
Author SHA1 Message Date
OrcaWinandm4air bd54fc7fa3 refactor(renderer): delete the direct-SSH authority and reconnect coordinator (#27587)
No ssh: git provider is registered in production, so the reconnect
coordinator's catalog and lineage reads always came back non-authoritative
and its worktree refresh never merged. Its only live effect was clearing an
unconverted target's stale PTY bindings on each connected push and on wake;
the state bridge now does that directly.

Deletes the coordinator, preparation, worktree refresh scheduler, host
hydration, rollout flag and their product telemetry event, the scoped
direct-SSH reconnect/hydration options, and the now caller-less
repos:listForExecutionHost and worktrees:listLineageForHost IPC handlers.

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 14:41:17 -07:00
OrcaWinandm4air 34e2e5f469 refactor(remote): clients connect through a paired-connection handle, not a pairing offer (#27533)
Desktop, CLI, web and runtime transports now take a PairedRuntimeConnection
(endpoint, pinned host key, credential) instead of a PairingOffer. Offers stay on
the pairing input path (codes, QR, links, environment add) and are converted with
pairedRuntimeConnectionFromOffer. The credential is token-only today; request
envelopes still carry the same deviceToken, so the wire is unchanged.

This lets device-key and ticket credentials replace the token later without
touching every connect call site again.

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 14:02:03 -07:00
Jinwoo Hong 9ad45591af refactor(layout): keep compatibility code in named folders behind one entry (#27556)
* refactor(layout): keep compatibility code in named folders behind one entry

Moves the old-client translators into src/shared/workspace-layout/old-client/
and the remote layout snapshot recorder and persistence into
src/main/remote-layout-snapshot/, each behind an index.ts. A new
check-compat-boundary-imports script, run by pnpm lint and the PR workflow,
fails when code outside a compatibility folder imports past its index or
when a file other than the folder's named importer imports it. Moves only;
no behaviour change.

* test: drop whole-repo scan from boundary check unit tests; CI step runs it

* fix(lint): list only existing compatibility folders; resolve renderer aliases in renderer files
2026-10-11 16:06:05 -04:00
Jinwoo HongandNeil e22fd9c901 feat(macos): show terminal-host Full Disk Access, tag stable-copy daemons, and give Helpers folder strings (#27354)
* fix(macos): preserve terminal privacy identity across app updates

* test(macos): make launchd fixtures portable across host platforms

* fix(macos): probe protected contents and close handles on denial

* fix(i18n): register terminal permission status labels

* fix(macos): fork the daemon when no stable-bundle job can claim the endpoint

A copy, signature, plist or deadline failure before bootstrap, a bootstrap
launchd refused with the job absent, a job whose daemon exited before
answering, and a foreign endpoint all threw out of the launcher. The app then
lost its persistent daemon and fell back to local PTYs, where main forks one.
These now fall through to the existing fork path, which also adopts a foreign
occupant normally instead of as a degraded endpoint. A timed-out bootstrap or
a job still running/unreadable stays fatal, so no second daemon races it.

* feat(telemetry): split daemon origin events by stable-copy launch method

daemon_adopted, daemon_pty_cwd_denied and daemon_pty_cwd_readable gain a
closed-enum launch_method (stable-copy / app-fork / unknown), read from the
spawner path the daemon recorded, so the update fix is measurable per daemon
even when an old forked daemon is adopted across updates. The reliability
gate is a test manifest with no runtime switch, so there is no gate state to
report; spawner_path_class reports the private copy as 'other' alongside
other non-/Applications installs.

* refactor(telemetry): report the stable copy as a spawner path class

The separate launch_method property duplicated what the recorded spawner
path already says. spawner_path_class gains `stable-copy`, classified from
the same path under the private runtime root, and launch_method, its enum
and classifier are removed from all three .strict() daemon origin events.
A deleted copy still reports `missing`.

* refactor(settings): gate terminal-host Full Disk Access on macOS once

The permissions handler already asks for the terminal-host status only on
macOS, so the status function's own platform check and the pane's
'unsupported' branch could never fire. The off-macOS guarantee is now
tested at the handler.

* fix(macos): keep the fork fallback inside the startup PTY gate

The stable launch could run until 55 s after the launch started, so a late
"unavailable" verdict left the fork fallback's own 10 s readiness wait to
finish after the 60 s gate had already moved the app to local PTYs.

The launcher now gives the stable attempt one handoff deadline: the gate
minus the fork's readiness timeout and the 5 s lease/adapter allowance
(45 s). Copy, signature check and plist conversion share one abort signal
that ends early enough for bootstrap and the job's own readiness wait to
meet that deadline; every command otherwise keeps a fixed timeout. An
unavailable verdict past the deadline is reported instead of forking a
daemon that cannot arrive in time. The per-step budget module is gone (its
deletion landed in the previous commit by a staging slip).

The launch body is now two blocks: preparation (plist, plutil, deadline
check) fails as unavailable after removing the copy and job file, and
bootstrap runs in its own try/finally that always deletes the job file.

Retirement is simpler and claims more orphans: one in-flight collection per
process instead of a per-root map and rotating 20-record scan, and the job
record (written before copying) no longer carries a bundle name because
lsof +D scans the whole runtime directory (measured equal cost, same
result on a live copy). A never-submitted copy is retired once its producer
has exited and its job is missing, so a bootstrap timeout or a crash
mid-copy no longer leaves a copy forever; the open-file check still guards
deletion. Collection now starts from the launcher after the attempt.

* docs(macos): ship the daemon launch rules, not the investigation

The signed investigation harness left the tree in the previous telemetry
commit by a staging slip; its results and pinned link stay in the PR. This
trims the reference doc to why the copy and launchd exist, the fallback
states and the cleanup rule, trims the reliability gate to what its
validator needs, and points AGENTS.md at the reference doc next to the
Windows relocation entry.

* fix(macos): hand off to the fork as soon as the stable job exits

A copied daemon refused at exec (for example by Gatekeeper) never answers,
so every fresh start paid the full 10 s readiness wait before the fork
fallback. The readiness loop now reads the job state every 500 ms and
abandons as soon as the job is stopped or missing; a running or
unreadable job is still waited out and never forked beside.

Also drops the dead forkEntryPath the stable launch never read (its
options type now omits it) and un-exports the handoff constant.

* fix(macos): keep daemon copies out of Spotlight and unregister retired ones

Each runtime copy is a full Orca.app, so Spotlight indexed it as an app
and LaunchServices kept a record for it that outlived the copy. A
registered copy claims orca: links and Markdown/CSV files, and becomes
their default handler whenever /Applications/Orca.app is not registered.

New copies go under runtime-*/app.noindex/, which Spotlight skips and which
stops the record LaunchServices made while the bundle was being copied.
Retirement now runs lsregister -u on every bundle in the copy (both the
app.noindex layout and the earlier runtime-*/Orca.app layout) once lsof
shows it unused, then deletes it. A running copy stays registered,
because Local Network access may be resolved through that record.

Collection and stable-copy telemetry match the runtime-* directory, so
copies made before this change are still adopted, classified and retired.

* docs(macos): record the LaunchServices trade-off and the rollback plan

The reference doc now explains why copies sit in a .noindex folder, why
only retired copies are unregistered, and how to turn the copy launch off:
make launchMacDaemonFromStableBundle return null, and keep the retirement
code until no copy-launched daemon can remain, because the old code never
deletes copies.

A test pins that rollback: with the stable launcher returning null, the
launcher forks and still runs the copy collection.

* fix(macos): keep daemon copies registered so Local Network can resolve them

The `.noindex` folder kept copies out of Spotlight and LaunchServices, but
Local Network access is resolved from the signing identifier through
LaunchServices to the executable's Mach-O UUID. After an update that changes
Electron only the copy carries the running daemon's UUID, so an unregistered
copy risks the same Local Network loss main has today. Copies go back to
`runtime-*/Orca.app`; retirement still unregisters each copy before deleting
it, so deleted copies leave no LaunchServices entry. The remaining trade-off:
while `/Applications/Orca.app` is missing, a running registered copy can be
picked for `orca:` links and documents.

* fix(macos): hide daemon copies from Spotlight and tie the job to Orca

Spotlight listed each runtime copy as a second Orca app. New copies go
back under runtime-*/app.noindex/Orca.app, which Spotlight skips.
Retirement and stable-copy telemetry match the runtime-* directory, so
copies in the earlier runtime-*/Orca.app layout still retire and classify
the same way.

Local Network access identifies the daemon by its code signature and
main-executable UUID, not by a LaunchServices record, so an unregistered
copy keeps it. The launchd job now sets AssociatedBundleIdentifiers to
com.stablyai.orca, which is how a job not installed through SMAppService
names the app it belongs to. Copies are never registered explicitly;
retirement still runs lsregister -u before deleting each one.

* feat(macos): package a signed terminal host helper that runs the daemon on the pinned Node

Contents/Helpers/Orca Terminal Host.app carries the pinned Node, the daemon's
out/main JS and this slice's node-pty, signed inside-out as com.stablyai.orca.
afterPack boots it through one PTY round trip, then fails the build on layout,
Info.plist, macOS floor (NODE_RUNTIME_DARWIN_MINIMUM_OS vs Node's minos),
architecture or signature drift.

* ci(macos): cache and prefetch the pinned Node before packaging

Packaging now fetches the darwin Node from nodejs.org, whose download has no
retry. Each mac workflow caches it with the electron-builder downloads, keyed on
the pin, and warms both darwin targets inside the existing retry wrapper.

* feat(macos): launch the daemon from a copy of the terminal host helper, not the whole app

The stable launch now clones Contents/Helpers/Orca Terminal Host.app (~124 MiB
instead of ~628 MiB) and runs its Node on its daemon entry, without
ELECTRON_RUN_AS_NODE. The copy must still match the running app's designated
requirement. A missing helper falls back to the fork, and macOS below the
helper's Node floor forks without copying.

* docs(macos): describe the terminal host helper, its floor and its identity

* fix(macos): bound the terminal host boot check by its deadline and always clean up

A daemon that stayed silent hung packaging past the 20 s deadline; one that closed the
connection left the hello unsettled so Node exited 0 without cleanup; a malformed line threw
uncaught. Every connect, hello and PTY wait now races the deadline, close/error/parse failures
reject, and the daemon and scratch dir are removed on every path.

* ci(macos): cache the pinned Node on its own key with no restore-keys fallback

Sharing the electron-builder cache let old Node archives accumulate through restore-keys.
Also moves release-mac-build's Electron cache comment back onto its step.

* fix(macos): reject a universal slice for the terminal host and pin its name in one test

packageMacTerminalHost turned an unknown arch into darwin-undefined; it now throws through the
shared mac arch mapping. A test asserts the packager and the desktop agree on the helper bundle
and executable names, replacing a Linux/Windows test that could not fail.

* fix(macos): pass the launchd adoption lease its options object after the main merge

* test(macos): accept a reset as the daemon closing the boot-check connection; keep only passing gate commands

* fix(daemon): bump the terminal daemon protocol to v45 so new macOS tabs start in the terminal host helper

* fix(macos): ship the daemon's required packages in the terminal host and boot it outside the build tree

* style(macos): use a template literal in the terminal host resolution check

* fix(macos): derive the terminal host's packages from the daemon's own module graph

* fix(macos): treat any non-literal daemon load as computed, follow directory loads, and keep nested dependencies

* refactor(macos): move terminal-host telemetry, Settings status and Helper folder strings to a follow-up

Keeps this change to the update-safe terminal host itself. The follow-up restores:
- the `stable-copy` spawner path class in daemon adoption telemetry;
- the per-terminal-host Full Disk Access row in Settings and its daemon RPC;
- writing folder usage descriptions into the app's Electron Helpers for the fork fallback.

The helper still takes its Desktop, Documents and Downloads descriptions from
`macos-folder-usage-descriptions.cjs`, so that constant stays.

* feat(macos): report terminal-host Full Disk Access, classify stable-copy daemons, and give Helpers folder strings

Restores what a32018836b moved out of the terminal host change:
- daemon adoption telemetry classifies a daemon spawned from the private copy as `stable-copy`;
- Settings shows the terminal hosts' own Full Disk Access verdict beside the app's, through a
  new `fullDiskAccessStatus` daemon request that older daemons answer as unknown;
- packaging writes the Desktop, Documents and Downloads descriptions into the app's Helper
  bundles, which still host terminals when the fork fallback runs.

* fix(i18n): restore en.json to this branch's base, not newer main

The previous commit took the file from a newer main, which pulled unrelated strings into this PR.

* refactor(macos): publish one terminal-host copy per helper build under a fixed launchd label

Name the copy after the helper's CDHash, reuse it when present, and
publish a new one by cloning into a staging dir, verifying it once
against the app's designated requirement and renaming it into place.
One launchd label per profile and protocol replaces per-launch labels,
so job.json, producer pids and per-label collection go away. Retirement
deletes any other copy lsof shows unused. The stable launcher returns
null for every fork fallback and logs its reason.

* fix(packaging): copy terminal-host dependencies from where Node resolves them

A nested package's own dependencies were never walked, so a hoisted
dependency of a nested package was left out of the helper. Resolve each
dependency through the node_modules lookup Node performs from its parent
and copy it to the same relative place, unless a copied package already
carries it.

* fix(packaging): fail release and CI mac builds that cannot boot a terminal-host slice

An arm64 runner without Rosetta skipped the x64 helper's PTY boot check
and shipped the slice untested. Release channels and CI now fail there;
local cross-arch builds still skip with a log line.

* docs(macos): describe the per-build terminal-host copy and its fallback reasons

* docs(macos): keep the codesign command on one line

* test(macos): type the launchd test state without an assertion

* docs(macos): name what the login-session flag gates in the stable launcher

* fix(macos): wait for launchd to drop a booted-out terminal job before reusing its label

* refactor(daemon): keep the health request types in their own module

types.ts hit the line limit once the Full Disk Access status request joined the union.

---------

Co-authored-by: Neil <neil@stably.ai>
2026-10-11 16:03:26 -04:00
OrcaWinandm4air b15c2c6dd6 refactor(persistence): retire the dead SSH PTY lease writers (#27555)
* test(persistence): seed relay-era SSH lease rows from disk

* refactor(persistence): retire the dead SSH PTY lease writers

* test(persistence): read relay-era recovery rows without a cast

* chore(ratchet): prune layout-writer baseline after lease writer removal

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 12:46:50 -07:00
Jinwoo HongandClaude 5e5865a226 refactor(layout): drop unused snapshot receipt fields and add the snapshot read method (#27536)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-11 15:20:21 -04:00
OrcaWinandm4air 2779dfeb2b refactor(pty): drop SSH spawn, liveness and kill branches (#27529)
* refactor(pty): drop SSH spawn, liveness and kill branches

* test(pty): drive the commit-durability tests through local PTYs only

* test(pty): drop the SSH host arm from the exit-durability fixture

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 12:05:22 -07:00
Jinwoo HongandNeil 3f8d296b8d fix(macos): preserve terminal permissions across app updates with a small signed helper (#27021)
* fix(macos): preserve terminal privacy identity across app updates

* test(macos): make launchd fixtures portable across host platforms

* fix(macos): probe protected contents and close handles on denial

* fix(i18n): register terminal permission status labels

* fix(macos): fork the daemon when no stable-bundle job can claim the endpoint

A copy, signature, plist or deadline failure before bootstrap, a bootstrap
launchd refused with the job absent, a job whose daemon exited before
answering, and a foreign endpoint all threw out of the launcher. The app then
lost its persistent daemon and fell back to local PTYs, where main forks one.
These now fall through to the existing fork path, which also adopts a foreign
occupant normally instead of as a degraded endpoint. A timed-out bootstrap or
a job still running/unreadable stays fatal, so no second daemon races it.

* feat(telemetry): split daemon origin events by stable-copy launch method

daemon_adopted, daemon_pty_cwd_denied and daemon_pty_cwd_readable gain a
closed-enum launch_method (stable-copy / app-fork / unknown), read from the
spawner path the daemon recorded, so the update fix is measurable per daemon
even when an old forked daemon is adopted across updates. The reliability
gate is a test manifest with no runtime switch, so there is no gate state to
report; spawner_path_class reports the private copy as 'other' alongside
other non-/Applications installs.

* refactor(telemetry): report the stable copy as a spawner path class

The separate launch_method property duplicated what the recorded spawner
path already says. spawner_path_class gains `stable-copy`, classified from
the same path under the private runtime root, and launch_method, its enum
and classifier are removed from all three .strict() daemon origin events.
A deleted copy still reports `missing`.

* refactor(settings): gate terminal-host Full Disk Access on macOS once

The permissions handler already asks for the terminal-host status only on
macOS, so the status function's own platform check and the pane's
'unsupported' branch could never fire. The off-macOS guarantee is now
tested at the handler.

* fix(macos): keep the fork fallback inside the startup PTY gate

The stable launch could run until 55 s after the launch started, so a late
"unavailable" verdict left the fork fallback's own 10 s readiness wait to
finish after the 60 s gate had already moved the app to local PTYs.

The launcher now gives the stable attempt one handoff deadline: the gate
minus the fork's readiness timeout and the 5 s lease/adapter allowance
(45 s). Copy, signature check and plist conversion share one abort signal
that ends early enough for bootstrap and the job's own readiness wait to
meet that deadline; every command otherwise keeps a fixed timeout. An
unavailable verdict past the deadline is reported instead of forking a
daemon that cannot arrive in time. The per-step budget module is gone (its
deletion landed in the previous commit by a staging slip).

The launch body is now two blocks: preparation (plist, plutil, deadline
check) fails as unavailable after removing the copy and job file, and
bootstrap runs in its own try/finally that always deletes the job file.

Retirement is simpler and claims more orphans: one in-flight collection per
process instead of a per-root map and rotating 20-record scan, and the job
record (written before copying) no longer carries a bundle name because
lsof +D scans the whole runtime directory (measured equal cost, same
result on a live copy). A never-submitted copy is retired once its producer
has exited and its job is missing, so a bootstrap timeout or a crash
mid-copy no longer leaves a copy forever; the open-file check still guards
deletion. Collection now starts from the launcher after the attempt.

* docs(macos): ship the daemon launch rules, not the investigation

The signed investigation harness left the tree in the previous telemetry
commit by a staging slip; its results and pinned link stay in the PR. This
trims the reference doc to why the copy and launchd exist, the fallback
states and the cleanup rule, trims the reliability gate to what its
validator needs, and points AGENTS.md at the reference doc next to the
Windows relocation entry.

* fix(macos): hand off to the fork as soon as the stable job exits

A copied daemon refused at exec (for example by Gatekeeper) never answers,
so every fresh start paid the full 10 s readiness wait before the fork
fallback. The readiness loop now reads the job state every 500 ms and
abandons as soon as the job is stopped or missing; a running or
unreadable job is still waited out and never forked beside.

Also drops the dead forkEntryPath the stable launch never read (its
options type now omits it) and un-exports the handoff constant.

* fix(macos): keep daemon copies out of Spotlight and unregister retired ones

Each runtime copy is a full Orca.app, so Spotlight indexed it as an app
and LaunchServices kept a record for it that outlived the copy. A
registered copy claims orca: links and Markdown/CSV files, and becomes
their default handler whenever /Applications/Orca.app is not registered.

New copies go under runtime-*/app.noindex/, which Spotlight skips and which
stops the record LaunchServices made while the bundle was being copied.
Retirement now runs lsregister -u on every bundle in the copy (both the
app.noindex layout and the earlier runtime-*/Orca.app layout) once lsof
shows it unused, then deletes it. A running copy stays registered,
because Local Network access may be resolved through that record.

Collection and stable-copy telemetry match the runtime-* directory, so
copies made before this change are still adopted, classified and retired.

* docs(macos): record the LaunchServices trade-off and the rollback plan

The reference doc now explains why copies sit in a .noindex folder, why
only retired copies are unregistered, and how to turn the copy launch off:
make launchMacDaemonFromStableBundle return null, and keep the retirement
code until no copy-launched daemon can remain, because the old code never
deletes copies.

A test pins that rollback: with the stable launcher returning null, the
launcher forks and still runs the copy collection.

* fix(macos): keep daemon copies registered so Local Network can resolve them

The `.noindex` folder kept copies out of Spotlight and LaunchServices, but
Local Network access is resolved from the signing identifier through
LaunchServices to the executable's Mach-O UUID. After an update that changes
Electron only the copy carries the running daemon's UUID, so an unregistered
copy risks the same Local Network loss main has today. Copies go back to
`runtime-*/Orca.app`; retirement still unregisters each copy before deleting
it, so deleted copies leave no LaunchServices entry. The remaining trade-off:
while `/Applications/Orca.app` is missing, a running registered copy can be
picked for `orca:` links and documents.

* fix(macos): hide daemon copies from Spotlight and tie the job to Orca

Spotlight listed each runtime copy as a second Orca app. New copies go
back under runtime-*/app.noindex/Orca.app, which Spotlight skips.
Retirement and stable-copy telemetry match the runtime-* directory, so
copies in the earlier runtime-*/Orca.app layout still retire and classify
the same way.

Local Network access identifies the daemon by its code signature and
main-executable UUID, not by a LaunchServices record, so an unregistered
copy keeps it. The launchd job now sets AssociatedBundleIdentifiers to
com.stablyai.orca, which is how a job not installed through SMAppService
names the app it belongs to. Copies are never registered explicitly;
retirement still runs lsregister -u before deleting each one.

* feat(macos): package a signed terminal host helper that runs the daemon on the pinned Node

Contents/Helpers/Orca Terminal Host.app carries the pinned Node, the daemon's
out/main JS and this slice's node-pty, signed inside-out as com.stablyai.orca.
afterPack boots it through one PTY round trip, then fails the build on layout,
Info.plist, macOS floor (NODE_RUNTIME_DARWIN_MINIMUM_OS vs Node's minos),
architecture or signature drift.

* ci(macos): cache and prefetch the pinned Node before packaging

Packaging now fetches the darwin Node from nodejs.org, whose download has no
retry. Each mac workflow caches it with the electron-builder downloads, keyed on
the pin, and warms both darwin targets inside the existing retry wrapper.

* feat(macos): launch the daemon from a copy of the terminal host helper, not the whole app

The stable launch now clones Contents/Helpers/Orca Terminal Host.app (~124 MiB
instead of ~628 MiB) and runs its Node on its daemon entry, without
ELECTRON_RUN_AS_NODE. The copy must still match the running app's designated
requirement. A missing helper falls back to the fork, and macOS below the
helper's Node floor forks without copying.

* docs(macos): describe the terminal host helper, its floor and its identity

* fix(macos): bound the terminal host boot check by its deadline and always clean up

A daemon that stayed silent hung packaging past the 20 s deadline; one that closed the
connection left the hello unsettled so Node exited 0 without cleanup; a malformed line threw
uncaught. Every connect, hello and PTY wait now races the deadline, close/error/parse failures
reject, and the daemon and scratch dir are removed on every path.

* ci(macos): cache the pinned Node on its own key with no restore-keys fallback

Sharing the electron-builder cache let old Node archives accumulate through restore-keys.
Also moves release-mac-build's Electron cache comment back onto its step.

* fix(macos): reject a universal slice for the terminal host and pin its name in one test

packageMacTerminalHost turned an unknown arch into darwin-undefined; it now throws through the
shared mac arch mapping. A test asserts the packager and the desktop agree on the helper bundle
and executable names, replacing a Linux/Windows test that could not fail.

* fix(macos): pass the launchd adoption lease its options object after the main merge

* test(macos): accept a reset as the daemon closing the boot-check connection; keep only passing gate commands

* fix(daemon): bump the terminal daemon protocol to v45 so new macOS tabs start in the terminal host helper

* fix(macos): ship the daemon's required packages in the terminal host and boot it outside the build tree

* style(macos): use a template literal in the terminal host resolution check

* fix(macos): derive the terminal host's packages from the daemon's own module graph

* fix(macos): treat any non-literal daemon load as computed, follow directory loads, and keep nested dependencies

* refactor(macos): move terminal-host telemetry, Settings status and Helper folder strings to a follow-up

Keeps this change to the update-safe terminal host itself. The follow-up restores:
- the `stable-copy` spawner path class in daemon adoption telemetry;
- the per-terminal-host Full Disk Access row in Settings and its daemon RPC;
- writing folder usage descriptions into the app's Electron Helpers for the fork fallback.

The helper still takes its Desktop, Documents and Downloads descriptions from
`macos-folder-usage-descriptions.cjs`, so that constant stays.

* fix(i18n): restore en.json to this branch's base, not newer main

The previous commit took the file from a newer main, which pulled unrelated strings into this PR.

* refactor(macos): publish one terminal-host copy per helper build under a fixed launchd label

Name the copy after the helper's CDHash, reuse it when present, and
publish a new one by cloning into a staging dir, verifying it once
against the app's designated requirement and renaming it into place.
One launchd label per profile and protocol replaces per-launch labels,
so job.json, producer pids and per-label collection go away. Retirement
deletes any other copy lsof shows unused. The stable launcher returns
null for every fork fallback and logs its reason.

* fix(packaging): copy terminal-host dependencies from where Node resolves them

A nested package's own dependencies were never walked, so a hoisted
dependency of a nested package was left out of the helper. Resolve each
dependency through the node_modules lookup Node performs from its parent
and copy it to the same relative place, unless a copied package already
carries it.

* fix(packaging): fail release and CI mac builds that cannot boot a terminal-host slice

An arm64 runner without Rosetta skipped the x64 helper's PTY boot check
and shipped the slice untested. Release channels and CI now fail there;
local cross-arch builds still skip with a log line.

* docs(macos): describe the per-build terminal-host copy and its fallback reasons

* docs(macos): keep the codesign command on one line

* test(macos): type the launchd test state without an assertion

* docs(macos): name what the login-session flag gates in the stable launcher

* fix(macos): wait for launchd to drop a booted-out terminal job before reusing its label

---------

Co-authored-by: Neil <neil@stably.ai>
2026-10-11 14:54:36 -04:00
OrcaWinandm4air 3359fd2bfc refactor(ipc): worktree handlers are local or runtime only (#27528)
* refactor(ipc): worktree handlers are local or runtime only

* refactor(runtime): explain the route witness on removal listing

* test(runtime): pin the SSH create refusal and drop the deleted branch's reliability gap

* test(runtime): drop a needless cast in the SSH create refusal test

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 11:48:57 -07:00
OrcaWinandm4air 0df25b7ac9 refactor(pty): delete relay-fed SSH output intake and projection threading (#27515)
Nothing in production feeds the SSH PTY output intake: its only data and exit
entry points were called from tests, and the SSH PTY provider that would have
fed it is gone. Delete the intake, its source-obligation ledger, model
admission, projection publication and source-ack plumbing, and strip the
projection ids and source-credit bookkeeping they threaded through the local
renderer delivery path (accept, pending, flush, payload, accounting, exit,
session). Local terminal delivery is unchanged.

Because the intake also installed the runtime's remote source-range hooks,
this desktop host now answers a terminal.subscribe that asks for
ackOutputSourceRanges the way every orcad host already does: without the
capability, so the client uses the byte-window ACK. No source ranges were
ever attached to local output.

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 10:25:42 -07:00
20f8cb02c1 feat(mobile): verified https pairing links; custom scheme carries no secret (#27475)
* feat(mobile): verified https pairing links; custom scheme carries no secret

The desktop's copy-link now emits https://www.onorca.dev/pair#<code>, with the
code only in the fragment. Orca Mobile claims that link as a verified app link
and refuses any pairing code delivered through orca://, which any app can claim;
orca://pair alone opens the scanner with an explanation. In-app QR scan and
paste still accept the custom scheme, so older desktops and phones keep pairing.

The confirm screen shows the desktop address and host key fingerprint before
pairing. config/verified-links/ holds the apple-app-site-association,
assetlinks.json and fallback page to deploy on www.onorca.dev, with a test that
keeps them, mobile/app.config.js and the link builder in agreement.

Part of #17397.

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

* fix(mobile): refuse pairing codes nested in or normalized out of unverified links

The router unwraps expo-development-client ?url= links and normalizes dot
segments after +native-intent runs, so an orca:// wrapper could still deliver a
code to the confirm screen. Refuse any unverified link carrying code= at any
encoding depth, and match pairing routes on decoded, dot-free segments.

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

* fix(mobile): escape the apostrophe in the refused-link notice

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

* fix(mobile): decode escapes byte by byte before refusing nested pairing codes

An invalid UTF-8 escape made the whole escape run fail to decode, hiding an
adjacent encoded &code= that the router still decodes leniently.

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

* fix(mobile): decode nested pairing links to any depth before refusing codes

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

* fix(mobile): ignore URL-stripped whitespace when refusing nested pairing codes

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

* refactor(mobile): decide pairing-link refusal on the router's own parse

Stop re-implementing URL decoding. The guard now runs expo-router's own
extractExpoPathFromURL and parseUrlUsingCustomBase, the functions its linking
applies after +native-intent, and refuses any link from which the router would
read a `code` param or reach a pairing route with a payload. A link the router
cannot parse is refused. Release builds refuse expo-development-client
wrappers outright. A property-style test walks wrapping depths, encoding
layers, stripped control characters, case and unicode, and asserts that the
guard never passes on a link from which the router would read a code.

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

* fix(mobile): match pairing routes on the top-level segment only

A host link with a segment named pair was sent to the scanner.

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

* test(mobile): pin line-terminator fragment links as refused

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

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-11 10:06:05 -07:00
OrcaWinandm4air 6a303be248 refactor(main): agent detection, automations, skills, AI vault and ephemeral VM drop SSH provider arms (#27511)
* refactor(main): agent detection, automations, AI vault and ephemeral VM drop SSH provider arms

* refactor(skills): refuse SSH skill destinations at the branch and delete the relay skill services

* test(preflight): clear the local-detection mock before the SSH refusal cases

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 09:54:46 -07:00
OrcaWinandm4air a4a96fee70 refactor(pty): delete the SSH PTY provider and its registry slot (#27499)
* refactor(pty): delete the SSH PTY provider and its registry slot

Nothing in production registers an SSH PTY provider, so the registry's SSH
map was always empty: every SSH lookup answered undefined or threw the
'No PTY provider for connection' error. Delete the provider stack, its
consumer-session and source-recovery contracts, the registry maps and the
getSshProvider dependency, keeping each caller's existing result for an
absent provider. The registry now dispatches only 'local'; any other host
throws PtyHostNotDispatchableError, which keeps the renderer-matched
message for SSH ids and is never read as a gone or exited PTY.

* test(pty): keep the no-provider SSH routing cases in local-only form

* test: reword a comment naming the deleted SSH PTY provider

* test: drop an import the rebase left unused

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 09:23:28 -07:00
OrcaWinandm4air 215ee3e625 refactor(runtime): runtime git and file callers drop SSH arms (#27501)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 08:23:12 -07:00
Neil bbac3af5fc [CI] Clean up runtime test callbacks (#27483) 2026-10-11 06:51:00 -07:00
OrcaWinandm4air 08b65edc5c feat(serve): let the owner resume paused automatic restores (#27459)
* feat(serve): let the owner resume paused automatic restores

The terminal daemon now answers automaticWorkStatus and
resumeAutomaticWork. Resume clears a recorded same-boot crash pause or
a pressure pause, writes the cleared state durably, and wakes restores
that were waiting. The runtime exposes runtime.automaticWork.status and
runtime.automaticWork.resume (host-admin; advertised as
runtime.automatic-work.v1), the CLI adds 'orca serve restore resume',
and a sleeping pane paused by its host offers 'Resume automatic
restores'. A daemon that predates the requests reports no gate.

* fix(serve): a stale daemon cannot resume, and an old one says so

Resume now re-reads endpoint ownership right before it runs, so a
daemon replaced by a successor cannot rewrite the successor's run
marker. A gate-bearing daemon from before resume existed is reported as
'terminal host predates resume' instead of 'no gate'; the CLI says to
run the command again after the terminal host restarts, and a sleeping
pane still resumes when the user asks for it.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 06:31:43 -07:00
OrcaWinandm4air ec14775f93 refactor(providers): an ssh: route carries no provider and throws one typed error (#27477)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 06:25:41 -07:00
e9279b613e refactor(ssh): drop the legacy-relay bridge and the relay branch of the terminal gate (#27471)
The terminal gate's relay lister always resolved to null in production (no SSH PTY
provider is ever registered), so its relay and previous-relay branches, the legacy
relay route/router/routing, the previous-relay census and the once-connected
re-check could never run. The gate now takes only the host census; census-only
verdicts are pinned by a characterization test written before the change.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-11 06:12:22 -07:00
BingZandm4air b4aec8a896 fix(sidebar): stop a removed folder workspace from leaving a blank row (#26376)
* fix(sidebar): stop a removed folder workspace from leaving a blank row

A folder-project workspace exists only as its worktreeMeta row. When it is
deleted while a terminal is still bound, the renderer keeps the row until its
teardown runs, and a PTY reattach in that window stamps lastActivityAt through
worktrees:updateMeta. setWorktreeMeta then minted a default row with an empty
name, which the sidebar listed under the project name and which came back after
every delete. The metadata IPC now refuses to create a row for a folder
workspace instance that no longer exists.

Fixes #22712

Claude-Session: https://claude.ai/code/session_01XZXEdbC4RKp288k2gBzLUW

* fix(sidebar): also refuse runtime writes to a removed folder workspace

Move the existence check into a shared predicate that matches any same-id
folder owner, so an owner at a different path on another host cannot bypass
it, and apply it to the runtime's worktree.set path, where a resolution
snapshot taken before the removal still returns the workspace.

Claude-Session: https://claude.ai/code/session_01XZXEdbC4RKp288k2gBzLUW

* test: type the removed-folder-workspace fixture meta for the anti-slop lint

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 05:39:31 -07:00
OrcaWinandm4air 10d5108671 feat(runtime): accept a declared spawn intent from paired clients (#27452)
* feat(runtime): accept a declared spawn intent from paired clients

Paired clients can now say whether a terminal restore is the pane the
user is looking at (user-resume) or a hidden one (automatic-restore).
The host forwards the intent to its fork gate, advertises
terminal.spawn-intent.v1 so clients only send it to hosts that accept
it (terminal.ensureAgentSession params are strict), and reports a
deferred fork as terminal_spawn_deferred with its reason.

No client sends the field yet; that lands next with the sleeping-pane
handling.

* fix(daemon): a claimed agent resume cannot declare itself interactive

Interactive forks skip the gate, and paired clients can now declare
intent, so the host keeps inferring from the claim when a request
says interactive.

* fix(runtime): clients may only declare restore intents

A daemon kept running across an update still lets a declared
interactive request skip its gate, even for a claimed resume. The RPC
and local IPC now accept only user-resume and automatic-restore, and
the daemon request boundary turns a claimed interactive into an
automatic restore.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 05:34:23 -07:00
061cb226e2 refactor(orcad): shared serve-option validation; no daemon barrel (#27443)
orcad now checks its flags with the same validator Electron serve and the
CLI use, so contradictory pairing flags fail with the same message instead
of one silently winning. The CLI launcher drops its duplicate recipe check
(the CLI handler already ran the shared validator).

Removes the daemon-init and cli/runtime/index re-export barrels and the
parsePtySessionId / OrcadInstanceLockError re-exports; callers and mocks
import the defining modules.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:58:58 -07:00
OrcaWinandm4air 0024735e82 fix(ssh): remove the relay-only remote workspace sync so reconnect never sticks on pulling (#27409)
* fix(ssh): remove the relay-only remote workspace sync so reconnect never sticks on pulling

* refactor(ssh): drop snapshot apply tokens and terminal finalization only the sync used

* test: drop leftover sync-status mock

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 00:56:48 -07:00
Jinwoo Hong e9928c3542 feat(mobile): let the desktop list its servers and their workspaces for the phone (#26442)
* feat(mobile): show SSH host health on the workspace row host badge

Rows on an SSH host the desktop cannot reach looked healthy on the phone. The badge now
adds the desktop sidebar's own word ("devbox · Disconnected"), derived by the same shared
function from the connection status ssh.listTargetSummaries already sends; old desktops keep today's badge.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): name an unhealthy SSH host even in a single-host list

The desktop card shows SSH status on any host count, so the phone now reads target summaries
whenever a repo is on SSH and badges an unhealthy host alone too. Labels and health share one
target-to-host keying, and a hook-level test pins the reply-to-row wiring.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): clear stale SSH host badges and announce them without the dot

A catalog that no longer spans hosts or holds an SSH repo now clears the health and label maps,
so a host that left cannot keep badging rows. The badge gets a screen-reader label ("devbox,
Disconnected") instead of reading the middle dot.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(runtime): let a paired desktop register its phones as delegated mobile devices

A desktop paired to this runtime can now call pairing.delegatedMobileDevice.sync to make each of
its phones an ordinary mobile-scope device it owns, so the phone mirrors that host through the
desktop. Children are never pending, inherit the parent's reach, and die with the parent's grant.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(runtime): move the mobile device revoke primitive below dispatch

Dispatch binds the caller-bound delegated-device context, so the revoke path it needs now lives in
a layer under it as a pure move; this drops the abstract method seam. No behaviour change.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(runtime): keep a runtime grant until its delegated phones are revoked

A failed child cleanup now leaves the parent in place so a retry can still find the children, and
revokeRuntimeAccess lives beside the revoke it cascades to. Adds restart, rename and bounds coverage,
shares credential minting, and drops the timing-dependent census.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(runtime): fail a delegated sync whose dropped phone cannot be revoked

Matches the parent revoke: a child whose cleanup could not be saved is kept and the sync errors
before minting anything, so the desktop retries instead of believing the server matches its list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): relay a paired phone's server-workspace requests through the desktop

A phone mirrors its desktop, including workspaces on the desktop's configured servers. The desktop
now forwards a request that names `executionHost: runtime:<env>` to that server over one socket per
(phone, server), signed in as the phone's delegated device; replies pass through untouched except
stream ids. Hidden until the phone client ships: the relay capability is not advertised yet.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): type the single-host health fixtures inline so the test tsc passes

Fixtures hoisted into consts widened their health maps to string, which the now-required
hostHealthById rejects under the tests-typecheck ratchet; passing them inline keeps the union.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): state the desktop relay contract in its entry module

main dropped docs/reference, so the relay's contract lives beside the code. Also pins that a
server whose delegated sync fails (e.g. delegated_device_revoke_failed) is unavailable until a
later request syncs it again.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin the relay's configured-server adapter

The production hosts adapter was only typechecked; this pins that it resolves configured servers
by exact id only (never a name or a removed server), fences by pairing revision and runtime
identity, and stops reporting retirements once unsubscribed.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-record the repo-metadata goldens for SSH host health

The settled metadata state now carries hostHealthById, and an SSH-only catalog now reads the
target summaries so a lone unhealthy host can be badged. Scoped re-record via rpc-recording.mts.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): tighten the desktop relay after elegance review

Reuses the shared E2EE socket opener, folds relay state into fewer fields, never sends a request
whose link ended while opening, passes failure codes through, relays only this desktop's own
phones, and drops the unused host-state reader until S2 needs one.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): bound relay frames and stop sticky relay verdicts

Restores the 8 MiB frame cap on every remote runtime socket, re-checks an update-needed server on
each new link so an in-place upgrade relays, answers a phone the server just declined without
syncing again, and ends a link whose mirrored capability update cannot be queued.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): run targeted desktop-owned calls on the desktop

The phone tags every call on a workspace screen with its execution host, so a
paired-desktop method carrying `runtime:<env>` now runs locally instead of
failing 'forbidden'. The Mac stays the only reader of the routing table.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): list the desktop's configured servers and their last workspace rows for the phone

The phone needs the same host list the desktop shows, including servers that are offline or
too old to relay to. mobileRelay.hosts.list/worktrees answer from the desktop's own status and
its own worktree.ps fetches, cached per server and fenced by pairing, never from relayed replies.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): list server rows through one desktop fetch, with no side effect on listing

The phone now always lists a server's rows through mobileRelay.hosts.worktrees, so the
desktop's own fetch is both the live source and the cache, and stamping stays in main.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): fetch a server's full workspace list within a bound, and forget removed servers

The desktop now asks for the phone's full-catalog limit, answers within 5s with the last rows
when a server hangs, drops a server's rows when it is removed or re-paired, and checks the
server's identity after the fetch so a reply from before a re-pair is never served.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): list managed servers as the desktop sidebar does, and hide rows mid-delete

A server Orca manages on an SSH host now lists once under that host's name, or not at all while
the host is reached over the SSH relay, using the sidebar's own folding. Rows the server is
deleting are left out at reply time, as a phone asking the server itself would see.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): keep a disconnected server's last rows, and never join a re-paired server's fetch

Retirement also fires on a manual disconnect or managed-server update, which wrongly emptied
the phone's list; rows are now pruned only once a server is no longer configured. Fetches are
shared per server identity, so a poll after a re-pair asks the new server right away.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(ci): run the remote-runtime WebSocket opener test on Node

Under the bun vitest project, `ws` resolves to Bun's built-in client, which
ignores maxPayload, so the frame-cap test hung. The opener only ships on
Node/Electron, so run its test in the Node runtime project like its siblings.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile-relay): derive the phone allowlist from the routing table

Adding a phone method now takes one edit (its route). A server-targeted call on a
legacy non-E2EE socket is refused instead of running locally, and only
execution-host methods enter the relay.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile-relay): drop unread freshness fields from server workspace lists

The phone reads only the rows, so stale, fetchedAt, totalCount and truncated
were dead wire weight. The desktop still serves a server's last rows while
the server is unreachable.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(runtime): classify the mobileRelay RPC namespace as host-owned

main now requires every runtime RPC namespace in the host-operation ownership table; the relay-hosts methods are served by the desktop the phone is paired to.
2026-10-11 03:43:52 -04:00
e3be586ef0 fix(ssh): a fast remote directory listing no longer reports a false failure (#26940)
ssh2 can deliver the exec ack and the exit status in one chunk, so 'exit'
fires before the browse handler subscribes. Fall back to the status that
'close' repeats when no exit was observed.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: suharvest <suharvest@users.noreply.github.com>
2026-10-11 00:30:22 -07:00
Jinwoo Hong ff5c284cc1 feat(mobile): relay a paired phone's server-workspace requests through the desktop (hidden) (#26435)
* feat(runtime): let a paired desktop register its phones as delegated mobile devices

A desktop paired to this runtime can now call pairing.delegatedMobileDevice.sync to make each of
its phones an ordinary mobile-scope device it owns, so the phone mirrors that host through the
desktop. Children are never pending, inherit the parent's reach, and die with the parent's grant.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(runtime): move the mobile device revoke primitive below dispatch

Dispatch binds the caller-bound delegated-device context, so the revoke path it needs now lives in
a layer under it as a pure move; this drops the abstract method seam. No behaviour change.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(runtime): keep a runtime grant until its delegated phones are revoked

A failed child cleanup now leaves the parent in place so a retry can still find the children, and
revokeRuntimeAccess lives beside the revoke it cascades to. Adds restart, rename and bounds coverage,
shares credential minting, and drops the timing-dependent census.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(runtime): fail a delegated sync whose dropped phone cannot be revoked

Matches the parent revoke: a child whose cleanup could not be saved is kept and the sync errors
before minting anything, so the desktop retries instead of believing the server matches its list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): relay a paired phone's server-workspace requests through the desktop

A phone mirrors its desktop, including workspaces on the desktop's configured servers. The desktop
now forwards a request that names `executionHost: runtime:<env>` to that server over one socket per
(phone, server), signed in as the phone's delegated device; replies pass through untouched except
stream ids. Hidden until the phone client ships: the relay capability is not advertised yet.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): state the desktop relay contract in its entry module

main dropped docs/reference, so the relay's contract lives beside the code. Also pins that a
server whose delegated sync fails (e.g. delegated_device_revoke_failed) is unavailable until a
later request syncs it again.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin the relay's configured-server adapter

The production hosts adapter was only typechecked; this pins that it resolves configured servers
by exact id only (never a name or a removed server), fences by pairing revision and runtime
identity, and stops reporting retirements once unsubscribed.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): tighten the desktop relay after elegance review

Reuses the shared E2EE socket opener, folds relay state into fewer fields, never sends a request
whose link ended while opening, passes failure codes through, relays only this desktop's own
phones, and drops the unused host-state reader until S2 needs one.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): bound relay frames and stop sticky relay verdicts

Restores the 8 MiB frame cap on every remote runtime socket, re-checks an update-needed server on
each new link so an in-place upgrade relays, answers a phone the server just declined without
syncing again, and ends a link whose mirrored capability update cannot be queued.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): run targeted desktop-owned calls on the desktop

The phone tags every call on a workspace screen with its execution host, so a
paired-desktop method carrying `runtime:<env>` now runs locally instead of
failing 'forbidden'. The Mac stays the only reader of the routing table.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(ci): run the remote-runtime WebSocket opener test on Node

Under the bun vitest project, `ws` resolves to Bun's built-in client, which
ignores maxPayload, so the frame-cap test hung. The opener only ships on
Node/Electron, so run its test in the Node runtime project like its siblings.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile-relay): derive the phone allowlist from the routing table

Adding a phone method now takes one edit (its route). A server-targeted call on a
legacy non-E2EE socket is refused instead of running locally, and only
execution-host methods enter the relay.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-11 03:12:24 -04:00
OrcaWinandm4air 2b4b60a87e refactor(relay): delete the SSH relay server, its build assets and tests (#27215)
* wip: delete the SSH relay server and its build, assets and tests

* wip: drop main/relay plugin conformance; WSL reader lookup test

* fix: drop the emptied relay bench; prune baselines

* test: keep the native halves of tests that also covered the relay

* chore(ci): drop shard timings for the deleted relay server tests

* fix: keep the directory listing budget that darwin terminal names now import

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 22:36:42 -07:00
Neilandmmarabel c2c63e7f9a Avoid downloading unchanged paired-runtime session history (#27383)
* perf: avoid retransferring unchanged paired-runtime session history

Keep selected finite-depth history refreshes content-conditional while preserving scans, full old-peer replies, force refreshes and the existing cache lifetime.

Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>

* perf(ai-vault): keep full host list behind unchanged replies for cache readers

---------

Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>
2026-10-10 22:31:20 -07:00
ff27338299 refactor(ssh): connect fails for hosts orcad can't serve; remove the relay client path (#27214)
* wip: connect fails for hosts the managed server can't serve; remove relay session, deploy and relay UI

* wip: delete relay-only client modules left unreachable, prune their tests and gates

* wip: format

* wip: exhaustive unserved message

* wip: drop relay attestation test for the deleted ownership proof

* fix: prune stale baselines; type the test transport stub

* fix(ssh): census proves leftover relay leases; name the remedy for older terminals; translate every connect-failure surface

* test(ssh): leftover leases defer to the host census

* test(e2e): remove relay-path SSH e2e specs and their CI lanes

* test(e2e): drop the auto-convert case that built its relay era through a live relay

* test(e2e): mark relay-era browser and catalog journeys fixme until they seed a profile

* test(e2e): seed relay-era browser and catalog journeys from a saved profile

The three journeys built their old-version state through a connect without an orcad template, which now refuses the host. They now start from a seeded relay-era profile, so the first connect converts the host as an upgrading user's would.

* chore(ci): drop shard timings for the deleted relay tests

* test(e2e): carry a relay-era login cookie across the browser conversion; own every launch

* test(ci): drop the deleted SSH layout oracle from its route

* test(e2e): open the retained browser after conversion, retrying a pre-connect card

* test(e2e): bind the relay-era browser partition before seeding its cookie

A relay-era build always recorded the partition binding before a page stored
data in it. Seeding the cookie into an unbound partition made the binding store
refuse it (browser_route_partition_binding_store_invalid), so the retained tab
showed the generic routing card and Retry could never clear it.

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

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-10 22:10:33 -07:00
OrcaWinandm4air 22bcda1074 fix(ssh): a restore racing a disconnect never leaves a forward behind (#27368)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 21:39:26 -07:00
OrcaWinandm4air bcc50483bb fix(vm): a recipe VM's SSH root runs on a managed Orca server (#27366)
* fix(vm): a recipe VM's SSH root runs on a managed Orca server

A recipe VM's hidden SSH target was counted as owned by another runtime, so
every connect refused to deploy a managed server and fell back to the relay.
Its runtime owner only records when the VM is created and destroyed, so it no
longer blocks the deploy. The VM's workspace then lives on that server: the
server adopts the provisioned root as its main worktree, and removing the
project destroys the VM and forgets its server.

* fix(vm): release the server of a relay-era VM that converted on connect

* fix(vm): resume reconnects an SSH VM's server; removal stays on its own VM

* fix(vm): removal and the remove dialog find a VM's server before the catalog loads

* fix(vm): a catalog refresh failure never blocks a VM create

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 21:39:23 -07:00
Jinwoo Hong 74856c83b3 fix(dev): keep Node-only Quick Open path accumulator out of the renderer (#27372)
The renderer imports quick-open-listing-limits for the result cap, but the
same module also held a Buffer-backed accumulator that imports the CommonJS
@orca/process-host dist. Vite dev serves that dist as raw ESM, so the named
import failed and the workspace workbench and right sidebar crashed. Move the
accumulator to its own main-only module.
2026-10-11 00:22:18 -04:00
Jinwoo Hong 666d990bc0 Record a display-only snapshot of each paired server's tabs (inert) (#27359)
* feat(layout): record a display-only snapshot of each paired server's tabs

Records the session.tabs.subscribeAll frames main already relays to the window
into a new remoteLayoutSnapshots profile domain (design 6.3, D3). No new server
requests; written only by the recorder; dropped when the server is removed.
Nothing reads it yet.

* fix(layout): keep only what the snapshot draws and save it on structure changes

Drop retirement proofs (sent as deltas) and one-shot navigation from recorded
workspaces; save immediately only when tabs or groups change, letting title
ticks ride the next save; drop the protocol field.
2026-10-10 23:36:37 -04:00
OrcaWinandm4air 5f350a3786 fix(ssh): restore saved port forwards on managed hosts and show their Ports tab (#27361)
* fix(ssh): restore saved port forwards on managed hosts; show Ports for managed workspaces

* test: annotate the mock connection casts

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 20:01:11 -07:00
OrcaWinandm4air e6af89741f feat(ssh): a paired client can manage the server's own SSH hosts (ssh.target-management.v1) (#27329)
* feat(ssh): manage a paired server's own SSH targets over RPC (ssh.target-management.v1)

* fix(ssh): keep a browsed remote path untrimmed

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 18:25:54 -07:00
Jinwoo HongandClaude f6d536dcda Save runtime-started tabs in the tab bar before their terminal starts (#27083)
* Write runtime-started panes with their tab bar entry and group before they spawn

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

* List the headless reorder's row-order finding and mark the headless group writer for the switch

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

* Admit runtime-started tabs into the targeted or focused tab group

A phone, CLI or orchestration create on a headless host with two tab groups was written into the
first group, so after a restart the tab moved groups. The phone's targetGroupId, else the host's
focused group, now reaches the pane admission.

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

* Admit an untargeted runtime-started tab into the first tab group

Defaulting to the host's focused group moved a CLI-created tab to the right group after a restart
and in the desktop window, while main and the live view keep it in the first group. Only the
phone's explicit targetGroupId now picks the group.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-10 21:22:45 -04:00
Neil 91f9744d3a fix(settings): return current preferences after slow updates (#27199)
* fix(settings): return current preferences after reconciliation

* test(settings): run SQLite reply regressions in Node
2026-10-10 17:36:49 -07:00
OrcaWinandm4air 42be4c50d4 chore(settings): rename "Active Server" to "Default host for new projects" (#27286)
* chore(settings): rename "Active Server" to "Default host for new projects"

The setting (still activeRuntimeEnvironmentId) now only picks the host for new
projects with no source and the initial host of the Settings host picker, so
the copy says that. Refusal and help text say existing projects stay on their
own host, the provider-scope hints point at the Settings Host menu, and the
Remote Orca Servers search entry keeps "active server" as a keyword.

* fix(settings): credential hints point at the setting that still scopes them

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 15:01:12 -07:00
Neil 26ee9bbfab [CI] Reduce repeated test setup and unnecessary packaging (#27308)
* [CI] Reduce repeated test setup and remove source text checks

* [CI] Consolidate transcript tests and preserve fixture-only CI routing

* [CI] Retarget reliability gate to consolidated transcript suite
2026-10-10 14:26:04 -07:00
Neil 0a73d1e093 [CI] Reduce repeated test setup and real CLI waits (#27297)
Combine related chat, worker-release, and filesystem tests while retaining their contracts. Replace fixed Codex terminal waits with observed readiness and hook completion. Keep runtime database contracts under Node.

Local measurements and all CI lifecycle costs are documented in PR #27297.
2026-10-10 13:09:48 -07:00
OrcaWinandm4air 6cbbe456c0 fix(hosts): agent launch platform and shell come from the execution host (#27259)
* fix(hosts): agent launch platform and shell come from the execution host

Agent startup commands were quoted for this client's OS (and its Windows shell
setting) whenever the workspace's repo row had no SSH connection, so a Windows
client driving a Linux Orca server sent PowerShell to bash. SSH hosts were
guessed from path shape.

One resolver now reads the OS from the host that runs the agent: the server's
reported hostPlatform, the relay-reported SSH remotePlatform (nested SSH read
through its server), WSL as linux, and this client only for local workspaces.
A missing fact withholds the launch with a reason. Plain-SSH mode now
publishes the OS the runtime ladder detected.

* fix(hosts): new-tab and background launches read the worktree's own host

Review follow-up: the new-tab resolver reads folder workspace paths (WSL folders),
a caller's platform only refines a local launch, background launches resolve the
worktree's host (nested SSH) and drop this client's shell setting for remote hosts.

* fix(hosts): background launches on an SSH worktree keep the relay CLI name

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 07:45:38 -07:00
OrcaWinandm4air f050e57a98 fix(agents): one agent inventory per host and execution context (#27225)
* fix(agents): one agent inventory per host and execution context

Launch surfaces that pick an agent for a workspace now read and load the
agent list through one helper keyed by the workspace's own host, instead of
each re-deriving "SSH connection id, else this computer". A paired server's
workspace (including one on the server's own SSH target) reads the server's
list; this client's own list and its SSH map are never consulted for it.

- Source Control AI actions (Resolve with AI, recovery, Fix checks), the
  new-agent-tab shortcut, Explain commit, session continuation and the
  checks panel use readDetectedAgentsForWorktree / ensureDetectedAgentsForWorktree.
- An old paired server without workspace-scoped detection probes its own SSH
  target with preflight.detectRemoteAgents instead of answering its default list.
- A disconnected SSH host rejects detection instead of answering [], so it is no
  longer cached as "no agents installed"; Quick Launch says the host is unreachable.
- On Windows, an active workspace on another host no longer drops this machine's
  agent context to the Windows host; it uses the global runtime default.

* chore(i18n): add the unreachable-host quick launch copy

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 04:22:15 -07:00
OrcaWinandm4air 7aa91f730a fix(cli): check the runtime source before sending (#27206)
* fix(cli): check the runtime source before sending

A terminal Orca launches now carries which Orca launched it
(ORCA_RUNTIME_SOURCE_ID = the profile's installation id,
ORCA_RUNTIME_SOURCE_INCARNATION = the launching runtime id). Before a
request is sent, the CLI compares that with the runtime the profile's
runtime record names. Same runtime: sent as before. Different runtime:
a read-only status.get asks which profile it serves; the same profile (a
restart or update) is accepted, anything else is refused before the
request reaches it.

The vars cross into WSL through WSLENV. The SSH bridge stamps the
bridging runtime itself rather than trusting the remote env. The
existing orchestration compatibility stamp is untouched.

* fix(orcad): drop an inherited runtime source at startup

* fix(cli): an explicit switch to another profile is not fenced

The stamp also carries the launching runtime's userData path. A CLI that
dials a different profile than the one that launched its terminal, and
not the platform default an unpinned terminal falls back to, was pointed
there on purpose (orca-dev from a packaged Orca's terminal), so the
fence does not apply.

* refactor(cli): reuse the shared userData path comparison

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 03:19:14 -07:00
OrcaWinandm4air 31b2b17d9b refactor(wsl): move the WSL guest bundle sources out of src/relay (#27174)
* refactor(wsl): move the WSL guest bundle sources out of src/relay

The WSL hook relay and WSL browser network relay are built from src/relay but are
not part of the SSH relay. Move them, and the 46 relay modules the hook relay
bundles, to src/wsl-guest so the SSH relay can be deleted on its own. Pure
renames; the built bundles are byte-identical.

Adds a boundary test so nothing outside src/relay imports it except relay tests.

* fix(ci): exempt renamed files' existing lint debt in the changed-lines gate

* test(relay): keep the cast suppression on the wrapped line

* test(relay): only parse files that mention relay in the import boundary check

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 02:46:27 -07:00
Neil 991fd5ca4b Keep folder Git upgrades responsive and recheck project ownership (#26791)
* Keep repository detection off the main IPC thread

Share repository discovery decisions across synchronous compatibility callers and the existing asynchronous Git runner. Await discovery in project registration, nested imports, and folder upgrades, and recheck folder ownership after discovery completes.

* Make repository detection fixtures work on native Windows

* Await asynchronous Git checks in folder upgrade fixtures

* Prevent duplicate project registration after async discovery

* Check authoritative registration identity before renderer convergence

* Narrow repository responsiveness to detection and registration

* Keep one local project identity after concurrent registration

* Run registration SSH coverage in the existing Docker lane

* Keep folder upgrades responsive and recheck pending project ownership

* test(git): cover classifier failure stages and literal path identity

* test(git): use the current process host package

* test(repos): use current process host in folder upgrade fixture

* test(repos): use current process host in registration fixture
2026-10-10 02:02:16 -07:00
Neil 803ab30598 Await Git checks while discovering and importing nested projects (#26790)
* Keep repository detection off the main IPC thread

Share repository discovery decisions across synchronous compatibility callers and the existing asynchronous Git runner. Await discovery in project registration, nested imports, and folder upgrades, and recheck folder ownership after discovery completes.

* Make repository detection fixtures work on native Windows

* Await asynchronous Git checks in folder upgrade fixtures

* Prevent duplicate project registration after async discovery

* Check authoritative registration identity before renderer convergence

* Narrow repository responsiveness to detection and registration

* Keep one local project identity after concurrent registration

* Run registration SSH coverage in the existing Docker lane

* Await Git discovery when scanning and importing nested projects

* test(git): cover classifier failure stages and literal path identity

* test(git): use the current process host package

* test(repos): use current process host in registration fixture
2026-10-10 02:01:40 -07:00
Neil 4bdeff3ec3 chore: remove verified unused Orca code (#27175)
* chore: remove verified unused Orca code

* chore: remove translations orphaned by dead code cleanup
2026-10-10 02:01:24 -07:00
Neil b63d39ebe5 Keep repository detection off the main registration thread (#26760)
* Keep repository detection off the main IPC thread

Share repository discovery decisions across synchronous compatibility callers and the existing asynchronous Git runner. Await discovery in project registration, nested imports, and folder upgrades, and recheck folder ownership after discovery completes.

* Make repository detection fixtures work on native Windows

* Await asynchronous Git checks in folder upgrade fixtures

* Prevent duplicate project registration after async discovery

* Check authoritative registration identity before renderer convergence

* Narrow repository responsiveness to detection and registration

* Keep one local project identity after concurrent registration

* Run registration SSH coverage in the existing Docker lane

* test(git): cover classifier failure stages and literal path identity

* test(git): use the current process host package

* test(repos): use current process host in registration fixture
2026-10-10 02:00:15 -07:00
OrcaWinandm4air 1a6d570412 fix(shell): OS open actions follow the file's owner, not the focused server (#27165)
* fix(shell): OS open actions follow the file's owner, not the focused server

Every shell open request (openPath, openInFileManager, openInExternalEditor,
openFilePath, openFileUri) now carries the path's owning host. Main allows
only a local owner, plus the SSH editor launch when the owner is that same
SSH target, and no longer consults the focused server. The renderer reveal
and Open-in checks drop their focused-server check to match.

* test(e2e): name the owner on the local editor control open

* fix(explorer): Open in follows the catalog owner for folder and host-only SSH workspaces

The explorer toolbar and the source-control Open in menu took their SSH
connection from the repo row, which a folder workspace or a host-only SSH
repo does not have, so the route read as local. Both now derive the route
from the catalog-confirmed owner.

* fix(sidebar): Open in keeps the SSH target of folder and host-only SSH rows

The row resolver now returns the SSH connection it resolved alongside the
runtime owner, so a folder workspace or host-only SSH repo with no repo
connection no longer reads as local.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 23:39:05 -07:00
Neil 915c4b74a2 Update Claude usage without rereading the whole conversation (#26967)
* Verify pinned transcript reads and stream Codex usage aggregation

* Resume Claude usage scans with compact token checkpoints

* Harden transcript snapshots and reject incomplete discovery

* Keep Codex race fixtures inside their temporary data home

* Verify pinned transcript rotation with portable file moves

* test: keep Claude usage mock registration in test suites

* Compress saved Claude usage without dropping history

* Keep Claude cache writes bound to their original request

* Register Claude contracts without conflicting with chat contracts

* Assert unchanged Claude sources skip the actual cache writer
2026-10-09 22:26:46 -07:00
OrcaWinandm4air b23623681b fix(hosts): the unresolved-owner sentinel is never routable (#27111)
* fix(hosts): the unresolved-owner sentinel is never routable

When a worktree's owner publications conflict, the renderer marks it with
runtime:unresolved-owner so nothing treats it as local. That id parses as
a paired runtime, and AI Vault and the port scanner dialed environment
'unresolved-owner'. The sentinel is now a named shared constant with a
routable-host parser that rejects it; AI Vault list/titles/resume and the
renderer's runtime target builder use it, and the scattered string
compares collapse onto the same helpers.

* fix(worktrees): refuse a create captured on the unresolved owner

Without a routable target, the direct-SSH desktop IPC fallback would have
created the workspace on this machine.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 20:59:55 -07:00