Skip to content

[pull] main from stablyai:main - #74

Open
pull[bot] wants to merge 9461 commits into
steelburn:mainfrom
stablyai:main
Open

pull[bot] wants to merge 9461 commits into
steelburn:mainfrom
stablyai:main

Conversation

@pull

@pull pull Bot commented Jun 5, 2026 •

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

@pull pull Bot locked and limited conversation to collaborators Jun 5, 2026
@pull pull Bot added ⤵️ pull merge-conflict Resolve conflicts manually labels Jun 5, 2026
nwparker and others added 27 commits October 11, 2026 00:30
…ailure (#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>
…or 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.
…roxy (#26942)

Use the host HTTP client for usage, reset-credit reads and redemption, keep
those account requests cookie-free, and pass configured proxy settings to the
native Codex probe. WSL probes keep the guest's own proxy environment.

Co-authored-by: petersindex <287557875+petersindex@users.noreply.github.com>
Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>
…ver 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>
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix(network): answer authenticated proxy challenges on cold desktop requests

* fix(network): destroy a stalled proxy-fetch body on cancel

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
…rns (#26954)

Co-authored-by: m4air <m4air@Mac.localdomain>
…l-list read (#27405)

* fix(relay): keep seat polls off the cell-list read and report event-loop freezes

The seat poller no longer waits on its database cell-list read before polling
cells once it has a list, so a stalled read cannot starve the polls that feed a
reserve cell's dead-man. Runtime metrics add eventLoopDelayMsMax: a single long
freeze is one sample, so p99 read 68 ms across a 134 s staging freeze.

Adds a pool test against a database that accepts and never answers, and a
real-Postgres stop/start recovery test.

* fix(relay): state the seat-poller gain precisely, justify the AddressInfo cast
…ng), adoptable deploy shape, event-loop stall alert (#27406)

* fix(relay): director reaches Cloud SQL through a proxy sidecar (staging), gated event-loop stall alert

On Cloud Run's built-in /cloudsql socket a connect() blocks the director's whole
event loop until the connector's upstream dial ends, 10 s per attempt while the
instance starts. Staging froze both directors for 134 s on 2026-10-11 and
production four directors for 40-50 s on 2026-09-28.

relay_director_cloud_sql_proxy_sidecar runs the Cloud SQL Auth Proxy beside the
director on 127.0.0.1:5432, as the GCE cells already do, with its own TCP
database URL secret. relay_event_loop_stall_alert adds the event_loop_ms_max
metric and a 5 s stall alert. Both default off, so production plans unchanged;
staging turns both on.

* fix(relay): adopt a reviewed Terraform director shape on deploy; counter-based stall alert; cheaper probed sidecar

- deploy-relay-blue-green: --shape-baseline-revision names the 0% revision a Terraform
  apply created; the deploy preserves that shape instead of the serving one. It must be the
  latest created, unserved revision on the serving image. Wired into the staging and
  production director workflows behind ADOPT_RELAY_DIRECTOR_TERRAFORM_SHAPE.
- Event-loop stall alert is now a gated counter metric on eventLoopDelayMsMax>=5000.
- Sidecar gets a /liveness probe and 0.25 vCPU (Cloud Run rejects only a total under 1 vCPU
  with concurrency above 1; checked with validateOnly against the API).
- staging.tfvars pins the serving director's concurrency 1000 and timeout 3600 s, so the
  adopted apply shape does not fall to the 80 / 30 s defaults.

* fix(relay): shape adoption refuses identity, topology, pruning and image changes; GOMAXPROCS=1 on the sidecar

parseArguments now refuses --shape-baseline-revision with a runtime identity bootstrap, a
director topology change or revision pruning, so every caller is covered, and the deploy
refuses a shape baseline whose image differs from the serving one. Both director workflows
also refuse the combinations up front. The proxy sidecar runs with GOMAXPROCS=1 to match its
0.25 vCPU quota. The long-hold alert comment is back above its resource.
…27412)

* ci: drop the relay Windows process-tree job from desktop packaging

* ci: refuse relay-era tags in a directly dispatched mac release build

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
…ge (#27416)

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>
#27396 deleted the write-only registry that consumed it; nothing sets the
option now, and its doc comment implied a cancellation path that no longer exists.
* Add manual worktree storage cleanup command

* Validate cleanup exclusions and preserve failure diagnostics
…on (#27422)

Container devpts instances reuse pts numbers, so selecting kill targets by
tty alone could SIGKILL same-uid processes in other namespaces. On Linux,
require each candidate's /proc session id to equal the PTY shell's pid.

Fixes #27337

Co-authored-by: m4air <m4air@Mac.localdomain>
Fold the empty owner-routing baseline into the focus-read ratchet. The three
deleted focus-routing helpers stay banned as a forbidden-name list.

Co-authored-by: m4air <m4air@Mac.localdomain>
…7423)

A layout stream's cleanup is now registered under a key that includes the
subscriber's connection, and layout.unsubscribe builds that key from the
caller's own connection, matching session.tabs.unsubscribe. Another
connection can no longer end a stream by passing its id.
…away (#23459)

* fix(linux): stop the AppImage crashpad handler before the mount goes away

Crashpad's handler runs from the AppImage FUSE mount and outlives the main
process, so it SIGBUSes once the runtime unmounts it. On a validated
AppImage running from its own mount, SIGKILL this launch's handler from the
main process's exit hook.

Fixes #22542

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

* docs: clarify AppImage Crashpad exit hook

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The relay client is gone, so the three relay-ladder cells and the
@orcad-cli-relay-kept matrix case could not run. Delete them, their
last helper (ssh-test-target-connection), a stale path filter and the
unused build:relay step; rename the runner to invoke-windows-host-cells.ps1.

Co-authored-by: m4air <m4air@Mac.localdomain>
…27424)

* feat(daemon): observe-only fork gate for automatic terminal restores

* fix(daemon): keep a canceled fork charged until reaped; never strand a user resume

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>
)

* feat(daemon): pace automatic terminal restores on headless hosts

* fix(daemon): cap the paced-restore window below remote client timeouts

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Removes seven modules that only their own tests import, dead orcad exports
and protocol constants, spells orcad's configuration exit code once, reuses
the shared errorMessage helper, and drops links to the deleted
docs/reference folder.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Nothing mints an SSH-bridge credential since the relay client was removed
(#27214), so the 'ssh-bridge' caller kind, its host binders, result filter,
the CLI bridge-mode policy and the ORCA_SSH_BRIDGE_CREDENTIAL env read could
never run. The allowRemoteCliControl target field stays for older peers.

Co-authored-by: m4air <m4air@Mac.localdomain>
* feat(daemon): pause automatic restores after a same-boot crash

* fix(daemon): per-version crash marker, owner-only writes, orderly login retirement, no Windows boot-time guess

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
OrcaWin and others added 30 commits October 12, 2026 08:36
…pp (#28007)

* feat(serve): run the --supervise supervisor from a copy outside the app

* fix(serve): record supervisor copy users beside the copies so a cleanup never races a fresh copy

* fix(serve): remove a partial supervisor copy when copying fails

* test(serve): skip the unreadable-copy case where mode 000 does not block reads

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
…progress (#28015)

Direct AppImage serve now unpacks its stable copy once, under one overall
deadline that also bounds the registration lock wait and both extraction
attempts, prints progress to stderr, and refuses fast when the private
cache is not writable, not executable, or too full.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
…es are gone (#28011)

* fix(daemon): stop giving new terminals to a daemon whose AppImage files are gone

On Linux AppImage, a terminal daemon can outlive the mount it runs from and
keep answering health checks until it SIGBUSes. Orca now reads the daemon's
/proc exe link and mount table at adoption: an idle severed daemon is
replaced; a busy one is kept for its sessions but quarantined, so fresh
terminals use the local provider and a healthy RPC never re-admits them.
Status and a verified restart are available over RPC and as
`orca terminal-daemon status|restart`.

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

* fix(daemon): lift the severed quarantine once a new daemon owns the endpoint

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

* fix(daemon): a severed-daemon restart counts fallback terminals and never stops a replacement

The restart re-checks the authenticated endpoint owner before any teardown
and again at shutdown, and never falls back to a pid kill when targeted.
Status counts the local fallback terminals the restart also ends.

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

* chore(daemon): merge duplicate import

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

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

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

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
…files are gone (#28021)

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
)

CodeBuddy spawns hooks registered with shell: 'powershell' as a bare
`pwsh`, so on Windows without PowerShell 7 every hook failed with
spawn pwsh ENOENT and the failed UserPromptSubmit hook blocked prompts.
Register CodeBuddy's hooks the way Claude's are: the managed .cmd path
on cmd-safe profiles (runs under CodeBuddy's Git Bash or Windows
PowerShell default shell) and the absolute powershell.exe encoded
launcher otherwise. The next install rewrites existing registrations.

Fixes #27182

Co-authored-by: Orca Worker <orca-worker@localhost>
…re (#27755)

* fix(browser): end a page's mount admission whenever it leaves the store

A background-opened page's start admission was released only by the two
close actions, so a bulk worktree purge (listing refresh, repo or folder
removal) dropped the page while its admission stayed in the set. Release it
in the one cleanup every page removal already runs through
(destroyRemovedBrowserWebview), and drop the per-path copies.

STA-7436

* refactor(browser): one removed-page sweep shared by the app and its test

Moves the store sweep that destroys removed pages into
destroyWebviewsRemovedFromStore so the admission regression test runs the
same code the app runs, and adds an eviction case.

* test(browser): assert pages were admitted before the eviction case
…ca (#27731)

* perf(notifications): save mobile dismissal history off the main thread

Every agent notification recorded its id in mobile-notification-dismissals.json
through the secure-file writer. On Windows that writer runs about five
synchronous icacls launches per write, so each notification froze every Orca
window (STA-9692).

The file holds notification ids, sequence numbers and session ids, not
credentials, so it now goes through the existing AtomicSnapshotWriter: async
temp-plus-rename, mode 0600, bursts coalesced into one write of the latest
entries. Memory stays authoritative, so delivery and retirement are unchanged.
The pending write is flushed in the quit barrier after chat and RPC teardown,
and in the pre-relaunch hook, since renderer relaunch exits without the quit
pipeline.

* perf(notifications): flush dismissal history when a headless host shuts down

orcad never runs the desktop quit barrier, so its cleanup list awaits the
pending dismissal write after RPC stops.

* fix(persistence): drain a snapshot write requested just after a drain ends

AtomicSnapshotWriter cleared its pending-write promise one microtask after
the drain loop saw no more requests. A write() in that gap set
writeRequested, got the settling promise back, and nothing drained it, so
the newest bytes stayed unwritten and waitForPendingWrite() (the dismissal
store's flush) resolved without them. The drain now clears the pending
promise in the same tick its loop exits, on success and failure, so a later
write() always starts a new drain.

* test(notifications): flush dismissal writes before deleting temp state dirs

Dismissal history now saves asynchronously, and the writer recreates its
directory before each write. Tests that deleted their temp dir with a write
still pending left recreated dirs behind in the system temp folder and could
race the delete on Windows. They now await the controller's flush first.

* perf(notifications): close dismissal history at committed exits

The shutdown flush only waited for writes already queued. A notification
recorded after it (orcad cleanups still running, or the 150 ms before a
renderer relaunch calls app.exit) started an async write the exit dropped,
so the phone could keep an alert the desktop could no longer withdraw.

The store's flush() becomes close(): it waits for the pending write, and
every later record saves synchronously through the snapshot writer's
existing writeSync (plain fs, no icacls; a failure only warns). It is called
only where the exit can no longer be vetoed: the will-quit teardown, a new
exit-only hook that app:relaunch runs before app.exit(0) (restart and
profile switches share onBeforeRelaunch but quit through will-quit), and
orcad's cleanup after RPC stops.

The quit test now holds chat teardown and RPC stop and proves the close
starts only after both settle, in either order. The orcad push-startup
runtime mock gains the method the entry now calls.

* fix(notifications): bound the dismissal close before a renderer relaunch

app:relaunch waited on the dismissal store's close with no limit, so a
stalled userData disk left Restart stuck on "Restarting…" with a half-quit
app saving synchronously. The close now races a 2 s cap through the
existing settleWithinMs helper, the same cap the restart path's auth
preservation uses, and logs when it gives up.

* test(notifications): keep async dismissal writes under test between reads

Mid-test disk reads used closeDismissals() as a barrier, which switched
every later record to the exit-only synchronous path. The store gains a
non-closing flush() (close() = mark closed + flush), exposed on the
controller as flushDismissals for tests; mid-test barriers use it, and
close stays only where a test simulates an exit or teardown. Also notes why
the writer's pendingWrite assignment is safe, renames a store test to what
it covers, and groups the orcad cleanup-order test under a describe.
…unches (#27756)

* ci: ratchet synchronous child-process launches in main and shared code

The Electron main process freezes every window while a synchronous child
process runs, and on Windows each launch is slow because antivirus scans
it. Nothing stopped new synchronous launches from landing in main-process
code, and the spawn observer behind ORCA_MAIN_THREAD_DIAGNOSTICS only saw
async spawns, so the worst blocks were invisible to it.

Ratchet: check:main-sync-spawn-ratchet parses production src/main and
src/shared files (test-only paths excluded) and fails when a file not in
config/main-sync-spawn-baseline.txt reaches execFileSync, spawnSync or
execSync from child_process, runProcessSync from @orca/process-host, or
one of the synchronous secure-file writers (they run icacls on Windows).
Named, aliased, namespace, default, require() and import() forms count.
A baseline entry whose file no longer launches synchronously fails as
stale, so the list can only shrink. It runs in lint and in the PR
static-analysis job next to the process-host import check.

Observer: runProcessSync now reports to the spawn observer, timing the
whole call (that is how long the thread was stuck) and reporting from a
finally so failing, throwing and timed-out children are counted too.

Runtime behavior is unchanged except that the diagnostics observer now
sees synchronous launches; with no observer registered the sync path
pays one extra branch.

* ci: count sync launches per file and gate the repo's sync wrappers

The sync-spawn ratchet only tracked which files launched synchronously, so a
file already on the list (wsl.ts, win32-utils.ts, git-exec-file.ts) could add
more launches unnoticed, and new callers of the repo's own synchronous
wrappers passed CI.

The baseline now stores a per-file count of uses, in the same format as the
focus-read ratchet: a rise or a new file fails, a lower or zero count fails
until `pnpm check:main-sync-spawn-ratchet --prune` lowers it, and --prune
never raises an entry. Every value reference to a gated function counts
(call, hand-off, alias and its uses, re-export); imports alone count nothing.

Gated wrappers: gitExecFileSync (and the git/runner re-export),
restrictWindowsPathSync, hardenSecurePath when called with sync: true,
grantDirAcl, getWslHome, listWslDistros, wslUncDirectoryExists,
isWslAvailable (and the wsl.ts re-export), isPwshAvailable. Their 18
current caller files join the baseline.

Failure messages now give advice per family: the async process-host API for
child-process launches, the async twin for wrappers, and for the secure
writers a note that no async secure writer exists yet, so the sync one stays
and must never become a plain write (that drops the Windows ACL hardening).

Also refresh two stale blockMs descriptions and drop a no-op line from the
spawn-observer "stays silent" test.

* ci: count hardenSecurePath unless visibly async, and tighten ratchet tests

hardenSecurePath was counted only when `{ sync: true }` was written inline,
so moving the options into a variable or a spread dropped the count and the
check then asked for a prune. It now counts every use except a call with no
options or a spread-free object literal whose `sync` is absent or false.

Also: resolve backslash paths in the importing file as well as the
specifier; still visit heritage and instantiation expressions
(`extends mixin(fn)`, `fn<T>`); state the reference-only counting limit;
and point the secure-writer advice at raising this file's baseline count.

Tests cover the hardenSecurePath shapes, the alias chains that need the
fixpoint loop, and a prune that must not raise a grown row.

* test(ci): pin the main sync-spawn ratchet background step

* fix: count secure hardening calls with spread arguments
… load (#27754)

* fix(browser): end a background page's mount admission after its first load

A page opened in the background is admitted so its guest starts, but the
admission was only released on close. Its overlay stayed laid out (invisible)
in the visible workspace and its browser chrome never unmounted in hidden
workspaces. Release the admission when the guest's first load settles, so the
normal active-page and paint-retention rules own the page afterwards.

STA-7435

* test(browser): pin how a released mount admission parks a background page

Covers the overlay contract the release relies on: in a hidden worktree the
page's browser chrome unmounts once the admission ends; in the visible worktree
it stays mounted and only its slot stops being laid out. Also corrects the
page-record comment that still said background tabs sit idle until shown.
…ete (#27625)

* fix(browser): settle WSL tunnel reads so responses past 256 KiB complete

The WSL execution route hands the deferred socket a credit-accounted tunnel
duplex, but the deferred socket forwarded its data without settling it, so the
relay's 256 KiB receive window was never replenished and larger responses
stalled. Settle each chunk once tunnel-socket listeners have taken it; their
pause() still holds the source, so unsettled bytes stay bounded by the window.

Fixes #27356

* test(browser): bound the WSL backpressure wait so a regression fails instead of hanging

* test(browser): keep unchanged WSL route test lines in their original form

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
…i, dsh, Kiro, Droid and Command Code (#27717)

* perf(hooks): register direct .cmd hooks for Gemini, DSH, Kiro, Droid and Command Code on Windows

On a cmd-safe profile path these agents now register the managed .cmd path
itself instead of a `powershell.exe -EncodedCommand` launcher, so each hook
event no longer pays a second PowerShell start-up on top of the runner's own
shell. Profiles the bare path cannot carry (spaces, non-ASCII, UNC) keep the
encoded launcher. Same pattern as Claude (#18875) and Muse (#26755).

Refs #20913

* refactor(hooks): one Windows batch-hook helper and one payload-delivery table

- Add wrapWindowsBatchHookCommand (bare .cmd path, encoded launcher
  otherwise) and use it for Muse, Gemini, DSH, Kiro, Droid and Command Code.
- Fold the five agents into windows-hook-payload-delivery.test.ts as rows
  run under each vendor's shell, from a plain and a spaced profile, with a
  stale encoded registration seeded per row; drop the separate runner suite
  and its CI list entries.
- Replace the Windows-only per-agent registration tests with ones that stub
  process.platform, so they run on every CI OS.
- Fix stale launcher, helper and DSH comments.

Refs #20913

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
* Show recorded turn diffs inline in native chat

* Resolve only selected turn diff tool pairs
…view (#27184)

* feat(terminal-pane): header button resumes the CLI session in a new native chat

The pane header's "Show chat view" toggle is replaced by the tab menu's
"Resume in New Native Chat" move. The Session History lookup runs on click,
so an always-visible header never scans in the background. A pane already
in chat view keeps its "Show terminal" way back.

* fix(terminal-pane): fold the resume gate into the pane's existing store subscription

Keeps a mounted terminal pane inside its pinned store-listener budget.

* fix(terminal-pane): resume the pane's own conversation, open the chat that already holds it

The header "Resume in New Native Chat" button gated on chat-view eligibility but looked up the
tab's best conversation, so a Grok pane beside a Claude pane resumed the Claude conversation, and
Grok/OpenCode panes showed a button that always failed with a "try again" toast.

- Map one pane to its conversation with the tab-title mapping narrowed to that pane (same sources,
  same Claude/Codex rule), for both the button's visibility and its click; this also keeps the
  button after the agent exits, as the tab menu does.
- A conversation a native chat already holds now matches its history row; the move's gate still
  refuses it (tab menu unchanged, pinned in the parity test), and the button opens that chat.
- Distinct messages: nothing saved yet, cannot be resumed in a native chat, or the existing
  generic failure. The click forces a fresh scan like the panel's Refresh does.
- Call the plain resume launch directly; drop the shared actions module and restore the tab menu.
- A tab closed during the lookup no longer opens a chat.

* fix(terminal-pane): hide Resume in New Native Chat where the chat route is refused

On a local workspace whose project runs in WSL (or needs runtime repair), the header button showed
on Claude/Codex panes, but native chat cannot launch there, so every click said the conversation
can't be resumed while the tab menu offered nothing. The button's visibility now also asks the
same launch-route check the Session History row gate uses (structuredAgentSessionLaunchFeasible).

* fix(terminal-pane): offer the header resume only while the pane's agent is running

The tab menu now offers Resume in New Native Chat only while the tab's agent
runs (#27520). The header button still found its pane's conversation from the
retained or sleeping record an exited agent leaves behind, so it stayed after
exit. The pane lookup now honours the same runningAgentsOnly option, and both
the button's visibility and its click pass it.

* refactor(terminal-pane): the header's pane lookup reads only the running agent

Every caller of resolvePaneAiVaultTitleRequest asked for running agents only,
so its retained and sleeping sources were never read. The pane lookup now maps
just the pane's live agent, and collectAiVaultTitleRequests returns to main's
shape, sharing only the live-agent mapping with it.
…was working (#27821)

* fix(native-chat): light the tick of a message sent mid-turn on the message rail

A message sent while the agent is working joins the running turn's key, and the
rail lit ticks by turn key, so that message's tick never lit: not at the bottom,
not after clicking it. Light the newest tick at or above the read row within its
turn instead.

* fix(native-chat): light the nearest tick above the read row, and keep a clicked tick lit

Review round 1. The within-turn walk stopped at a row of no turn landing in the
running turn, and could return a tick from the turn above. The lit tick is now
the newest loaded rail item at or above the row at the fold, falling back to its
turn key above the first loaded one (that prompt is in the outline).

A jump that cannot bring its row to the top lands clamped at the bottom, and the
settled position report then lit the last tick instead of the clicked one. A
clicked tick now stays lit until the reader scrolls or starts another navigation
(the existing reader-input path), or the set of prompts changes.

Rail picks and the start of every navigation move into their own hook, as the
list is at its line limit.

* fix(native-chat): hold a clicked rail tick by position, not by reader input

Review round 2. Releasing the clicked tick on reader input needed every scroll
source to report itself, and several do not (scroll-to-top button, inline diff
reveal, prompt refold, selection-drag autoscroll); releasing it whenever the
prompts changed dropped it on every head trim of a long live session.

The hold now records where the jump settled, at the first idle read after the
click, and keeps the clicked tick lit while the view rests there. Any movement,
whatever caused it, releases it, as does the clicked message leaving the rail.
Drops the navigation wiring and the hook it was moved into.

* fix(native-chat): anchor a clicked rail tick's hold to its row on screen

Review round 3. Holding while scrollTop stayed put recorded the reader's spot when
they kept scrolling within the idle period of the jump, kept an off-screen row lit
after the viewport shrank, and let go when a row above re-measured late and shifted
scrollTop with the content.

The hold now records how far below the viewport top the clicked row settled, and
keeps the tick lit while the row is still on screen at that offset.
* [CI] Remove wall-clock waits and stabilize GC fairness check

* [CI] Include the final GC timer gap in the fairness check

* [CI] Advance test clocks and preserve cleanup checks
…#28045)

* fix(chat): name orchestration worker chats after their task title

Worker chats were skipped by chat naming and stayed "Claude Chat".
Name them from the task they were started for instead, with no model call.

* Fix legacy worker chat titles without prompt generation

* Remove unrelated lockfile dependency
…result (#27580)

* fix(native-chat): keep a 'chats resumed' bottom-bar entry until the result is seen

After every chat resumed, the bottom-bar entry vanished the moment the run
finished, leaving a fading toast as the only way back to the list. Show
'N chats resumed' while a finished run's summary has not been shown yet,
reusing the run's existing finishedViewShown flag so the entry ends exactly
when the summary is viewed and closed.

* Keep resume results until their rows are seen

* Retire viewed empty resume runs

* Show finished resume rows after in-flight view settles
…ary (#28054)

#27756's script imported baseline helpers from check-focus-read-ratchet.mjs
after #27788 had moved them into ratchet-baseline.mjs, so it crashed on start
and failed every PR that merges main. Use the library's helpers, and record the
two daemon-process launches from #27769 that the broken check never saw.

Co-authored-by: Orca Worker <orca-worker@localhost>
* feat(native-chat): wrap long lines in code blocks

Code blocks in native chat now have a wrap-lines toggle next to Copy.
It is off by default, per block, and not persisted; when on, the block
wraps long lines instead of scrolling sideways.

* fix(native-chat): give each document code block its own component

The document pre renderer called the code-block renderer as a function after
an early return, so a renderer with hooks ran them as the pre's own hooks and
React saw the hook order change when a fence switched between diagram and code.

* fix(native-chat): keep a block's line wrap when its row scrolls away

Wrap lived in the code block's own state, so a row that windowing unmounted,
or a growing reply re-cutting its top-level blocks, came back unwrapped and
shorter than its measured height. The flag now lives in the transcript's
disclosure store, keyed by the row and the fence's offset in the whole
message, which appended text never moves. Outside a transcript the block
keeps local state as before.

* fix(native-chat): key a block's line wrap by its line, not its character offset

While a reply streams, its last block is parsed after the streaming fixes
escape characters such as x~y mid-line, so a later fence's character offset
moved by one when the block was next parsed as written (the reply finishing,
another block following, or the reply merging into one block), and the wrap
reset. Those fixes never add or remove a line break before a fence, so the
block splitter now records each block's starting line and the wrap key uses
the fence's line in the whole message.

* fix(native-chat): keep line wrap in plan, reasoning and approval cards too

Plan cards and reasoning bodies rendered their markdown without a row key,
so their code blocks fell back to local wrap state and lost it when the row
scrolled away. Reasoning text now uses its existing disclosure key, shared by
the live line and the finished row, and a plan card uses plan:<message id>, so
neither can share a key with the reply of the same message. The approval card
sits in the composer slot, outside the transcript and its store, and is never
windowed, so it keeps local state.
…27714)

* perf(hooks): register direct .cmd hooks for Gemini, DSH, Kiro, Droid and Command Code on Windows

On a cmd-safe profile path these agents now register the managed .cmd path
itself instead of a `powershell.exe -EncodedCommand` launcher, so each hook
event no longer pays a second PowerShell start-up on top of the runner's own
shell. Profiles the bare path cannot carry (spaces, non-ASCII, UNC) keep the
encoded launcher. Same pattern as Claude (#18875) and Muse (#26755).

Refs #20913

* fix(kimi): register a cmd.exe-runnable hook on Windows

Kimi runs hooks with asyncio.create_subprocess_shell, which is cmd.exe on
Windows. Orca registered a POSIX `if [ -f ... ]` command there, so cmd failed
with "-f was unexpected at this time" before reaching the script and, since
Kimi fails open, no status event was ever sent.

On win32 write a cmd+curl kimi-hook.cmd and register it the way the other
direct .cmd agents do: the bare forward-slash path on a cmd-safe profile,
the encoded launcher otherwise. The ownership matcher already covers every
extension of the stem, so the old kimi-hook.sh entry is replaced on the next
install. SSH remotes keep the POSIX script.

Kimi joins the direct .cmd runner suite as a row run the way CPython's
shell=True does, and every row now also runs from a profile with a space
(the encoded launcher) and outside an Orca pane.

* refactor(hooks): one Windows batch-hook helper and one payload-delivery table

- Add wrapWindowsBatchHookCommand (bare .cmd path, encoded launcher
  otherwise) and use it for Muse, Gemini, DSH, Kiro, Droid and Command Code.
- Fold the five agents into windows-hook-payload-delivery.test.ts as rows
  run under each vendor's shell, from a plain and a spaced profile, with a
  stale encoded registration seeded per row; drop the separate runner suite
  and its CI list entries.
- Replace the Windows-only per-agent registration tests with ones that stub
  process.platform, so they run on every CI OS.
- Fix stale launcher, helper and DSH comments.

Refs #20913

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
…nd UTF-8 safe (#27718)

* perf(hooks): register direct .cmd hooks for Gemini, DSH, Kiro, Droid and Command Code on Windows

On a cmd-safe profile path these agents now register the managed .cmd path
itself instead of a `powershell.exe -EncodedCommand` launcher, so each hook
event no longer pays a second PowerShell start-up on top of the runner's own
shell. Profiles the bare path cannot carry (spaces, non-ASCII, UNC) keep the
encoded launcher. Same pattern as Claude (#18875) and Muse (#26755).

Refs #20913

* fix(kimi): register a cmd.exe-runnable hook on Windows

Kimi runs hooks with asyncio.create_subprocess_shell, which is cmd.exe on
Windows. Orca registered a POSIX `if [ -f ... ]` command there, so cmd failed
with "-f was unexpected at this time" before reaching the script and, since
Kimi fails open, no status event was ever sent.

On win32 write a cmd+curl kimi-hook.cmd and register it the way the other
direct .cmd agents do: the bare forward-slash path on a cmd-safe profile,
the encoded launcher otherwise. The ownership matcher already covers every
extension of the stem, so the old kimi-hook.sh entry is replaced on the next
install. SSH remotes keep the POSIX script.

Kimi joins the direct .cmd runner suite as a row run the way CPython's
shell=True does, and every row now also runs from a profile with a space
(the encoded launcher) and outside an Orca pane.

* fix(copilot): run the Windows status hook as a direct curl .cmd

Copilot runs a Windows hook as `pwsh|powershell -nop -nol -c <command>`. Orca
registered an encoded `powershell.exe -EncodedCommand` launcher there that
started a second PowerShell to run copilot-hook.ps1, which posted with
Invoke-WebRequest: two PowerShell starts per event (~850-900 ms median), and
the .ps1 read stdin through the console code page, so non-ASCII prompt text
arrived as "caf?? ...".

On win32 write copilot-hook.cmd (answer `{}`, refresh the endpoint, env guard
before stdin, curl form POST with hookEventName) and register its bare path
with the event as its argument; non-cmd-safe profile paths keep the encoded
launcher, which now sets ORCA_COPILOT_HOOK_EVENT for the .cmd. The managed
matcher covers every extension of the stem, so old .ps1 entries are replaced
on the next install while user hooks stay. SSH remotes keep the POSIX script.

Copilot joins the direct .cmd runner suite as a row run under both
PowerShells, checking the byte-exact payload and the posted hookEventName.

Refs #20913

* refactor(hooks): one Windows batch-hook helper and one payload-delivery table

- Add wrapWindowsBatchHookCommand (bare .cmd path, encoded launcher
  otherwise) and use it for Muse, Gemini, DSH, Kiro, Droid and Command Code.
- Fold the five agents into windows-hook-payload-delivery.test.ts as rows
  run under each vendor's shell, from a plain and a spaced profile, with a
  stale encoded registration seeded per row; drop the separate runner suite
  and its CI list entries.
- Replace the Windows-only per-agent registration tests with ones that stub
  process.platform, so they run on every CI OS.
- Fix stale launcher, helper and DSH comments.

Refs #20913

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
An exited node.exe copy can stay locked for a moment on Windows, so the
afterAll rm failed the hourly build's windows-11-arm persistence job.
…focus doesn't freeze the UI (#27707)

* fix(rate-limits): spawn the Codex usage probe off the main thread

On Windows, creating a cold standalone codex.exe can spend seconds in code
integrity checks on the calling thread. The usage probe runs on window focus,
so that froze the UI. The probe's child is now created and piped on a worker
thread and proxied back to the main thread.

Fixes #27362

* refactor(rate-limits): type the worker's children through @orca/process-host

* fix(rate-limits): kill an orphaned Codex probe by pid when its worker dies

Once the worker thread that owns the probe's child is gone, kill() can no
longer reach the child through it. The proxy now falls back to a pid-addressed
kill; the Codex probe uses the app-server tree kill so a node launcher's
codex.exe child goes too, while SIGTERM stays a graceful root signal.

* refactor(build): move the main build's entry map out of electron.vite.config.ts

The new worker entry pushed electron.vite.config.ts past the 300-line limit.
The Rollup input map for the main build now lives in its own module.

* refactor(rate-limits): drop the orphan pid kill and trim the worker child-process stand-in

A worker thread's death closes its children's pipes, so an abandoned Codex
probe already gets stdin EOF, its graceful stop. The pid-addressed orphan kill
was redundant with that and with terminateCodexProbeChild's own gated Windows
tree kill. Its hard kill also bypassed the tree-kill gate's recycled-pid refusal
by turning the handle-addressed fallback into a pid kill.

- Remove orphan-pid-kill, killOrphanedCodexProbe, the injected killOrphan, and
  the orphaned flag. kill() now reports false once a child is no longer tracked.
- Process-creation failure is one 'spawn-error' event that the stand-in expands
  into error/exit/close, replacing the duplicated triple and the exported exit
  code. Stdin errors arrive as child errors, as the probe already treated them.
- Fold both suites into one over a real MessageChannel and real children. Add a
  real-worker test showing that terminating or crashing the worker lets the child
  exit on stdin EOF.

* test(rate-limits): run the worker child-process suite under Node

The suite asserts Node behavior that Electron main relies on: a failed spawn
closes with a null signal, and a dead worker thread closes its children's
pipes. Bun reports an undefined signal and, on Linux, leaves the pipes open,
so the Bun lane failed on runtime differences, not on the product code.

* test(rate-limits): count a reaped-by-nobody zombie as an exited child

On Linux and macOS a child whose worker thread died gets stdin EOF and exits,
but libuv reaps a child only through the handle on the loop that spawned it.
Once that loop is gone, nothing waits on the pid, so it stays a zombie and
kill(pid, 0) still succeeds. The real-worker tests now treat a zombie as
exited, and still require the child's EOF marker.

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
* feat(chat): read one saved row per subagent

Adds the singular `subagent` journal block (one row per child, revised in
place, with a plain-text twin for older clients) and teaches every reader
to draw it: desktop and phone fold a turn's sibling rows into the existing
"Ran N subagents" row, the client and off-page rosters name children from
their own rows, the reopen pass settles a row a dead host left working,
and the phone and worker-transcript wires bound it. No producer writes the
block yet; that switch lands in the follow-up.

* fix(chat): open subagent background flag, separate nested group ids, page window skips subagent rows

- background is an open boolean so a later build's value never fails a chat load
- a helper's own helpers fold under a groupId of their own, so the phone opens them apart
- each subagent's own row rides along with the page window instead of counting as conversation

* refactor(chat): a subagent's group entry as one object
Preserve all 52 Markdown cases while consolidating one file and importing existing direct owners. Advance editor debounce windows through public selective timers, exclude unrelated lint tests from Performance PR triggers, and remove a pane assertion case entailed by retained coverage.
* refactor: move provider-retry pieces into a sibling words file

Pure move of the six provider-retry translation pieces out of
agent-session-failure-words-text.ts, which sits at the 300-line limit.
No key or behaviour change.

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

* fix(chat): name the agent when it refuses a message

"The provider did not accept this message" becomes "{{agent}} didn't
accept this message" ("The agent" when the name is unknown), in English
and every locale. The providerRejected failure kind is unchanged.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
* Require Codex 0.136.0 for native chats

* Add host-owned Codex install and update actions

* Bind Codex checks and maintenance to the selected installation

* Resolve Windows updater cleanup through system helpers

* Use one child environment for Codex checks and launch

* Expire Codex readiness and reconcile current host jobs

* Resolve Codex checks against the child command search path

* Restrict global npm repair to the selected Codex install layout

* Pin the host updater directory through Windows supervision

* Synchronize Codex maintenance fixtures on readiness

* Unify Codex notices and recover version-refused chats

* Route Codex recovery to the operation that remains

* Preserve Codex setup execution errors in the job log

* Preserve UTF-8 in Codex setup logs

* Remove unused Codex host installation detector

* Require host administration for Codex maintenance

* Show a missing Codex explanation once

* test(native-chat): give scripted Codex runtimes their installation fact; keep the registry out of the relay

Runtime tests that drive a scripted Codex app-server still ran the new
version check against whatever codex was on the machine's PATH: refused
on CI (no codex), and on a dev machine it ran the real codex --version.
A scripted transport now must state the Codex it stands for
(readCodexInstallation), asserted at install, and the tests get it from
one scriptedCodexTransport fixture.

The relay's Codex maintenance path reached @orca/windows-registry through
resolvePathEnvKey's re-export in windows-environment-path; import it from
its registry-free module instead.

Also update the catalog-probe test for the shared child environment.

* refactor(native-chat): drop the Update Codex action; keep Install for a missing Codex

A too-old Codex now gets only the plain message ("Codex X is too old for
chats. Update to 0.136.0 or newer.") with no button and no job. The
existing rechecks (window focus, the <=30 s expiry of a saved result)
retry the refused chat once the user has updated Codex themselves.

A missing Codex keeps the Install Codex button: `npm install -g
@openai/codex` with the npm on the chat's PATH, as a host-run job with
its log, then a recheck and recovery. A configured Command that is
missing gets the message only, since a global npm install would not
land at that path.

Removed: the `codex update` command and its 0.126 floor, manual
instructions, npm layout and launcher identity detection, the action
descriptor (install|update) and its titles, the legacy capability, and
the SSH relay maintenance path (structured chat never runs over SSH or
WSL), so the relay preflight handler is back to main's.

* refactor(native-chat): keep Install Codex in Settings only

When Codex isn't installed, main already hides Codex from the New tab menu,
so the in-chat Install button was almost never reachable. Settings → Agents
already shows "Codex isn't installed." with Install Codex.

- Chats and the new-workspace composer now only state "Codex isn't
  installed." (no button, no install job from the chat).
- The Install action, its busy spinner and the host-busy subscription move
  into the Settings Codex row, the only place that starts the job.
- The store no longer re-checks every other workspace context when a job
  finishes. A refused chat recovers through the same re-check a too-old
  Codex uses (window focus, or the <=30 s expiry of a saved result).
- The chat recovery key no longer includes the install job id.
- Tests: Settings drives the install; a new test proves a chat refused for
  a missing Codex recovers through focus after a Settings install, not
  through the job; the expiry recovery test covers missing and too old.

* refactor(codex): check chat compatibility when acquiring the child

* Restore workspace submit guard and simplify Codex maintenance

* Keep only the Codex chat minimum version
… Devin, ZCode and Antigravity (#27713)

* fix(agent-hooks): register forward-slash Windows hook paths for Grok, Devin, ZCode and Antigravity

These four agents registered their native-Windows status hook as a bare
backslash path. A Git Bash hook host strips the backslashes, the command exits
127, and no status event reaches Orca. Use the same direct forward-slash form
Claude, Codex and Muse already register, keeping the cmd-safe gate and the
encoded PowerShell fallback. Existing backslash entries still match the
managed-command matcher, so the next install replaces them and keeps user hooks.

* perf(hooks): register direct .cmd hooks for Gemini, DSH, Kiro, Droid and Command Code on Windows

On a cmd-safe profile path these agents now register the managed .cmd path
itself instead of a `powershell.exe -EncodedCommand` launcher, so each hook
event no longer pays a second PowerShell start-up on top of the runner's own
shell. Profiles the bare path cannot carry (spaces, non-ASCII, UNC) keep the
encoded launcher. Same pattern as Claude (#18875) and Muse (#26755).

Refs #20913

* test(agent-hooks): expect forward-slash Antigravity hooks after the Unicode upgrade

ASCII profiles now register the direct forward-slash wrapper path, so the
upgrade-delivery test expects that form and also runs it through Git Bash.
Unicode profiles keep the encoded launcher.

* test(agent-hooks): assert the registered hook lives under the mocked home

Pins that the os mock reaches the agents' node:os imports, so the host test
never installs into the real profile.

* refactor(hooks): one Windows batch-hook helper and one payload-delivery table

- Add wrapWindowsBatchHookCommand (bare .cmd path, encoded launcher
  otherwise) and use it for Muse, Gemini, DSH, Kiro, Droid and Command Code.
- Fold the five agents into windows-hook-payload-delivery.test.ts as rows
  run under each vendor's shell, from a plain and a spaced profile, with a
  stale encoded registration seeded per row; drop the separate runner suite
  and its CI list entries.
- Replace the Windows-only per-agent registration tests with ones that stub
  process.platform, so they run on every CI OS.
- Fix stale launcher, helper and DSH comments.

Refs #20913

* refactor(agent-hooks): register Grok, Devin, ZCode and Antigravity through wrapWindowsBatchHookCommand

Use the shared batch-hook helper from #27717 instead of an inline direct-or-cmd
fallback. A UNC home is cmd-safe but has no drive letter; the old fallback
registered it as a bare backslash path that Git Bash cannot start, while the
helper sends it to the encoded launcher.

wrapWindowsCmdHookCommand has no production callers left, so it is removed and
its tests point at the helper. Grok, Devin and ZCode join the Windows payload
delivery table with a legacy backslash entry to sweep, replacing the separate
host test, and the Antigravity migration test now runs on every platform.

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
…ile downloading (#28056)

The OTA download screen now shows a progress bar over bytes received, labelled "Downloading the latest app" with a "from <host> · X.X of Y.Y MB" caption, instead of a spinner with raw file and byte counts. Related offline and failure copy says "the latest app" instead of "the workspace".

The bar barely moved before because React never committed progress during the download. SHA-256 of each asset ran in JS (3.8 s of a 5.7 s download), and React Native's scheduler runs queued native callbacks ahead of normal-priority renders, forcing a render only after 5 s. Assets are now hashed with expo-crypto's native digest (~5 ms total), so progress renders every 100–250 ms at 1 MB/s. Verified on an Android emulator and the iOS simulator.

The bar glides between bursts of progress (moving average of burst gaps, clamped 200–2000 ms) and is ShellWaitingFrame's indicator, so the label keeps the same position as on "Opening workspace". On an uncapped local link, decryption still keeps the JS thread busy and the bar can jump; that is left for a follow-up.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

⤵️ pull merge-conflict Resolve conflicts manually

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants