Commit Graph
13609 Commits
Author SHA1 Message Date
Jinwoo-H 643aaa007a fix(mobile): keep polling the server list and read a server's reachability from relay
A hosts-list read slower than the poll stopped the feed for good, since the next tick was
scheduled when the read started; it is now scheduled from the finish. A server workspace now
reads connected only when the desktop relays to it, so a server with no status yet is not shown up.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-11 22:40:24 -04:00
Jinwoo-H 4b0728b7cc refactor(mobile): export the server-list feed's React reader beside the feed
useRelayHosts moves from workspace-server.ts into relay-hosts-feed.ts, so every reader (workspace screens now, the new-workspace sheet next) subscribes through the feed's own module. No behaviour change.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-11 06:07:05 -04:00
Jinwoo-H ce71823be3 build(mobile): re-measure the page's script sweep for server workspaces
Main already sat on its 78-script ceiling (re-swept: 78, table: 74); the workspace
route's shared modules split importer sets three ways more, reaching 81. Re-swept per
route prefix on this head, as #24257 did for its feature; the spread is now 1 to 11.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-11 05:18:20 -04:00
Jinwoo-H 96863da6f4 test(e2e): read a server's reachability from the hosts list, not a dropped stale flag
eff410fe30 dropped `stale` from mobileRelay.hosts.worktrees, so the mirror spec's
schema never parsed and its first poll timed out. It now waits on the server's
relay/health from mobileRelay.hosts.list, the field the phone itself reads.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-11 05:18:20 -04:00
Jinwoo-H e5cd05ffe6 refactor(mobile): share one desktop server-list feed between the host list and workspaces
A server workspace polled mobileRelay.hosts.list beside the host list's own read, so
the phone could ask the desktop twice per tick and the row badge and the screen could
disagree. One feed per desktop client now owns that read; both readers join it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-11 05:09:55 -04:00
Jinwoo-H d6fc1c34de fix(mobile): show a server workspace as disconnected while its server is down
The phone read the desktop's connection state for a server workspace, so a stopped
server kept a green dot and "Loading tabs" spun forever. The workspace route now
narrows that state by the server's health from mobileRelay.hosts.list, reusing the
existing disconnected session UI and refetching tabs when the server returns.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-11 05:02:48 -04:00
Jinwoo-H dd5689f9a1 Merge remote-tracking branch 'origin/main' into feat/mobile-remote-workspaces
# Conflicts:
#	mobile/src/host-screen/use-host-display-worktrees.ts
#	src/main/ipc/runtime-environments.ts
#	src/main/runtime/delegated-mobile-device.integration.test.ts
#	src/main/runtime/device-registry.ts
#	src/main/runtime/mobile-desktop-relay/mobile-desktop-relay.integration.test.ts
#	src/main/runtime/mobile-desktop-relay/mobile-desktop-relay.ts
#	src/main/runtime/rpc/core.ts
#	src/main/runtime/rpc/dispatcher-stream-options.ts
#	src/main/runtime/rpc/methods/mobile-relay-hosts.ts
#	src/main/runtime/rpc/methods/pairing.ts
#	src/main/runtime/rpc/rpc-streaming-dispatcher.ts
#	src/main/runtime/runtime-rpc/runtime-rpc-mobile-method-routing.test.ts
#	src/main/runtime/runtime-rpc/runtime-rpc-mobile-method-routing.ts
#	src/main/runtime/runtime-rpc/runtime-rpc-mobile-paired-desktop-methods.ts
#	src/main/runtime/runtime-rpc/runtime-rpc-pairing.ts
#	src/main/runtime/runtime-rpc/runtime-rpc-state.ts
#	src/main/runtime/runtime-rpc/runtime-rpc-websocket-dispatch.ts
#	src/shared/delegated-mobile-device-contract.ts
#	src/shared/mobile-desktop-relay-contract.ts
#	src/shared/protocol-version.ts
#	src/shared/skill-install-capability.ts
#	tests/e2e/helpers/paired-mobile-client.ts
2026-10-11 03:46:47 -04: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
Neilandm4air d56c0fbbb3 fix(runtime): publish project setup changes and retire stale worktree scans (#26941)
Runtime setup create/update/delete changed the store without telling open
clients, and a path/kind change or deletion left cached worktree scans
answering for the old registration. Publish reposChanged after each
successful mutation and run the shared worktree-change invalidators on
path/kind changes and deletes.

Fixes #26562

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 00:30:06 -07:00
OrcaWinandm4air cc5c85fb7a ci: ship the orcad template in hourly and daily builds (#27407)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 00:19:28 -07:00
OrcaWinandm4air 68dc48a21c fix(packaging): unpack the orcad serve-selection entry so orca serve can run it (#27307) (#27404)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 00:19:25 -07:00
Neil 6a77d05ee0 refactor(runtime): compose typed file search and listing ownership (#27396)
Compose typed file search and listing with narrow workspace/store dependencies and preserve the existing runtime command facade. Move the mobile inventory cache to its owner, remove four inheritance classes and six unchecked-file entries, and delete unused process bookkeeping while retaining producer-owned cancellation.

Preserve file access, provider capability fallbacks, host isolation, remote negotiation, budgets and cache lifetime. Independent audits verify moved method bodies, parameters, defaults and literals. Local tests and matched native Windows controls, typechecks, quality checks and CI qualification are recorded in the PR.
2026-10-11 00:12:36 -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
38aa54f716 fix(doc-preview): serve authorized web fonts as binary bytes (#26849)
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: noan98 <187374956+noan98@users.noreply.github.com>
2026-10-11 00:08:02 -07:00
Jinwoo Hong c19e2dd363 fix(relay-load): reuse an Asia load principal set for shards past the fourth (#27403)
Staging auth mints principal sets for shards 0-3, so the second hosted runner's shards 4-7 were
refused. Their hosts stay distinct by peer index; each set carries the same host count as the
proven 4 x 710 run.
2026-10-11 03:05:10 -04:00
Jinwoo Hong 5a0795a59b fix(browser): double-click in the address bar selects a word, like other browsers (#27371)
* fix(browser): double-click in the address bar selects a word, like other browsers

The click that focuses the address bar selects the whole URL. Chromium treats
a press inside a selection as a possible text drag, so the second press of a
double-click never picked a word, and re-clicking a blurred bar restored its
old selection instead of selecting all. Clear that selection before the press
(focusing first, as Firefox's urlbar does) so single, double and triple clicks
match Chrome and Firefox.

* refactor(browser): keep address-bar click selection to one flag inside its hook

The focusing press now focuses and clears the selection itself, so focus can
always select all and the bar no longer shares the press flag with its focus
and blur handlers. A second press only meets a range our click made, so
event.detail === 2 replaces the select-all flag.
2026-10-11 03:04:38 -04:00
Jinwoo Hong 29b0aa534e test(relay): load generator can reopen a closed phone splice instead of failing the run (#27379)
* test(relay): load generator can reopen a closed phone splice instead of failing the run (--splice-retry-max-seconds)

* fix(relay-load): declare a reconnect after an established control, as desktops do

Without it the director's sticky lane is never reached, so a cell restart under load
cannot exercise the sticky re-booking.

* fix(relay-load): close a control after 75 s of silence, as desktops do

The load balancer keeps a dead backend's client websocket open, so a stopped cell's
simulated hosts never noticed and never re-assigned.
2026-10-11 03:03:30 -04:00
Jinwoo Hong b197bc611f fix(remote): move tabs into a split-off group on the server too (#26522)
In a server workspace the desktop and the server mint different ids for a
split-off group, so a drag into it named a group the server didn't know and
was silently dropped. The desktop now names the server group that holds
another tab of the target group, falling back to its own id as before.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-11 02:58:28 -04:00
Jinwoo Hong 08c8d30f10 fix(e2ee): stop dropping terminal output on a backed-up legacy connection (#26528)
* fix(browser): retry a screencast frame a viewer's socket refused

An idle page sends no next frame, so a refused last frame left the viewer on a stale picture. The
desktop's relay leg to a server uses legacy E2EE, which drops on a full socket where a direct v2 phone
queues; retrying the newest pending frame restores parity. Retries never count toward ghost eviction.

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

* fix(e2ee): queue legacy binary replies with text instead of dropping them

Legacy E2EE (every direct phone, the desktop's server link) sent binary straight to the socket and
silently dropped it once the outbound budget was full, so terminals lost output and binary could
overtake queued text. Binary now shares the ordered text queue and closes on overflow like v2; the
lossy screencast opts into dropWhenBacklogged and its retry delivers the newest frame after a drain.

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

* fix(e2ee): keep a backlogged screencast viewer instead of evicting it as a ghost

A lossy frame skipped on a busy socket returned false like a dead socket, so a long backlog evicted a
live viewer. Skips now return 'backlogged', which retries without counting toward eviction; legacy
judges idleness on the sealed size before encrypting, so skipped frames cost no encryption.

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

* test(browser): expect the ready gate to forward screencast send options

The pre-ready gate now passes the per-frame send options through to the
socket sender, so the replayed startup frame arrives with an options slot.

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

* test(browser): share the streamed-guest double across screencast tests

Main's renderer throttle lease now calls setBackgroundThrottling on the guest,
so the bare { isDestroyed } guest in two branch tests threw. One shared double
keeps all three screencast suites on the shape the lease needs.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-11 02:54:27 -04:00
Jinwoo Hong de00a4f03c feat(runtime): let a paired desktop register its phones as phone devices (#26427)
* 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

* fix(runtime): refuse pairing admin to phones delegated through a paired desktop

A delegated child phone could call the mobile-allowlisted pairing-admin methods
(push registration, relay provisioning) that its parent desktop is never granted.
The caller scope now marks delegated phones and denies them that permission.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-11 02:54:09 -04:00
Jinwoo Hong 7abee44e78 feat(mobile): show SSH host health on the workspace row badge (#26415)
* 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

* 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

* 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
2026-10-11 02:53:57 -04:00
Neilandm4air 58d1d8d101 fix(persistence): save editor-only host sessions during quit (#26848)
Normalize the incoming host slice and the saved partition before retirement and
membership rebasing read their terminal maps.

Fixes #25924

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 23:52:32 -07:00
c37ce7add2 fix(zsh): keep deferred line-init defined for plugins that call it by name (#26842)
* fix(zsh): keep deferred line-init defined for plugins that call it by name

Fixes #26029

Co-Authored-By: petersindex <petersindex@users.noreply.github.com>

* test(zsh): keep the named-call regression test out of the relay module

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: petersindex <petersindex@users.noreply.github.com>
2026-10-10 23:51:24 -07:00
Jinwoo Hong 305474ceba ci(relay-staging): split the staging load across two hosted runners (#27402)
One hosted runner tops out near 2,048 connections to one cell (4 shards each stalled at 512 with
control_open_timeout), short of the 2,840 the Asia flip-back test needs.
2026-10-11 02:48:37 -04:00
Jinwoo Hong b241ba5c01 test(ci): point server-scope qualification fixtures at SSH after relay deletion (#27392)
The relay server deletion removed src/relay/ from the cross-host qualification
prefixes, but the scope tests merged alongside it still used src/relay/ paths
as their cross-host example. Use src/main/ssh/, which still carries that rule.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-10 23:37:15 -07:00
Jinwoo Hong 76687bf4ff ci(relay-staging): let the staging load ask for asia-east2 from a hosted runner (#27393)
The Asia load runner is registered to the old repository, so asia-east2 load could not run.
Where the load comes from does not change what c4 does with it.
2026-10-11 02:21:50 -04:00
Jinwoo Hong 43645e5274 feat(push): carry a notification's workspace server to the phone (#26516)
A desktop relays alerts for agents on its paired servers, but the push named
only the server's worktree, so a tap opened it on the desktop and bounced.
The gateway accepts an optional runtime execution host and forwards it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-11 02:09:36 -04:00
Jinwoo Hong 92fb883cf2 fix(runtime): refuse headless tab split/move into an unknown group (#27381)
The headless split and move-to-group handlers answered {moved:true} when the
target group did not exist, hiding desktop/server group-id drift (#26522).
They now refuse with target_group_not_found, as reorder already does.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-11 01:54:29 -04:00
Jinwoo-H 7873c58f4a refactor(mobile): read only the rows of a server's workspace list
The desktop no longer sends a freshness flag, so the phone's schema drops it.
The relay capability list names the delegated-devices constant instead of
repeating its literal, which also retires a now-circular type check.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-11 01:52:32 -04:00
Jinwoo-H 46ff087757 Merge branch 'feat/mobile-relay-hosts' of /Users/jinwoo/orca-lanes/s2a-relay-hosts into feat/mobile-remote-workspaces
# Conflicts:
#	src/main/runtime/runtime-rpc/runtime-rpc-mobile-method-routing.test.ts
2026-10-11 01:49:05 -04:00
Jinwoo-H eff410fe30 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
2026-10-11 01:48:35 -04:00
Neil ac156c0302 perf(ci): overlap Bun typechecking and skip redundant alias scans (#27382)
* perf(ci): skip unnecessary alias scans and compare preflight compilers

* perf(ci): overlap measured Bun typechecking with static checks

* perf(ci): use measured Bun lint runtime and strengthen compiler parity checks
2026-10-10 22:48:25 -07:00
Jinwoo-H 91c25ad51e Merge branch 'feat/desktop-mobile-relay' of /Users/jinwoo/orca-lanes/s1b-mobile-relay into feat/mobile-relay-hosts
# Conflicts:
#	src/main/runtime/runtime-rpc/runtime-rpc-mobile-method-allowlist.ts
2026-10-11 01:46:12 -04:00
f425cab536 fix(agent-status): keep nested models off parent panes (#26846)
* fix(agent-status): keep nested models off the parent pane

Preserve the parent's model when the existing identity fence retains an
active parent for a nested CLI hook. Exercise native SessionStart and Stop
bodies alongside model updates and legitimate pane takeovers.

Adapted from PR #25687, with real native-hook and hidden renderer proof.

Co-authored-by: Regis <92858615+Regis-RCR@users.noreply.github.com>

* chore: record original authorship for Parent-pane model ownership

Records the retained adapted contribution from #13602 in replacement PR #26846.

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>

* test: drop redundant single-agent model case

---------

Co-authored-by: Regis <92858615+Regis-RCR@users.noreply.github.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 22:46:06 -07:00
Jinwoo-H 1cd5663d36 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 01:45:47 -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
Neilandmmarabel d07ac4b1bc perf(worktrees): skip unchanged lineage after sidebar order saves (#27384)
* perf(worktrees): skip unchanged lineage after sidebar order saves

Mark order-only remote events and skip redundant lineage downloads only
after a successful current-host read. Preserve independent cold, failed,
pending and structural refreshes, including reconnect and re-pair.

Addresses #27268.

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

* fix(worktrees): notify after recording SSH-created worktree lineage

Paired clients now skip lineage refetch on sort-order-only events, so the
pre-lineage create notify on the SSH route could leave lineage stale until
the next structural change.

---------

Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>
2026-10-10 22:31:16 -07:00
Neil a13c2b3633 [CI] Reduce test teardown and scope server qualification (#27380)
* [CI] Reduce test teardown and scope server qualification

* [CI] Retain raw platform paths in server qualification
2026-10-10 22:23:53 -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
Jinwoo Hong 4c4f1ee18c test(e2e): reusable phone-mirror test setup and baseline specs (#26412)
* test(e2e): add a paired phone client helper and phone-mirror baseline specs

Later phone-mirror slices need one oracle that drives a real mobile-scoped device with the
phone's own capabilities against a serve host or a paired desktop. Pins #26022 across a serve
restart and today's gap: a desktop's phone does not list the desktop's server workspaces.

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

* test(e2e): compose the phone-mirror topology and check the cold snapshot

Slices need one composer for host, desktop and phone with a single teardown, so specs stop
copying setup. The serve-restart spec now reads the first tab-stream snapshot after the cold
start, which is what the phone builds its tab strip from.

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

* test(e2e): keep phone-mirror teardown errors from masking test failures

Teardown failures become a test attachment instead of replacing the real failure, and the
cold-snapshot poll reports stream errors together with the PTY list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-11 00:54:03 -04:00
Jinjing 3aaa6a854a Require pointer rest before opening activity hover cards (#25931)
* Require pointer rest before opening activity hover cards

Defer hover card open until pointer stops moving, preventing accidental
popover display on rapid mouse movement. Add progress indicator showing
rest duration. Keyboard focus still opens the card immediately.

* refactor to keep file small
2026-10-10 21:40:35 -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
0081ea0941 fix(clipboard): paste a macOS image copied beside its file URL (#27346)
* fix(clipboard): paste a macOS image copied beside its file URL

When a screenshot tool saves a file and also copies the image, the
pasteboard holds public.png and public.file-url, and Chromium reports
only text/uri-list from availableFormats(). readClipboardImageSource()
then saw no image and Cmd+V dropped the paste, although readImage()
returns it. On macOS, also check public.png and public.tiff with
clipboard.has() before giving up.

Fixes #27345

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HzCzr11hHmaEhiUoGJCrDy

* test(clipboard): cover macOS file URL image paste safety

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>
2026-10-10 21:30:27 -07:00
VladislavandNeil d2ba67d109 fix(claude): register a short PostCompact hook command (#25211)
* fix(claude): register a short PostCompact hook command

Claude Code echoes every PostCompact hook's full command into the
post-compact transcript, so Orca's ~3KB portable wrapper landed in the
model's context after every /compact. On POSIX hosts (local macOS/Linux and
SSH remotes) PostCompact now gets only the POSIX branch of the wrapper;
every other event, and Windows, keep the full wrapper.

Fixes #25209

* fix(claude): guard runtime home and verify hook process identity

---------

Co-authored-by: Neil <neil@stably.ai>
2026-10-10 21:25:20 -07:00
Neilandmmarabel e4f599df05 perf(worktrees): preserve Git scans when saving sidebar order (#27343)
* perf(worktrees): preserve Git scans when saving sort order

Credit @mmarabel for identifying the repeated scans and the host-only approach in #27271.

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

* perf(worktrees): skip Git scan invalidation for order saves on desktop hosts too

The desktop window notifier ran the worktree-change invalidators unconditionally,
so a desktop (or paired-host) Orca still cleared Git scan caches on every sidebar
order save. Carry the invalidateGitScans flag through the notifier; every other
caller keeps the default.

---------

Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>
2026-10-10 21:24:34 -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
Neil dae2cc9110 test(runtime): enforce ANSI preview-final pre-filter invariant (#27376)
Follow-up to #27344. isTerminalPreviewLineControl now gates on
isTerminalPreviewLineControlFinal, so the normalizeTerminalChunk
pre-filter cannot drop a final the predicate accepts. Add a test over
every CSI final 0x40-0x7e and replace String.prototype.charCodeAt spies
with behavior assertions.
2026-10-10 21:21:27 -07:00