Commit Graph
10202 Commits
Author SHA1 Message Date
Jinwoo Hong 174039a14e fix(workspaces): re-seed a terminal when an emptied workspace is opened (#15513) 2026-08-19 14:58:00 -07:00
Brennan Benson 9b8dbd8930 fix(mobile): keep a terminal lease alive when the handle lookup fails (#15463)
* fix(mobile): keep a terminal lease alive when the handle lookup fails

`waitForTerminal(..., 'exit')` rejects with `terminal_handle_stale` whenever the
handle cannot be resolved right now. The loudest source is
`record.rendererGraphEpoch !== this.rendererGraphEpoch` — every renderer graph
reload invalidates handles issued before it, on panes whose PTY is untouched.

All three terminal.subscribe branches wired that rejection straight to teardown
(`.catch(() => registration.releaseIfCurrent())`), so the host retired a live
lease and emitted `end`. Mobile reads `end` as "PTY gone" and rearms; after
MAX_REARM_ATTEMPTS = 3 it stops and leaves the composer on "Waiting for
terminal…", recovering only when a replacement handle arrives. A live PTY keeps
its handle, so no replacement ever comes: the pane is dead until the app is
backgrounded and reopened.

Demand proof instead. Retire on abort (this socket is going away) or when
isLeafPtyProvenAbsent says the process is gone; a real exit still resolves and
retires through the .then leg. Absence must be proven, never inferred from a
lookup that failed.

Reproduced live: a paired 0.0.44 client over LAN, fault injected on the handle
lookup, composer locked on "Waiting for terminal…" and STILL locked 97s after
the fault was lifted with the PTY alive and the handle unchanged.

Narrows one behavior #14992 pinned: a stale handle alone no longer retires the
owner. The ownership property is unchanged — once absence is proven, the owning
registration is still what retires. That test is updated, not deleted.

* fix(mobile): observe PTY exit independent of stale handles

* fix(mobile): stop subscribe setup when an exited pty releases synchronously

The runtime doubles for terminal.subscribe never stubbed subscribeToPtyExit,
so the new lifetime watcher threw and killed every subscription under test.

Also return early when the watcher releases synchronously: cleanup had already
run, so the stream handlers, view subscribers, and mobile presence registered
afterwards were never torn down.
2026-08-19 14:14:00 -07:00
Brennan Benson a3b3254885 fix(browser): grant storage-access so requestStorageAccess() stops rejecting (#15481)
* fix(browser): grant storage-access so cross-site frames can use their cookies

AUTO_GRANTED_BROWSER_PERMISSIONS omitted storage-access, and the installed
permission handlers deny anything absent from that set, so every
document.requestStorageAccess() in the embedded browser was refused silently.

Granting it unconditionally looks like a privacy hole, but that objection
assumes Orca blocks third-party cookies. It does not: nothing blocks or
partitions them and no Chromium switch touches cookie policy, so Electron's
default applies. A cross-site frame therefore already reads and writes its
unpartitioned cookies at the network layer, and denying the permission grants
no protection - it only breaks sites that take the API's failure path. Chrome
resolves requestStorageAccess() without a prompt under the same cookie policy;
Electron has no such fast path and forwards to the embedder, so the embedder
supplies it. The comment records the condition that would invalidate this.

top-level-storage-access stays denied. requestStorageAccessFor() is a separate
platform decision: Chromium consults Related Website Sets and has no
third-party-cookie auto-grant, and Orca has no such data source, so granting it
would invent a permissive answer to a restrictive question. A test pins it.

Does not fix Google sign-in - that was #15216. Sign-in completes with this
denial in place; this is an independent defect found while investigating it.

* test(browser): pin the denial notice storage-access used to raise

The suite proved the handler answers true but never pinned the symptom users
actually reported — the "asked for storage-access, and Orca denied it" notice.
The existing notified-list assertion runs before the storage-access request, so
a regression that re-denied it would have left that list untouched. Assert the
list after the new requests instead; it goes red pre-fix with 'storage-access'
present.

Drop the two vi.waitFor wrappers around the same requests. The request handler
is synchronous for every non-media permission, so the wait bought nothing and
imposed vi.waitFor's 1000ms default on a suite that allows 30s. The waitFor
guarding the media path is genuinely async and stays.

* refactor(browser): tighten the storage-access rationale and cover isolated partitions

Trim the grant's comment to the facts that change a reader's decision. The
mechanics of how the request reaches the embedder already live in the commit
message; what belongs at the call site is why the answer is grant, and why the
check handler must agree with the request handler — Electron builds neither
Chrome's activation gate nor its auto-grant, so a disagreeing check pushes
compliant sites onto the gesture path, where a rejection consumes the gesture.

Move the top-level-storage-access note below the set. It sat after the last
element with no trailing comma, so it read as a commented-out entry, and any
permission appended at the natural insertion point landed above it.

Cover the isolated-partition install path. createProfile and hydrateFromPersisted
call installBrowserSessionPartitionPolicies separately from the default-partition
path the persistence suite drives, and the two previous changes to this set each
added a matching isolated test. Verified red without the grant.

Rename the anti-detection case that claimed storage-access has a native denied
state; it is granted in production now, and the case really pins pass-through.

* docs(browser): correct the storage-access rationale

The revisit trigger was backwards. If Orca ever blocked third-party cookies the
grant would not become dangerous, it would become useless for cookies: Electron
builds no HostContentSettingsMap, so no STORAGE_ACCESS content setting is ever
written, and IsAllowedByStorageAccessGrant needs one. Point the tripwire at a
cookie or storage-partitioning control instead, which is the change that would
actually invalidate the reasoning.

The premise was also narrower than the grant. Third-party storage partitioning
is enabled by default and independent of cookie policy, and the same permission
lifts it for localStorage, IndexedDB, CacheStorage and friends via
StorageAccessHandle, which gates only on IsFullCookieAccessAllowed. So the frame
does not "already have" everything this grants. It stays the right answer
because Chrome grants the same permission under the same cookie policy, but the
comment should not claim a narrower blast radius than the change has.

* docs(browser): give the storage-access tripwire its consequence

Say what goes wrong, not just when to look. If Orca ever blocks third-party
cookies or gains a partitioning control, three separate gates stay shut in
Electron - the network-service grant check, the frame's trusted status, and the
STORAGE_ACCESS content setting that is never written - so the promise would
resolve while access stayed blocked. Sites follow the documented pattern of
reloading after a successful request, and on reload the check handler still
reports granted, so no gesture is needed to ask again. That loops.

Qualify the non-cookie clause: the no-arg call resolves undefined and touches
only cookies. It is the dictionary form that returns a handle, and since the
handler sees the permission name and never the call shape, one grant covers
both. Also give "check must agree with request" its reason.

Justify the isolated-partition test by the precedent it follows rather than by
a call-site divergence the shared mock cannot actually distinguish.

* docs(browser): scope the non-cookie clause to the handle

A live probe pinned down what the grant actually widens. The frame's ambient
window.localStorage and window.indexedDB stay partitioned before and after a
successful request; the unpartitioned view is reachable only through the handle
the dictionary form returns. Existing code in the frame is unaffected unless the
site explicitly calls through that handle, so say handle-scoped rather than
leaving a reader to assume the globals change.

* docs(browser): correct the isolated-test justification and the tripwire scope

"the isolated twin every other entry in this set already has" is false. Counting
occurrences in browser-session-registry.test.ts: fullscreen, clipboard-read and
clipboard-sanitized-write have none. Cite the pointerLock precedent the test
actually mirrors, which is the case directly above it.

Separate the two gates in the tripwire. The STORAGE_ACCESS content setting gates
cookies; the handle path is gated on IsFullCookieAccessAllowed instead, and a
live probe confirmed it works today. Saying "access stayed blocked" read as a
claim that the handle is backed by nothing, which contradicted the sentence
above it. Say cookie access, and say the handle survives.
2026-08-19 14:12:44 -07:00
GideonandBrennan Benson 0b8107a41f fix(browser): stop the auth-host UA write from cancelling redirects (#15221)
* fix(browser): stop the auth-host UA write from cancelling redirects

WebContents.setUserAgent() from will-redirect makes Chromium abort the
in-flight navigation (ERR_ABORTED) and replay the original request, so any
redirect crossing the Google auth-host boundary dies on a blank tab. Route
that write through the CDP override, which retargets navigator.userAgent
without touching the navigation.

Fixes #15216

* fix(browser): recover auth UA override state

* fix(browser): keep the viewport UA override on the session identity

The auth-host switch writes the Firefox UA through WebContents.setUserAgent on a
direct navigation and then moves to the CDP override, and nothing restores that
WebContents UA. sendViewportUserAgentOverride read it back as its base identity,
so a tab with a viewport preset republished the Firefox UA on every ordinary host
afterwards, with sec-ch-ua still saying Chrome. Read the profile's session UA
instead, which is the stable base identity at both call sites.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-19 13:06:54 -07:00
Brennan Benson 73f7767edd feat(sidebar): name the host when a delete batch collides on one id (#15423)
* feat(sidebar): name the host when a delete batch collides on one id

Two hosts can publish the same worktreeId, so a batch confirmation showed two
rows with identical names and paths and nothing to tell them apart. Label each
row with its execution host, but only when the batch actually contains a
same-id collision — an unconditional chip is noise.

* fix(sidebar): qualify colliding delete targets by saved host

* fix(sidebar): preserve delete host collision scope
2026-08-19 01:33:29 -07:00
OrcaWinandm4air 4fc8b65792 docs: add WeChat group 8 as fallback when group 7 is full (#15466)
Provides alternative community group with QR code when primary group
reaches capacity. Updates documentation in both English and Chinese.

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-08-19 00:57:49 -07:00
Brennan Benson 391385fd5c fix(workspace-cleanup): require a host-qualified key for every candidate row (#15422)
The virtualizer keyed rows by bare worktreeId, so two hosts sharing an id
collapsed onto one key in the confirm-remove dialog. getRowKey is now a required
prop supplied host-qualified by each caller, making omission a compile error
rather than a silent fall back to the colliding id.
2026-08-19 00:29:11 -07:00
github-actions[bot] 41c4d8e8a8 Update README downloads badge 2026-08-19 06:28:03 +00:00
OrcaWinandm4air 90a292eec2 Fix WSL stall tests to reset gate state and fake performance timer (#15414)
Reset persistent WSL transcript filesystem gate state in beforeEach to
prevent prior test stalls from quarantining subsequent tests.

Fake the performance timer used by the route quarantine clock so tests
don't block on real time. Update affected tests to wait out the back-off
window rather than advancing by zero time.

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-08-18 23:20:24 -07:00
Neil 1ca752a7a4 test(e2e): keep the native Hangul reproduction harness (#15438)
* test(e2e): keep the native Hangul reproduction harness

This is the spec that reproduced #15299: it drives a real ibus-hangul
engine through a real compositor and asserts the bytes reaching the pty.
It is the first setup here that can exercise an input method end to end,
and three IME issues this week were unreproducible without one.

It does not run in CI, and the header says so rather than implying
coverage. It needs a compositor session CI does not have, and this repo
already carries native IME specs that are skipped everywhere and were
mistaken for protection they never gave. The run recipe is in the header
so the next person does not rebuild it.

Recorded there too are the five things that decide whether a run is real
or a silent false negative - nested rather than headless, an unused
display, a session script that does not exit, forcing the window
visible, and sending Escape before the byte reader starts. Each cost a
failed attempt, and four of them are what defeated an earlier try.

Keys and expected text are environment-tunable so other IME issues can
reuse it unchanged.

Refs #15299

* test(e2e): record three more silent-false-negative traps in the native IME harness

A Hanja candidate-selection run on the same rig hit all three. Each produced an
empty or misleading event log that reads as "the IME ignored the key" rather
than as a broken harness, which is the failure mode this header exists to
prevent.

The panel one is the least obvious: a session whose ibus-daemon runs with
--panel=disable never draws a lookup table, so any run that depends on seeing
candidates measures nothing while appearing to work.

Refs #15299
2026-08-18 23:15:32 -07:00
Neil d4460d34f3 fix(terminal): stop a Hangul-terminating digit being eaten as a candidate pick (#15429)
* fix(terminal): stop a Hangul-terminating digit being eaten as a candidate pick

A digit typed immediately after a Hangul syllable is dropped in the
terminal on Wayland. Typing 아1 produces 아. The syllable composes and
commits correctly; only the keystroke that ends it is lost.

Reproduced on Ubuntu 24.04, GNOME Shell 46, ibus-hangul 1.5.5, in a
nested Wayland session with real key injection. The pty receives 아 on
Wayland and 아1 under X11 on the same machine with the same engine, and
a GTK client in the same Wayland session receives 아1 - so the
compositor, the input method and the digit are all behaving.

The difference is where the digit is delivered. Under X11 ibus-hangul
swallows it into the preedit and commits 아1 as composition text. Under
Wayland it commits 아 and lets the digit through as an ordinary key -
and the jamo before it arrive with no keydown at all, only keyups.

That orphaned keyup is what breaks it. A plain letter keyup with no
matching keydown arms a 1500ms window in which a bare digit is treated
as a candidate selection, because a Pinyin engine indexes its candidate
list by digit. The digit lands microseconds later, inside the window,
and is suppressed. The window disarms after exactly one digit, which is
why the syllable survives and only the terminating key vanishes.

A Hangul engine has no numbered candidates over its preedit - a digit
ends the syllable and is literal text - so the guard was spending its
one suppression on a keystroke meant for the shell. The tracker now
records whether the current preedit is Hangul and the guard declines.

Read from compositionupdate rather than compositionend: a Pinyin preedit
is the Latin spelling being narrowed while its commit is the Han text,
so reading the commit would misclassify Pinyin and reopen the bugs the
guard was added for. Space is untouched; only digits are reclassified.

Verified A/B/A on that machine: unfixed drops the digit, this change
preserves it, reverting drops it again, three runs each. The X11 case
and both existing ibus-hangul specs still pass.

Closes #15299

* fix(terminal): expire the Hangul preedit flag and narrow its digit exemption

Review of the #15299 fix found the Hangul classification could not be
retired and that it exempted more than the reported bug.

The flag was written only on a non-empty compositionupdate and cleared
only on blur, so it latched for the whole focus session. Switching input
engine (Hangul -> Pinyin) under fcitx/ibus moves no DOM focus, and the
#8241 orphan-digit path emits no composition and no input events, so
nothing could refresh or clear it. The stale flag then turned the
#8241/#7543 Pinyin candidate-digit guard off silently and permanently -
the regression those guards exist to prevent. It now expires against
lastCompositionEventAt on the same staleness window as the neighbouring
guards, and compositionstart clears it because the following
compositionupdate re-reads the preedit script.

It is deliberately not cleared on compositionend or on input: the bug is
a digit arriving after the commit, and both recordings deliver the
commit's own insertText before it. Clearing on either reverts the fix -
the orphan-window test fails when input clears it.

The exemption also gated suppressCandidateKey as a whole, so it fired
during a live composition as well. That is broader than the bug, which
is exclusively a digit after compositionend, and it is unsafe: the
earlier claim that no Hangul engine indexes candidates by digit over a
preedit is wrong. ibus-hangul's Hanja conversion (Hanja key / F9) puts a
numbered lookup table over a live Hangul preedit, and its symbol table
behaves the same. Only the orphan-keyup guard - which arms off a bare
keyup and cannot see which engine produced it - now declines.

The tests drove a shape neither recording produces: an empty
compositionupdate immediately before compositionend, which armed the
250ms post-composition window. The Wayland reproduction has no empty
update, and the X11 fixture follows its empty update with
deleteContentBackward + insertText, which disarms that window. The suite
now follows the recorded trace, asserts the 250ms window never arms, and
covers both the expiry and the live-preedit Hanja case.

Refs #15299
2026-08-18 23:15:12 -07:00
Neil 3ffab9a6b3 feat(terminal): read the rendered screen with terminal read --screen (STA-4792) (#15380)
* feat(terminal): read the rendered screen with `terminal read --screen` (STA-4792)

`terminal read` returns accumulated pty output with escape sequences stripped.
That is the right answer for "what happened over time" and the wrong one for
"what is on screen": any program that repaints a line comes back as stacked
fragments, so one `clear` typed key by key reads as `cclclecleaclear`, and a
prompt that draws a space by moving the cursor loses it. Nothing in the output
said which question had been answered, so it was used as rendering evidence and
produced false conclusions.

The runtime already knew how to render — it replays the byte stream through a
headless emulator — but only as a fallback for blank reads, alternate screen,
and never-attached ptys. A normal attached terminal never reached it. `--screen`
asks for it directly.

Every read now reports its source, which also surfaces the pre-existing
snapshot fallback that until now swapped rendered lines into an ordinary read
with no indication. `screen-unavailable` distinguishes "asked for a screen,
none could be rendered, here is the stream" from a stream the caller asked for,
and an absent source means the host predates the field. Because an older host
strips the unknown param and answers with its ordinary read, `--screen` against
one fails with that explanation rather than passing the stream off as a screen.

`--screen` and `--cursor` are mutually exclusive: a screen is the current frame
and has nothing behind it to page.

* refactor(terminal): stamp the screen source where rendered lines enter the read

Inferring it from tail array identity worked but made a load-bearing contract
out of reference equality; any later path spreading the read would silently
mislabel. Rendered lines only enter through one builder, so it stamps there and
anything still unlabelled is the stream.
2026-08-18 22:51:37 -07:00
Jinwoo Hong 9b5538d786 fix(runtime): scope create-with-activate navigation to the requesting client (STA-2802) (#15407) 2026-08-18 21:37:35 -07:00
Brennan Benson 12724068b7 fix(sidebar): render folder workspaces under every Group by mode (#15362) (#15404)
* fix(sidebar): render folder workspaces under every Group by mode (#15362)

Folder-workspace rows were emitted in exactly one place, inside the
repo-grouping branch of buildRows, so switching the sidebar to Status,
PR status or None dropped them from every lane.

Membership is now decided once, above the groupBy switch; a mode only
chooses which lane a workspace lands in. Lane assignment is an exhaustive
WorktreeGroupBy switch with no default clause, so a future grouping mode
is a compile error here rather than a silent regression.

Not a regression: the groupBy !== 'repo' gate predates folder workspaces
by two weeks (#2866), and #5172 added folder-workspace emission below it,
unreachable for non-repo grouping from its first commit.

Also fixes four further paths where the same row kind went missing:

- Flat mode gated its whole section on worktree count, so an account with
  only folder workspaces rendered nothing at all.
- A collapsed folder-only lane header rendered globally instead of under
  its host section, because empty worktree lists yield undefined host
  counts. Host id maps now carry explicit empty arrays so the host
  fallback cannot leak global worktree ids.
- Reveal resolved folder workspaces to project-group header keys only, so
  revealing one inside a collapsed lane never expanded it. Reveal now
  reuses the same lane function as grouping, expands the host header, and
  covers the agent-send path and the host-qualified row lookup.
- The Clear Filters empty state ignored folder workspaces, so a
  folder-only account lost them whenever any filter was active. This one
  reproduced under Project grouping too.

Behaviour deliberately unchanged: archived folder workspaces are still
not filtered, matching current behaviour rather than making membership
mode-dependent. Pinned folder workspaces still render in their natural
lane rather than the Pinned section, now uniformly across modes.

* fix(sidebar): count folder workspaces in host badges
2026-08-18 19:24:38 -07:00
Brennan Benson 200d8a5738 test(relay): stop port-scan fixtures colliding with the worker pid (#15413)
Both port scanners drop any row whose pid is the relay process or its
parent. The tests hardcoded fixture pids, so a vitest worker that happened
to be assigned one of them had its row filtered out and the assertion saw
an empty result.

Reproduced exactly: forcing the worker pid to 2468 fails
windows-port-scan with "expected [] to deeply equal [ { host: '0.0.0.0',
...(2) } ]", the same message seen in CI, where node 26 shard 1 failed
while node 24 passed on the identical SHA. Forcing pid 1000 fails
port-scan-handler the same way.

Pick fixture pids that cannot match the running process instead: the
literals stay 1234/2468/1000 unless the worker owns one, and only then
shift. No production change.
2026-08-18 18:55:56 -07:00
Brennan Benson 6386b540ad fix(quick-open): stop node:path from white-screening the renderer (#15405)
* fix(quick-open): stop node:path from white-screening the renderer

#15158 pulled `quick-open-filter` into the renderer graph, but the module
still used `import { posix, win32 } from 'node:path'`. The bundler stubs
`node:path` in the renderer with an object that throws on any member read,
and named bindings resolve at module evaluation — so the renderer threw
before React mounted. `pnpm dev` white-screened on main.

Switch to a namespace import and have `pathFlavor` return the flavor NAME
instead of the module, so `node:path` stays untouched until the out-of-root
`relative` fallback actually needs it. A namespace import alone is not
enough: `pathFlavor` is called unconditionally by `buildExcludePathPrefixes`,
so it still threw on any Quick Open with nested-worktree excludes.

No behavior change for the main process or the relay.

* fix(quick-open): keep exclude containment browser-safe
2026-08-18 18:47:53 -07:00
Brennan Benson fae9282f54 test(wsl): account for the route quarantine in stalled-mount tests (#15408)
* test(wsl): account for the route quarantine in stalled-mount tests

#15381 added a route-level quarantine: a timed-out task blocks new
admissions on that route for a back-off window, so the next real task
probes recovery instead of hammering a hung mount. It merged with four
tests already failing on main, and they still fail.

Two causes, both test-side:

- The recovery half of three tests read again immediately after releasing
  the stall, which the quarantine now refuses. They wait out the window,
  which is what a real caller does — resetting the gate would skip the
  admission the recovery assertions exist to prove.
- `blockedRoutes` is module state that no test cleared, so a stall in one
  test refused an unrelated read in the next. Both files now reset the
  gate in beforeEach.

Production behavior is unchanged and is working as designed: eviction
still happens, only the immediate re-read is deferred by the back-off.

* test(wsl): match the sibling suites' quarantine idiom

#15381 applied this same fix to session-scanner-core-parser-wsl-stall,
session-scanner-discovery-wsl-gate, and opencode-usage/scanner-wsl-gate,
and missed these two files. Adopt that established shape rather than a
parallel one: fold the back-off wait into releaseAndSettle instead of a
separate helper at each call site, and fake performance explicitly, since
performance.now drives the quarantine clock.

The explicit toFake is not strictly required today — vitest's default
already fakes performance — but it documents the dependency and stops a
default change from silently unfaking the quarantine clock.
2026-08-18 18:37:24 -07:00
Brennan Benson c228030516 fix(tabs): keep an open diff focused while agents stream (STA-4697) (#15390)
* refactor(tabs): put the visible-tab-type projection in one place

Three copies of toVisibleTabType had drifted: the runtime one omits 'simulator'.
Move the canonical projection next to the two unions it maps between and replace
the two copies that are already identical to it. The runtime copy is left alone
on purpose - unifying it would change behavior, so it goes with the follow-up.

* fix(tabs): keep an open diff focused while agents stream (STA-4697)

resolveWebSessionVisibleTabId answered 'which tab is the user looking at' by
inverting a many-to-one projection: it compared tab.contentType against the
coarse activeTabType. Diff tabs open with activeTabType 'editor' but carry
contentType 'diff', so the match never succeeded and the guard returned null -
which is the reconciler's signal to fall through and activate a terminal. Every
agent status echo republished the snapshot, so the diff lost focus ~300ms after
opening, once per click. Same for conflict-review and check-details.

Resolve the visible tab from group state instead, which is what is actually on
screen and is the rule deriveActiveSurfaceForWorktree already uses. The coarse
address survives only when there are no group records, now projected rather than
compared exactly.

Also follow the entity within the group when reconcile rematerializes the visible
tab under a new id, and teach the browser-create focus guard to observe the group
records the resolver now reads.
2026-08-18 18:20:56 -07:00
Jinwoo Hong 8612a2dabb Fix skill install dialog overflow and simplify file summaries (#15406) 2026-08-18 18:10:00 -07:00
OrcaWin a3f3a74dcf test(agent-hooks): prove the Windows hook actually receives its payload (#15403) 2026-08-18 17:59:37 -07:00
OrcaWinandOrcaWin 4ec6bbf588 Kill hung WSL transcript filesystem operations via child process with route quarantine (#15381)
* fix(native-chat): kill hung WSL operations via child process

Stalled UNC file operations hold libuv permits even after the gate
timeout expires, blocking Chat tab recovery. Two stalled operations
fill both permits and freeze all WSL access until restart.

Fork file I/O for UNC paths into a separate child process. On deadline
expiry, kill the process to force the hung syscall to exit. This frees
the permit for the affected tab's next read. Temporarily quarantine the
stalled route to avoid retry storms.

* chore: drop internal review artifact from the repo root

* fix(native-chat): harden the WSL transcript fs sidecar

Review follow-ups on the sidecar isolation change:

- Only the deadline may abort running gate work. The sole waiter's
  same-duration timeout fired first, killed healthy children on caller
  abandonment, and settled the task before the deadline could quarantine
  a stalled route - leaving the back-off dead for every dedupe:false op.
- Resolve the fork entry from out/main/chunks too: the resolver compiles
  into a shared chunk, and the scanner service child has no
  process.resourcesPath, so packaged WSL vault scans threw entry-not-found
  (masked as an empty tree).
- Allowlist the fork env instead of spreading process.env; ambient
  NODE_OPTIONS would halt or --require code into every child.
- Wrap transport faults (spawn failure, child death) in
  WslTranscriptFsError('unavailable') so discovery reports them as scan
  issues instead of misreading them as missing paths or empty trees.
- Gate the vitest in-process fallback on the vitest worker global so a
  leaked VITEST=true cannot revert production to in-process UNC syscalls.
- Reap idle sidecar processes after 60s instead of holding them for the
  app session.
- Split 'open' into its own protocol union member so the reusable-call
  Exclude actually strips it from the pooled-process API.
- Guard kill('SIGKILL') against the teardown race where an exiting child
  emits an unlistened 'error', and dispatch reads by handle kind before
  path spelling.

* fix(native-chat): probe stalled WSL routes instead of a fixed quarantine

Remaining review follow-ups:

- Escalating route quarantine: first strike lifts after 5s so a distro
  that was cold-booting when its op hit the deadline recovers on the
  next poll (~35s total instead of ~90s); repeat stalls double the
  back-off toward the prior 2x-timeout cap, and any settle the deadline
  did not force clears the strikes. Queued same-route tasks fail fast
  at quarantine instead of stranding one waiter deadline per file in
  sequential scans.
- Single request implementation: the vitest in-process fallback now runs
  the child's own dispatcher (WslTranscriptFsProcessOperations + decode),
  so unit suites exercise exactly what the forked process executes and
  the per-call-site fallback closures are gone. Dirent fixtures gained
  the full kind-flag set the serializer reads.
- Dropped the production-dead per-route close queue; UNC FileHandles
  (test fallback only) mirror the process-handle close contract.
- Error class, messages, and factories move to wsl-transcript-fs-error
  (re-exported from the gate) to keep the gate under the lines budget.

* fix(native-chat): harden WSL transcript fs with route quarantine strike

Extract quarantine logic into a dedicated module with strike decay: stalls older
than 5 minutes restart from base back-off, and concurrent-lane timeouts count as
one incident. Allow joining live in-flight tasks on quarantined routes (they cost
no new I/O). Preserve quarantine across transport faults (child death). Handle
file shrinking during tail reads by detecting short reads and returning empty.
Defer file closes that arrive mid-read instead of refusing, preventing slot
leaks. Separate process slot and boundary-finding concerns into focused modules.

* fix(native-chat): enforce route quarantine windows and isolate lanes per

A late result arriving after the deadline was incorrectly lifting the route
quarantine, allowing subsequent work to start before the back-off period
expired. Now late results are correctly recognized as stale and never cut
the quarantine short.

Process work is now isolated per (route, priority) lane so a scan stall
cannot block exact reads on the same distro. Each lane gets its own client
and process pool; late results and handle faults stay scoped to their lane.

Tests now fake performance.now() alongside timers (the quarantine clock
depends on it) and wait for the full back-off window to expire rather than
advancing by 0. Gate state is reset between test cases since late releases
never lift the quarantine.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-18 16:57:35 -07:00
Neil 1b5cc00870 fix(shell): content-address shell wrapper trees so builds stop clobbering each other (#15285)
Every writer sharing a userData dir -- main's local PTY path, the daemon fork,
and the daemons of other builds that outlive the app that spawned them -- wrote
one fixed `shell-ready/` tree. Last writer won, and the guard only re-checked
that the files were present, never that they were this build's. A daemon whose
spawn env no longer agreed with the wrapper on disk kept launching shells it
could not read: the ready marker never fired and every startup command waited
out the full 15s timeout, silently, with restarting the app powerless to fix it
because the daemon outlives the app.

Measured 15.17s vs 1.10s once the tree matched. The five live daemons on the
machine this was found on spanned app versions 1.4.181-1.4.185, all from the
same installed app across auto-updates, so this reaches ordinary installs.

Name each tree after a hash of its contents, at
`<userData>/shell-wrappers/<hash>/shell-ready/`. Different bytes are a different
directory, so "present" means "written by this build" again. The hash sits above
the `shell-ready` leaf because ZDOTDIR self-reference guards match that exact
suffix. Nothing collects old trees: ~48KB each, a couple of MB a year against a
userData dir in the tens of GB, not worth an `rm -rf` on the spawn path.

Also publishes the resolved root to WSL over WSLENV `/p`, since the in-guest
script cannot derive a hash, and reports readiness failures to the daemon's
NDJSON log rather than a console the detached daemon discards.

Generated wrapper content is byte-identical; snapshots unchanged.
2026-08-18 15:59:17 -07:00
Jinjing 3ac4171d14 fix(cmd-j): host-qualify worktree resolution and disambiguate list render keys (STA-4343) (#15371)
* fix(cmd-j): host-qualify worktree identity to fix STA-4343

- Same repo::path across hosts now render as two distinct rows, each resolving
  to its own worktree on activation — closes wrong-host activation bug.
- Disambiguate repeated persisted entry ids with render-key suffixes so React
  never leaves ghost rows mounted; prevents frozen display glitches when a
  session holds duplicate tab records.
- Collapse query whitespace runs, treat emoji/symbols as content, deduplicate
  query tokens, and improve CJK compound matching in palette search.
- Track filter-field cursor by option id rather than position so re-ranking
  after a toggle doesn't snap to the wrong row.
- Dedup tabs by id at hydration so corrupt sessions don't render duplicates.

* test(palette-search): add rankMode and current-state fields to palette o

Adapt test fixtures and function calls to include new rankMode parameter and
isCurrentPage/isCurrentWorktree tracking fields required by the palette search
algorithm fix.

* fix(palette-search): add host-qualified repo and worktree resolution

Handle repo and worktree collisions in multi-host environments by
querying host-qualified maps and comparing execution host IDs when
determining current worktree status. Ensures the palette correctly
displays repo identity and marks active status for worktrees across
multiple hosts.

* fix(cmd-j): namespace-safe duplicates and script-aware token matching

- Carve out `palette-dup:` namespace for duplicate keys to prevent collision with real persisted ids, which previously could match generated keys like `id#dup1`
- Gate reverse-containment token scoring to unsegmented scripts (Han, Hiragana, Katakana, Hangul), preventing false matches like "database" matching keyword "base" in Latin text
- Host-qualify worktree identity in empty-query ordering to allow switching between same-id worktrees on different hosts, which were previously treated as duplicates (STA-4343)

* Shorten comments in palette-list-entry-render-keys

Replace verbose comments with concise one-liners per style guide.
2026-08-18 15:32:36 -07:00
Brennan Benson 9312c5cd9b fix(browser): serialize cookie imports per partition (#15095)
* fix(browser): serialize cookie imports per partition (STA-4601)

Two concurrent cookie imports on one partition can erase a session. Nothing
serialises imports per partition — neither the renderer IPC handler
(browser-session-profile-ipc.ts) nor the runtime RPC method
(orca-runtime-browser.ts) — and the clear lock covered only the clear itself,
so it was released before the writes and before any rollback.

Reachable interleaving:

  import A replaces cookies for its imported domains, releases the lock, and
  later fails a write; import B clears and writes; A's rollback then removes
  cookies B already wrote and reported as imported.

Path B has the same shape: A clears, B clears, then A writes its cookies on top
of B's jar.

Fix: one lock spans the whole live-jar transaction on both paths — the
clear/replace, the writes, and the rollback. withCookieClearLock becomes
acquireCookieMutationLock/withCookieMutationLock so path A can hold it across a
try/finally rather than a single callback.

Rebased onto #15030 (STA-4300), which rewrote this file: imports now write
through CDP identities, so path A's writes moved from an inline cookies.set()
loop into writeImportedCookies(), path B's into the same helper, and — the part
that matters for a lock — importValidatedCookies no longer holds the Electron
Session at all. It receives a CookieImportTarget that hides the Session behind
openWriteStore(), and openWriteStore() builds a FRESH adapter per call, so
keying the lock on anything reachable from it would serialise nothing. The
target therefore carries mutationLockOwner, set to session.fromPartition's
instance, which is the same object the native path locks on. That is what keeps
both paths on one key per partition.

Deliberately bounded: this covers the LIVE JAR only. Staging and cold-start
replay keep their existing semantics, so two other pre-existing hazards in that
subsystem are untouched and still open — the crash window between the clear and
the result, and a permanently-armed replay when replay keeps failing. Both need
the pending-image operations to become provable, which is a different change.

The lock is keyed per owner, so imports into different partitions still run
concurrently.

Mutation-proved against the rebased tree, every mutation structure-preserving
(brace/paren balance pinned) and every one re-run rather than carried over:
dropping path A's acquire reddens the file-import detector; keying path A's
acquire on a fresh object instead of mutationLockOwner reddens it too — the
lock-present-but-miskeyed shape this rebase risks; replacing path B's lock with
a passthrough reddens the native detector; doing both reddens both, which rules
out incidental serialisation; and neutering the cold-init probe's lock reddens
the new probe detector. That last detector is new: neutering the probe lock
previously left all 779 browser tests green, so the probe's protection was
unproven.

The concurrency suite's store stub also gained writeCookieIdentity and
getStoragePath. Without them the STA-4300 code throws a TypeError that
writeImportedCookies catches as a write rejection, so the imports would have
taken the failure path while the ordering assertions still passed. Each real
import test now asserts writeCookieIdentity was CALLED, and that cookies.set
never was.

src/main/browser: 72 files, 780 tests, green; typecheck, oxlint, type-aware
code quality, oxfmt and the max-lines ratchet all clean.

* fix(browser): serialize native cookie staging

* test(browser): pin native flush serialization

* test(browser): pin staged cookie replay ordering

* test(browser): remove vacuous staged replay detector

* test(browser): detect stale native staging images

* test(browser): guard cookie import ordering events

* test(browser): cover staging lock boundary
2026-08-18 15:32:13 -07:00
Neil ef788c80c7 test(terminal): assert wide-char buffer content across repaints (#15280)
* test(terminal): assert wide-char buffer content across repaints

The first reproduction for the Korean duplication asserted on the byte
stream a pty emits, and it passes on real Windows on both ConPTY
backends. The reporter's evidence says that is the wrong surface: their
copied text pastes doubled, so the corruption is in the buffer, and in
one run the command echo is doubled while that same command's output is
clean - text that was correct when it left the shell and went wrong
while being placed on screen. A redraw landing on the wrong cells emits
perfectly legitimate bytes, so no stream assertion can see it.

These assert buffer content instead, against a cell-level model of a
grid where one glyph spans two cells. The model shares no code with the
emulator, but its rules were chosen to match observed behaviour, so it
is a regression detector rather than a first-principles oracle - the
header says so rather than overclaiming.

The largest case is a cursor positioned onto the second cell of a
two-cell glyph, then erase-to-end-of-line, then rewrite, swept across
every row and column at widths 8 to 44. That is the only place a wide
character can be half addressed. Also covered: a whole row re-emitted
over wide characters, which is the shape a console redraw takes;
snapshot round-trip of a half-addressed buffer; and reflow across 148
width pairs, which is the resize the reporter uses as a workaround.

Nothing reproduces. The emulator blanks the orphaned half correctly at
every width and column, and reflow is lossless.

The two pty specs run in the existing Windows packaging job rather than
a new lane. A dedicated runner cost roughly four minutes, almost all of
it checkout and a native rebuild, to run thirteen seconds of tests, and
Windows minutes bill at double - the packaging job already installs the
same dependencies and already runs a Windows test step.

No production code changes. An earlier revision added an environment
variable to select the system ConPTY, and it is dropped: its only effect
would have been to let a user disable the fix for an earlier duplication
bug, it logged nothing so a support bundle could not confirm it took
effect, and the tests drive the backend directly without it.

Refs #15192

* test(terminal): share the wide-glyph predicate instead of copying its regex

The duplication detector carried its own copy of the range the grid
model already exports, so the two could drift and only one would be
updated.
2026-08-18 15:26:49 -07:00
Neil 3c676ed13d fix(ssh): validate hashed known_hosts fields through one strict base64 decoder (STA-4717) (#15345) 2026-08-18 15:26:48 -07:00
Neil ccb2305c8d refactor(runtime): name every mobile-session close outcome so the tombstone decision is explicit (STA-4718) (#15346) 2026-08-18 15:17:33 -07:00
Brennan Benson e5a1744e27 fix(agent-status): stop an idle title retiring a pane pending a human answer (#15351)
A Claude pane parked on a permission prompt published `done` to mobile and
paired clients — the state that retires the card — while the user was still
being asked.

The title layer cannot express permission for Claude or OpenCode: a title reads
`permission` only from a vendor glyph or a synthesized `<Agent> - action
required` label, and SYNTHETIC_AGENT_TITLE_PROFILES has no entry for Claude
(OpenCode sets synthesizeTerminalTitle: false). So `titleConfirmsState` is
unreachable for them and the hook's only protection was a timestamp comparison.

That comparison loses because the two title clocks advance on different events:
lastAgentStatusRichInvalidatedAtEpochMs moves only when the title's status class
changes, while lastOscTitleEpochMs moves on every write. Measured against a real
claude 2.1.234 (two runs, identical structure): the title settles working->idle
~12ms after PreToolUse, PermissionRequest fires ~39ms later, then one more
same-class repaint lands ~123ms after the hook. That last repaint pushes title
evidence past a hook that is still current. The title then goes silent for the
whole prompt (31s and 85s in the two runs), so the wrong verdict is frozen for
as long as the user takes to answer.

Treat a fresh `waiting`/`blocked` under an idle title as non-renewable. Scoped
to idle on purpose: idle is the absence of activity evidence, whereas a
`working` title or a null/shell/identity-only one contradicts the hook and must
still retire the row and its stale question (#11761). Bounded by the stale
window so an agent killed while parked still decays.

Both state names matter: Claude's PermissionRequest normalizes to `waiting`, not
`blocked`, so a guard written against `blocked` alone passes its own tests and
misses every Claude permission prompt.
2026-08-18 15:10:36 -07:00
Neil 1be6a035b9 fix(shell): carry a WSL pane's shell across relay revive and stop forking to probe zsh emulation (STA-4682) (#15361)
* fix(shell): carry a WSL pane's shell across relay revive and stop forking to probe zsh emulation (STA-4682)

Two residuals left behind by #15236 and #15258.

Relay revive called resolveDefaultShell() and ignored the entry's shell
override, so a restarted relay handed the user a PowerShell pane where a
WSL one had been -- and with it the host default shell's history rather
than the worktree-scoped one #15236 injects on spawn. The override and
the requested distro are now serialized (optional fields; state from an
older relay still revives the default shell) and re-resolved through the
same allowlist a fresh spawn uses, so revive re-launches the same shell
with the same guest-visible HISTFILE and the same WSLENV carrier. A bad
override degrades that one pane instead of failing the whole batch.

The wrapper's three `$(emulate)` probes each fork a zsh carrying
everything the user's config has loaded, and since wrapping widened to
every zsh pane every pane pays for them. Each probe now sits behind a
fork-free option test that is true whenever `emulate sh`/`emulate ksh`
has run; the exact probe still runs when that test passes, so no answer
changes. Measured on zsh 5.9 / macOS: 9.97 ms/run unwrapped, 14.20 ms/run
wrapped, 12.27 ms/run wrapped after -- about half the wrapper's cost.

* fix(shell): harden the revive override path and silence the option probe on an exotic zsh

Readiness-review follow-ups on this branch.

- `[[ -o <name> ]]` prints "no such option" to stderr and returns false rather
  than aborting, so a zsh lacking one of the three Bourne option names would put
  that text in the user's pane. All three predate every supported zsh, so
  `2>/dev/null` is belt and braces -- but the belt costs nothing.
- Bound the WSL distro name revive replays into `wsl.exe -d <name>`. It is the
  one field this branch newly routes from untrusted serialized state into argv,
  and reviveEntry's stated job is to re-apply fresh-spawn bounds.
- Skip a pane whose overridden shell can no longer spawn instead of letting the
  throw escape the revive loop and cost every later entry its state. Skipping,
  not falling back to the host default shell: substituting a different shell is
  the defect the override exists to fix, and the args and history env are built
  for the shell that is gone. The worktree-removal fence throws before this,
  outside reviveEntry, so it stays a hard failure.
2026-08-18 14:24:26 -07:00
Jinwoo Hong 79be5b7fde feat(orchestration): report a worker blocked on a human prompt (STA-4513, STA-3714) (#15261)
* feat(orchestration): report a worker blocked on a human prompt (STA-4513, STA-3714)

A lane parked on an approval, trust, or permission prompt looked exactly like a
lane that was thinking or inside a long tool call. On origin/main, driving a real
cursor-agent through Orca:

  surface                        running `sleep 60`   awaiting approval
  worktree ps agents[].state     working              working
  terminal show / list           no such field        no such field
  terminal wait --for tui-idle   satisfied: true      satisfied: true
  worker-show                    no agent state       no agent state

The runtime already fuses hook state, OSC title, and matched prompt text into a
`permission` verdict inside getTerminalAgentStatus — it was reachable only from the
renderer, and it was blind to cursor-agent approvals. Two gaps, one boundary.

Exposure: getTerminalInteractiveWait publishes that same fusion, minus the async
foreground probe, as `agentWait` on `terminal show` and on `worker-show`'s
observation. It carries the evidence that proved the wait (hook, prompt-text, or
title) so a coordinator can weigh it. Null means no proof; a missing field means
the host predates it — absence is never read as "not waiting".

Detection: cursor-agent's hook set has no approval event and beforeShellExecution
fires identically for auto-allowed commands, so its rendered menu is the only
authority. Matched on the key-bound choices rather than the prose, requiring two,
and self-clearing when the follow-up input line returns. Its live spinner title is
exempted from the staleness rule that clears startup modals, because cursor keeps
spinning while it waits.

Falls out of routing it through the shared verdict: `dispatch --inject` into a
cursor pane on an approval now refuses with agent_prompt_blocked instead of typing
the preamble into the dialog.

Fixtures are captured verbatim from cursor-agent 2026.08.11-e8db854 driven through
Orca; the same case matrix was replayed live against a built runtime.

terminal list stays untouched: its rows would each need a full tail scan, and
STA-4694 owns the one-call-per-run aggregate.

* fix(orchestration): only call a Cursor approval live while it owns the screen

Independent review found the approval detector trusted one dismissal string, so
any later output that did not contain cursor's follow-up line left the menu
reading as a live wait. Reproduced: a tail of the real menu followed by two lines
of ordinary output returned agent-approval-prompt, which fails tui-idle and
refuses prompt injection on a healthy lane.

Replaced with the structural property the string was standing in for: a live
dialog owns the bottom of the screen, so the last choice may sit at most one line
above the end of the retained tail. That tolerates a status footer or a partial
line mid-redraw without admitting scrollback, and it drops the vendor prose.

Being bottom-of-screen is also the dating this reason needed, so it no longer
requires waitBlockedAt. A tail restored from terminal history carries none, and a
lane parked on a prompt emits no bytes — so before this, an Orca restart made
exactly the lane both issues are about go quiet for good. The startup modals keep
the timestamp rule: their text lingers in scrollback with nothing to say whether
it was answered.

Also from review:
- worker-show and federationShow reuse the verdict showTerminal already computed
  rather than rescanning the tail, so the two can no longer disagree.
- The worker-show test now drives a real runtime, real PTY tail, and the real
  detector; it previously mocked getTerminalInteractiveWait, so it would have
  passed with detection permanently returning null.
- The guard claim is now asserted against the guard: a blocked pane rejects both
  assertTerminalAgentSendable and sendTerminalAgentPrompt, and a working pane
  still passes.
- Added a non-local (connectionId) pane case, since the verdict is derived from
  retained tail and title state on every host.

* fix(agent-status): stop a hook wait from outliving its agent

A third reviewer caught that the hook branch proved agent ownership from the pane
title alone, while the shared verdict it claimed to reuse also probes the
foreground process. A shell that takes a pane back usually sets something like
`user@host: ~/repo`, which no title rule recognizes, and a hook row stays fresh
for AGENT_STATUS_STALE_AFTER_MS — so a dead agent could be reported as waiting on
a human for half an hour.

Hook evidence now goes through getTerminalAgentStatus, which is the only thing
that can answer whether an agent still owns this PTY. The two prompt branches skip
it: a matched prompt is on the pane's screen now, so it proves itself. That makes
the probe cost fall exactly where correctness needs it, and getTerminalInteractiveWait
async, which only showTerminal had to absorb.

Also trims the comments the same reviewer flagged as longer than the repo's rule.

* test(agent-status): pin that a dead pane stops reporting a human wait

A fourth reviewer noted the approval menu sits at the bottom of a dead pane's tail
forever, and that no test covered process exit with no trailing output. The
snapshot already refuses an exited pane, and worker-show gates agentWait on proven
identity — this pins both so neither can drift into reporting a worker that needs
intervention as one that needs an answer.

* fix(orchestration): never report an unchecked worker as not waiting

Automated review caught that the three worker paths which return before the wait
is ever evaluated — unattached, missing, and identity_changed — then had their
undefined coerced to null by the emitters. A worker whose process was replaced was
reported as `agentWait: null`, which reads as "Orca looked and nobody is waiting"
when Orca never looked. That is the false negative this field exists to remove.

The field is now emitted only when it was evaluated, so a present null is a claim
about the pane and an absent one means nobody looked — because the host predates
the field, or the worker's identity could not be verified. The CLI and the
worker-show note say that rather than blaming an old host.

Covered on the context-only path, where the regression test fails against the
previous behavior; the supervised and federated emitters take the identical
one-line change.

Also trims the two test-file headers to one statement of purpose.

* fix(agent-status): tighten the Cursor menu match and stop guessing on unknowns

Fourth review round, three findings, each reproduced before acting.

Matching each choice marker with an independent lastIndexOf let text outside the
menu carry the anchor. An agent narrating "next time I'll suggest Run Everything"
after the menu was answered pulled the match down to the bottom of the screen and
revived it. The match is now confined to the last lines of the tail, and a choice
is a line that ends in the key that picks it — prose writes the same words but not
the same shape.

The one line of slack under the dialog went with it. It was a guess; every capture
of a live dialog ends on its last choice, and one line is exactly enough room for
that narration. A redraw caught mid-flight now reads as no wait until the next
poll, which is the safe way to be wrong.

The hook branch awaited a foreground probe that reaches a PTY controller which may
be a remote host, so a wedged probe stalled every caller of showTerminal — a path
that never probed before. It is bounded now, and a timeout leaves the wait
unevaluated rather than claiming there is none.

Which is the same distinction the previous commit only fixed one level up:
getTerminalInteractiveWait itself turned an unreadable pane into `null`, so
showTerminal published "looked, nobody waiting" for a pane it could not read. It
returns undefined there, showTerminal omits the key, and worker-show's text output
prints unknown rather than rendering it the same as none.

* fix(agent-status): bound the wedged probe's cost and stop matching prose keys

Fifth review round. No correctness defects in the shipped behaviour this time; two
robustness holes and the documentation of the contract.

The bounded probe abandoned the wait but not the request, so a coordinator watching
a wedged remote host added one live probe on every poll. It is single-flighted per
PTY now, the way the leaf-absence probe already is.

The trailing-key rule that separates a menu row from the agent narrating a choice
was written as a character class, and any lowercase run up to twelve characters
satisfied it — "…suggest Run Everything (as before)" passed. Spelled out as key
names instead, which also lets the glyph forms of those keys through.

The contract wording said an absent agentWait meant an old host or an unverifiable
identity. It also covers an unreadable pane and a probe that did not answer, and a
reader diagnosing an old peer from that would be wrong. Corrected on the type, the
worker-show note, and in docs/reference/remote-wire-compatibility.md, which had no
entry for a field whose absent and null states mean different things.

Also strengthens the worker-show agreement test, which compared the terminal and
observation payloads without asserting either held the expected wait, so it passed
when both were absent.
2026-08-18 14:19:20 -07:00
Brennan Benson 2b2529c32a fix(codex): stop a locked auth.json from deleting a just-authenticated account (STA-4734) (#15279)
* fix(codex): stop a locked auth.json from deleting a just-authenticated account (STA-4734)

A successful Codex login could be reported as a failure, and the rollback then
deleted the managed home holding the credentials it had just written.

Three reads on the add path answered "no credentials" from a read that had
merely failed:

- `service.ts` decided the login verdict with `existsSync(authJsonPath)`, which
  returns false for EPERM/EBUSY as readily as for ENOENT. The post-auth tree
  kill only arms after the watcher has already observed new credential bytes, so
  an unreadable file there is a lock, not a failed login.
- `loadOAuthCredentials` read auth.json bare, so a denial surfaced as a generic
  error indistinguishable from a real one.
- `importCodexAuthFromHome` turned an unreadable source file into "No Codex
  credentials found ... run `codex login` first" — advice to redo a login that
  had already succeeded.

Each now classifies: only a definitive ENOENT/ENOTDIR means absent, everything
else is `ManagedCodexHomeTemporarilyUnavailableError`.

Classifying alone is inert, because the add path's rollback deletes the home for
any error. `removeManagedHomeUnlessUnproven` holds the rollback back on an
unproven failure — a kept home is a recoverable leak, a deleted one is permanent
data loss. The mutation check for this is in the test: with the typed error but
no rollback guard, the home is still destroyed.

`isDefinitiveAbsence` is exported so the second credential lane reuses the host
predicate instead of keeping its own copy of the errno allowlist.

* refactor(codex): move the definitive-absence predicate to src/shared

Why: STA-4737 needs the same predicate from src/main/codex, which the CLI
tsconfig project does not admit imports from codex-accounts into. Both branches
now carry byte-identical copies so they merge cleanly in either order.
2026-08-18 14:14:50 -07:00
Brennan Benson ddaaa4628c fix(codex): keep two host-lane records that a failed read used to destroy (STA-4735) (#15289)
`snapshotCodexRuntimeHookTrustProvenance` rebuilt `.orca-hook-trust-provenance.json`
from the current `config.toml` on every install and refresh, including when the
existing record could not be read. That record is the only thing separating a
trust entry Orca wrote from one the user approved inside Codex, so rewriting it
after a failed read stamps the approval as Orca-written — and
`promoteCodexRuntimeHookApprovalsToSystem`, which runs earlier in the same pass
and had already bailed on the same unreadable file, then skips it on every
later pass too. One denied read, permanent loss.

Only the unreadable case is preserved. A malformed or absent record is still
rebuilt, because resetting those IS the intent; conflating the two would wedge a
user on a corrupt file forever.

`fileContentsEqual` returned `false` from a bare `catch`, so "I could not read
this" reached `writeRuntimeAuth` as "these differ" and sent it to the
unconditional write below — replacing a refresh token Codex may have rotated a
moment earlier with Orca's stale copy. It now reports the difference only when
the bytes were actually compared, and the caller refuses instead.

`fileContentsMatchExpected`'s `!existsSync` has the same collapse but is left
alone deliberately: the write it guards is `writeFileAtomicallyIfUnchanged`,
whose rename-and-compare re-checks the real file and refuses on its own, so
classifying there would add a guard no test can drive. Noted in a comment.

The three `writeRuntimeAuthAtPath` call sites have the same overwrite shape but
are all on the WSL lane, which STA-4606 restructures; refusing there without its
lane bookkeeping would set a baseline for a write that never happened.
2026-08-18 14:14:44 -07:00
Neil 4cc7e7859a fix(cli): route --host runtime:<id> to that server instead of answering locally (#15364)
* fix(cli): route --host runtime:<id> to that server instead of answering locally

`--host` was only ever a local filter over whatever runtime the CLI happened
to connect to, so `--host runtime:<id>` silently answered for (and mutated)
the local machine. A real environment id and a made-up one were
indistinguishable: both returned ok:true with an empty list and the local
runtimeId in _meta, and `project setup-clone --host runtime:<id>` cloned into
the caller's own machine.

Resolve the flag before the client is built: unparseable host ids and runtime
ids that no paired environment owns are rejected, and a known runtime id
selects that environment as the connection (conflicting with --pairing-code or
a different --environment is an error). Once routed, a host filter also
accepts the runtime's own `local`-stamped rows, since both spellings name the
machine we are now talking to.

* fix(cli): close --host routing gaps found in review

- Conflict-check an ambient ORCA_ENVIRONMENT, not just the --environment flag.
  `ORCA_ENVIRONMENT=staging orca ... --host runtime:<prod-id>` silently routed
  to prod while the flag spelling errored. An ambient pairing code still loses
  to the explicit flag, because it cannot be resolved to an id to compare.
- Attach the known environment ids to the unknown-id error as `error.data`, so
  a --json consumer can retry without parsing prose, and say outright that
  runtime:<id> matches ids only and never environment names.
- Fix four command examples that documented `--host runtime:gpu`. `gpu` is an
  environment name, so every one of them would now be rejected; use an id.
- Cover the routed connection on `worktree create` and `automations create`
  (the mutating paths), the `--environment X --host local` filter-only case,
  and assert error.code/error.data rather than only substrings.

* test(cli): pin execution-host-flag to the deferred error-class import

index.ts now loads execution-host-flag.ts on every invocation, making it the
sixth module on the --help path. It imports RuntimeClientError from
./runtime/types today, but nothing enforced that; switching it to the barrel
would silently drag zod/ws/tweetnacl back onto --help, which is exactly what
this guard exists to prevent. Verified the assertion fails when the import is
flipped to the barrel.
2026-08-18 14:12:24 -07:00
Brennan Benson 0b80a773a4 fix(codex): stop overwriting and deleting Codex files that were merely unreadable (STA-4737) (#15287)
* fix(codex): stop overwriting and deleting Codex files that were merely unreadable (STA-4737)

Three modules shared by the host and WSL Codex lanes decided a file was absent
from a read that had only failed, and then wrote over it or removed it.

- `codex-config-mirror`: `existsSync` on the RUNTIME config.toml returned false
  for a locked file exactly as for an absent one, so the mirror took the
  "seed a fresh runtime config" branch and replaced the user's config wholesale.
- `config-settings-promotion`: an unreadable ~/.codex/config.toml counted as
  having no promoted settings, and the write path then rebuilt the user's
  canonical Codex config from Orca's runtime copy.
- `codex-home-paths`: both delete branches in `linkSystemCodexResource` remove
  Orca's mirrored copy because the system resource "is not there". `existsSync`
  and `systemResourceIsRegularFile`'s `catch { return false }` both reported
  that for a source nobody could read, so one denied read on ~/.codex/AGENTS.md
  removed the managed copy on the next launch.

`src/shared/definitive-filesystem-absence.ts` now owns the one errno allowlist —
ENOENT and ENOTDIR, with every other code including unrecognised ones treated as
indeterminate — and `host-codex-managed-home-ownership.ts` drops its private
copy rather than letting the two drift. `codex-path-observation.ts` builds the
three-valued observation on top of it.

The resource sync's two `existsSync`/`statSync` probes collapse into one
resolved stat, which answers reachability and regular-file-ness together and
closes the window between them.

`config-settings-promotion.ts` crossed its max-lines budget, so the write-target
resolution moves to its own module rather than taking a lint exemption.

Deliberately not here: the hook-service trust writes that run after a refused
mirror, and the promotion write target's own classification, which is
unreachable because it always resolves to the same file the read above already
refused. Both are noted in comments rather than half-built.

* fix(codex): preserve resource copies on indeterminate reads
2026-08-18 14:10:32 -07:00
Neil 487e43d619 refactor(test): use __fixtures__ for wrapper snapshots and teach the LoC bot (#15365) 2026-08-18 14:09:12 -07:00
Brennan BensonandQA f71ef7ee03 fix(sidebar): clear the delete state when a removal is refused (#15187)
* fix(sidebar): clear the delete state when a removal is refused

Callers mark rows deleting up front for immediate sidebar feedback, but a
refusal in beginHostQualifiedRemoval returns before removeWorktree's
try/catch — the only other place that clears the flag. The failure toast
auto-dismisses after 10s, so the workspace was left on a 'Deleting…'
spinner indefinitely with no explanation still on screen.

Also removes workspace-cleanup-removal-host-guard.ts: #14731 refused a
colliding cleanup removal, #15013 replaced refusing with routing to the
confirmed host, and nothing imports the module any more — not even its
own tests.

* test(sidebar): pin the two states that actually reach the removal refusal

The existing case used an empty store, which proved the clear but not that a
user can land there. Cover a stale folder-workspace id and a hostless row under
an ambiguous legacy runtime — both resolve to no route from populated state.

---------

Co-authored-by: QA <qa@local>
2026-08-18 14:08:25 -07:00
Neil 12550fcc28 refactor(test): move shell wrapper fixtures where they read as test data (#15363) 2026-08-18 13:50:33 -07:00
Jinwoo Hong a77a2f93f7 fix(remote): search Quick Open paths on the host (#15158) 2026-08-18 13:32:48 -07:00
Neil a3edabcd7b fix(package): keep cached dev Electron bundles out of app.asar (#15359)
`files` is an all-negation list, so electron-builder's default `**/*` packs
anything without an explicit `!` entry. out/electron-dev holds `pnpm dev`'s
per-branch Electron.app copies (~270MB each), so packaging on a machine that
has run dev bundled them all. CI never creates the directory, so releases were
never affected.
2026-08-18 13:27:25 -07:00
Brennan Benson 4b0e01f613 fix(github): scope GHES host-auth cache to the executing connection (#14948)
* fix(github): scope GHES host-auth cache to the executing connection

The gh auth answer cached for a connection-backed repo is probed without
the repository cwd, so it describes that connection's runtime — not the
local host. Keying only on repoPath+wslDistro let a local repo and an
SSH-hosted repo at the same path share one entry, and whichever resolved
first decided "is this GHES host authenticated" for both.

Include the connection identity in the runtime cache key. Local and WSL
keys are unchanged.

* fix(github): fence GHES auth cache across SSH reconnects

* fix(github): fence origin cache across SSH reconnects

* fix(github): fence repository identity cache on reconnect
2026-08-18 13:18:56 -07:00
Jinwoo Hong bef76953d7 fix(orchestration): record why a terminal's process is gone (STA-4603, STA-4536) (#15244) 2026-08-18 13:17:42 -07:00
Neil a3da91b10a fix(dev): stop caching unsigned bundles and reclaim stale dev copies (#15247) 2026-08-18 13:08:04 -07:00
Brennan Benson fe95698b95 fix(relay): let one owner hold pty.ackData instead of relying on construction order (#15079)
PtyHandler registered a no-op pty.ackData handler and SshPtyConsumerSessionAdapter
registered the real sourceCredit.acknowledge for the same method. onNotification is a
single slot, so only the adapter's survived — and only because relay.ts constructs it
second. Reversing those two lines would have made every credit-mode delivery wedge
permanently once the 256KB window emptied, with no error and no log.

Delete the dead no-op, and make onNotification throw on a duplicate registration the way
registerPtyDataPublicationAdmission already does for the admission slot. pty.ackData was
the only double-registered method in the tree, so nothing else changes behavior.

The existing test asserted only that pty.ackData appeared in the handler map, which stays
true when the real handler is shadowed. It now asserts PtyHandler does not own the method.
2026-08-18 12:37:39 -07:00
Jinwoo Hong a27527ae06 fix(orchestration): deliver worker-exit escalations to lightweight Run coordinators (STA-4604) (#15235) 2026-08-18 12:35:24 -07:00
Brennan Benson 2a760e310b fix(computer): report unasserted accessibility actions (#15028)
* fix(computer): report unasserted accessibility actions

* fix(computer): fail closed on missing action metadata

* Fix merged tab search test fixture
2026-08-18 11:29:26 -07:00
Brennan Benson 2f0f9a8a39 Revert "fix(agent-hooks): bind agent status to the pane its session was spawned into (STA-2069) (#14615)" (#15295)
Reverts #14615. Its premise does not reproduce, it does not reach the failure that does, and the correction it installs can misattribute status on a path that worked before.

1. PREMISE FALSE. #14615 asserts Claude Code >= 2.1.206 hosts TUI sessions under a shared daemon. On 2.1.233 `claude daemon status` reports "not running" with 69 live interactive sessions, and every client is a direct child of its own pane's shell. Measured across the fleet: 68 distinct pane keys, zero collisions. Foreground attribution was never broken.

2. DOES NOT FIX THE REAL BUG. The failure in #9236 is real but scoped to BACKGROUNDED sessions, whose workers inherit the dispatching pane's whole ORCA_* set. #14615 mints a binding only for launches Orca constructs, so a typed `claude --bg` produces none. Fixed properly in #15304.

3. INTRODUCES A MISATTRIBUTION. Bindings are removed only on PTY death, and a user who exits Claude keeps the pane's PTY. Resuming that session in another pane does not rebind (`--resume` is a session selector, so the pin declines), and resolveBoundPaneOverride then rewrites paneKey and tabId onto the ORIGINAL pane despite a correct posted key. Demonstrated with a failing test against main; causation isolated to resolveBoundPaneOverride.

Kept #14706's observations.rebind() in the conflicting hunk — it postdates #14615 and is not part of this revert.
2026-08-18 03:20:48 -07:00
Brennan Benson 53bd956ef6 ci(e2e): keep the relay version markers in the e2e build artifact (#15303)
build-relay.mjs writes each relay's marker as out/relay/<platform>/.version,
and upload-artifact excludes dotfiles unless include-hidden-files is set. The
markers were therefore stripped from e2e-build-out, so every consumer that
actually starts a relay failed with:

  Orca's local relay build is missing its version marker at
  out/relay/linux-x64/.version

This stayed latent because the sharded e2e lane never sets ORCA_E2E_SSH_DOCKER,
so its SSH specs skip instead of touching the relay. The changed-specs lane does
set it whenever a changed spec needs Docker SSH, which is why the failure only
appears on PRs that touch SSH-adjacent code.
2026-08-18 03:13:31 -07:00
Neil 66a5e5d245 fix(shell): repair worktree HISTFILE in plain zsh panes via one positive feature channel (#15258)
* fix(shell): repair worktree HISTFILE in plain zsh panes via one positive feature channel

A plain zsh pane — no startup command, no agent overlay — was never wrapped, so
Orca's HISTFILE repair never ran in it. macOS `/etc/zshrc` assigns
`HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history` with no check-before-set and runs
before any file Orca controls, so per-worktree history was a silent no-op for
every ordinary pane on the primary platform.

Wrapping those panes needs a way to say which wrapper features a shell should
turn on. That channel is one exported variable, ORCA_SHELL_FEATURES, carrying a
comma-separated positive allowlist from a closed set (history, markers, ready,
identity, overlay). The wrapper .zshenv reads it into a plain, non-exported
array and unsets it in its first executable lines, before the user's own
.zshenv — so the selection survives .zshenv -> .zprofile -> .zshrc -> .zlogin in
this process but physically cannot reach a child. There is no negative or
suppression variable anywhere; an absent or inherited value can only ever mean
fewer features. ORCA_HISTFILE is consumed and destroyed the same way, which
removes the root cause of #11146 instead of patching it.

All order-sensitive wrapper work now lives in one `__orca_shell_epilogue`
defined in .zshenv and invoked exactly once, from .zshrc for a non-login shell
and .zlogin for a login shell, with each feature an independent guard.

Selection is a pure function of spawn env and launch intent, so a pane wrapped
only for history gets no OSC 133 and is observably identical to the unwrapped
pane it used to be.

Generation is now fail-closed: wrapper files are written to a temp name and
renamed, every required path is verified non-empty, and ZDOTDIR is only set when
that holds. Previously a failed write still pointed ZDOTDIR at an empty dir and
the user silently lost their entire zsh config.

Orca also recognised only its own `*/shell-ready/zsh` dir shape when deciding
what the user's ZDOTDIR was, so being launched from any other terminal that had
hijacked ZDOTDIR captured that as the user's config dir. Ownership is now
established positively — a stamped marker file, or Orca's own dir shape for
wrappers written by older builds — and an inherited ZDOTDIR holding no zsh
startup file is ignored. No vendor is detected by name.

* fix(shell): make the zsh epilogue option-proof and stop history widening relay wrapping

Review follow-ups on the feature-channel PR.

- `emulate -L zsh` as the epilogue's first statement. It runs after the user's
  own config, so `setopt no_unset` made the precmd_functions append a fatal
  error that returned from the whole function (no ready widget, ZDOTDIR left at
  Orca's wrapper dir), and `setopt ksh_arrays` made the 1-based feature
  subscript drop whichever feature is listed first.
- The `/etc/zshrc` HISTFILE repair is no longer behind the `history` guard: it
  undoes damage Orca's own ZDOTDIR caused, so it must also run for a shell that
  re-enters the wrapper after the allowlist was consumed.
- The relay keeps its own wrapping gate. Its .zshenv resolves the user's config
  dir from a ZDOTDIR Orca has already overwritten, so wrapping a remote pane
  just for `history` cost a relocated-ZDOTDIR user their whole shell config.
- A failed primary spawn no longer leaks the primary shell's launch env
  (wrapper ZDOTDIR + feature channel) into an unwrapped fallback pane.

* fix(shell): drop the relay wrapping gate and stop HISTFILE inheriting across Orca instances

The relay-specific gate added last round rested on a false premise:
main's hasOverlayRestoreEnv already included ORCA_REMOTE_CLI_BIN_DIR, and
ssh-pty-spawn-env sets that on every SSH pane whose session has a CLI
bridge — so ordinary remote zsh panes were already wrapped. The gate only
bit where remoteCliBridgeEnv is null (a host too old to report its
platform), where it silently dropped that pane's worktree history. All
three transports now share the features.length rule.

HISTFILE stays exported, so a newly wrapped pane handed the worktree
history path to every child, including a nested Orca whose panes then all
hit injectHistoryEnv's check-before-set and appended into the launching
worktree's file. Same class as the fish_history fix in #15195: recognise a
path Orca minted and drop it before the check, on the desktop, daemon and
relay injection paths and both history-disabled branches.

Also: run the epilogue from the wrapper .zshrc when zsh is in sh/ksh
emulation, since sourcehome() then reads $HOME/.zlogin and the wrapper's
.zlogin never runs; track fallback launch-env keys per attempt rather than
once from the primary; and guard the cross-file epilogue call so a wrapper
dir shared by two builds degrades quietly.

* fix(shell): make every wrapper file self-sufficient and retire the deleted marker vars from tests

- .zprofile/.zshrc/.zlogin each define __orca_resolve_user_config_dir. They
  called it on line 2 while only .zshenv defined it, so a wrapper dir written by
  two concurrently installed builds printed three "command not found" and
  skipped the user's entire zsh config. New live-shell test covers it.
- Retarget every remaining ORCA_SHELL_READY_MARKER/ORCA_SHELL_STARTUP_IDENTITY
  reference onto ORCA_SHELL_FEATURES, or delete it where the key is now dead.
- isOrcaMintedHistFile requires a leading '/', so a relative path of the same
  shape stays the user's.
- Drop an unused no-control-regex disable, and register the two real-zsh suites
  in the dedicated shell-contracts lane.

* fix(shell): stop the zsh wrapper colliding on REPLY and degrade under sh emulation

Widening wrapping from overlay/startup panes to every zsh pane turned three
latent wrapper defects into user-visible ones.

- The config-dir resolver used `REPLY`, zsh's shared scratch global, as its
  out-parameter. `typeset -r REPLY` in a user config made the wrapper's first
  executable assignment fatal, `typeset -i REPLY` silently resolved every path
  to 0; both left HISTFILE inside Orca's wrapper dir. It now writes an
  Orca-private `_orca_resolved_config_dir`, declared `typeset -g` so the
  rename introduces no `warn_create_global` noise. A new rule test fails on any
  generated wrapper file that writes a global outside Orca's namespace.

- The daemon dropped an inherited HISTFILE but never an inherited
  ORCA_HISTFILE, which now both wraps a pane the client scoped nothing for and
  re-exports another worktree's history path. The relay had the same gap on its
  isolation-off and revive paths. Both now mirror the desktop.

- A user .zshenv or .zprofile ending in `emulate sh` makes zsh ignore ZDOTDIR,
  so no later wrapper file is read and the epilogue never runs. Nothing can
  repair HISTFILE from there, so the wrapper now detects the emulation and
  hands the pane back unwrapped instead of leaving history somewhere invisible.

Also `typeset -g __orca_in_command` so the OSC 133 preexec hook prints no
warning under `setopt warn_create_global`.
2026-08-18 03:12:47 -07:00
6efd4061dc fix(git): run WSL git reads without a shell (#15257)
* fix(git): run WSL git reads without a shell

WSL-routed git ran through the distro user's interactive login shell for
one reason: to inherit their PATH. That shell also runs the distro's
rc/motd and writes it to the stdout callers parse, which is why #10917
reports a shell banner breaking GitHub source detection.

A shell-free route already existed (`--exec /usr/bin/env PATH=... git`,
added for status reads in #13207) but it was opt-in, and only
gitStatusReadOptionsForWorktree opted in. Every other read -- remote
get-url, config --get, log, show, rev-parse -- took the login shell on
every call, so the reported parse never benefited.

Classify reads at the resolver instead of at each caller. A read needs
nothing the login shell provides, so it takes the direct route without
the caller asking. Writes and network operations stay on the login
shell: they can depend on credential helpers and ssh-agent that the
user's profile sets up.

Subcommands that both read and write (config, remote, branch,
submodule) require an explicit read flag before they qualify, so
`config --get` goes direct while `config user.email x` does not.

`git show` blob reads stop forcing the login shell and go direct too.
The fence stays on that path: the login shell is still the fallback when
the environment probe is cold or rejected, and a banner there would
become file content.

Behavior note: the first WSL read per distro now also warms the
environment probe in the background, so a cold read spawns one extra
wsl.exe. Subsequent reads start no shell at all.

* fix(git): keep queried WSL remote reads on login shell

* style: format WSL runner test

* fix(git): match WSL read markers positionally

The conditional read markers were matched anywhere after the subcommand,
so a positional argument that happened to share a marker's name routed a
write shell-free: `worktree remove list` read as a listing, and
`submodule foreach status` as a status query -- the latter can run
arbitrary commands, including network ones.

Split the two kinds of marker apart. `config`/`branch` are flag-marked
and still match anywhere; `remote`/`worktree`/`submodule` are
action-marked and must match the first non-flag argument. The queried
`remote show` rule and the `symbolic-ref` arity rule are unchanged.

Neither case is reachable today -- Orca issues no `submodule foreach`,
and the worktree paths are its own CLI, not git argv -- but the loose
match was the shape of the defect, not those two instances.

* test(git): pin read routing behind global options

Writes hidden behind -c/-C/--git-dir must not reach the shell-free
route, and an unparsed global form must fall back to the login shell
rather than guess at the subcommand. Both directions fail safe.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-08-18 02:54:19 -07:00
OrcaWinandOrcaWin b7f2e17712 fix(git): fence buffered WSL login-shell reads (#15060)
* fix(git): fence buffered WSL login-shell reads

Two git paths force the login shell unconditionally and buffer its whole
stdout, so the distro's rc banner lands in front of the payload:

- gitExecFileAsyncBuffer backs `git show :<path>` blob reads and hands
  the bytes straight to the diff/blob viewer, so the banner is prepended
  to displayed file content.
- buildNetworkSshPolicyEnv probes `core.sshCommand` and treats any
  non-empty answer as a user-configured wrapper. A banner reads as
  configured, so the code skips the `ssh -o BatchMode=yes` fallback and
  silently disarms the guard that keeps non-interactive SSH from
  hanging on a prompt.

Fencing is opt-in per call site rather than applied to the login-shell
branch as a whole: streaming consumers (`git grep`, `ls-files -z`) parse
records as they arrive, so an opening marker would be glued onto their
first record. Only these two, both buffered by construction, opt in.

Blob content can be binary, so the payload is sliced out of the raw
bytes; decoding to find the fence would corrupt it. The markers are
exposed on the captured command because the shared module is bundled for
the renderer and cannot reference Buffer.

Note this path is not a rare fallback: `preferWslDirectGit` is only set
by gitStatusReadOptionsForWorktree, so every other WSL-routed git call
takes the login shell on every invocation.

* test(git): use findLast for the ssh-policy call lookup

Satisfies the code-quality rule that flags filter-then-index.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-18 02:40:06 -07:00