* feat(ssh): support Kerberos/GSSAPI hosts via the system OpenSSH transport
ssh2 has no gssapi-with-mic support, and adding it would mean forking its
protocol layer plus packaging the kerberos native module for three
platforms. Instead, route GSSAPI hosts through the existing system-OpenSSH
transport, which delegates Kerberos (tickets, SSPI on Windows) to the
platform ssh binary.
Two tiers, because RHEL-family distros enable GSSAPIAuthentication
globally in /etc/ssh/ssh_config and ssh -G therefore reports it for every
host:
- Targets whose ~/.ssh/config Host block explicitly sets
GSSAPIAuthentication yes (imported as target.gssapiAuthentication) try
system ssh first, falling through to ssh2 so key auth and credential
prompts still work when no ticket is available.
- When ssh2 exhausts key/agent auth and the ssh -G-resolved config
enables GSSAPI, retry over system ssh before prompting for credentials,
so Kerberos-only hosts on distro-default configs connect without a
password prompt. Hosts where keys work never leave the ssh2 path.
Manual targets flagged for GSSAPI pass -o GSSAPIAuthentication=yes
explicitly since they bypass ssh_config. Both tiers work headless (no
credential callbacks required).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ssh): harden GSSAPI transport selection (review fixes for PR #7507)
Review fixes on top of the Kerberos/GSSAPI feature branch (s546126/kerberos-ssh):
- HIGH: reset useSystemSshTransport on the ssh2 fall-through. doSystemSshProbe
sets the flag before spawnSystemSshCommand, which throws synchronously when no
system ssh binary is on PATH (outside the probe try/catch). The proactive
fall-through previously reset only 2 of 3 transport fields, so exec/sftp kept
routing through the failed transport - breaking GSSAPI on Windows-with-Git-ssh
and headless Linux.
- MEDIUM: throw a cancellation error (not the stale ssh2 authError) when a
disconnect supersedes the reactive probe mid-flight, and guard connect()'s
catch on disposed, so a deliberate disconnect is not overwritten with
auth-failed.
- MEDIUM: skip the encrypted-key passphrase prompt when the GSSAPI fallback
applies, so a Kerberos ticket is tried before prompting; the general prompt
still fires if the probe fails.
Adds 3 mutation-verified regression tests and hardens two existing tests to
assert the probe actually ran. Not connected to any PR remote.
Co-authored-by: Orca <help@stably.ai>
* fix(ssh): isolate GSSAPI system transport
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: s546126 <268420947+s546126@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* fix(mobile): detect and repair overriding Windows Firewall Block rules for pairing
Firewall inspection now reports an overriding inbound Block rule as
blocked instead of false success, the UAC repair removes only
conflicting rules for the current Orca executable, TCP pairing port,
and Private profile before recreating the scoped allow rule, and the
notice re-inspects Windows policy after repair instead of optimistically
reporting success. Stale focus-triggered inspections can no longer
overwrite a newer result during UAC elevation.
Fixes#8371
* fix(mobile): inspect the ActiveStore so GPO firewall rules are visible
Without -PolicyStore ActiveStore the NetSecurity queries read only the
local persistent store, so a GPO-applied Block rule was invisible and
the post-repair re-inspection could report a false success on managed
hosts.
* fix(speech): resume interrupted voice model downloads and surface real errors
Voice model downloads that died late in the transfer (flaky networks,
scanning proxies) failed at the UI's 90% progress cap and restarted from
byte zero on every retry, so affected users could never complete any
model download (#8620 / STA-1775).
- Retry transient download failures (net::ERR_* transfer cuts, resets,
timeouts, HTTP 5xx/429) with an HTTP Range resume from the bytes
already on disk, restarting from the canonical URL each attempt since
signed CDN redirect URLs expire; bounded backoff, gives up after
repeated zero-progress attempts with a diagnosable error.
- Correct catalog sizeBytes to exact upstream asset sizes (parakeet
v2/v3 are 482/487 MB, not 170/180; paraformer is 1,047 MB, not 115) —
also the progress denominator when content-length is missing.
- Show the underlying failure cause in the download-error toast and log
it in main; it was previously invisible everywhere.
All 7 pinned catalog SHA-256 hashes verified against current upstream
assets. Live-verified end to end: a transfer killed mid-body resumes
with a 206 and completes through checksum, extraction, and validation.
Fixes#8620
* fix(speech): harden resumed model downloads
* fix(speech): validate resumed download completion
* fix(speech): keep segmented download resumes progressing
* fix(speech): don't abandon a download that keeps making progress
The retry loop gave up after 8 total failures regardless of progress, so
a large model (e.g. the ~1GB Paraformer) over a network that resets every
few hundred MB was abandoned mid-download even though every attempt was
resuming and advancing — the exact 'stuck then fails' symptom, just later.
Replace the all-time failure budget and the separate advancing-segment
cap with a single rule: give up only on a genuine stall (repeated
attempts with no forward progress) or an absolute request ceiling that
bounds a pathological tiny-segment server. Any download that keeps
advancing now runs to completion.
Verified live under Electron 43: a transfer reset mid-stream on every
attempt but advancing each time previously gave up at 67% after 8
attempts; it now completes. All prior resume/cancel/timeout scenarios
still pass.
* Surface Grok unified-billing monthly usage instead of a permanent warning
Unified-billing Grok accounts have no weekly credits: the
/billing?format=credits view returns a config without
creditUsagePercent, so the status bar was stuck on 'Grok billing
response did not include credit usage' even though the account has a
real quota. The default (format-less) /billing view reports it as an
included monthly budget (monthlyLimit/used with the billing period).
When the credits view has no weekly credit usage, read the default view
and surface monthly usage as the provider's 30-day window (already
supported by the tooltip and chip visibility for OpenCode Go). If the
fallback read fails, the previous 'unavailable' presentation stands
rather than escalating to an error chip.
* Review fixes: chip renders monthly-only usage; fallback failures keep stale data
- StatusBar ProviderSegment: monthly window is chip-visible when it is the
sole window (Grok unified billing); fetching/error no-data guards and the
icon-only dot now count monthly, matching tooltip.tsx. OpenCode Go chips
are unchanged (monthly stays tooltip-only next to session/weekly).
- grok-fetcher: monthly-fallback request failures propagate as 'error' so
applyStalePolicy keeps the last good monthly snapshot; 'unavailable' is
reserved for a successful response without monthly fields.
* Settings: show Grok monthly usage row for unified-billing accounts
Why: the Grok accounts section only rendered the weekly-credits row, so
unified-billing accounts showed a signed-in state with no usage at all.
* Use generated localization keys for Grok monthly copy
* Stop usage chips flashing and back off retries for failing providers
Two behaviors made the status bar unusable when any provider was
persistently failing (bad auth, unsupported plan):
1. Every refetch repainted all providers as 'fetching', so a settled
error chip flashed to a loading "…" chip and back on every cycle.
withFetchingStatus now keeps settled states (ok/error/unavailable)
visible until the new result lands; only providers with no settled
state (first load, explicit account-switch clear) show loading.
2. Error providers on the fast activation-retry lane (claude/codex/
grok) were retried every 30s on any focus/show/restore event —
forever. Repeated hits drove Claude's tight-budget usage endpoint
into 429s, flipping the chip between 'Limited' and its actual error.
Retries now back off exponentially per consecutive applied failure
(30s, 60s, 120s, … capped at the 15-minute poll cadence) and reset
on success or account/target switch.
* Count full fetches as failure-lane retries and keep Grok pane refresh feedback
- Stamp failing providers' activation-retry clocks when a stale-driven full
fetch runs, so the individual failure lane does not fire a redundant retry
right after the full fetch already retried them.
- GrokUsagePane: manual refresh spinner/disable is now renderer-local, since
settled snapshots no longer repaint as 'fetching' during refetches.
- Strengthen the backoff-reset test so it distinguishes a reset streak from a
stale retry timestamp (CodeRabbit), and add a regression test for the
full-fetch retry stamping.
* Fall back to legacy keychain when stale scoped Claude credentials 401
Claude Code maintains the legacy 'Claude Code-credentials' Keychain item
for the default config dir, but a scoped item (service suffixed with
sha256(configDir)) can be left behind by sessions that ran with
CLAUDE_CONFIG_DIR set. Once its access token expires, nothing refreshes
it: readFromKeychain returned the scoped token unconditionally, every
usage fetch 401'd as 'stale-token', and system-default auth has no CLI
recovery lane — so the status bar showed 'Refreshing sign-in' forever
while the legacy item held a perfectly valid token.
Two changes:
- readFromKeychain: an actual access token from the legacy item now
beats scoped refresh-only credentials (Orca cannot refresh tokens
itself, so refresh-only must not shadow a working token).
- On a stale-token OAuth failure with scoped-keychain credentials, retry
once with the legacy item's token before classifying the failure.
Host system-default auth only — managed/WSL credentials never fall
back to the host user's legacy keychain item.
The legacy retry runs before CLI repair, so a readable working token is
preferred over launching a hidden 25s claude PTY.
* Pin WSL-gate and same-token short-circuit for legacy keychain retry
The legacy-keychain retry must never answer a WSL target with the host
user's keychain account, and must not double the usage request when the
legacy item mirrors the failed scoped token. Add tests locking both.
* Classify Codex app-server chatgpt-auth-required as an auth error
When auth.json holds only an OPENAI_API_KEY (no ChatGPT tokens), the
app-server RPC rejects account/rateLimits/read with "chatgpt
authentication required to read rate limits". That string matched none
of CODEX_AUTH_ERROR_PATTERNS, so fetchCodexRateLimits fell through to
the hidden PTY /status probe, which cannot render usage for such
accounts and burned the full 15s PTY timeout on every refresh cycle,
surfacing as a permanent "Refresh failed — PTY timeout" status chip.
Classify the message as an auth error so the RPC result is returned
directly (fast, accurate) and no PTY is spawned.
* Keep auth-required usage errors from rendering as a rate-limit Limited label
The new Codex app-server error 'chatgpt authentication required to read
rate limits' mentions rate limits only as the object it failed to read,
but the status bar's rate-limit classifier matched the phrase and
labeled the chip 'Limited'. Classify authentication-required messages as
auth failures so they get the standard softened refresh copy instead.
* Suppress Git Credential Manager OAuth popup on git clone (fixes#7652)
Orca's git runner disables the interactive credential prompt on every git
call that goes through gitExecFileAsync/gitStreamStdout, but the two raw
'git clone' spawns (desktop repos:clone and the runtime clone path) passed
no env, so they inherited process.env with no guard. On Windows a clone
that needs GitHub auth then makes Git Credential Manager pop its
'Connect to GitHub' OAuth window, and in a network-restricted intranet the
browser/device flow never completes while git's credential retry re-pops it.
Apply nonInteractiveGitEnv() to both clone spawns so the prompt is
suppressed (GCM_INTERACTIVE=never, credential.interactive=false,
GIT_TERMINAL_PROMPT=0). The credential *helper* is kept, so cached-token
clones for private repos still work; only the interactive fallback popup is
disabled and the clone fails fast with a clear error instead.
* Suppress GCM OAuth popup in agent terminals and setup hooks too (#7652)
The clone-spawn fix stopped Orca's own managed git from popping Git
Credential Manager, but git run in terminals and setup scripts inherited
process.env with no guard. That is the more likely source of the reported
loop: agents are told to run 'git pull --rebase'/'git fetch'/retry 'git
push' (preamble + conflict/push-failure prompts), and each retry re-pops
GCM's 'Connect to GitHub' window in a network-restricted intranet.
Apply the credential-prompt guard to:
- setup/archive/hook scripts (hooks.ts non-WSL exec env), which run
unattended on worktree create/archive.
- the shared PTY host env (buildPtyHostEnv), via a small
applyTerminalGitCredentialPromptGuard helper. Agent terminals are
guarded unconditionally (they cannot dismiss a GUI popup); user
terminals are guarded by default via the new
terminalSuppressGitCredentialPrompt setting so power users can opt out.
The credential helper is kept, so cached gh auth still works; only the
interactive fallback prompt is disabled. Verified end-to-end in a real
Orca terminal (GIT_TERMINAL_PROMPT=0 + GCM_INTERACTIVE=never by default;
absent when the opt-out is set).
* Scope user-terminal credential guard to Windows, add settings toggle, forward guard into WSL (#7652)
* Retrigger PR checks (Actions dropped the synchronize dispatch for 57e7ce249)
* Keep shell locale out of the terminal/hook credential guard (#7652 review fix)
* Fix Fable review findings: guard WSL hook branch, wire settings search, catalog keyword keys, sparse-env askpass, one-shot agent classification (#7652)
* fix(terminal): harden Git credential popup guard
* test(pty): cover SSH credential guard setting
* fix(git): guard remote clones and setup runners
* fix(git): scope credential guards to unattended work
---------
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
* fix(settings): collapse Projects list to one row + pane per project (#8566)
Settings and its nav enumerated repo rows, so a project set up on multiple
execution hosts (local + a Remote Orca Server, or the same repo cloned on two
machines) rendered two nav rows + two panes that collided (duplicate React
key/DOM id) or mirrored. Derive the Settings list from the project projection
(repos-only, deterministic) so nav, panes, and the Cmd+J palette all collapse
to one entry per project. Deep-link repoId targets resolve to the project's
representative section.
* fix(settings): switch project host in place, host-scoped edits + deep links (#8566)
Add an ephemeral per-project host selection driven by the pane's "Available
Hosts" switcher, so the single collapsed pane shows the selected host's setup
(path, worktree base, runtime, fork-sync, hooks, source-control AI). Route
edits to the selected host by threading an optional hostId through updateRepo
(mirrors removeProject's host routing), fixing the same-id/self-pair case where
a local + runtime share one repo id. Couple Settings deep links to the switcher
so host-specific subsection anchors resolve, load hooks for the selected host,
and make pane-level Remove Project remove every host setup.
* fix(settings): isolate selected project host state
* fix(settings): review follow-ups for per-project Projects pane (#8566)
- Remove Project copy now states it removes the project on all
configured hosts (the button removes every host setup); new catalog
key translated across all 5 locales
- ensureHooksConfirmed forwards hostId to readRuntimeIssueCommand so
the issue-command trust read resolves duplicate repo ids the same way
as its sibling checkRuntimeHooks call
- translate the multi-host "N hosts" nav description
- extract removeSettingsProjectFromAllHosts with unit tests; drop the
now-unused getRuntimeTargetIdentity
Codex, Antigravity, and Devin launch their agent-hook `command` as a program
(argv[0]), not through cmd.exe. PR #8430 changed wrapWindowsCmdHookCommand to
emit an `if exist "path\." (drain) else if exist "path" (call "path") else
(drain)` compound whose argv[0] is the cmd builtin `if` — unspawnable — so every
Codex/Antigravity/Devin hook (SessionStart, UserPromptSubmit, Stop, ...) failed
with "hook exited with code 1" on Windows starting in v1.4.138.
Revert the cmd-safe fast path to the bare, directly-spawnable .cmd path (the
proven pre-#8430 form). A cmd-builtin drain and direct-spawnability are mutually
exclusive, and nesting cmd.exe /d /c breaks large-payload draining; the
missing-script stdin drain stays on the encoded-PowerShell fallback (used for
spaced/non-ASCII paths). Upgrades self-heal on first launch: startup install()
unconditionally rewrites the command and Codex trust entry, sweeping the old
compound form.
Add a platform-independent regression guard (launcher must resolve to a real
file, never a cmd-builtin fragment), update the lifecycle test + docs, and fix a
stale Devin comment.
* Add version-matched bundled skill guides
* Clarify skill freshness rollout PRs
* Add canonical skills show alias
* fix(skills): address guide review feedback
* fix(skills): make guide commands cross-platform
* fix(skills): apply the ORCA convention to the emulator guides
Review follow-up: the emulator guides still instructed literal
`orca emulator ...` in sh fences with no Linux disambiguation, so on
unmanaged Linux they could launch the GNOME screen reader — the exact
failure the executable-selection preamble prevents. Both emulator
guides now carry the preamble and ORCA placeholder across fences,
tables, and prose, and the cross-platform safety test covers all four
converted guides. Also replaces computer-use's "unless a block names a
shell" carve-out, which contradicted its own POSIX example, with the
unconditional placeholder rule.
The shell-ready zsh wrapper restored ZDOTDIR from the env-imported
`${ZDOTDIR}`. On Windows+WSL the wrappers are generated with a Windows
path baked in but sourced via /mnt/c, and for non-ASCII Windows
usernames (e.g. a Korean login) zsh corrupts environment values whose
UTF-8 bytes fall in its 0x84-0x9D token range while processing startup
files. The corrupted `${ZDOTDIR}` failed the self-check, so the wrapper
fell back to the unusable baked Windows literal and the user's ~/.zshrc
never loaded — a bare `HOSTNAME%` prompt with no theme/aliases/PATH.
Derive the wrapper dir from `${${(%):-%x}:h}` instead — %x is zsh's
internal script name for the file being sourced and is not subject to
the env-import corruption. `${ZDOTDIR}` is kept only as a fallback when
%x expansion yields nothing; the existing final restore still validates
with -f before trusting the value. On native macOS/Linux the derived
value equals the old one, so behavior there is unchanged.
Covers local PTYs, the daemon, and the Windows→WSL launch path, which
all share getZshEnvTemplate. Adds a live-zsh regression test that sources
the wrappers from a non-ASCII (token-range) runtime path.
Supersedes the ORCA_ORIG_ZDOTDIR path-normalization approach with a wrapper self-location fix that also covers the non-ASCII (token-range) corruption trigger.
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
* fix(daemon): replace a permanently wedged daemon instead of preserving it forever (#8689)
A daemon whose socket accepts connections but whose event loop never answers the
'hello' handshake was adopted by the launcher unconditionally and never re-evaluated,
so every terminal spawn failed with 'DaemonProtocolError: Hello response timed out'
with no recovery.
- daemon-init.ts: bound the launcher's 'preserve any unresponsive-but-connectable
daemon' with a grace window. A transient wedge (Windows update-relaunch AV/disk
pressure) drains within ~20s and is preserved WITH its live sessions; a permanent
wedge exhausts the grace and is replaced. Stays well under the 60s local-PTY
fail-open cap.
- daemon-pty-adapter.ts: isDaemonGoneError now treats 'Hello response timed out' as
daemon-gone, so a runtime wedge triggers withDaemonRetry's respawn (re-entering the
same grace-bounded launcher) instead of failing every spawn until app restart.
Tests pin the grace magnitude so it cannot be silently shrunk.
Co-authored-by: Orca <help@stably.ai>
* fix(daemon): widen wedged-daemon grace window to ~60s
Bump WEDGED_DAEMON_GRACE_RETRIES 3 -> 11 (~20s -> ~60s) to keep live-session
loss on the transient-wedge (Windows update-relaunch) path as close to zero as
possible. A transient wedge drains early and stays under the 60s fail-open cap;
only a permanent wedge runs the full window. Export the constant and pin its
floor in tests so it can't be silently shrunk.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Take-over of #8605 (issue #8591). Ships #8498 (worktree resync + pull-to-refresh + cache write-through) and #8129 (idempotent notification replay on reconnect). Fixes the original PR's field mismatch (seq vs notificationSeq) and adds the missing notifications.getMissedSince mobile RPC allowlist entry. #6784 and #4500 held back to avoid conflicting with the relay work (#8536). Co-authored-by: Brandon Bennett (@branben).
* fix(github): pin work-item list ordering to updated-desc so cursor pagination reaches every page
The Tasks page paginates work items with an updatedAt cursor
(updated:<oldest-item), but the underlying gh calls never pinned a sort:
'gh issue list' defaults to created-desc and '--search' defaults to
best-match. Items created long ago but updated recently therefore never
appeared on any page — page 0 (created order) skipped them and every
later page excluded them via the cursor — so the pager advertised pages
the fetch chain could never reach, clicks on them clamped to the last
real page, and cross-page ordering was scrambled.
Append sort:updated-desc to every list/search invocation so the fetch
order matches the cursor field on the first and all subsequent pages.
Verified against a live 588-issue repo: the cursor chain previously
died around page 5; it now traverses 585/588 unique issues (the
remainder is the pre-existing strict '<' boundary edge for items
sharing the cursor's exact timestamp).
Fixes#8649
* fix(github): make work-item cursor pagination lossless at updatedAt boundaries
Builds on the sort-pin fix: switch the pagination cursor from strict
'updated:<' to inclusive 'updated:<=' so items sharing the boundary row's
exact updatedAt are no longer skipped between pages (the residual 3/588 edge
in #8649).
The inclusive bound re-fetches the boundary rows, so dedupe them by repoId+id
(a bare item.id like 'issue:9' collides across repos). Extract the page
accumulation out of the 12k-line TaskPage component into a pure, unit-tested
helper (accumulateWorkItemPages) that dedupes and backfills: it accumulates
fresh rows across fetches and emits uniform pageSize pages, so deduped pages
never shrink below the size totalPages (count / effectivePageSize) assumes —
which would otherwise strand the tail items and break the no-count degraded
pager.
Also hoist the updated-desc ordering into a named WORK_ITEM_LIST_SORT_QUALIFIER
constant so the cursor's ordering contract has one home.
Tradeoff: when per-repo fetch size equals pageSize, the boundary dedupe costs
one extra fetch per page; acceptable for interactive pagination and bounded by
the gh rate-limit guard. Persisting the cursor/buffer across calls is a
possible follow-up.
---------
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
* Fix stacked-worktree PR creation targeting a local-only parent branch
- Resolve the eligibility default base to a remote-tracking ref instead
of blindly trusting the submitted parent branch, since a stacked
worktree's base is often a local-only branch the remote can't resolve
- Add a create-time hard block (base_not_on_remote) so a stale or
unpushed submitted base fails with actionable copy instead of the
provider's opaque error
- Update the dialog's default-base resolution and blocked-action/
dropdown copy to match the new remote-validated default
* Split hosted-review-creation.test.ts to fix max-lines lint error
Moved getHostedReviewCreationEligibility tests to a separate file (hosted-review-creation-eligibility.test.ts) to reduce the original file size from 880 to 579 lines, satisfying the max-lines lint constraint.
Co-authored-by: Orca <help@stably.ai>
* Fix Create PR intent flow to use remote-validated eligibility default fo
Prefer eligibilityDefaultBaseRef over the raw compare base when resolving
the review base for the one-click Create PR intent flow, since eligibility
is recomputed from the same compare base right before creation and already
corrects a local-only stacked parent to the repo default. Falls back to
the compare base only when eligibility supplies no default.
* Simplify base-ref remote existence check into a single for-each-ref call
Combine the wildcard and exact-tracking-ref lookups into one for-each-ref
invocation with multiple patterns instead of two sequential git calls,
removing the redundant rev-parse fallback path.
---------
Co-authored-by: Orca <help@stably.ai>
* fix(runtime): preserve terminals during headless desktop activation
* rm design doc
* Fix desktop activation launch ordering and blocked-window status resolut
- Check desktopWindowStatus before spawning the Orca app so a blocked
runtime no longer launches a doomed second instance.
- Reuse resolveDesktopWindowStatus for remote runtime status so it
honors the same authoritativeWindowId fallback as local status.
- Re-check the authoritative window at spawn time instead of trusting
a possibly-stale snapshot, since it can be destroyed mid-await.
- Harden the e2e activation spec against silent spawn failures.
---------
Co-authored-by: bbingz <zzb@gxsmjx.com>
* fix watcher lifecycle cancellation bounds
* fix(review): guard remote watcher installs against post-shutdown resurrection
closeAllWatchers aborted the in-flight install *tokens* it could see, but a
same-key joiner awaiting a 'cancelled' resolution (and a fired retry tick)
calls installRemoteWatcher directly and, on the fresh-generation recursion,
builds a brand-new non-aborted AbortController and calls provider.watch()
after teardown — leaking an SSH watcher into the just-cleared remoteWatchers
map. Latch the subsystem shut in closeAllWatchers and refuse installs while
latched; a genuine new fs:watchWorktree clears it. Adds a regression test
(fails without the latch) plus a test for the same-tick handoff-revival guard
that had no coverage.
Also extract the duplicated isolated-quarantine-vs-fuse branch shared by
retireSlot and releaseFailedRoot into quarantineOrFuse (behavior-preserving).
* Add lifecycle generation guard to refuse stale remote-watcher joiners
- A boolean latch alone can't distinguish a pre-shutdown joiner from a
fresh call once a genuine new watch reopens the subsystem, letting a
stale joiner recurse and register a post-shutdown provider.watch()
- Each installRemoteWatcher call now captures a generation counter that
closeAllWatchers bumps, so a waiter that resumes after a later
shutdown+reopen is refused instead of resurrecting
- Adds a regression test covering the shutdown-then-reopen race
Silently swallowing chmod failures let installs proceed with a
non-executable launcher, surfacing as a confusing runtime error later
instead of a clear install-time failure.
* fix(mobile): validate Windows firewall remote scope
* fix(review): simplify string-guard ternary to boolean AND
The ternary returned only boolean literals, so cond ? f() : false is
equivalent to cond && f() (addressScopeIsSufficient returns boolean).
Co-authored-by: Orca <help@stably.ai>
* fix(mobile): accept dotted-netmask firewall scopes and lock in fail-safe edges
- Parse dotted-netmask CIDR (192.168.0.0/255.255.255.0) via a contiguous-mask
check, failing closed on holey masks.
- Factor subnetFromParsed so CIDR parsing no longer re-parses the address.
- Document why single-host (/32, /128) subnets and family-specific keywords with
an unknown interface family fail closed, and add regression tests covering
those deliberate false-deny edges plus policy-defined keywords (Intranet, DNS).
Co-authored-by: Orca <help@stably.ai>
* Add explanatory comment on why firewall scope check isn't unioned
Documents the fail-safe rationale behind checking coverage per rule
instead of merging rule scopes, so future edits don't "fix" this
into a less conservative union check.
---------
Co-authored-by: Orca <help@stably.ai>
* fix(cli): harden Windows launcher transports
* Fix csc.exe compile failures on space-bearing Windows install paths
- Legacy csc.exe mangles absolute paths containing spaces, so the
compile step now cd's into the bin directory and passes bare
file names for /out and the source file instead of full paths
* fix(source-control): route GHES remotes to the GitHub provider for PR creation
A GitHub Enterprise Server user could not submit a PR — Orca demanded
ORCA_GITEA_TOKEN — while issue sync worked fine (#8312).
Root cause: GitHub owner/repo resolution (parseGitHubOwnerRepo) hard-rejects
any host that is not literally github.com. A GHES remote lives on a custom
host, so GitHub's forge resolveRepository returned null and provider detection
fell through the list to Gitea, whose KNOWN_NON_GITEA_HOSTS denylist cannot
enumerate arbitrary GHES domains. Issue sync was unaffected because gh
issue/pr list run with cwd=repoPath and let gh resolve the GHES host natively.
Fix mirrors GitLab self-hosted detection (getGlabKnownHosts): a new
getEnterpriseGitHubRepoSlug resolves a custom-host origin to owner/repo only
when gh is authenticated to that host — gh only ever manages GitHub/GHES
credentials, so a logged-in host is definitively GitHub. Wired into:
- forge-provider GitHub resolveRepository (fallback after github.com miss),
so detection claims GHES before Gitea is consulted;
- createGitHubPullRequest owner/repo resolution;
- isGitHubAuthenticated, which now probes the repo's real host instead of a
hardcoded --hostname github.com.
github.com repos keep the cached getRepoSlug fast path and never spawn the
extra gh auth probe.
* fix(github): host-qualify GHES gh commands and probe auth in the repo runtime
Addresses two correctness issues found in review of the #8312 fix.
1. GHES host was discarded before `gh pr create`. `--repo owner/repo` shorthand
resolves against gh's default host (usually github.com), so for a user
authed to both github.com and GHES it could target a same-named github.com
repo or fail — deterministic for SSH repos, which run gh with no cwd. Now
`createGitHubPullRequest` and the `findOpenPRByHeadBase` fallback pass a
host-qualified `HOST/owner/repo` for GHES (github.com keeps the shorthand).
Also generalize `parseCreatePRPayload`'s URL regex off github.com so a GHES
PR URL parses directly instead of limping through the list fallback.
2. GHES auth was probed on the wrong gh runtime. `getAuthenticatedGitHubHosts`
ran a global `gh auth status` with no cwd/WSL/SSH context and cached every
runtime under one "local" key, so a GHES login present only in the repo's
WSL distro was missed and the repo fell back to Gitea. Replaced with
`isGitHubHostAuthenticated`, which runs `gh auth status --hostname <host>`
with the repository's execution options (cwd/WSL distro, or SSH-local like
the create path) and caches per runtime+host — mirroring GitLab's
isGlabConfiguredForRemoteHost. This also honors GH_ENTERPRISE_TOKEN inferred
from repo context. Spawn failures stay indeterminate (uncached).
Adds createGitHubPullRequest-level tests asserting the actual gh `--repo`
arguments (create + fallback) and the WSL/SSH runtime of the auth probe.
* perf(source-control): drop redundant GHES gh auth probe in eligibility
Review follow-up. Detection only routes a GHES remote to the GitHub provider
after getEnterpriseGitHubRepoSlug has confirmed gh is authenticated to its
host, so isGitHubAuthenticated can trust a non-null slug as authenticated and
skip a second, rate-limited `gh auth status` spawn per eligibility poll.
Reaching the github.com probe now implies the remote is github.com. Tests
assert the enterprise path fires no redundant gh probe.
* fix(window): extend startup reveal fallback to Linux so first launch never stays hidden (#8421)
On Linux/X11, ready-to-show can never fire (GPU/driver quirks), leaving the
only BrowserWindow hidden until a second launch triggers the second-instance
reveal path. Reuse the existing bounded Windows fallback timer on Linux; the
handledInitialReadyToShow guard and headless E2E check already make the
reveal idempotent and safe.
Claude-Session: https://claude.ai/code/session_017rio4rnPiCUh8jHWxkq4xH
* docs(window): drop win32-only qualifier from tray-fallback comments
The tray-create fallback comments referenced 'createMainWindow's win32 10s
reveal fallback', but #8421 extends that reveal fallback to Linux too. Since
the tray itself is win32-only (createSystemTray no-ops off win32), naming a
platform in these comments is both stale and misleading. Drop the qualifier;
the surrounding Windows-only tray context already scopes it.
---------
Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* Fix folder workspace Git status path
* fix(git): resolve folder-workspace path for all local filesystem ops
Extend the folder-workspace suffix stripping beyond git status: every
local Git subprocess cwd, the WSL-context probe, the first-work
branch/folder rename hooks, and the renderer session placeholders now
resolve the synthetic `::workspace:<uuid>` instance id to its backing
folder via splitWorktreeIdForFilesystem. Without this they would spawn
Git (or fs ops) against a nonexistent directory (ENOENT).
Co-authored-by: Orca <help@stably.ai>
* Preserve folder workspace rename instance ids
* Read folder workspace suffix from worktree id
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* feat(mobile): start a workspace from a branch, issue/PR, or Linear ticket
Unify mobile workspace creation with desktop. The "+" Create Workspace
modal now has a primary "Start from" field that opens a tabbed search
drawer (Branch · GitHub · GitLab · Linear), letting a user start a
workspace from an existing/new git branch, a GitHub issue/PR, a GitLab
issue/MR, or a Linear ticket — in addition to the default blank workspace.
No new backend is required: the search RPCs (github.listWorkItems,
gitlab.listWorkItems, linear.searchIssues/listIssues, repo.searchRefs) and
the worktree.create linked-item params were already used by the mobile
Tasks screen. This surfaces them in the create flow, reusing the existing
pure modules (buildTaskWorkspaceCreateParams, shouldResolveHostedReviewStartPoint,
filterAvailableTaskProviders).
Details:
- New pure modules: workspace-source-selection, use-workspace-source-search,
source-workspace-create, worktree-create-retry, blank-workspace-create
(the blank/retry path extracted from the modal for reuse + line budget).
- New UI: WorkspaceSourcePickerDrawer (+ row) and SetupHookTrustDrawer
(extracted from the modal).
- Older paired desktops (missing the mobile.tasks.v1 capability) degrade to
Branch + Blank only; GitLab/Linear tabs appear only when available.
- GitHub/GitLab sources pin their repo; switching repos resets the source.
PR/MR sources resolve their base branch at create time; SSH repos gate
search until connected (Linear search is repo/SSH-independent).
* fix(mobile): hydrate settings/trust before availability probes settle
Review fixes for #7985: setTrustedOrcaHooks/setRuntimeSettings no longer
wait on status.get/preflight.check/linear.status (a first-open
preflight.check can take seconds, widening the spurious setup-trust
re-prompt window). Also adds param-parity tests for createBlankWorkspace
and a GitLab MR base-resolve test.
* feat(mobile): match desktop's Smart source picker exactly
Rework the mobile create-workspace source picker to be a faithful port of
desktop's Smart picker instead of the earlier divergent "Start from" drawer.
The mobile field is now the workspace-name input AND the source search, with the
exact desktop tabs — Smart · GitHub · Linear · GitLab · Branch · Name. "Smart"
fans out across GitHub + GitLab + Linear + branches, prepends a "Use '<name>'"
row, and resolves pasted URLs / #123 / STA-42 to exact items (with a cross-repo
switch prompt). Selecting a source shows a pill and moves the editable name into
Advanced. The invented "Blank workspace" concept is removed — the neutral state
is just a typed/empty name (blank submit still yields a creature name).
DRY: the pure desktop logic (smart-workspace-source-results, -command-value,
github-links, gitlab-links, work-item-link-query-bounds, github-work-item-identity)
moves to src/shared/new-workspace/ with re-export shims at the old renderer paths,
so both renderer and mobile share one implementation. composer-branch-selection
and workspace-name were already shared and are reused directly.
Two read-only lookup RPCs are allowlisted for mobile so pasted GitLab URLs and
cross-repo GitHub URLs resolve to exact items (github.workItemByOwnerRepo,
gitlab.workItemByPath).
New mobile modules are split for max-lines: use-mobile-composer-source (selection
state + desktop-parity handlers, PR/MR base resolve), use-smart-workspace-source
+ smart-source-fan-out/-search-requests/-paste-intent (RPC orchestration),
composer-linked-work-item / work-item-lookup-text / mobile-smart-source-modes
(pure logic), and SmartWorkspaceSourceField/Drawer/Row + SmartWorkspaceAdvancedFields.
Replaces WorkspaceSourcePickerDrawer/Row, workspace-source-selection,
use-workspace-source-search, and MobileWorkspaceNameInput.
Reviewed by three adversarial agents + re-reviewed after fixes: GitHub search now
returns issues AND PRs (not issues-only), Linear defaults to assigned, create-branch
preserves slashy names, cross-repo PR base resolves against the item's own repo,
displayName is suppressed for user-edited names, and the smart-mode GitHub fan-out
respects availability. tsc/oxlint/max-lines-ratchet clean; 1328 mobile tests pass.
* fix(mobile): keep smart source drawer fully visible
* refactor: share workspace creation behavior across clients
* fix: address workspace creation review findings
---------
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
* fix(native-chat): retry not-yet-flushed transcripts instead of settling into a permanent error
A freshly-created session's transcript .jsonl lands on disk seconds to
minutes after the process starts. Native chat's one-shot read raced that
first flush: a miss became a permanent "No transcript found" error and
the live-tail subscription silently degraded to a no-op, so the pane
never recovered even after the file appeared.
- transcript-reader/read-cache: mark the miss with notFound so callers
can tell "not flushed yet" from a real parse/IO error (never cached).
- transcript-watch: poll resolve+install (500ms backoff, 5s cap) for the
subscription's lifetime instead of returning a dead no-op watcher.
- use-native-chat-live-session: retry a notFound read with backoff for
up to 60s while staying in the loading state, and let live appends
render over a stale initial-read error.
Fixes#8401
Claude-Session: https://claude.ai/code/session_01HA5g3X7wCakBttBpDru9Fp
* fix(native-chat): address CodeRabbit review — ENOENT retryable, content over spinner, blank-id guard, unref poll timer
- transcript-reader: an ENOENT after a successful resolve is the same
first-flush/rotation race as an unresolved path — mark it notFound.
- use-native-chat-live-session: live appends landing mid-retry render
instead of the loading state (mirrors the stale-error gate).
- transcript-watch: bail out for a blank session id with no explicit
file (nothing to resolve-poll), and unref the poll timer so headless
serve shutdown is never held open by an unresolvable session.
Claude-Session: https://claude.ai/code/session_01HA5g3X7wCakBttBpDru9Fp
---------
Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
* Fall back to a merge when a divergent pull has no reconciliation strateg
- Git 2.27+ refuses `git pull` on divergent branches unless pull.rebase or
pull.ff is configured. Retry with `--no-rebase` (Git's historical default)
so pulls succeed out of the box on fresh hosts.
- Skip the fallback whenever the caller already specified a reconciliation
strategy (e.g. --ff-only, --rebase) so explicit policies still fail as
expected on divergence.
- Applied identically in the local git pull path and the relay/SSH git
handler so both surfaces behave the same way.
* Refactor divergent-pull merge fallback into shared helper
Extracts the retry-as-merge logic (duplicated between local git and
relay SSH pull paths) into `runPullWithDivergenceFallback` in
git-remote-error.ts, so both callers share one implementation and
test coverage.
* fix(startup): bound serve WSL reconciliation wait
* test(startup): cover WSL barrier fail-open on early reconciliation rejection
Co-authored-by: Orca <help@stably.ai>
* feat(serve): surface managed WSL reconciliation status to headless clients
Expose reconciliation state ('pending'|'settled'|'failed') in the
orca_server_ready payload and a wsl-cli-barrier startup milestone, so
headless/SSH agents can tell the fail-open barrier outlived its budget
and a WSL PTY launch may still race an un-migrated registration.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
An index rewrite cannot move HEAD, yet every quiet local Git-common status
burst was calling readGitCommonHeadIdentities, which scans the primary
checkout plus every linked worktree sequentially (~1,500 file reads for 500
worktrees) even when only index files changed.
Split the conflated git-status signal into two: index churn stays
`gitStatusRepoIds`, while logs/HEAD (and other reflog head moves) become a
distinct `headIdentityRepoIds` signal. Source Control is still notified for
both, but readGitCommonHeadIdentities now runs only for true head triggers
(and structural ticks, which re-baseline silently). Index-only bursts do zero
head-identity reads.
Adds deterministic tests: index-only bursts read zero head identities across
linked and primary checkouts; logs/HEAD still refreshes identities for both;
plus coalescing and debounce coverage.
* Keep running-agent workspaces visible under "Hide sleeping" (#7197)
The "Hide sleeping" sidebar filter judged a workspace active only when it
had a live PTY (or a browser tab), so a workspace with a running agent
whose live-PTY entry was momentarily absent — an SSH reconnect grace
window, an unmounted pane, a remote surface not yet `ready`, or an
orchestration worker reporting before its tab is mirrored — was
classified as "sleeping" and hidden while its session was still open.
The smart sort already treats a fresh `agentStatusByPaneKey` entry as
"working" independent of live-PTY, so the filter and sort disagreed. Add
`getWorktreeIdsWithLiveAgent`, which derives the worktrees with an open
agent session from the live agent-status map (sleep/teardown drop those
entries via dropAgentStatusByWorktree, so slept/hibernated workspaces
still hide), and consult it in `hasActiveWorkspaceActivity`. Wire it
through the sidebar list, Cmd+J jump palette, and kanban board.
* fix(agent-status): align live workspace attribution
* fix(mobile): preserve live-agent workspace activity
* fix(mobile): prefer newest agent status source
* chore: restore unrelated benchmark formatting
* fix(mobile): resolve projected agent worktree ids
* perf(mobile): index projected worktree summaries
* fix(mobile): preserve projected activity under limits
* fix(mobile): preserve POSIX path identity
* perf(mobile): cache projected summary fallbacks
* fix(mobile): preserve remote path and priority contracts
* perf(mobile): index projected paths by host flavor
* test(mobile): prove projected path index keys
* perf(mobile): bound projected path fallback
* perf(mobile): reuse projected repo platforms
* test(mobile): enforce projected lookup bounds
* chore(runtime): remove review instrumentation
* perf(mobile): skip unresolved repo platform scans
* perf(mobile): batch represented project runtimes
* perf(mobile): batch cold project runtime scans
* fix(mobile): couple worktree platform snapshots
* fix(sidebar): prioritize attributed headless agents
* fix(sidebar): activate smart sort for headless agents
* fix(sidebar): prefer mirrored agent ownership
* fix(mobile): follow mirrored agent ownership
* fix(sidebar): resolve mirrored unstamped agents
---------
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
* Fix hook scripts to drain stdin before any early-exit path
Generated agent hook scripts and missing-script launchers could exit
successfully before consuming the payload written to their stdin,
leaving the writer with a broken pipe (EPIPE/ERROR_BROKEN_PIPE) once
the reader closed early. Capture stdin (or drain it via a shared
epilogue/fast-path guard) before any whole-script success exit across
all POSIX, batch, PowerShell, and Git Bash launcher variants, and add
a cross-agent lifecycle test suite plus a live Electron verification
script to guard the contract going forward.
* Harden hook scripts against unreadable managed scripts and add a Claude/
- Extend the POSIX launcher guard to also require `[ -r ]`, not just `-f`/`-x`,
so an executable-but-unreadable managed script still drains stdin instead of
erroring or silently misbehaving.
- Add a verifier case (`verifyClaudeDevinSkip`) that spins up a local HTTP
server and confirms the Claude hook never forwards a request that Devin
already imported, catching accidental double-forwarding.
- Update installer-utils tests and stdin-lifecycle docs to match the new
readable-file guard and the added verification case.
* Fix hook-launcher verification to derive script paths from the installed
Extract the quoted path from the launcher's `if [ -f '...'` clause instead of
reconstructing it via join(home, ...), so missing/failing-script test cases
can't silently fall through to the real script if the install layout changes.
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* Prevent index churn from refreshing worktrees
* Cover IPC contract in worktree reliability gate
* Refresh background worktree heads without re-entering structural fanout
External commits, amends, and soft resets in non-active worktrees now reach
store rows through spawn-free Git metadata reads diffed in the watcher's
existing debounce, emitted only on real head moves. HEAD reflog appends become
status-only triggers, config.worktree becomes structural for sparse-flag
freshness, and the non-darwin poller gains a periodic ungated index re-stat
so in-place rewrites on coarse-mtime filesystems cannot be missed forever.
* Reject unsafe symref paths and validate object ids in the head reader
Ref content comes from repo files an attacker can craft. Backslash segments
traverse on Windows where join treats them as separators, and colons are
forbidden in Git ref names; both now fail isSafeRefName before any path is
built. Resolved values are additionally emitted only when they match a hex
SHA-1/SHA-256 object id, so no file content can leak through the identity
event even in principle.
---------
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
Co-authored-by: Brennan Benson <>
* Hide console window for Windows agent foreground-process scan
Agent foreground-process inspection re-forks powershell.exe (or the wmic fallback) to detect which agent runs in each terminal. Both spawns omitted windowsHide, so on Windows each fork popped a fresh conhost console window that flashed and stole keyboard focus from the foreground app — including Orca's own terminal — recurring roughly once every few tens of seconds while an agent session was open (and more often under continuous agent output).
Add windowsHide: true to both probes (matching the codebase-wide convention) plus a regression test asserting the spawn options.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(windows): include scan root in foreground fixtures
---------
Co-authored-by: xucongwei <xucongwei@bytedance.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>