* fix(cli): spawn a version-manager CLI with its own node runtime
resolveCliCommand falls back to scanning every version-manager install when
PATH misses, so it can hand back ~/.nvm/versions/node/v20.x/bin/codex while
PATH still leads with v22. Nothing paired the binary with the runtime it was
installed against, so its `#!/usr/bin/env node` shebang loaded a v20-built
native module under a v22 ABI and the agent died on first require (#10932).
Reproduced with a real addon rather than asserted: a CLI requiring a
cpu-features build for NODE_MODULE_VERSION 115, spawned with v24 leading
PATH, fails with ERR_DLOPEN_FAILED and exit 1. With the CLI's own bin
directory prepended it runs clean.
withCliRuntimeOnPath prepends the resolved command's directory when that
directory ships a sibling node, and is a no-op otherwise — so a Homebrew or
/usr/local CLI is untouched, and the WSL paths pass a bare `codex`/`claude`
that is not absolute and so never matches.
Host CLI resolution in the Claude login path is now lazy, keeping the WSL
branch from resolving a host binary it never spawns.
* fix(cli): split PATH on the delimiter we join with, pair app-server too
Readiness review findings, all four addressed.
withCliRuntimeOnPath chose its join delimiter from the platform option but
split with the host's. Passing platform:'win32' from a posix host turned
`C:\Windows;C:\Windows\System32` into `C;\Windows;C;\Windows\System32` —
every drive letter torn off at its colon. Latent, since no shipped caller
passes platform, but the sole win32 test was written against the corrupted
value and asserted one split segment, so it green-lit the shredding.
That test's other assertion was vacuous: it seeded only `Path`, so the
`PATH` key it asserted absent could never exist. Deleting the whole
case-dedupe block left the suite green. It now seeds both keys and asserts
the full joined string; removing the block fails it.
Nothing covered the wiring, and the argument choice is the easy thing to get
silently wrong. Note it only diverges on win32 — on posix
getSpawnArgsForWindows returns the CLI itself, so pairing the spawn command
is indistinguishable there. The new test drives the win32 branch with a .cmd
fixture; pairing spawnCmd or dropping the wrapper both fail it now.
codex-trust-grant-host and codex-session-index-heal spawn the same
`codex app-server` subcommand through runCodexAppServerSession and were left
unpaired. Pair centrally there via a new optional cliPath, since
invocation.command may be a cmd.exe wrapper.
Pairing tests live in their own file: adding them inline pushed
codex-fetcher.test.ts past the 800-line ratchet.
* fix(cli): read the Windows path key the child will actually use
Round-2 review finding. The read was narrower than the delete: the key was
picked from exactly two spellings (`Path`, else `PATH`), while the twin
dedupe removed every key whose lowercase form is `path`. A block spelling it
`path` or `pATh` therefore had its value deleted without ever being read,
handing the child a PATH containing only the CLI's own directory — a strictly
worse outcome than not pairing at all.
Win32 resolves env names case-insensitively and object order preserves block
order, so the entry the child reads is the first case-insensitive match. The
repo already encodes that rule in resolvePathEnvKey
(src/main/pty/windows-path-segment-merge.ts); src/shared cannot import from
src/main, so mirror it locally.
Verified by execution across six env shapes: lowercase, mixed-case, Path-only,
PATH-only, both twins, and a PATHEXT control that must not be touched. All
preserve the original PATH; before the fix the first two lost it entirely.
Reverting the selector fails the new test and nothing else.
* fix(codex): stop a surviving descendant wedging the Codex home process lock
The per-Codex-home process lock was released only by the child's 'close'
event. 'close' fires when the child AND every process that inherited its
stdio have exited, so a grandchild that outlives codex (an MCP server, a
helper, an `sh -c` wrapper) keeps the pipe open and 'close' never arrives.
killProcessTree only SIGKILLs the direct child on POSIX, so Stop and the
60s generation timeout both leave the lock held. Every later AI commit
message, PR field, branch name, and quota probe for that home then queued
forever with no error and no recovery until the app restarted.
Release the hold on 'exit' as well as 'close': once the codex process
itself is gone it can no longer rotate that home's auth.json, which is the
race the lock exists to prevent, so waiting on descendants bought nothing.
As a backstop for a child that never reports exit at all (a kill that does
not land), bound the hold: while another run is queued behind it, a hold
that outlives CODEX_HOME_PROCESS_LOCK_MAX_HOLD_MS releases the queue. The
cap is armed only when someone is waiting and only counts time the entry
actually holds the lock, so uncontended runs arm no timer and queueing
behind a slow predecessor never shortens a run's own budget.
* fix(codex): preserve home lock exclusivity
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(codex): make quota probes credential-safe
Codex OAuth uses rotating refresh tokens, and Orca's quota probes spawned
real codex app-server processes inside live credential homes, hard-killed
them at a 10s deadline (cold starts run 10-25s), re-probed every inactive
account on each switch, and deselected accounts on torn auth.json reads.
- arm the RPC read deadline only after initialize responds (30s/40s boot
budget), and terminate probes via stdin EOF + SIGTERM with a bounded
drain before any hard kill; resolve only once the child exits
- serialize Orca-spawned codex processes per credential home (probe vs
probe, probe vs commit-message/PR-fields/branch-name/model-discovery)
- keep the inactive-probe debounce across account switches and stagger
inactive probes; the active account still refreshes immediately
- grade credential reads (present/missing/unreadable/no-credential) and
require absence to outlive a grace window before deselecting
* fix(codex): close remaining credential races
* fix(codex): keep failed probes under home lock
* fix(codex): observe probe pipe failures
* fix(codex): await Windows generation tree kills
* fix(codex): preserve incomplete shared credentials
* fix: detect agents via install-dir resolver when which misses on GUI launch
The Agents settings pane reports every agent as "Not installed" on a cold
GUI launch even when the CLIs are installed and runnable from Terminal.
detectInstalledAgents resolves each agent with `which`, which only sees
process.env.PATH. GUI-launched Electron inherits a stripped PATH from launchd
that omits the user's shell-rc dirs (~/.local/bin, ~/Library/pnpm,
~/.asdf/shims, nvm/volta/fnm/mise shims, ...). The synchronous seed in
patchPackagedProcessPath plus the async shell-PATH hydration both race the
first detection, and the first result is cached for the session, so the pane
can latch onto an empty "nothing installed" snapshot until the user relaunches
from a terminal or clicks Refresh.
Fall back to resolveCliCommand (the same resolver the spawn path uses in
src/main/codex-cli/command.ts) when the PATH lookup misses. Detection now has
the same reach as launch: if Orca can run it, Orca detects it, independent of
PATH-hydration timing. No false positives — an uninstalled agent still resolves
to a bare command name rather than an absolute path.
Verified on macOS that resolveCliCommand resolves claude (~/.local/bin), codex
(~/.asdf/shims), and opencode (~/Library/pnpm) under a stripped PATH.
* fix: bulk resolve agent install-dir detection
Co-authored-by: Orca <help@stably.ai>
* fix: ignore node navigator for linux webgl policy
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>