Commit Graph
7297 Commits
Author SHA1 Message Date
Neil 88ecc739e3 fix(browser): bound the agent-browser daemon's life instead of hoping teardown runs (#16367) (#16588)
* fix(browser): bound the agent-browser daemon lifetime (#16367)

`agent-browser` is a client/daemon CLI. Orca only ever spawns the short-lived
client; that client forks a daemon Orca holds no handle on, which reparents to
pid 1 immediately. Nothing in Orca reclaimed it, so a crashed or SIGKILL'd run
left one daemon per browser tab alive forever — two of them at ~25.6 GiB and
~7.2 GiB RSS saturated a 64 GiB cgroup under headless `orca serve`.

Three fixes, in order of how much they cover:

1. Set `AGENT_BROWSER_IDLE_TIMEOUT_MS` on both spawn paths (the bundled-binary
   bridge and the orcad external-Chromium provider). This is the only bound
   that survives every way Orca can die, including SIGKILL, where no teardown
   code ever runs. 10 minutes: >6x the bridge's 90s `EXEC_TIMEOUT_MS`, so it
   can never cut a command, a retry chain, or an ordinary gap between two user
   commands, while capping an abandoned daemon at minutes instead of days.
   Verified against agent-browser 0.27.0: an idle daemon exits and takes its
   Chromium tree and socket sidecar files with it, and a daemon attached over
   `--cdp` (the bridge's case) leaves the attached browser running, so an Orca
   tab is never closed by its daemon idling out.

2. Await `destroyAllSessions()` in the will-quit teardown barrier. It was
   fire-and-forget and the only browser member missing from
   `settleTeardownWithinDeadline`; each session's close is its own
   agent-browser child taking hundreds of ms, so `app.quit()` won.

3. Sweep daemons a previous run left behind, using agent-browser's own
   `session list` / `close` rather than a pid walk (see `windows-pty-job.ts`
   for why walking your own orphans is guesswork). `closeStaleAgentBrowserSession`
   only ever reset the one name a new tab was about to reuse. The sweep runs
   only when `AGENT_BROWSER_SOCKET_DIR` is set, because that private
   per-profile directory is what proves the enumeration can only see this
   Orca profile's daemons; it is never set on Windows, so Windows gets no
   enumeration rather than a machine-wide sweep that could close a daemon Orca
   does not own. Windows stays bounded by the idle timeout, which needs no
   ownership proof. orcad's session name is stable across runs, so it closes
   that one name at start instead — a killed orcad's daemon would otherwise be
   reused while still holding the previous run's Chromium on a dead serve port.

Where the 25 GiB went is inference from code, not a measurement: `captureStart`
sets `activeCapture` and only an explicit `captureStop` ends it, so a HAR
capture in a daemon living for days is unbounded. Not claimed as proven; the
idle bound caps it either way.

Not re-landed: the queue bounds from #10179 (reverted by #10255) bound Orca's
own main-process heap, not the daemon's RSS, so they do not address this report.

Also true but left alone: the 3-strike breaker's `destroySession` is an
unawaited call whose `close` is `catch {}`-swallowed, and it only fires while a
command is in flight — an idle-but-bloated daemon is never noticed. The idle
timeout now bounds that case. `getOffscreenBrowserBackend()?.destroyAll?.()` is
declared `void` and fully synchronous, so unlike `destroyAllSessions` it has no
promise to lose and needs no barrier entry.

* fix(browser): scope the daemon idle bound and close every daemon Orca owns

Review follow-ups on the agent-browser orphan fix.

- Never idle-bound the orcad external-Chromium daemon: it owns the user's
  remote browser, so the 10-minute bound closed a live session and every tab
  in it. Per-tab helper daemons keep the bound; the stable session name plus
  the `close` in start() is what reclaims a killed orcad's Chromium tree.
- Retire a page's daemon from the headless offscreen backend, which is the
  only place `orca serve` closes a page and never reached the bridge. Credit
  to @Jinwoo-H (#16564) for identifying this owner-boundary gap.
- Bound the teardown close at 5s so the will-quit barrier member cannot
  inherit the 90s exec timeout, and close sessions still being created.
- Gate the startup sweep on a socket directory Orca derived itself; an
  inherited AGENT_BROWSER_SOCKET_DIR is no proof of per-profile ownership.
- Replay a session's network routes when the daemon idled out between two
  commands, instead of silently serving unstubbed requests.

* fix(browser): give the orphan sweep a kill switch

Of the three behaviours this PR adds, two are already recoverable in the field
without a build: the idle bound is an env passthrough an operator can raise, and
the quit close is bounded by its own timeout inside the teardown deadline. The
startup sweep was the exception — it fires unconditionally, and if it closes a
daemon it should not, or spawns one process per stale name on a profile holding
hundreds, the only remedy was a revert.

ORCA_DISABLE_AGENT_BROWSER_SWEEP=1 turns it off, matching the existing
ORCA_DISABLE_CODEX_TRUST_RPC / ORCA_DISABLE_HTTP2 convention.

Note for anyone reaching for it on macOS: a Finder-launched Orca does not see
shell env, so it needs launchctl setenv or a terminal launch.

* fix(orcad): reuse a surviving browser session instead of closing the user's

start() closed the daemon before every open, killing the Chromium tree with it.
That runs on every provider start, not just after a crash — so an `orca serve`
restart took the remote user's browser and every tab in it.

The justification was borrowed from the pane bridge, which passes --cdp and so
really does hold a port that dies with its Orca. This session passes only
--session and --profile: nothing binds it to the old process, and the daemon
owns its Chromium independently. A survivor is reusable as-is.

start() now probes for an active tab first and returns it untouched. Only a name
that answers nothing gets closed and reopened — which is still the killed-orcad
case the stable session name exists to recover.

This matters more as orcad becomes the backend the remote host runs on: the
browser it manages belongs to a user, not to the process that happens to be
driving it this minute. It is also the same principle that already exempts this
path from AGENT_BROWSER_IDLE_TIMEOUT_MS.
2026-08-26 16:49:31 -07:00
Neil 26721bd632 fix(codex): stop blocking the main thread on trust grants (#16441) (#16594)
* fix(codex): stop blocking the main thread on trust grants (#16441)

Codex hook trust was granted by blocking the Electron main thread on
`spawnSync` of a bundled ELECTRON_RUN_AS_NODE entry for the whole
app-server deadline: 15s native, 35s WSL, ~45s on the real-home path
(rebase inspect + repair + grant). Cold start and every Codex pane
launch showed "Not Responding"; the reported event-loop gap was
15,049 ms.

The subprocess only ever existed to donate an event loop to a
deliberately blocked parent — `runCodexHookTrustGrantSession` was
already the real async implementation. Make the callers async and the
fork is unnecessary, so the bridge, the forked entry and its envelope
are deleted along with their build/knip/tsconfig registrations. The CLI
`agent hooks prepare-codex` handler is already async, so it awaits the
in-process session and saves a process spawn per managed-home shell.

`resolveCodexTrustGrantHost` is async too; the WSL identity probe moves
from `execFileSync` to `runProcess`, dropping that file from the
child-process import allowlist. Status reads keep a synchronous
native-only stamp path.

Two invariants that held only because the lane blocked:

- Overlapping capability probes were impossible by construction.
  `GitCapabilityCache`'s dedupe engine is extracted to a shared
  `CapabilityProbeCache` and `CodexAppServerCapabilityCache` now
  inherits it, so concurrent launches against a cold host share one
  app-server session instead of one each.
- Two grants on one `config.toml` could not interleave capture and
  restore. A reentrant per-file lane now serializes the whole install
  sequence (managed, WSL runtime, real-home ensure, legacy sweep) and
  the grant and rebase inside it.

Cold-start work moves off the critical path: retained-home
reconciliation (N sequential sessions) is fire-and-forget behind the
daemon provider, and the startup real-home ensure chains into managed
hook reconciliation instead of blocking app init.

Every preserved semantic is unchanged: never throws, the
ORCA_DISABLE_CODEX_TRUST_RPC kill switch, ledger hits, backfill-pending
and cooldown fallbacks, config rollback on every failure path,
pre-grant self-computed trust removal, the verify-failure taxonomy,
diagnostics and telemetry.

* fix(codex): widen the trust-config lane to every config.toml writer

Review follow-ups on #16441's async trust grant:

- `markCodexProjectTrusted` now runs inside the runtime+system config.toml
  lanes, so a project-trust write can no longer land inside a hook grant's
  capture->restore window and be silently reverted. Its callers await it.
- `install`/`refreshRuntimeUserHooks`/`remove` hold the system config.toml
  lane as well as the runtime one — they promote approvals into
  ~/.codex/config.toml and mirror it back. Lock order is runtime-before-system
  everywhere.
- The real-home ensure chain resumes after a rejection instead of returning
  the same rejected promise to every later pane launch, and resolving the real
  home is now inside the module's never-throws boundary.
- `buildSpawnEnv` awaits inside a cancelable pending-spawn registration, so
  shutdown during the (now long) env build stops the PTY from launching.
  `prepareLocalPtySpawn` generalizes into `awaitCancelableLocalPtySpawn`.
- CapabilityProbeCache drops the test-only `nowMs` passthrough; its probe
  backstop comment now describes what it actually guards.
- Preflight is a plain async function; the trust dispatch in orca-runtime
  collapses into one `markWorkspaceTrustedForAgent`.

* test(codex): exercise the trust-config lane under real concurrency

The async grant makes two pane launches overlap for the first time. These
drive the real modules end to end on real files: a rollback swallowing a
sibling's grant, a markCodexProjectTrusted write landing inside a capture
-> restore window, shared capability-probe dedupe on a cold host, the
host-scoped transient cooldown, and reentrancy from inside an installer.

Each was verified to fail against a deliberately broken implementation
(lane removed, dedupe disabled, cooldown made global, reentrancy pass-
through disabled).

* test(codex): stop hook-service suites spawning the developer's real codex

The forked grant bundle never existed under vitest, so the RPC lane was
unreachable in tests on main. Running it in-process makes these suites
spawn a real `codex app-server` when one is installed: 38 spawns and two
failures in hook-service-runtime-trust-repair on a machine with codex,
green in CI where there is none. Stand in for the missing binary so both
environments exercise the same fallback lane.

* docs(codex): scope the trust-RPC kill switch comment to what it actually gates

The comment read as though the flag forces the fallback lane everywhere. It
gates the managed grant only: the real-home rebase still runs its own
inspect/repair app-server sessions when Orca's insertion shifts a user's hook
positions, and never reads the flag.

Verified by exercise, not by reading — with the flag set, both
inspect-user-hook-trust and repair-user-hook-trust still ran. Pre-existing:
main has no check there either, it just blocked the main thread while doing it.

Widening the flag to cover the rebase is a follow-up; this only stops the
comment promising something the constant does not do.
2026-08-26 16:44:55 -07:00
Neil 68d5b9206e fix(agent-prompt): stop reporting delivered prompts as stalled (#16095) (#16590)
* fix(agent-prompt): stop reporting delivered prompts as stalled (#16095)

Enter is written before verification runs, so `agent_prompt_stalled` can only
ever mean "turn start not observed" — never "prompt not delivered". Three of the
verifier's blind spots made that misreading routine, and the coordinator then
treated it as non-delivery and pasted the whole preamble a second time into a
worker already running it.

- Accept a hook-reported `working` recorded after the baseline. Hook rows reach
  the runtime through getAgentStatusSnapshot with no window involved, unlike the
  synthetic-title route that feeds workingSequence (suppressed for codex, absent
  for kimi, and gated on window visibility for everyone else).
- Accept pane output after Enter when the agent was already working: a
  `->working` edge is unreachable there, so the old predicate could never be
  satisfied by a follow-up prompt. An idle agent still owes a real turn start,
  so a swallowed Enter stays detectable.
- Give codex/kimi panes a longer effect window; their only turn-start proof is
  an out-of-process hook round-trip, not a TUI repaint.
- Coordinator dispatch no longer fails (and re-dispatches) a task whose prompt
  stalled; the dispatch stays active with its capability intact so the worker's
  own report settles it.

* fix(orchestration): let a worker's own report correct an unobserved prompt (#16095)

Follow-up to f9f973c on two review findings.

Anchor the hook signal on a turn, not a refresh: `receivedAt`/`updatedAt` bump on
every same-state hook ping, so an in-progress turn could have passed for a new
one and silently accepted every prompt to a working agent. `stateStartedAt` is
the documented per-turn identity (pinned across same-state pings), so the
verifier now reads that.

Close the worker-start path: a `dispatch_input` stall settled the dispatch as
failed *and* revoked the capability, so a worker that ran the preamble to
completion had its result rejected. Revocation is now skipped for that cause,
and a worker report can re-settle a dispatch whose `last_failure` is
`agent_prompt_stalled` — retaining the capability alone was not enough, because
settlement also gates on dispatch/task status.

* fix(orchestration): let a failed worker report correct a stalled-prompt record (#16095)

The duplicate short-circuit ran before the unobserved-prompt branch, and for
outcome 'failed' both expected statuses are exactly the state failWorkerStart
leaves behind. A worker that reported a real failure was answered
duplicate:true, so its cause and result body were dropped and the record kept
'agent_prompt_stalled'. Evaluate settledByUnobservedPrompt first so one failure
report can re-settle that dispatch; a repeat report is still a duplicate.

Also derive the previous dispatch/worker states once instead of two parallel
ternaries, reuse getPtyAgent in createAgentPromptRenderGate, cite the real
30s relay request budget the hook window is sized against, and make the
coordinator test settle an actual worker report rather than calling
completeDispatch under a name that promised otherwise.

* test(orchestration): carry the new dispatch-depth fields into this PR's fixtures

main added required creator/maxDepth on createStartingWorkerDispatch and
nestedWorkerMaxDepth on dispatchTaskToWorker while this branch was open. The two
fixtures added here predate them, so the merge typechecked clean on each side
and failed once combined. Mechanical; no behaviour asserted here changes.
2026-08-26 16:44:40 -07:00
Neil 08a447dfb2 fix(terminal): size the pre-Enter wait to what the host actually ingests (#15925) (#16586)
* fix(terminal): size the pre-Enter wait to what the host actually ingests

The Windows agent-prompt submit delay was a flat 1_500 ms frozen from the
client's process.platform at import. Measured on two real Win11 hosts, ConPTY
ingests a bracketed paste linearly at ~0.009-0.010 ms/byte, so the constant was
both far too long for a 2-8 KB prompt (14-89 ms of real cost) and too short past
~145 KB — at 160 KB one host took 1_499 ms, meaning Enter landed mid-paste,
exactly the corruption the delay exists to prevent, up to the 16 MB input ceiling.

Replace it with getTerminalPasteIngestMs(platform, byteLength) and derive every
pre-Enter wait from it:

- open-loop fallback = 500 ms settle + ingest bound, uncapped
- claude/codex render gate cannot start its quiet window before the ingest bound
  elapses (an agent that repaints mid-ingest could otherwise satisfy
  marker-then-quiet while ConPTY was still feeding the paste), and its 8 s hard
  cap now sits on top of the ingest bound instead of standing in for it
- the plain terminal.send suffix path, which had an undocumented flat 500 ms

The rate follows the host that owns the pty transport, not the client: a WSL pane
is spawned as wsl.exe behind the Windows pseudoconsole so it still pays ConPTY,
while an SSH pane follows the relay's reported remotePlatform.

Also swap the inter-chunk setTimeout(0) for setImmediate. It cost a full ~15 ms
Windows timer tick per 16 KiB chunk (~0.95 s/MB) while pacing ~1.07 MB/s — 11x
above ConPTY's drain rate — so it never provided backpressure; the event-loop
yield it did provide is preserved.

* fix(terminal): stop double-charging paste ingest in the render gate

The render gate's hard cap is armed twice -- once at arm() and again when the
show-cursor marker arrives -- but it re-added the whole ingest window each time
while the ingest clock itself runs once from gate construction. A marker seen
mid-ingest pushed the cap out by a second full ingest term (~34 s instead of
~24 s for a 1 MB prompt on ConPTY). Capture the ingest deadline absolutely and
arm with what is left of it.

Also thread the request AbortSignal through terminal.send so the now
payload-scaled suffix wait can be cancelled: at 16 MB it runs ~262 s, well past
the CLI's 60 s request budget, and previously nothing stopped the eventual Enter.

Cleanups: a pty record's connectionId is only ever an SSH target id, so the
wsl: relay-id guard in getPtyWriteHostPlatform was dead; and hoisting
action.text removes both non-null assertions in writeTerminalAction.
2026-08-26 16:30:26 -07:00
Brennan Benson 9135b6f004 feat(orchestration): surface nested worker depth and propagate it across hosts (#16669)
* feat(orchestration): surface nested worker depth and propagate it across hosts

Builds on the depth enforcement in the previous commit, which shipped with the
setting reachable only by editing settings.json and with workers never told they
could nest.

Adds the Settings -> Agents control (a 1/2/3 select rather than a free-form
number, which bounds the value without inventing a numeric input primitive). The
key stays absent from the SettingsUpdate RPC schema, matching agentSkillSharingEnabled:
settings.update is reachable from the CLI, so an RPC-writable depth would let a
worker raise its own cap.

Adds a SUB-DISPATCH block to the dispatch preamble, emitted only when the worker
actually has budget left. A worker told it "usually cannot" delegate still tries and
then reports the refusal as a blocker, so the section is omitted entirely rather
than softened.

Propagates depth to federated worker hosts. Previously the home side computed and
stored a depth the remote host never received, so a remote attachment always read
as depth 1. That is correct at the default cap and wrong as soon as the cap is
raised — precisely when someone starts relying on nesting. The field is optional,
so an older Run home simply omits it and the attachment's NOT NULL DEFAULT 1 keeps
the fail-closed behaviour. Enforcement still runs on the executing host against
that host's own cap, consistent with the SSH execution boundary.

* fix(orchestration): close nested depth readiness gaps

* fix(settings): defer nested depth translations

* fix(orchestration): drop federated depth keys that main already landed

The enforcement PR's review pass added the same federated depth propagation
before it merged, so replaying this branch onto main produced duplicate object
keys. Keep main's versions -- its schema entry validates an integer >= 1 rather
than any finite number.

* fix(settings): label nested worker depth select

* fix(settings): move nested depth to orchestration

* fix(settings): refine nested depth placement
2026-08-26 16:16:05 -07:00
Neil 0096e47850 fix(windows): keep windows-process-tree gyp paths absolute under pnpm (#16688)
* fix(windows): keep windows-process-tree gyp paths absolute under pnpm

Hourly Windows builds have failed since #16598 at
`build-windows-process-tree-relay-addon`: `require('node-addon-api').targets`
is cwd-relative, so node-gyp evaluates it from the pnpm store realpath and
then loads it from the `node_modules` symlink. That resolves
`node_addon_api.gyp` outside the repo.

Use `require.resolve` for an absolute path, matching the node-pty patch.

* i18n: keep ja skill-filter labels on the catalog's Agent brand

#16682 merged with a failing localization catalog: ja used エージェント
in three new skill-filter strings, and repair-locale-catalog rewrites
those to Agent. Match the rest of ja.json so static analysis can pass.
2026-08-26 16:15:03 -07:00
Neil 96565fe370 perf(source-control): stop re-running every git read on each file selection (#15036) (#16600)
* perf(source-control): stop blocking main on four sync git-dir probes per status poll

detectConflictOperation ran four existsSync calls against the git dir on every
status poll. On a `\\wsl.localhost\...` worktree each one is a 9p round trip, and
being synchronous they landed on the Electron main thread back to back.

Replace them with concurrent fs/promises access probes: same "any failure reads
as absent" semantics existsSync had, one wave instead of four serialized blocking
calls. The outer try/catch went with them -- neither resolveGitDir nor the probes
can throw now, so it was unreachable.

Part of #15036 (source-control latency).

* perf(wsl): let git reads take the shell-free route from a cwd-derived distro

shouldAttemptWslDirectGit required options.wslDistro, so a `\\wsl.localhost\...`
worktree without a resolved WSL project runtime never qualified -- even though the
distro is right there in the cwd and wslDistroForCommand already knew how to read
it. Every `git show` behind a diff therefore ran through the user's login shell,
executing their rc once per blob read.

Three changes:

- Derive the distro from the cwd when no override was supplied. This is the fix;
  the routing decision now depends on where the repo actually lives.
- Wait, bounded, for a cold read-environment probe instead of resolving without it.
  The probe is one wsl.exe call shared per distro, so the wait is paid at most once,
  and past WSL_GIT_READ_ENVIRONMENT_WAIT_MS the shell route runs exactly as before.
  It returns null rather than a settled promise when there is nothing to wait for,
  so a non-WSL git call is not pushed into a later microtask.
- Opt the blob reads into preferWslDirectGit via gitReadOptionsForWorktree (renamed
  from gitStatusReadOptionsForWorktree; it was never status-specific). Belt-and-
  braces only: `show`, `config --get-regexp`, `ls-files` and `rev-parse` were all
  already matched by isWslDirectGitReadCommand, so this changes no routing today --
  it just stops the diff path depending on a heuristic it knows the answer to.

git-blob-read also gains a `failed` flag distinguishing "git ran and reported the
path absent" (exit 128) from "the read never got an answer"; nothing consumes it
yet, the settled diff cache does.

Part of #15036 (source-control latency).

* perf(source-control): give diff reads a settled cache keyed on stamped git state

gitDiffReadDedupe coalesces only while a read is in flight, so every file
selection re-ran the whole read: a `git config --file .gitmodules` spawn, one or
two `git show` spawns, and a working-tree stat+read. On a WSL/UNC worktree each
git spawn is a wsl.exe invocation, which is the ">3s Loading diff..." in #15036.

Correctness first -- a stale diff is worse than a slow one. The cache never
expires on a clock and there is no TTL to tune. Instead:

- worktree-diff-stamp.ts takes a subprocess-free stamp of exactly the inputs a
  file diff is built from: HEAD (by resolved tip *content*, so a commit is
  visible even though HEAD's own bytes never move), `.git/index` (mtime+size),
  `.gitmodules` (submodule routing), and the working-tree file. A linked
  worktree's commondir and the packed-refs/reftable fallback are handled; an
  unborn branch is caught by recording "no loose ref" rather than only the
  packed stamps.
- The stamp is captured BEFORE the read and stored with the result. Anything
  that moves during or after the read leaves the stored stamp behind, so the
  next lookup misses. That, not a freshness window, is why a stale diff cannot
  be served.
- A store is refused unless the stamp was taken a full mtime bucket (2s, FAT's
  granularity) after its newest component. Below that, a second write inside the
  same bucket would be invisible -- git's own racy-index rule.
- `null` stamp means "cannot prove" and never caches: a folder workspace, a repo
  whose layout cannot be read, or a filesystem reporting no usable mtime.
- Submodule routes and reads that failed rather than proved absence are not
  reusable. A wsl.exe hiccup produces the same empty left side a new file does,
  and pinning that would persist a wrong diff.
- invalidateGitReadCaches clears it and bumps a generation, so a read that
  started pre-mutation cannot store its result post-mutation.

`ino` is deliberately optional in the working-tree component: Windows reports 0
for it on the redirector behind `\\wsl.localhost`, and requiring an unstable 0 to
match would make the cache silently never hit on the exact host it exists for.
Cache counters are exposed for the same reason -- a miss storm and a cold start
otherwise look identical.

Also drops gitDiffReadDedupe.clear() from getStatus. A status poll is a read; all
it did was destroy a live coalescing entry so a concurrent identical request
started duplicate git work. Mutations still invalidate through the shared point.

Memory is bounded by retained characters, not entry count -- one diff result can
legitimately hold megabytes.

Fixes the source-control half of #15036.

* perf(source-control): reuse BoundedMap and stop the WSL probe wait from outliving its answer

Review follow-ups on the settled-diff-cache work:

- SettledDiffCache now sits on the shared BoundedMap instead of hand-rolling the
  same Map + character ledger + evict-oldest loop.
- pendingWslDirectGitReadEnvironment returns null once the probe has settled
  either way, so a distro whose direct route was disabled no longer pays for a
  1.5s timer and two microtask hops on every git read.
- That wait now honours the read's abort signal and goes through withTimeout, so
  an aborted read is not held for the full bound and a probe rejection can never
  surface as a read failure.
- The settled-cache generation fence is taken before the stamp read, so a
  mutation that lands entirely inside the stamp's stats can no longer store an
  entry whose stamp is torn across it.
- The cache counters are folded into the main-thread churn probe report, which is
  what tells a permanently-cold cache apart from a cold start in the field.

* fix(source-control): tell WSL clock skew apart from a genuinely fresh write

The racy-write margin compares two clocks: capturedAtMs is this host's, while
the component mtimes come from whatever wrote the files. On a \\wsl.localhost
worktree the guest sets them, so a guest running ahead pushes every
recently-touched file past the margin and the cache refuses to store — for as
long as the skew lasts, on exactly the platform this cache exists for.

Nothing was wrong with the refusal; it was invisible. racyWrites alone cannot
distinguish "the repo was just edited" from "the clocks disagree and this will
never resolve on its own", so a permanently cold cache looked like a cold start.

isDiffStampClockSkewed flags the one thing no local write can produce — an mtime
in this host's future — and the cache counts those separately as
clockSkewedWrites. A nonzero count is the signal that the cache is off for a
reason idling will not fix.

Found by review of #16600; behavior is unchanged, only observability.
2026-08-26 15:44:11 -07:00
Neil 64c992cd56 fix(memory): report the Windows number that predicts paging, not just resident pages (#16211) (#16589)
* fix(memory): report Windows commit charge, not just working set (#16211)

On Windows the per-process figure was working set — resident pages only.
An agent whose pages Windows has trimmed to the pagefile shrinks its
working set while still holding the commit that pushes the host into
paging, so Resource Manager and `orca diagnostics memory` understated an
owned tree by 10-40x (9 codex.exe: 1.4 GB working set, 13.4 GB private)
and could not warn before the host was already thrashing.

Add committed private bytes as a second, separately-labelled quantity
rather than redefining the existing one:

- CIM sweep gains one property (PageFileUsage, UInt32 KB); the typeperf
  fallback gains one counter (\Process(*)\Private Bytes). Both ride the
  sweep that already runs.
- MemorySnapshot gains optional `privateMemory` per app/worktree/session
  plus `processCommitMetric` and `totalPrivateMemory`. Rule 1 additive
  optional fields: old clients ignore them, and absence reads as "not
  measured", never as zero — Unix hosts and older hosts send nothing.
- `totalMemory` and `processMemoryMetric` keep their exact meaning, so
  the "shared pages may repeat" copy stays true; the working-set copy now
  also says paged-out memory is not counted.
- Resource Manager shows "Σ Private" beside "Σ WS", and tints the badge
  yellow/red once tracked commit passes 60/80% of physical RAM — the same
  thresholds `usageTextColorClass` already uses for host usage. Tint and
  tooltip only; no toast, and the badge number is unchanged.

The parsers move to windows-process-sample-parsing.ts and the Windows
sweep tests to their own file to stay under max-lines.

Not migrating the collector to windows-process-table.ts: the native
snapshot exposes no commit figure and no CPU times, and truncates
WorkingSetSize through a DWORD. Documented in the enumeration reference.

* fix(memory): derive the typeperf field cap from the counter list

The fallback parser's 8192-field cap was sized for three `\Process(*)`
counters. Adding `Private Bytes` cut the parsable process count from ~2730
to ~2047, and overrun is a blackout (`parseTypeperfCsvLine` returns `[]`, so
the whole sweep reports nothing) rather than a truncation. The counter list
now lives beside the decoder that reads those names back out of the PDH
header, and the cap is derived from it.

Also collapses the four spellings of "omit privateMemory when unmeasured"
in collector.ts onto one `commitField` helper, drops the unread parameter
and the never-rendered `columnLabel` from `getResourceCommitMetricCopy`,
folds `getCommitPressurePercent` into the only function that called it, and
reverts unrelated Prettier churn in the Windows enumeration doc.

The commit tint's doc comment no longer claims to predict host paging: it
measures Orca's own share of physical RAM. Host commit charge / commit
limit stays a follow-up (#16211).
2026-08-26 15:43:02 -07:00
Neil 015f904fca fix(codex): stop re-scanning all Codex session history on every launch (#16251) (#16593)
* fix(codex): stop re-scanning all Codex session history on every launch (#16251)

A launch deleted the backfill completion marker, and a marker could never
be written while a Codex pane was open, so every launch re-derived
"needs full scan" and walked the entire .codex/sessions tree — on Windows
with a large history that read as a hung window.

- v4 marker keeps a durable full-history baseline plus a bounded set of
  pending dates. v3 is read as a baseline, so upgrades pay no full scan.
- A launch now marks dates pending instead of deleting the marker, and a
  full pass certifies the baseline even while a pane is still running;
  the live pane's own date just stays pending.
- Pending dates are persisted, so an abnormal exit or a cross-midnight
  pane recovers a bounded window instead of a full walk.
- A date-limited pass can only extend an existing baseline, never create
  one, so it can no longer certify history it never looked at.
- Marker and index-heal target roots compare through
  normalizeRuntimePathForComparison, so Windows spellings of one
  directory stop invalidating each other.
- Both append-only ledgers stream instead of readFileSync + whole-file
  JSON.parse, keeping the main thread responsive on large histories.

* fix(codex): keep the backfill marker's full-scan demand durable

Review follow-ups on the v4 backfill marker:

- markCodexSessionBackfillMarkerPending no longer erases a persisted
  needsFullScan; the demand survives until a generation-current full walk
  retires it, and the function now reports it so the launch path folds it
  into its own in-memory flag (as @rumoii's #16252 does).
- A full pass settles the whole pending set instead of subtracting the
  empty set, so a date a full walk provably covered stops forcing an extra
  bounded pass on every startup.
- isCodexSessionBackfillDate does a real calendar check, so a corrupted
  marker cannot carry 2026/99/99. No age or future bound: the same guard
  gates rollout publication and a clock-skewed directory holds real
  sessions.
- 'scans only the current date once a baseline exists' now has a second
  date directory, so it fails on a full walk instead of passing either way.
2026-08-26 15:42:54 -07:00
Jinjing 614d2d4a28 fix(cmd+j): always enable See more for soft preview hints (#16661)
The leading preview section now shows an actionable 'See more' button
even when all rows fit within the hard cap, letting users expand and
browse more tabs without scrolling past the worktrees section.
2026-08-26 15:33:11 -07:00
Brennan Benson 81f89a705c fix(terminal): bound WebGL context-loss retries on tab reveal (#16338)
* fix(terminal): retry bounded WebGL recovery on tab reveal

* test(terminal): cover reveal repaint and pruned diagnostics

* fix(terminal): make WebGL diagnostics pure and cover reveal refusal

* fix(terminal): correct WebGL retry comments
2026-08-26 15:23:07 -07:00
Brennan Benson ac76e0dd06 fix(runtime): cancel pending driver timers on desktop reclaim (#16337)
* fix(runtime): cancel pending driver timers on desktop reclaim

* fix(runtime): centralize pending driver cancellation

* fix(runtime): complete pending driver cancellation extraction

* test(runtime): cover desktop reclaim mutation branches
2026-08-26 15:20:36 -07:00
Jinwoo Hong 0e10fc5925 fix(browser): retire helpers with page owners (#16564) 2026-08-26 15:09:22 -07:00
OrcaWinandOrcaWin 7d5c7aa9c3 i18n: Make skill install dialogs and errors translatable (#16682)
Extract hardcoded error messages and status labels from skill
installation components into the i18n system. Supports localized
UI for install flows in English, Spanish, Japanese, Korean, Chinese.

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-26 15:04:51 -07:00
Neil ef0d5931bc fix(source-control): budget WSL bulk git command lines by bytes, not path count (#16634)
Selecting ~100 changed files in a WSL worktree and hitting Stage All did
nothing: the files stayed unstaged and the operation reported a failure.
Bulk stage/unstage/discard chunked pathspecs 100 at a time, a count picked
against a raw argv. A WSL-routed write is not a raw argv -- it is folded
into one login-shell command line that shell-quotes every pathspec, quotes
the result again, and embeds it three times (one branch per guest shell),
so the finished line runs ~3.4x the raw pathspec bytes. Realistic project
paths blew past the 32767-character CreateProcess cap at 100 paths and
wsl.exe refused to spawn, with nothing staged.

Chunking now measures the finished command line through the real resolver,
so the wrapper's quoting rules live in one place and native, WSL and SSH
hosts each get the budget of the host that actually spawns. A pathspec too
long to fit alone still ships alone rather than being dropped, and no chunk
is ever emitted empty -- a pathspec-free `clean -ffdx` would have swept the
whole worktree.

The tracked-path listing behind that discard also fences the WSL login
shell now. Its stdout was parsed NUL-delimited without a fence, so Ubuntu's
interactive rc banner glued itself onto the first record: that path failed
to match anything git reported and was treated as untracked, sending a
tracked file to `git clean` instead of `git restore`. Not observing a path
in ls-files output is not evidence the path is untracked.

The Windows command-line cap and its libuv-aware length estimate move out
of the WSL runner into src/shared/windows-command-line-budget.ts, shared by
both callers.
2026-08-26 14:37:15 -07:00
Neil d1a11b3299 fix(pty): match the echo shapes a real tty actually produces (#16542)
Reply echo suppression modelled two echo shapes from the spec rather than from
a tty. Captured under node-pty against real bash, at a readline prompt and
under `read`:

  - Readline mangles CSI replies, not just OSC: `ESC [ ?` becomes BEL and the
    residue echoes. The projection was gated on an OSC introducer, so a private
    DSR echo was never matched at a readline prompt. This is the reachable one:
    a mode-2031 theme push (`CSI ?997;1n`) left latched by an exited TUI paints
    `997;1n` on a bash prompt (#9993's scenario).
  - ECHOCTL carets EVERY control, not just ESC. A BEL-terminated OSC reply
    echoes as `^G`, but the needle kept a literal BEL — a string no tty
    produces. Hardening only: every in-tree OSC reply is ST-terminated
    (terminal-osc-color-reply.ts:112, xterm's own reply), so the changed byte
    is unreachable except from a foreign or older emulator.

Why this is not the CSI projection #13160 review dropped: that one was the
identity (`replaceAll('\x1b]', …)` is a no-op on a CSI reply), so it was
ESC-led and 500ms-held bare-ESC tails away from the query parser. This one is
BEL-led. The rule is now asserted for every shape rather than implied by the
gate: holdPartial iff the needle does not start with ESC.

The readline branch is keyed on the private-DSR grammar with a non-empty
parameter list, plus a floor on needle length. The containment grammar admits
`CSI ? n`, and `answerLiveQueryReply` takes client-supplied bytes on the relay
path, so a peer could otherwise arm a two-byte `BEL n` needle and delete the
first bell-then-`n` in ordinary output. #61c65151129 proved this system can eat
real output when a needle outlives its budget; a length floor is cheap.

Live coverage: pty-reply-echo-shapes.node-pty.test.ts writes a reply to a real
bash master and feeds back what it echoes, so a shell or libc change fails the
suite instead of silently disarming suppression. Registered in the
shell-contracts lane. The transcript tests and the caretEcho helpers that
encoded the same ESC-only assumption are corrected alongside.

Suppression is display-only. This does not change what reaches the child's
stdin — the reply is written to the master either way, in call order.
2026-08-26 14:36:45 -07:00
Brennan Benson e8005c3325 fix(codex): preserve WSL account home trust (#16496)
* fix(codex): preserve WSL account home trust

* fix(codex): preserve WSL drive path semantics

* fix(codex): preserve mounted-drive WSL config paths

* test(codex): preserve WSL path helpers in mock
2026-08-26 14:33:34 -07:00
Brennan Benson a624e7cd5d test(agent-status): inventory legacy pane identity surfaces (#16575)
* feat(agent-status): measure identity evidence before migrating any consumer

PR 1 of the identity migration. It changes no displayed or routed identity — it only measures.

Why measure first: the hierarchy shipped in #16148/#16157 has zero consumers, while ~31 sites still
derive identity independently. Every migration decision after this is currently a guess, including
the one that matters most — how often a real pane has no evidence at all. A live P0 reports "No
Claude status shown", and this design trades toward showing nothing when uncertain, so the blank
rate has to be a number before any surface moves.

- `pane-agent-identity-evidence.ts` — one assembler that gathers a pane's evidence, so consumers
  stop each inventing their own ladder.
- `pane-agent-identity-census.ts` — shadow-only counters keyed by host kind (native / wsl-host /
  wsl-distro / ssh / relay) and launch mode (typed / orca-launch / resume). Records a bitmask of
  which sources were present and whether the resolver returned null or ambiguous. No titles,
  prompts, paths, handles, or agent text.
- `pane-agent-identity-inventory.test.ts` — a ratchet that fails when a legacy identity helper
  gains a new production caller, so the surface cannot grow while the migration runs.

Three review findings are encoded rather than deferred: launch stays above run-key-less completed
hooks (promoting the hook lets a stale record hijack a pane); OMP/Pi evidence is owner-normalized
before assembly, since OMP emits Pi-compatible frames and a wrapper's hook would otherwise be read
as the agent it wraps; and Windows-side `wsl.exe` is rejected as process evidence, because the host
observes the distro wrapper rather than the agent inside it.

The census cannot be completed from a worktree. It needs representative native, SSH, WSL and relay
cohorts collected from real use, and that review is the gate on PR 3 — not this PR.

* test(agent-status): keep identity migration inventory-only

* test(agent-status): reuse reliable source scanner

* test(agent-status): bound inventory scan work

* test(agent-status): avoid inventory path false negatives

* test(agent-status): refresh identity inventory after base repair

* test(agent-status): correct inventory classifications

* test(agent-status): correct action boundary inventory

* test(agent-status): pin inventory occurrence counts

* test(agent-status): fail closed on scanner desync
2026-08-26 14:33:02 -07:00
Jinjing 87ede54eb8 Show all automation destination hosts, disable ineligible ones (#16665)
* Show all automation destination hosts, disable ineligible ones

Previously, filtering to only eligible hosts hid all connected hosts
on pre-host-scoping Orca servers. Now all offered hosts appear in the
picker; ineligible ones are disabled with a message naming which
servers need updating to support them.

* Show all automation destination hosts, keep create available when all ar

- Gate the create button on what the picker offers, not on readiness: with every
  offered host ineligible (e.g. all pre-host-scoping servers), the dialog is
  where the repair is stated, so the button must still open it.
- Fix StrictMode double-mount lifecycle: disposed controller revived by effect,
  unsubscribe before dispose to prevent event leaks under simulated unmount.
- Validate create destination early, before hooks load and trust prompt, so the
  user never answers a trust dialog for a destination that would reject.
- Dedupe capability probes per authority incarnation: concurrent callers share
  one in-flight status.get; confirmed capabilities never re-probed.
- Drop cache payload at retirement so revived hosts refetch instead of showing
  stale rows.

* Add TTL-based capability probe caching and fix automation dialog target

Extract capability probing to a separate module with improved caching strategy: confirmations now expire after 60 seconds and the cache is bounded to 32 entries, enabling in-place runtime replacements to invalidate old confirmations. For uncaptured automation owners, resolve the dialog target to the same host the save addresses rather than relying on ambient context, preventing stale host references. Optionally await external managers after mutations to ensure row re-reads reflect recent writes.

* Fix automation tests after rebase

* Refactor capability probe to fence fencing checks from in-flight probes

Fencing checks need fresh probes since in-flight probes may predate
in-place runtime replacements. Extract shared probe deduplication
into `sharedCapabilityProbe()` and unconditional probe start into
`startCapabilityProbe()`, then route based on cache preference.
2026-08-26 14:31:32 -07:00
Jinjing aab6464a6a feat(editor): implement Shift+Tab to unindent lists and code blocks (#16677)
Add keyboard handlers for Shift+Tab that unindent code block lines and
lift nested list items. Properly handles mixed bullet/task nesting by
retyping items to match their enclosing list. Provides symmetric control
over indentation to complement Tab's indent behavior.
2026-08-26 14:09:04 -07:00
Neil c1a7748267 fix(agent-hooks): stop the Windows hook launcher spelling the AV-denied flag triple (#16576)
* fix(agent-hooks): stop the hook launcher spelling the AV-denied flag triple

Orca's Windows agent-hook launcher ran

  powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden \
    -EncodedCommand <base64>

That exact combination is the textbook "hidden encoded PowerShell" malware
shape, and endpoint security denies it at process creation whatever the
payload decodes to -- even `exit 0`. Every injected hook then failed with
`powershell.exe: Permission denied` (exit 126 from bash's execve, EACCES)
on every turn, for both Claude Code and Codex, with no AV exclusion that
re-enabled it.

Dropping any one of the three flags clears the signature. `-ExecutionPolicy
Bypass` is the one that can move: it sets the Process scope, and so does
`Set-ExecutionPolicy -Scope Process`, which now rides inside the encoded
payload. `-EncodedCommand` is never policy-gated, so the bypass always gets
to run before the managed script does -- which is what keeps Copilot's .ps1
hook working under a Restricted or AllSigned machine policy.

The hidden window and the encoding are unchanged, so nothing regresses for
#14815, #14818 or #6078.

Closes #16003

* fix(agent-hooks): ship the launcher shape #16003 actually measured as allowed

The previous revision of this branch dropped only `-ExecutionPolicy Bypass`
and kept `-WindowStyle Hidden -EncodedCommand`, on the reasoning that
"dropping any one of the three flags clears the signature". That sentence is
not in the bisect. The reporter ran exactly four command lines on the affected
Kaspersky/Windows 11 host:

  -NoProfile -WindowStyle Hidden -Command 'exit 0'                       -> 0
  -NoProfile -EncodedCommand <b64>                                       -> 0
  -NoProfile -ExecutionPolicy Bypass -Command 'exit 0'                   -> 0
  -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand -> 126

Every passing row drops two flags. No row drops exactly one, so the shape the
branch was about to ship had never been executed on the machine that reports
the bug -- and it is `-WindowStyle Hidden -EncodedCommand`, which is the
"hidden encoded PowerShell" pair the denial is named for in our own comment.
Shipping it would have closed #16003 while leaving every hook on that host
dying at CreateProcess, with no tracking left open.

So emit the measured-passing encoded row instead: `-NoProfile -EncodedCommand`.
Of the two flags there was a choice between, `-EncodedCommand` is the one that
carries correctness -- it is what keeps paths and switches intact across
cmd.exe and MSYS (#6078, #14815). `-WindowStyle Hidden` costs at most a console
flash, and only where the parent has no console to inherit.

Second, the relocated bypass now runs inside try/catch. Under a MachinePolicy
or UserPolicy GPO scope, `Set-ExecutionPolicy -Scope Process` reports that the
process scope did not take. `-ErrorAction SilentlyContinue` covers only the
non-terminating half of that; the command-line switch it replaces was silent
either way. This file already documents that non-stdout PowerShell streams
corrupt consumers merging our output into JSON stdout, so a per-invocation
ErrorRecord on stderr is a regression we should not trade for the switch.

Refs #16003

* fix(agent-hooks): keep the hook console hidden while dropping the AV-denied flag

Round 2 of this PR widened the fix from "stop spelling -ExecutionPolicy Bypass"
to "stop spelling it and -WindowStyle Hidden", on the reasoning that the #16003
reporter never measured a shape that drops exactly one flag, so keeping the
hidden+encoded pair would be extrapolation.

That trades a reproduced regression for an unmeasured one. Window suppression is
the shipped fix for #14815 and its four duplicates (#14828, #15117, #15447,
#15767): a hook launched from a parent with no console gets a fresh console per
event, which takes foreground and eats whatever the user is typing into Orca,
and never closes at all on the stdin-blocking path hook-stdin-contract.ts exists
to guard. That fires on every prompt, tool call and stop of every managed agent.
The AV denial, by contrast, is measured only for the full triple; that the
remaining pair still trips it is a hypothesis. Between a certain regression and
a possible one, keep the certainty.

So the flag that leaves the command line is the policy bypass alone — the only
one of the three with an exact in-payload equivalent, hence the only one that
can move without losing behaviour. If the pair turns out to be denied too, the
answer is a different shape that still hides the window.

* fix(agent-hooks): silence progress before the policy bypass can autoload (#16621)

Hardware-measured on Windows 11 while exercising #16576.

Set-ExecutionPolicy autoloads Microsoft.PowerShell.Security, and that module's
"Preparing modules for first use." progress record is written before any later
assignment can suppress it. Running the bypass first therefore defeated the
silencer that runs immediately after it:

  bypass-first    stderr = 616 bytes, first merged line '#< CLIXML'
  silencer-first  stderr = 0 bytes,   first merged line '{"decision":"approve"}'

That is precisely the corruption HOOK_PROGRESS_SILENCER's own comment warns
about -- redirected progress becoming CLIXML that can corrupt merged JSON -- so
the PR reintroduced the hazard it documents, one line below documenting it.

Both existing tests asserted the broken order, so they enforced the bug rather
than catching it. Reordered them and added one that pins the ordering itself
rather than the literal string, since the string will drift again.
2026-08-26 14:08:58 -07:00
Jinjing fda213a4e8 Improve automations table layout and column sizing (#16667)
* Improve automations table layout and column sizing

- Wrap table containers with min-width constraint for horizontal scrolling
- Adjust grid column widths for better visual balance
- Simplify automation draft building with helper function
- Remove unused validation checks and imports

* Make automation list first column sticky

- Keep automation name visible when scrolling horizontally
- Adjust header z-index to layer above sticky cells

* Remove unused canCreateAutomation prop from test
2026-08-26 14:03:46 -07:00
Jinwoo Hong 8d61cb8b77 fix(relay): survivable mobile pairing recovery + desktop assign rate gate (#16659)
* fix(mobile): retry the stored assignment when the director reports no newer move

A director answering /v1/connect can only reply relay-moved with the stored
assignment; it has no 'assignment unchanged' verb, and sticky assignments make
equal-epoch replies the steady state. Treating every non-newer move as fatal
made pairing recovery unwinnable for any transient cell dial failure (DRAINING,
1006), which bricked off-LAN pairing on Android 0.0.44.

A non-newer move now confirms the stored assignment: the candidate re-dials it
with a 250ms floor instead of abandoning the relay path. The move is never
adopted or persisted, so the anti-rollback contract (requireStrictlyNewerEpoch
for persisted moves) is unchanged. 4429 stays out of director recovery: each
cell dial burns an invite attempt server-side and a director hop cannot relieve
cell load.

* fix(mobile): honor relay director Retry-After when pacing recovery

Mobile /v1/resolve collapsed every non-OK status into a generic error and
discarded Retry-After, so overloaded windows produced hammering instead of
paced retries. RelayDirectorHttpError now carries status and retryAfterMs
(clamped to 120s), and the reconnect controller floors its existing transport
delay with it — no new timers or retry state. The Retry-After parser is
extracted from the desktop relay client into src/shared and reused by both.

* fix(mobile): attribute pairing log lines to their candidate path

The pairing race interleaves the direct LAN and relay candidates into one
PAIRING LOG pane; direct lines (WebSocket closed, Reconnecting 10.x.x.x:6768)
carried no path label and repeatedly read as Relay retrying a private IP —
misleading users and two investigations. The coordinator now wraps each
candidate's sink with an idempotent Direct:/Relay: prefix at the one seam
where both paths are known.

* fix(relay): gate desktop /v1/assign at the per-host rate limit

The director rate-limits /v1/assign per host at 5s, but every desktop retry
path could fire immediately: both schedulers draw full jitter from [0, cap]
(floor 0), first attempts after a drain are undelayed, the 400-fallbacks issue
up to 3 assigns per round trip, and reconcile() cancels the armed Retry-After
timer from ~8 refreshDemand callers. Production shows hosts permanently
rejected at ~100-200 rejects per success.

A shared per-host gate now lives inside requestRelayAssignment — the single
assign call site — so every path books a >=5s (+jitter) slot. Retry-After
raises the gate persistently, surviving the coordinator's timer cancellation.
Concurrent callers serialize through a per-key chain. Callers with staleness
fencing pass isCurrent; a superseded caller aborts after the wait instead of
spending the host's slot. Internal 400-fallback retries stay one logical
attempt and do not re-enter the gate.

* refactor(mobile): rename the log-only assignment-echo predicate

isCurrentAssignmentMove no longer gates control flow — every non-newer move
retries the stored assignment — so the name overstated its role.

* fix(relay): honor mid-wait raises and cap the assign gate's inline wait

Review findings on the per-host assign gate: the deadline was read once
before sleeping, so a sibling's Retry-After landing mid-wait was ignored
(the exact storm the gate exists for), and the sleep was uncancellable —
a booked five-minute Retry-After could park pairing IPC, which awaits
reconcile inline, for its full duration.

The wait now runs in 1s slices, re-reading the deadline and the caller's
isCurrent fence each slice. Remaining waits beyond 15s fail fast as a
RelayHttpError 429 carrying the remainder, so the existing schedulers
pace with it while the gate keeps the deadline. Staleness aborts are
classified non-retryable. Also from review: the broker's isCurrent wiring
and the shared-gate default are now pinned by tests, the 4429 comment
states the reservation-order rationale precisely, the mobile Retry-After
ceiling is renamed to avoid colliding with the desktop's 5-minute one,
and a past-HTTP-date header case is covered.

* fix(relay): tag locally paced assigns and warn about frozen test clocks

Review polish: the synthesized 429 for a beyond-cap local wait now carries a
distinct message (relay_assignment_locally_paced_429) so log censuses can tell
it from a real director 429, with the comment stating the invariant that makes
the translation honest (local booking alone never exceeds ~5.5s). The gate's
sleep option documents that test fakes must advance the clock — the slice loop
re-reads it and never terminates against a frozen one.

* fix(relay): fence superseded callers at the assign send boundary

reserve() checks staleness while waiting, but a caller superseded after
booking — or between the 400 field-fallback retries — could still spend
one to two requests on an assignment nobody consumes. Re-check
isCurrent at the top of sendRelayAssignment so the fallback recursion
is fenced too.
2026-08-26 13:29:46 -07:00
Brennan Benson 8a07bbd8cf fix(orchestration): enforce nested worker depth instead of an accidental fence (#16668)
* fix(orchestration): enforce nested worker depth instead of an accidental fence

Orca documented that "dispatched workers cannot spawn their own sub-workers
(worker-start is coordinator-fenced)". No such check existed. What existed was a
single Run-binding check in the workerStart RPC: a worker's terminal is not bound
to a Run, so worker-start happened to fail. The rule was emergent, asserted by no
test, and written in no doc — and it leaked. A worker could run-create its own
Run, task-create, and worker-start: now bound, the check passed.

Replace it with a real, configurable depth cap.

Depth is derived from the caller's own active Dispatch rather than from Run
binding, which is what dissolves the run-create bypass: creating a Run does not
stop you being a worker. Enforcement lives in a single dispatch-row writer that
owns all three INSERTs that mint a live worker — the generic claim, the supervised
worker-start path (including every retry), and the remote attachment. Two of those
were missed by earlier drafts of this change, so `creator` and `maxDepth` are
required parameters: a new spawn path cannot compile without deciding, and a
boundary test refuses the SQL anywhere else.

Schema v30 adds depth to dispatch_contexts and remote_dispatch_attachments,
NOT NULL DEFAULT 1 and backfilled to 1 so an unstamped or pre-upgrade row fails
closed rather than reading as a root coordinator. The attachment pane indexes
widen to the five states in which a remote worker may still be running:
loss of contact is not evidence of process death, so an unverifiable worker still
counts as a nesting parent.

Also adds the caller-evidence assertion that workerStart was the only Run-scoped
verb to skip, so a declared --from cannot name another terminal's pane and inherit
its depth.

Default is 1, so behaviour is unchanged unless the new setting is raised. Two
limitations are deliberate and documented rather than papered over: this is a
guardrail and not a security boundary, since a caller whose launch evidence is
unverifiable (any ordinary restored terminal) can declare another handle; and it
is enforced at supervised dispatch creation, so a settled worker whose process is
still alive counts as a root again.

* fix(orchestration): share caller resolution and pin worker gaps

* refactor(orchestration): make the caller resolver's pane contract explicit

Overloads so requireStablePane callers get a non-null string instead of casting,
and rename the attestation opt-out to say what it means: the caller asserts it
itself. A flag called assertEvidence:false reads as "attestation optional",
which is the hole this helper exists to close.

* fix(orchestration): propagate dispatch depth to federated workers

* chore(cli): refresh bundled orchestration guide
2026-08-26 13:22:09 -07:00
Brennan Benson 256f23c7a0 fix(automations): validate create destination projects 2026-08-26 12:58:41 -07:00
Brennan BensonandSiddiqui Qamar 5a59bc5bc4 fix(grok): stop Orca's Grok hooks from costing anything outside Orca (#16666)
* fix(grok): stop Orca's Grok hooks from costing anything outside Orca

Orca registers Grok agent-status hooks in the global $GROK_HOME/hooks. Grok
loads that directory on every session, so a Grok run that Orca did not launch
still paid for the hook on every event, and Orca rewrote the file even after a
user had emptied it to opt out (#15518).

The registered POSIX command now guards on ORCA_PANE_KEY before doing anything.
That variable is part of the pane identity Orca injects into terminals it
launches, and unlike the port and token it never comes from the endpoint file,
so it is present exactly when the session belongs to Orca. A standalone session
short-circuits without spawning a shell for the managed script at all. The same
guard is applied to the remote install, because a remote host runs standalone
Grok sessions too.

PreToolUse is no longer registered. It is a blocking hook, so Orca sat on the
critical path of every tool call and doubled the per-tool spawns, for a
transition PostToolUse already reports.

Windows cannot use the guard: the command there must be a single spawnable
token, so it is a bare script path with no shell to evaluate a test. For that
case the hooks are removed when Orca quits -- locally, on WSL guests, and on
connected SSH hosts -- and reinstalled on the next launch. A config the user has
emptied is left alone on startup; turning the setting back on in Settings is an
explicit and later choice, so that path reinstalls.

Removal is careful about what it is deleting. It strips only Orca's own entries,
keeps user-authored ones, and deletes the file only when no hook entries remain
-- keying that off the whole object would leave a stray non-hook key behind, and
the emptied-config check would then read that remnant as a deliberate opt-out
and never reinstall. A config the user has symlinked into a dotfiles repo is
written through rather than unlinked, and is exempt from the emptied-config
check for the same reason: after a quit it is a file Orca emptied, not one the
user did.

Writes go through temp+rename. Grok refuses to build a sandbox profile for a
hook JSON with more than one hard link, so publishing by hard link would fail
any session that started during the write.

Install and removal on remote hosts now read the platform from the same field.
They did not, so a Windows remote whose bridge env was incomplete had hooks
installed and never removed.

Co-authored-by: Siddiqui Qamar <137684575+siddqamar@users.noreply.github.com>

* fix(grok): preserve hook state outside Orca

---------

Co-authored-by: Siddiqui Qamar <137684575+siddqamar@users.noreply.github.com>
2026-08-26 12:48:52 -07:00
Brennan Benson 588eec68b4 fix(native-chat): stop rendering a tool result whose call is outside the window (#15653)
* fix(native-chat): stop rendering a tool result whose call is outside the window

A tool result carries no call id, so it can only be attributed to a tool
call loaded alongside it. Both chat views read a windowed transcript tail
(mobile 40 messages, desktop 300), and the window regularly opens between
an assistant's `tool_use` record and the user-role record that answers it.
Claude also re-emits already-answered `tool_result` records at a `/compact`
boundary, long after their call scrolled out of the window.

`foldToolMessages` had no rule for those: with no assistant predecessor in
the output they were pushed through as standalone messages and rendered as
a bare, unowned block of raw tool output with no tool name — reading as a
message from nowhere mid-conversation. Sampling real Claude transcripts,
176 of 400 sessions (44%) produced one in a mobile-sized first page.

Drop a result no loaded call can own, before folding. It is not lost: it
comes back attached to its call as soon as the owning turn pages in.

* fix(native-chat): scope tool result attribution to folded turns

* fix(native-chat): preserve harness-attributed tool results

* fix(native-chat): keep interruption boundaries
2026-08-26 12:38:59 -07:00
Brennan Benson d9c77c5830 fix(automations): restore main checks 2026-08-26 12:21:48 -07:00
hwantage b755629f37 feat(i18n): add Korean translations for CLI-created workspace labels (#16212)
- Localize filter toggle labels in SidebarFilter and SidebarWorkspaceFilterSection.
- Localize card detail descriptions in WorktreeCardCliDetailSection.
- Localize meta badge accessibility label in WorktreeCardMetaBadges.
- Resolves English fallback for CLI-created workspace UI under Korean locale.
2026-08-26 11:47:06 -07:00
Jinjing cda2280d63 Show all automations (#16532)
* Add all-host automations with scoped ownership and multi-authority suppo

Enable automations to run on multiple hosts (SSH targets and local) with
owner-fenced mutations, scoped list queries per host, and conflict
resolution. Introduces desktop and runtime authorities as distinct
automation storage owners, with per-host caching, invalidation, and
retry scheduling on the renderer. Captures registration generations for
SSH hosts to survive re-adoption. Adds CLI support for destination
selection and conflict recovery.

* Filter automation create projects by destination host

Only offer projects available on the selected destination, preventing
the mismatches that would fail at submit time. Auto-adjust the project
selection if it becomes unavailable when the destination changes.

* Add runtime storage authority support for automations

- Support both runtime and desktop as automation storage authorities
- Make owner preconditions optional for legacy-client compatibility
- Cache automation list projections to improve performance
- Add per-row repo/worktree resolution for cross-authority collisions
- Extend automation.list RPC to always include owner metadata

* Replace child_process.execFile with runProcess for external automations

- Migrate external-manager to use cross-platform runProcess wrapper per child-process safety policy
- Abstract electron app/ipcMain APIs in orca-runtime via environment accessors
- Install fake app environment in automation tests for consistent setup
- Reorganize imports to use specific module paths (ssh-target-registry, agent-detection, browser-error)
- Remove external-manager from child-process import allowlists (no longer violates direct import)

* Unify desktop automation CRUD onto the local runtime RPC surface

The desktop authority now speaks the same automation.* RPC contract as
remote runtimes, via callRuntimeRpc({kind:'local'}) -> runtime:call ->
the shared RpcDispatcher. The automations:list/listRuns/create/update/
delete/runNow IPC arms, their preload members, and every renderer
desktop-vs-runtime transport fork are retired; the runtime methods are
the single implementation of scoped lists, owner fencing, and change
publication for both transports (mobile clients already exercised them).

The desktop probe scheduler's priority lease survives the move as an
AutomationService hook the IPC registration installs and the runtime
methods take, so Orca's own automation traffic still parks queued
external-manager probes.

External-manager scope arms and dispatch-loop plumbing stay on IPC by
design; automation change events keep their existing channels (renderer
ingestion already converges them by authority).

* Remove automation ghost SSH tombstone scanning

This functionality for synthesizing tombstones for automation-referenced SSH
targets is no longer needed as part of the automation system refactoring.

* Refuse orphan automations at dispatch time, not migration time

Remove migration-time disabling of orphan automations and the `enabledDecidedBy` field. Dispatch now refuses orphans at runtime instead, simplifying state management and UI. Orphans are left unstamped and enabled; dispatch refuses to run them via `resolveAutomationRunTarget`.

* Show all automations in flat table with unified filter menu

- Replace host picker component with comprehensive Filters menu supporting status, last run, agent, and host filters
- Flatten automation list layout to single table instead of host-grouped sections
- Add Host column to display execution host for each automation
- Display active filters as removable pills below toolbar
- Delete unused AutomationHostPicker* components

* Add automation owner fencing and destination validation

- New AUTOMATION_OWNER_FENCING_RUNTIME_CAPABILITY for owner preconditions; legacy clients get owner metadata snapshotted at RPC boundary for compatibility
- Editor captures and revalidates automation destination before save, preventing silent retargeting if SSH infrastructure changes mid-edit
- SSH target types now isolate renderer-authored fields; generation is server-owned and stripped by IPC handlers

* Route automation recovery actions to the origin host

When an automation action fails due to owner fencing, recovery verbs
("Update server", "Reconnect") must run on the host where the refusal
originated: the row's captured owner for row operations, or the
destination the create dialog captured, not the list's filtered host.

* Remove external manager scope limitation notices

Consolidate create destination eligibility checks with a unified predicate
and fix the bug where desktop repo IDs could be sent to runtime hosts where
they cannot resolve.

* Persist only store-derived automation contexts, not client-perspective o

Store contexts must never be based on client-provided runContext or sourceContext
values—clients speak a different perspective (e.g., 'runtime:<id>' for host IDs
they assign), and persisting those makes the store projection orphan automations
it actually owns. Derived contexts now take precedence in create and update paths,
with explicit null still honored to clear a value. Tests verify this by simulating
drift after storage and confirming that moves re-derive while toggles preserve.
2026-08-26 09:50:12 -07:00
Neil 885106baee fix(terminal): stop routing unowned workspaces to the focused runtime (#16584)
`createWebRuntimeSessionTerminalResult` collapsed an explicit
`environmentId: null` ("I resolved ownership and nobody remote owns this")
into "caller said nothing", then fell back to
`settings.activeRuntimeEnvironmentId`. The tab-strip "+" shell rows and the
guest-focus Ctrl+T relay both pass that explicit null, so a local workspace's
new terminal was created against whatever remote runtime happened to be
focused, which answered `selector_not_found` for a worktree id it had never
seen.

The same call selects the runtime as the workspace's execution host before
the create, and the error path never handed that selection back — leaving the
workspace latched to the runtime that just refused it, so every later
owner-routed action (the next Ctrl+T included) silently followed the latch
until a workspace switch reset it.

Fixes #16444
2026-08-26 04:37:17 -07:00
Neil 44a3ba59e6 test(git): isolate worktree-shared-directories git config portably (#16582)
`os.devNull` is `\\.\nul` on win32. Git normalizes it to `//./nul` and rejects
it as a config path, so every `git` call in this suite threw in `beforeEach`
and 15 of 19 tests failed on Windows. POSIX resolves the same constant to
/dev/null, which Git accepts, so CI never saw it.

Point GIT_CONFIG_GLOBAL at a real empty file in a private mkdtemp directory,
matching how skill-git-tree-identity and skill-windows-workspace already
isolate, and use GIT_CONFIG_NOSYSTEM instead of GIT_CONFIG_SYSTEM.

Set both on `process.env` rather than only on the suite's `git()` helper.
`resolveWorktreeSharedDirectories` runs its own `git check-ignore` through the
production runner, and `GitRuntimeOptions` carries no env, so the runner
inherits `process.env`. The per-call override never reached the code under
test: a host `core.excludesFile` could make a fixture that is not gitignored
come back as ignored.

Fixes #15409
2026-08-26 04:36:28 -07:00
Neil 19e9ec695b perf(windows): ship the native process table to Windows relay hosts (#16598)
* feat(windows): let a relay host bind the native process table directly

The CIM fallback from #16550 answers on relay hosts, but it costs a
powershell.exe and ~1.4s per scan where the native reader costs ~57ms.
It is a parachute, not the destination.

Teach the loader a second source: the desktop app keeps resolving the
npm package, and a relay host -- which has none of our node_modules --
binds a bare `windows-process-tree.node` staged beside the bundle. The
CIM scan stays as the last resort, so a host with neither is unchanged.

Bind the addon directly rather than its package wrapper. lib/index.js
adds only a queue over getProcessList, and that queue is the wedge this
module already defends against: it latches a module-global
requestInProgress with no try/catch. We hold our own single-flight and
deadline, so going straight to the addon drops the duplicate.

Measured on a Windows 11 SSH host with ~1490 processes, running the
relay-externals bundle from the deployed relay directory:

  no addon staged   nativeAvailable=false  1247ms  (CIM)
  addon staged      nativeAvailable=true     57ms  memory restored

Degradation was exercised on that host, not just in fakes: a truncated
upload, a text file, and a foreign-arch ELF each fall through to the
scan rather than throwing, and restoring a good addon recovers. A file
that loads but lacks getProcessList is rejected by shape, because
binding to it would reject every read forever where falling through
still answers.

No artifact is staged yet, so this is inert until the packaging change
lands: today every relay takes the same CIM path it does now.

* build(relay): ship the Windows process-table addon to relay hosts

The CIM scan restored correctness on Windows SSH hosts, but it costs a
powershell.exe and ~1.4s per read where the native addon costs ~57ms. It
was always the floor, not the destination.

The addon cannot be npm-installed on a relay host: it carries a
binding.gyp, so npm rebuilds from source and the build wants
Spectre-mitigated libraries even where MSVC is already present. The
binary inside the published tarball loads, but predates our patch and
still caps enumeration at 1024 processes -- on a 1486-process host it
returned exactly 1024 rows with the querying process among the missing,
which reads as unavailable only under load. No published alternative
clears the bar either; the one fork with a working prebuild story still
carries the same cap.

So build it where a compiler exists and ship the result. The build script
refuses unpatched source -- checking the source rather than trusting the
install, because the Spectre hunk fails loudly while the 1024 hunk fails
silently -- and verifies the PE machine field so a cross-build cannot
emit host arch for another target.

The artifact is optional: hashed when present so a relay carrying it
never shares an immutable directory with one that does not, and never
probed, since requiring a file only a Windows build machine can produce
would make a correct relay read as MISSING and redeploy forever. Builds
on any other OS keep using the scan, unchanged.

arm64 cross-compiles from the x64 runner but needs the optional MSVC
ARM64 toolset, so it stays best-effort: a runner image without that
component should cost arm64 relays the fast path, not fail the release
the x64 relay is riding on. ORCA_REQUIRE_RELAY_NATIVE_ADDONS is a
per-arch list rather than a flag for exactly that reason.

* build(relay): require the arm64 process-table addon too

The arm64 cross-compile is no longer unproven. On a Windows x64 machine
with the MSVC v143 ARM64 build tools component installed, node-gyp
--arch=arm64 produces a genuine ARM64 image:

  x64    machine=0x8664  152064 bytes
  arm64  machine=0xaa64  139776 bytes

So arm64 stops being best-effort and joins x64 in the required list. It
was only best-effort because the component is optional and I had not seen
it succeed; a runner image without it now fails the build with MSB8020
naming the missing component, and that step runs before the long
packaging step so the failure costs seconds rather than twenty minutes.

The env var stays a per-arch list rather than reverting to a flag, so a
future arch can land best-effort before being promoted the same way.
2026-08-26 03:14:45 -07:00
Neil 691759540a fix(terminal): blur suspended panes on the dispose branch too (#16592)
suspendPaneRendering blurred panes only on the WebGL-retention branch; the
dispose branch — taken by every pane past MAX_RETAINED_HIDDEN_WEBGL_CONTEXTS=6
— did not. Make it unconditional so both branches leave a suspended pane in the
same state.

No measured cost is being fixed, and the earlier cursor-blink-timer rationale
was wrong. Measured on Windows 11 against the shipped @xterm/xterm
6.1.0-beta.287 + @xterm/addon-webgl 0.20.0-beta.286, N=12 panes: display:none
and inert each make Chromium fire a real blur on the pane's helper textarea,
which pauses the WebGL blink interval on its own, and disposeWebgl() disposes
the blink manager regardless. Hidden panes measured 0 interval fires and 0 rAF
fires over 8s with and without the explicit blur. Focus is also a document-wide
singleton, so "one timer per hidden pane" was never possible.

Kept as defence in depth for opacity:0 without inert — TerminalOverlaySlot's
startup probe inside an active worktree — the one hide mode that keeps focus.
2026-08-26 03:12:03 -07:00
Neilandsanshengai e2cb797506 perf(sleep): park idle agents in the worktree you are working in (#16591)
The planner skipped the entire activeWorktreeId, so the tree a user actually
works in never parked anything — exactly where a 16 GB Windows host
accumulates its idle Codex/Grok panes and starts hard-paging (#16211).

The two guards that remain are the correct granularity and already existed:
foregroundTerminalTabIds covers the tab on screen, and the
foregroundTerminalLastSeenAtByTabId floor in getEligiblePane holds any tab
left inside the idle window.

Test lever taken from @sanshengai's #16214, which found this first: pinning
the existing sibling-tab regression to activeWorktreeId means it fails against
the pre-fix planner. A standalone background-worktree case does not, because
the fixture's active worktree is a different one — that is why the first cut of
this change shipped a vacuous test.

#16214 changed only the planner suite; the same one-line change also breaks
agent-hibernation-coordinator's two revalidation tests, which used
activeWorktreeId as their eligibility lever. Those now flip
setForegroundTerminalTabIds instead, which is the property they were written
to prove.

Co-authored-by: sanshengai <sanshengai@users.noreply.github.com>
2026-08-26 03:11:29 -07:00
Neil 87f5c6cd03 perf(terminal): stop rebuilding parked-watcher keys on every overlay render (#16596)
* perf(terminal): stop rebuilding parked-watcher keys on every overlay render

Every mounted worktree's TerminalPaneOverlayLayer rebuilt its parked-watcher
synchronization key from scratch on every render: JSON.stringify of the whole
split-tree root per tab, then a second JSON.stringify pass that re-escaped that
already-serialized string. Two app-global subscriptions in the cold-parking
hook (pendingStartupByTabId, sleepingAgentSessionsByPaneKey) made any write for
any tab in any worktree trigger that render everywhere at once, so the cost
scaled with mounted worktree count.

- Memoize the store-derived half of the reconciliation key on the already
  shallow-stable selector output. The captured-pane half still recomputes per
  render because that registry mutates outside React.
- Replace the outer JSON.stringify of already-serialized fragments with a
  length-prefixed join, which is injective for arbitrary fragments and does no
  escaping pass.
- Narrow both global subscriptions to worktree-scoped, value-comparable keys.

Measured on a 12-worktree x 4-tab x 4-leaf-split model: 9.5 us -> 1.3 us of key
work per worktree render (7.3x), before counting the renders the narrowed
subscriptions now avoid entirely.

Key semantics are unchanged: no hash is introduced, only memoization of an
identical serialization and an injective replacement for the outer pass.

* refactor(terminal): narrow the park subscriptions with useShallow, not string keys

Review follow-up. zustand's `shallow` already compares Sets and plain objects
structurally and order-insensitively, so the encode-to-string / parse-back pair
each subscription carried was doing by hand what `useShallow` does for free.

- Restore the Set-returning `selectSleepingRecordParkExemptTabIds` and subscribe
  through `useShallow`. Drops the NUL separator, the `.sort()` that existed only
  to keep insertion order out of the key, the O(k^2) `includes` dedup, the parse
  helper and the caller's `useMemo` — and removes the ordering invariant that
  was enforced by a comment alone.
- Same for the pending-startup presence hook: `useShallow` over the presence
  record, keeping the frozen empty singleton for the zero-allocation steady
  state.
- Drop the `useMemo` around the reconciliation selector. `useShallow` returns a
  fresh closure every render regardless, so the memo bought nothing and its WHY
  comment described behaviour zustand 5 does not have. The memo that is the real
  fix here, `reconciliationStoreInputsKey`, is untouched.

Adds a narrowing case for a sleeping record this worktree can never resume,
which pins both the blocked-record exemption and the narrowing itself; it fails
against the pre-narrowing code (2 renders, expected 0).

Net -25 lines of production code.
2026-08-26 03:08:11 -07:00
Neil 7f034a182f docs(windows): correct why the process-tree addon is not installed on relay hosts (#16565)
The note said the package "ships no prebuilds". It does: the published 0.8.0
tarball carries build/Release/windows_process_tree.node, apparently an
accidentally published MSVC build directory (.obj and .tlog files ship with it).
The conclusion was right and the reason was wrong, so record what was actually
measured on a Windows SSH host with 1486 processes.

Installing it normally rebuilds from source, because the tarball carries a
binding.gyp and npm runs node-gyp regardless of what is already compiled inside.
That build fails with MSB8040 (Spectre-mitigated libraries) even on a host that
already has MSVC Build Tools 2022 -- the requirement our binding.gyp patch
deletes, and patches do not cross SSH.

Skipping the build keeps the tarball binary, which loads (it is N-API) but
predates the src/process.cc patch and still caps enumeration at 1024. On that
host it returned exactly 1024 rows with the querying process among the missing,
which the self-presence guard rejects -- so it would work on a quiet machine and
fail only under load, the shape of bug that survives testing.

Also records the measured cost of the fallback, since the table's 706ms figure
is from a 1050-process host and reads as more headroom than there is, and names
the fix for the tracked gap: ship our own patched .node as a relay asset, as
config/relay-assets already does for node-pty.
2026-08-26 02:59:08 -07:00
Neil 1fafccb26b fix(settings): use Workspace Directory for the Create-project default path (#14767) (#16583)
* fix(settings): use Workspace Directory for the Create-project default path

`repos:getDefaultCreateProjectParent` hardcoded `join(homedir(), 'orca',
'projects')` and never consulted the settings store, so Settings -> General ->
Workspace Directory had no effect on the Location field of "Create new project".
Users had to retype the path every time, or fake it with an NTFS junction.

Resolve the parent from the store instead, through the same rule the rest of the
app uses for a host preference: `host override ?? client default`, i.e.
`getEffectiveHostSetting(settings, LOCAL_EXECUTION_HOST_ID,
'defaultWorktreeLocation', settings.workspaceDir)`. This handler only ever
answers for the local host, and a local-host override previously could not win
either.

A seeded value is not a user choice. `workspaceDir` is never blank -- new
installs seed it with `~/orca/workspaces` -- so treating any non-blank value as
configured would silently relocate every existing user's new projects into the
worktree root. Worktrees nest at `<workspaceDir>/<repoName>/<branch>`, so such a
project would then host its own worktrees inside its own working tree. Compare
against `getDefaultWorkspaceDir(homedir())` (now exported) via
`normalizeRuntimePathForComparison`, and keep `~/orca/projects` for blank,
whitespace-only, and untouched-default values.

Also scope the `~/orca/projects` shorthand in `formatCreateProjectParentSummary`
to the fallback path itself. Otherwise a user with Workspace Directory set to
`J:\PROJECTS` saw the summary line claim `~/orca/projects` while the field held
`J:\PROJECTS`.

Fixes #14767

* fix(settings): keep configured orca/projects paths verbatim in the create summary

The collapsed Location summary used a tail match on orca/projects, so a
configured directory like /data/orca/projects rendered as ~/orca/projects.
Scope the shorthand to usual home layouts and pin the lookalike cases.
2026-08-26 02:36:38 -07:00
Neil a27c691fdd fix(terminal): stop detached exit observers pinning evicted panes' xterm buffers (#16551) 2026-08-26 02:05:58 -07:00
Neil 5e900b10b3 fix(windows): let a build with no job exports still retire a dead agent (#16563)
* fix(windows): let a build with no job exports still retire a dead agent

#16419 (a1ec0479e2) routed the foreground poll's liveness question to the job
object. On a build whose node-pty lacks the job exports the read returns null,
which judgeCachedAgentJobEvidence reports as 'unavailable' -- correctly refusing
to treat loss of contact as death. But that is the wrong reading here, and it
matters more than it looks:

**every shipped Windows release is such a build.** The patch adding
listJobProcessIds is in no v1.4.* tag, 1.4.188 included (#16059). So for every
Windows user today the read is null on every poll, the verdict is always
'unavailable', and the retire path never fires at all. Their panes keep a dead
agent's name indefinitely, and because a non-null cache makes idleNoEvidenceShell
false, the refresh also stays pinned at the 1s TTL instead of backing off to 15s.

That is worse than what #16419 replaced: the forked probe was expensive but it
did retire.

The distinction the verdict was missing is between "we could have asked and could
not" and "there is nothing to ask". Only the first is unverifiable. A build with
no job exports is the second, so it now returns 'unsupported' and the
authoritative scan decides alone -- exactly as it already does off Windows.

Deliberately NOT falling back to the forked console probe: that is the #10857
storm this whole path exists to avoid, and re-forking per poll for the entire
current fleet would be the worse trade. The scan that reaches this branch has
already reported available and found no agent; trusting it needs no fork.

Also deliberately not age-based: an earlier draft retired on age alone, which
would expire a LIVE agent that a scan could have confirmed, since 'unproven'
short-circuits the scan.

isWindowsPtyJobReadable() sits beside the read rather than being imported from
windows-pty-job, so one module mock controls both facts. Without that, every
Windows-simulating test on a macOS runner silently took the unsupported path,
because isPtyJobOwnershipAvailable() is false off Windows -- the suite would have
been testing a configuration no Windows user has.

* test: give every job-membership mock the readability export

Nine of the nineteen files mocking windows-pty-job-membership supplied only
readWindowsPtyJobProcessIds, so isWindowsPtyJobReadable resolved to undefined in
those suites. They pass today only because none of them reaches the call; the
first one that does gets 'isWindowsPtyJobReadable is not a function'.

Left alone this is the same trap the export exists to prevent -- a suite that is
green about a configuration no user runs -- just arriving as a crash instead of a
wrong answer. All nineteen now declare which build they simulate.
2026-08-26 00:10:32 -07:00
Jinjing 4d2dc0fae5 test: pin cross-version browser placement test to explicit baseline (#16554)
* test: use explicit baseline for cross-version browser placement test

Pin to v1.4.184 to ensure consistent testing against the release
predating client placement. This avoids coupling the legacy-baseline
bump to unrelated schema refactors in newer versions.

* fix(windows): treat inaccessible processes as alive in tests

When checking process state on Windows, EPERM (permission denied) indicates
an inaccessible but live process. Only ESRCH (process not found) proves
exit. Correct isAlive() to distinguish these cases.

Also add windowsHide:true to child process spawns and use explicit SIGKILL
when force-killing the host process.
2026-08-26 00:02:12 -07:00
Neil 1c9fb84b77 fix(worktree): stop push-target rollback deleting a sibling's remote (#16569)
A push target that reuses an existing Orca-created fork remote inherits
ownership of it (`remoteCreated = isRemoteCreatedByKnownWorktree(...)`),
so the final worktree to be deleted can remove it. Rollback then reused
that same flag to decide whether to undo its own work -- but a reused
remote was not added by this call, and a live sibling worktree is still
pushing to it. A failed fetch during create therefore deleted a remote
another worktree depends on.

Track `remoteAddedHere` separately: ownership stays inherited for
cleanup, while rollback only removes a remote this call actually added.
Both the local and the SSH path had the same bug and are fixed together.

Original work by Jinjing (AmethystLiang) in 616d2a4ec8c; split out of
that branch so the release fix in #16550 stayed a clean cherry-pick.
2026-08-25 23:48:54 -07:00
Neil 2f5f5ce23c fix(ui): restore the light-mode dropdown shadow (#16570) 2026-08-25 23:43:28 -07:00
Jinwoo Hong 3c5c908451 fix(automations): scope project refs to destination host (#16552) 2026-08-25 23:32:21 -07:00
Jinwoo Hong 868fc39d32 fix(worktrees): refresh paired clients after external discovery (#16557) 2026-08-25 23:18:30 -07:00
JinjingandNeil e4d95e032d fix(windows): restore a CIM fallback for relay hosts with no native binding (#16550)
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-08-25 23:15:37 -07:00
Neil f72dcb908e fix(contextual-tours): stop measuring 60 times a second while nothing moves (#16453) 2026-08-25 22:38:33 -07:00
Jinjing 933345d347 Clarify upstream divergence stats for rebased branches (#16358)
* Clarify upstream divergence stats for rebased branches

When a branch is rebased, it still tracks the pre-rebase upstream
while comparing against the new base. Move upstream arrows to the
head line to prevent them being confused with compare-base counts.

* Show upstream divergence stats independent of compare base

Measure HEAD against upstream regardless of compare-base state,
so divergence indicators stay visible even when comparison is
missing, loading, or failed. Also use cross-platform temp paths
in tests.

* Show commit counts against compare base, not upstream

Upstream divergence (↑/↓ against tracking branch) was confusing for
rebased branches — the counts appeared beside the base ref but measured
against the upstream branch. Show only the compare base count instead,
on the line that names it.

* Report branch divergence in both directions

Rebased branches are typically ahead AND behind their base; a single count
hides this case. Use symmetric range with --left-right --count to capture
both directions efficiently, then expose commitsBehind in the UI alongside
commitsAhead.

* Use semantic names for i18n keys and template variables

Rename hash-based translation keys to descriptive identifiers and replace generic value0/value1 placeholders with semantic variable names like `count` and `ref`. Improves code maintainability and makes translation strings self-documenting.
2026-08-25 22:19:04 -07:00
Jinjing 07b82340f3 Route terminal file links to sibling workspace tabs (#16544)
* fix: route terminal file links to sibling workspace tabs

Detect when a clicked file is already open in a sibling workspace and route
to that existing tab instead of creating a duplicate. Reorganizes workspace
activation to dispatch by both worktree id and execution host, allowing the
same worktree name across different remotes to be disambiguated and routed
correctly.

* test: validate terminal file link opens in correct sibling worktree

Enhance test to check both file path and active worktree ID, ensuring
the linked file opens in the intended sibling workspace.
2026-08-25 22:17:59 -07:00
Jinjing 6a3bd2a1b8 fix: keep tab-cycle shortcuts in sync with rendered group order (#16549)
Tab-cycle shortcuts (Ctrl+Tab) were getting out of sync with what the
TabBar actually renders. When a tab hydrated into the strip before
group.tabOrder was updated, it fell out of the cycle until a click.

Align keyboard cycling to use the same reconcileTabOrder pass the
TabBar uses, so the cycle always walks what the user sees. Fixes STA-3475,
particularly in remote servers where hydration timing diverges from
local.
2026-08-25 22:10:29 -07:00