Commit Graph
10019 Commits
Author SHA1 Message Date
Jinjing 5ce356cc4a Update Android download links to 0.0.44 (#15898) 2026-08-22 00:06:42 -07:00
ecfc547ece fix(windows): reach the pty job on the teardown path that actually runs (#15886)
W2 gave every ConPTY pane a job object and wired `terminateOwnedTree` into
`local-pty-provider.ts`. Measured in #11047: worktree delete does not execute
there. It runs in the terminal daemon, so on the path that matters the sweep
still fell back to a parent-pid walk -- which a detached, reparented grandchild
is not in. That process is the one that holds the worktree cwd open, so the
delete this was meant to fix could still fail.

Expose the job on `SubprocessHandle` and use it at the three daemon call sites
(`terminal-session-teardown.ts` x2, `session-termination-controller.ts`).

Also: on Windows `forceKill()` returned early after any `kill()`, to avoid
double-closing the ConPTY handle node-pty owns. Correct, but it left force-kill
a permanent no-op -- and a wedged ConPTY never fires `onExit`, so such a session
had no escalation at all (#9854). Terminating the job is that escalation, and it
does not touch node-pty's handle.

A tree-walking guard now fails if any production `killWithDescendantSweep` call
omits `terminateOwnedTree`, since the option is optional by design and its
absence is invisible in review -- exactly how this gap survived W2.

Co-authored-by: OrcaWin <orcawin@users.noreply.github.com>
Co-authored-by: hanbong5938 <hanbong5938@users.noreply.github.com>
mobile-android-v0.0.44
2026-08-21 23:58:48 -07:00
Jinwoo Hong 1354ff534f fix(cmd-j): host-qualify browser and simulator tab candidates (STA-4965) (#15686) 2026-08-21 23:17:54 -07:00
Neil 2b1254d681 fix(windows): own PTY process trees with job objects (#15755)
* fix(windows): own PTY process trees with job objects

Teardown used to answer 'is this tree mine, and how do I kill it?' by
scraping the process table, walking parent pids back to Orca, and running
taskkill /T /F only if the walk said yes. Every step is a guess, and the
code said so itself: windows-pty-root-identity.ts:35 already named the
fix -- 'an inherited handle / Job Object'.

The guesses fail in the ways users report. A pid walk cannot survive pid
reuse, so teardown refused whenever it could not prove ownership, and a
refused kill is an orphaned agent tree holding the worktree directory
open (#9045, #10475, #10087). A descendant that reparented is invisible
to the walk. The scrape itself could be blocked by policy, which read as
'no evidence'.

node-pty now creates a job object per ConPTY and assigns the shell under
CREATE_SUSPENDED, before it can spawn anything -- assigning afterwards
leaves a window in which a fast child escapes. Termination is one
TerminateJobObject; liveness is QueryInformationJobObject.

Verified on Windows 11 against a shell whose grandchild was spawned
detached: job membership came back [shell, grandchild] and one call
killed both. Neither a parent-pid walk nor GetConsoleProcessList sees
that grandchild -- it leaves the console and reparents, which is exactly
the claude.exe/node.exe/cmd.exe orphan in #9045.

KILL_ON_JOB_CLOSE means a daemon that dies without unwinding no longer
strands shells (#9195, #10415). The job is the daemon's, not the app's,
so an app-main crash still leaves sessions alive -- the guarantee
win-crash-survival-e2e asserts.

Both entry points report unavailable rather than a false success when a
pty has no job: an outer job without BREAKAWAY_OK can refuse the
assignment, and a pty from an older build has none. Reading 'we could
not tell' as 'already dead' is the original bug, so the old probe stays
as the fallback.

* test(windows): pin job ownership against a real detached grandchild

The unit tests pin the contract; this pins what the contract is for. A
grandchild spawned detached leaves the pane's console and reparents, so
GetConsoleProcessList and a parent-pid walk both miss it -- that is the
process that outlived its pane and held the worktree directory open.

Includes a guard that this build actually has job support, so a node-pty
rebuilt from unpatched sources fails loudly instead of letting every
assertion pass vacuously.

* fix(windows): correct the job liveness contract to what Windows actually does

I claimed an emptied tree would report [] and that this was the evidence
a stale registry entry lacks (#15549). Running it on Windows 11 showed
otherwise: node-pty drops its handle record and closes the job when the
shell exits, so a dead tree reports null.

Null therefore means unverifiable in the sense of
docs/reference/ssh-execution-boundary.md -- no job support, not a ConPTY,
or no longer tracked -- and is never evidence that processes died. A
caller reading it as proof of death would have been right by accident
after a normal exit and wrong on a host that refused the assignment.

What the API does add is descendant liveness for a tree that is still
tracked, including children that detached from the console.

* fix(windows): stop a clean shell exit from reaping backgrounded processes

Measured on Windows 11: with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE on the
per-PTY job, releasing the handle when the shell exits also killed
whatever the user had backgrounded. Typing 'exit' in a pane reaped a
detached server that survived before this patch.

That is a behaviour change nobody asked for. The approved change was
that killing the terminal daemon reaps its shells -- not that a clean
exit reaps your background job. The job's purpose is to make an EXPLICIT
teardown exact, which TerminateJobObject still does.

Reaping a dead daemon's shells now needs the daemon-level job the design
called for: the daemon assigns itself, children inherit membership, and
its closure on daemon death reaps them without touching clean-exit
semantics. Not in this PR; noted in the reference doc.

* test(windows): pin that a clean exit leaves backgrounded work alone

The counterpart to the tree-kill test. Without it, re-adding
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE would look like a tightening rather
than the regression it is.

* fix(windows): stop a winpty pty id from matching a ConPTY job

winpty.cc and conpty.cc each mint their 'pty' id from an independent
counter, and windowsPtyAgent stores both in the same _pty field. So a
winpty-backed terminal's id can collide with a live ConPTY baton -- and
closing that pane would have terminated an unrelated pane's entire
process tree.

Both job entry points now take the shell pid and the native side refuses
unless GetProcessId(hShell) matches, which makes the id unforgeable.

Two more from the same read-through:
- ResumeThread's failure was ignored. A shell left suspended is a pane
  that never prints and never exits, which is far harder to diagnose
  than a failed spawn; it now cleans up and throws.
- handle->hJob was assigned before LoadConptyDll, which can throw. A
  baton carrying a job but never reaching SetupExitCallback has nothing
  left to close it, so the assignment moved down beside hShell.

* docs(windows): record the unsynchronised node-pty baton table

Pre-existing upstream -- the exit thread erases while the main thread
reads -- but terminatePtyJob adds an instance of it, so it belongs in
writing rather than in someone's head.

* fix(windows): close four gaps found in review

BREAKAWAY. The per-PTY job set no limits, so a child asking for
CREATE_BREAKAWAY_FROM_JOB was refused with ERROR_ACCESS_DENIED.
Installers, msiexec and some updater and service-control paths spawn
that way deliberately -- they worked before this patch and would have
failed only inside an Orca terminal, which is the worst shape a bug
report can take. JOB_OBJECT_LIMIT_BREAKAWAY_OK restores it; a child
still has to ask, so ordinary descendants stay owned.

EMPTY IS NOT UNAVAILABLE. The native reader returns an empty list --
not an error -- when CreateToolhelp32Snapshot fails, which is what an
EDR hook or a restricted token produces. Callers read that as 'nothing
is running' and teardown concludes a live PTY root is already gone. The
snapshot must contain the querying process; nothing else is
unfalsifiable, and one predicate catches empty, truncated and
permission-filtered tables alike.

NO DEADLINE. Replacing execFile dropped its 3s timeout. The vendored
reader latches a module-global while a request is in flight and clears
it only after draining its callbacks, with no try/catch -- so one wedge
leaves every later call queued behind a promise that never settles, and
the process table is dead for the life of the app. The bound is back.

GUESSED IMAGE PATH. executablePath was derived from the first
space-delimited token, which reads 'C:\Program' out of an unquoted
'C:\Program Files\nodejs\node.exe ...'. Wrong evidence is worse than
none, and the only consumer already had the full path in , so
the field is gone rather than repaired.

Also: remove_pty_baton no longer sits inside assert(), which NDEBUG
would compile away along with the call, and the job accessors hold a
lock across lookup and use -- handle values are recycled, so an
unguarded read could pass the shell-pid check against an unrelated
process and terminate the wrong job.

* fix(windows): apply the job lock once per accessor

The patch script matched a string its own replacement still contained, so
PtyTerminateJob got two lock_guards named guard and PtyListJobProcessIds
got none. MSVC caught it: error C2374 redefinition.

* test(windows): pin that a child can still break away from the job

Verified on Windows 11: 'start /b' writes its marker and no access-denied
appears. Without JOB_OBJECT_LIMIT_BREAKAWAY_OK this fails, and it fails
only inside an Orca terminal -- so the failure would look like Orca
corrupting unrelated software rather than like a job-object change.

* fix(windows): stop the ownership guard from reading a closing handle

The guard called GetProcessId(hShell) to prove identity, but the exit
watcher closes hShell on another thread -- so the guard could read a
closed handle, and under strict handle checks that is fatal rather than
merely wrong. Worse, it widened the gap between validating hJob and
using it from two instructions to a kernel round-trip, and handle values
recycle: the likeliest occupant of a freshly recycled value in this
process is another pane's job.

The pid never needed a handle. It is captured at spawn and compared as a
DWORD, so the guard touches no handle at all, and hShell is now closed
inside the same lock as hJob.

Also from review:
- reject CR/LF in a cmd argument. cmd ends the command at a raw line
  break whatever the quote state, so there is no escape for it; encoding
  one anyway truncates the argument and can leave the remainder to run
  as a command. Agent prompts are this encoder's motivating input.
- ask the process table only for the fields a caller needs. Memory and
  CommandLine each cost an OpenProcess per process, inline, for every
  process on the box -- and the 1024 bound is patched out. Ancestry
  reads now skip both.
- corpus gains the degenerate quote-only and two-quote arguments.
- PtyListJobProcessIds' docblock still taught the empty-list contract
  that was corrected on the TS side, and now records that the ConPTY
  console host is never a job member.
- drop a write to NumberOfAssignedProcesses, which is output-only.
- pty_baton::hShell is initialised; ownsShell was only safe because &&
  short-circuited ahead of it.

The backgrounded-child test is rescoped: 'start /b' uses
CREATE_NEW_CONSOLE, not CREATE_BREAKAWAY_FROM_JOB, so it proves job
membership does not block backgrounding -- not that BREAKAWAY_OK works.
That flag rests on the Win32 contract, and I have said so rather than
letting the test imply coverage it does not have.

* fix(windows): bound retries after the process table wedges

The 3s deadline stops a caller hanging, but the timed-out call leaves its
callback in the vendored module's queue -- and that queue drains only
when the latched request completes, which in this wedge never happens.
Retrying at the caller's poll rate would add a closure per tick forever.
A 30s cooldown bounds it to one probe, and a late callback clears the
cooldown because it proves the reader recovered.

Also pins the deadlock invariant in the patch: the exit thread's lock
must close before tsfn.BlockingCall, because that waits on the JS thread
and the JS thread can be waiting on the same mutex inside
PtyTerminateJob. Correct today by scoping; a comment so a later refactor
does not widen it.

* revert(windows): drop the field-selection API, which cannot pay off

I added it for a real perf finding -- Memory and CommandLine each cost an
OpenProcess per process -- and then never wired a caller, so the claim
that ancestry reads skip them was wrong.

Wiring it would have been worse than leaving it dead. The only ancestry
consumer is the teardown identity probe, which needs a snapshot that
started AFTER it asked, for pid-recycle detection. Bypassing the shared
reader to get narrow fields would let that request join a scan already in
flight -- trading a correctness guarantee for milliseconds.

Field selection only pays off if callers can ask for less, and they
cannot: one shared snapshot serves every caller so a 32-wide teardown
collapses into a single scan, which means it has to carry every field.
The reasoning now lives next to the flags instead of in a dead export.

* fix(process): three P1s from review — a crash vector and two wedge bugs

STDIN EPIPE COULD TAKE DOWN THE MAIN PROCESS. A child that exits without
reading makes the queued write fail with EPIPE, and an unhandled error on
a stream is an uncaught exception. The child's own error listener does
not cover its stdin stream, so runProcess({ input }) against a
short-lived child was a crash, not a failed call.

THE COOLDOWN LEAKED A BATCH PER CYCLE INSTEAD OF BOUNDING IT. At expiry
every concurrent caller passed the check before any of them re-armed it,
so each enqueued a callback into the still-latched native queue and each
cycle leaked another batch. The cooldown is now re-armed BEFORE probing,
so exactly one caller gets through.

A SYNCHRONOUS THROW LEFT ITS DEADLINE RUNNING. The timer was declared
inside the try, so catch could not clear it; it fired later and wedged a
reader that had already recovered. Hoisted and cleared, and wedge state
now carries a generation so a request that lost its deadline cannot
mutate it on behalf of the one that replaced it.

Found by review once the prompts were short enough for the reviewer to
finish -- the previous two rounds died on prompt length.

* fix(process): stop a stream error from crashing the main process

Same class as the stdin EPIPE finding, two instances further on: stdout
and stderr had data listeners and no error listeners, and an unhandled
error on a stream is an uncaught exception.

Scoped to runProcess, which owns the child outright. spawnProcess hands
the streams to its caller, and a blanket handler there defeats callers
that track and remove their own listeners -- the SSH ProxyCommand
transport does exactly that, and its cleanup test caught the attempt.
Documented on spawnProcess so the boundary is explicit rather than
inferred.

* fix(windows): validate the ConPTY DLL before creating the process

LoadConptyDll throws when conpty.dll is missing -- a real state, and one
this branch hit during development. It ran after CreateProcessW and
ResumeThread but before the baton and the exit watcher were installed,
so a throw leaked the job, process and thread handles and left an
untracked shell tree running. Once per attempt, so a broken install
accumulates orphan shells on every retry.

Resolving the DLL first costs nothing and leaves exactly two throws
after creation: the CreateProcessW failure, where nothing exists yet,
and the resume failure, which already cleans up after itself.

This also closes the same leak for hProcess and hThread, which predates
the job work.

* feat(windows): add the daemon-level job the design called for

The plan specified two nested jobs and I built one. That gap is why
dropping KILL_ON_JOB_CLOSE from the per-PTY job cost the approved
guarantee that a dead daemon reaps its shells -- I had one job trying to
answer two questions, and the two answers conflict.

They are separate jobs. The per-PTY job answers 'kill exactly this
pane's tree, now', and cannot be kill-on-close because its handle is
released when the shell exits, which would reap whatever the user
backgrounded. The daemon assigns itself to a second job that IS
kill-on-close; its handle is released only when the daemon dies.
Children inherit membership, so every pty is covered and the per-PTY
jobs nest inside it.

Daemon, never app: an app-main crash must still leave sessions alive,
which win-crash-survival-e2e asserts. Both jobs carry BREAKAWAY_OK, or a
child asking to break away is refused at whichever level lacks it.

Restores #9195 and #10415, which I withdrew from this PR earlier.

* docs(windows): record what the host job does not cover

An app-hosted PTY gets a per-PTY job but no crash reaping, because the
alternative is a kill-on-close job on the app -- which is precisely what
the crash-survival guarantee forbids.

* ci(windows): run the win32 suites in the PR windows job

Both were skip-on-non-win32 and had only ever run on one machine I drive
by hand -- which went unreachable at exactly the moment I needed to
verify the percent-escaping fix. Verification that depends on one box is
not verification.

The job already builds node-pty from patched source and already runs a
useConptyDll test, so the ConPTY runtime files are in place by this
step. This also makes the encoder a gate: the corpus is the only thing
standing between an agent prompt and a mangled argv, and it now runs
against real cmd.exe on every PR.

* fix(deps): refresh the lockfile for the current patch hashes

pnpm records a hash per patched dependency, and I regenerated both
patches repeatedly across the review rounds without refreshing the
lockfile. Every local run used --frozen-lockfile's looser sibling, so
nothing caught it until CI did:

  ERR_PNPM_LOCKFILE_CONFIG_MISMATCH  Cannot proceed with the frozen
  installation. The current "patchedDependencies" configuration doesn't
  match the value found in the lockfile

Verified with pnpm install --frozen-lockfile locally this time.

* ci(windows): build node-pty from source before the win32 suites

CI proved the encoder fix on real cmd.exe -- 26/26 -- and in the same run
proved the job suite had been testing an unpatched binary. node-pty
prefers its upstream prebuild, which does not contain this patch, so
every job-object export was absent and isPtyJobOwnershipAvailable() was
false.

That guard is why the failure was loud rather than a vacuous pass, and
it is the reason the assertion exists.

Packaging was never affected: rebuild-native-deps.mjs already builds
node-pty from source for Electron and restores the ConPTY runtime files.
The gap was the node-runtime test environment only.

Not changing requiresPatchedNodePtySourceBuild's win32 exemption here.
Its premise -- that the patch is Unix-only -- is now false, but lifting
it also needs pnpm rebuild to force a source build, and I cannot
validate that on macOS and Linux from here. Recorded as a follow-up
instead of changed blind.

* test(windows): gate the host-job guarantee in CI

The daemon-level job had one hand-run proof and no automated coverage --
the same shape of gap that let an unpatched node-pty go unnoticed until
CI caught it.

It needs a real second process, because the assertion is about what
happens when that process is force-killed: a host in a kill-on-close job
must strand neither its pty nor a grandchild spawned detached, which is
the process a parent-pid walk cannot see.

Runs in the Windows PR job alongside the per-pty and encoder suites, so
both halves of the two-job design are now gated rather than asserted.

* fix(windows): serialise host-job creation

Two callers racing PtyAssignCurrentProcessToJob would each create a job,
put the process in both, and leak the first handle -- and the handle is
what keeps a kill-on-close job alive, so a leaked one is never released.
'Only JS calls it' is not a guarantee: a worker thread with its own
N-API env shares these statics.

Also records the ordering requirement it depends on.
AssignProcessToJobObject adds only the named process; children inherit
membership, but a pty that already exists does not join retroactively
and would not be reaped. The daemon assigns at startup, before the
ConPTY warmup and before any session, which is correct today and now
stated rather than implied.

* fix(daemon): keep the host job off the startup path

Assigning the host job at daemon startup resolves the node-pty native
module, which loads the ConPTY addon -- and paying that before the
endpoint is published delayed readiness enough that daemon-boot-smoke
failed on windows-latest, deterministically.

windows-conpty-warmup already carries the comment for this exact
hazard ('setImmediate keeps the ready/handshake path ahead of the
warm-up') and I put an eager load in front of it anyway.

Moved to the pty spawn path, which already pays ConPTY cost, and
memoised. Children inherit job membership, so assigning immediately
before the first spawn still covers every pty -- and nothing can spawn
one before the endpoint exists.
2026-08-21 22:31:36 -07:00
Hwanseok Choi bb8c3b6360 fix(agent-catalog): update outdated Claude Code doc links (#15387) 2026-08-21 22:27:14 -07:00
Jason BrashearandClaude Opus 5 b63292bee1 fix(editor): detect .liquid files as Liquid (#15213)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 22:25:42 -07:00
Neil 057fbfcffc perf(windows): read the process table natively instead of forking PowerShell (#15749)
* perf(windows): read the process table natively instead of forking PowerShell

Seven independent readers each forked powershell.exe to run
Get-CimInstance Win32_Process, with a wmic fallback that Windows 11 24H2
has removed. On a domain-joined host with PowerShell Transcription
enabled by policy, one of them running every ~2s recorded ~289GB across
1.4 million files (#15209). The same scan cost ~700ms and ran per pane
(#15036), and a Group Policy or AV block turned it into 'unavailable',
which callers read as 'no evidence' -- which is how a PTY tree survives
its own teardown (#9045, #10475).

A Toolhelp32 snapshot answers the same question with no child process.
Measured on Windows 11 with 1050 processes, p50/p95:

  pid+ppid+name          15.9 / 17.5 ms
  +memory +command line  30.6 / 33.7 ms
  Get-CimInstance         706 / 723  ms

Two upstream defects needed patching, both found by running it on real
hardware. The binding requires Spectre-mitigated libraries our agents do
not carry (node-pty is patched the same way). And enumeration stopped
after 1024 processes: on a host with 1051 the module returned exactly
1024, and the querying process was itself among the 27 missing -- a
truncated snapshot silently hides the descendants teardown is looking
for, which is the failure this whole change exists to remove.

Migrated: the foreground/descendant reader (the #15209 scraper and the
teardown identity gate) and the port scanner's PID attribution. NOT
migrated: the memory collector and three identity probes, which need
Win32_Process.CreationDate and have no native equivalent. Start time is
a proxy for identity anyway; an inherited job handle is the real answer,
so those belong with the job-object work rather than here.

Packaging follows the windows-native-registry contract exactly:
optional, absent from onlyBuiltDependencies so macOS/Linux never run
node-gyp, win32-only in the packaged runtime. Asserted by the existing
contract test, which also stops pinning a whole source literal that only
tested its own formatting.

* chore(process): ratchet the child_process allowlist down

windows-foreground-process-rows.ts no longer spawns anything, so its
allowlist line is stale. The guard fails on a stale entry as well as a
new one, precisely so a migrated file cannot keep a slot open and hide
the next regression in the same path.

* fix(ports): import the process-table reader the scanner uses

Missing import: the migration replaced the PowerShell call but the new
symbol was never imported, so tsc failed. Vitest transpiles without
typechecking, which is why the port-scanner suite stayed green.

* fix(deps): sync this branch's lockfile with its patch set

Same class as the fix on the tip branch: pnpm records a hash per patched
dependency, and this branch introduces the windows-process-tree patch
without its lockfile entry matching. Every job here failed at install
with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH.

Verified with --frozen-lockfile, which is what CI runs and what my local
runs were not.

* test(relay): drive the relay's Windows fixtures from the native snapshot

Two relay cases fed a PowerShell CIM payload through a mocked execFile.
That reader is gone, so both failed -- deterministically, on every PR
run for this branch and the one above it.

I did not catch it because my own verification sweep was
'src/main src/shared config/scripts' and never included src/relay. The
relay is a first-class consumer of the process table; leaving it out of
the sweep is how a deterministic failure survived six review rounds.
2026-08-21 21:54:57 -07:00
Jinjing 42a42281bd Remove agent map view from dashboard (#15853)
* Remove agent map view from dashboard

Removes the view toggle and simplifies the dashboard to show only the kanban board layout.

* Assert boardProps is initialized on drawer open
2026-08-21 21:43:54 -07:00
Neil a157f4cfec fix(worktree): drop a just-added fork remote when its head fetch fails (#15850)
Fork-PR setup adds the contributor's remote, then fetches the head. If that
fetch fails the create aborts, but the remote stayed behind with no owner:
cleanup only runs on worktree removal, and no worktree was ever created. Each
retry then left another orphaned pr-* remote.

Roll the remote back on fetch failure, on both the local and SSH paths, and
only when this call is what added it -- a reused remote (Orca-created or not)
is left alone.
2026-08-21 21:24:19 -07:00
Denis Darii d5667376b0 feat(dashboard): add a keyboard shortcut to toggle the Agent Dashboard (#15353)
Adds a configurable, unbound-by-default `dashboard.toggle` action that toggles the Agent Dashboard (in-window drawer or pop-out, per the existing mode setting).

- Wired through window-shortcut-policy, main-window dispatch, browser-guest dispatch, preload, and the renderer IPC handler.
- Opening the in-window drawer reveals the sidebar first; closing leaves it alone.
- Gated on the `experimentalAgentDashboardPopout` experiment, and the Settings shortcut row is hidden while that experiment is off.
2026-08-21 21:12:12 -07:00
Neil b7e79b7ca6 fix(windows): one chokepoint for every child process (#15746)
* feat(process): add the Windows-correct child-process chokepoint

Six decisions have to be made every time Orca starts a child process --
console visibility, argument quoting, .cmd interpretation, binary
resolution, timeout policy, and how the tree is later terminated. POSIX
forgives all six. Windows punishes each differently, and made per-call
site across 172 files they were right in some and wrong in others.

runProcess/spawnProcess make them once:
- windowsHide unconditionally, shell:false unconditionally (shell:true
  concatenates argv unescaped and silently disables windowsHide)
- .cmd/.bat routed through cmd.exe /d /v:off /s /c with a verbatim line,
  because Node refuses to spawn them otherwise (EINVAL)

The encoding was derived by measurement on Windows 11, not from the
docs. An embedded quote is written "" rather than \" so cmd's naive
quote count stays even -- with \" the parity flips and every later &
| < > on the line stops being data. Measured before the fix, argv
["a b", 'c"d', "e%F%g", "h&i", "j^k"] arrived as
["a b", 'c"d', "e^%F^%g", "h"]: the & truncated the argument and
ran its remainder as a command. Each % is broken out of the quoted run
as "^%" because %VAR% expands even inside quotes.

The import-boundary test is a ratchet seeded at today's 172 files; it
only shrinks.

* fix(process): route the console-flashing spawn sites through the chokepoint

The ssh -G config probe fires on every connect and reconnect, and ssh.exe
is console-subsystem, so a GUI-subsystem parent gets a fresh visible
conhost that takes foreground -- keystrokes typed into an Orca terminal
at that moment go into the black box (#10488, #14543). Same for the
ProxyJump tunnel, the ProxyCommand cmd.exe wrapper, the font enumeration
and the DPAPI cookie decrypt.

Also stops spawning powershell by bare name: PATH under Electron is not
the user's, so where policy has pruned the System32 entry the spawn fails
and the font picker silently reports five hardcoded families rather than
an error (#11771).

Deletes system-fonts' 40-line bespoke execFileText -- timeout, output cap
and kill are the chokepoint's job now. Adds runProcessSync so the sync
callers have a compliant path; without one the ratchet could never
reach zero.

The three suites that mocked child_process directly now mock runProcess,
which is the point: how a process gets started is no longer each
module's business. Ratchet 173 -> 170.

* fix(process): do not report a deliberately killed child as timed out

runProcessSync inferred a timeout from signal === 'SIGTERM'. Measured:
a real timeout sets error.code ETIMEDOUT and kills with SIGTERM, but so
does anything else that terminates the child -- and those cases set no
error at all. Reading the signal alone reports a process someone stopped
on purpose as having timed out, which callers retry.

* refactor(process): hold the ratchet as data and migrate the pwsh probes

The allowlist and the adversarial argument corpus are read only by tests,
so they were production modules in name only; they move to __fixtures__.

pwsh.ts carried isTimeoutError() purely to reconcile two spellings of the
same event -- execFileSync reports a timeout as ETIMEDOUT, the execFile
callback as a SIGTERM kill with no code. runProcess reports one timedOut
flag, so the helper and the reasoning behind it both go.

Its sync probe also spawned without windowsHide, which flashes a console
and steals foreground on every cold cache read.

* refactor(process): migrate five more spawn sites onto the chokepoint

Each one deletes a hand-rolled promise/timeout/kill wrapper and stops
re-deciding console visibility for itself. Ratchet 170 -> 164.

Two things this surfaced, both kept:

runProcess now accepts string chunks as well as buffers. A stream someone
called setEncoding on emits strings, and concatenating those as buffers
throws inside a data handler -- where the rejection has nowhere to go and
the caller simply hangs rather than failing.

ProcessSpec keeps its AbortSignal. I had removed it as unused; the macOS
PAM preflight passes one through from its own caller.

ipc/app.ts is deliberately NOT migrated. Its probe spawns a three-stage
 pipeline detached so a timeout can reap the group with one
negative-pid SIGKILL; runProcess kills only the root, which would orphan
the plutil stages. Migrating it needs the chokepoint to own POSIX
process-group termination first -- the same guarantee job objects give on
Windows. Reverted and left on the ratchet.

* test(process): do not assert a POSIX signal on Windows

Windows has no signals, so the same deliberate kill reports an exit code
there and a signal on POSIX. What has to hold on both is that neither
shape reads as a timeout. Caught by running the suite on Windows.

(cherry picked from commit 0a6e9902a22a369a0e85e113ea8d87b726f82e1f)

* fix(process): settle a timed-out run even when the child ignores the kill

close only fires once the child is actually gone, so a child that traps
SIGTERM never emits it and the promise outlives its own deadline
forever. That is the same wedge shape just fixed for the process table,
and it is worse here: pwsh.ts and the snapshot reader both cache an
in-flight probe, so one unkillable child hands every later caller the
same dead promise.

After the deadline it now escalates to SIGKILL and settles regardless,
reporting timedOut with whatever output arrived.

(cherry picked from commit 78ac169197c4e6faee1b9310a7186029cc11acbc)

* fix(process): escalate an aborted child too, not just a timed-out one

The grace escalation I added covered the timeout path and left abort on
the old one, so an aborted caller with an unkillable child still waited
forever -- the same defect, one path over. The macOS PAM preflight is a
real caller that passes an AbortSignal.

Both paths now share one stop-and-settle, and the result reports
timedOut honestly: false when the caller aborted.

(cherry picked from commit 7e9523a9e31172bb8183661b56f04c3ab6a03d0d)

* fix(windows): stop percent escaping from forging an escaped quote

escapePercentForCmd ran as a post-pass over the quoted string, so it
inserted a quote wherever a percent was -- including straight after a
backslash. CommandLineToArgvW reads backslash-quote as an escaped quote,
so C:\Users\%USERNAME%\x arrived corrupted. That is about as common as
Windows paths get, and my 20-case corpus had no backslash-before-percent
entry to catch it.

Percent handling is now part of the quoting loop, where the backslash
run is known and can be doubled before the inserted quote. Two corpus
cases cover the shape.

The program path gets the same treatment. It was quoted but not
percent-escaped, so a launcher under C:\Users\%USERNAME%\ had its own
path expanded on the cmd hop.

quoteWindowsArgument no longer takes a boolean. Passing it to
values.map() handed map's index in as the flag -- which is how the first
version of this fix was written, and the corpus test caught it.

Separately: an AbortSignal that was already aborted never fires the
event, so runProcess ran the child to its full timeout for a caller who
had already given up.

(cherry picked from commit f7e2e56b1ee1f27ab6d1035dde4501b38f95b374)
2026-08-21 21:05:24 -07:00
Jinwoo Hong e7b047f53a fix(codex): suppress false restart notices after reauth (#15835) 2026-08-21 18:09:41 -07:00
Yeray 013eb539d5 Change 'SEÑOR' to 'MR' in Spanish locale (#15594) 2026-08-21 17:27:11 -07:00
Jinwoo Hong da6b9d8065 fix(terminal): stop orphaning live agent terminals across host restarts and graph syncs (#15644) 2026-08-21 17:11:17 -07:00
Neil 080c95940f fix(ssh): let fork-PR worktrees add their contributor remote via the relay (#15827)
* fix(ssh): let fork-PR worktrees add their contributor remote via the relay

Creating a workspace from a fork PR on an SSH host failed with "Destructive
git remote operations are not allowed via exec". The relay's git.exec
allowlist blocked every `remote` write subcommand, but SSH fork-PR creation
has to run `git remote add <fork> <url>` on the host before it can fetch and
track the contributor's branch, so the whole create aborted.

Allow exactly the two shapes that flow needs -- `remote add <name> <url>` and
`remote remove <name>` -- validated with the same remote-name and URL rules
the relay already applies to every pushTarget-carrying RPC. Everything else
(set-url, rename, prune, extra operands, flags before the action) stays
blocked, and the URL must be a github.com clone/ssh URL, so no new reach is
granted beyond what push/fetch already accept.

`remote remove` was blocked too, which silently leaked fork remotes on SSH
hosts: worktree removal swallows the cleanup error. It works again now.

A host still running an older relay gets an actionable "reconnect to deploy
the latest relay" message instead of the raw policy error.

* test(git-exec): pin remote read/write mutation classification

Misclassifying `git remote` / `remote get-url` as mutating would flush the
relay and SSH provider git read caches on every remote probe, so pin both
directions.
2026-08-21 14:44:08 -07:00
Brennan Benson 3fca1d1648 fix(linear): unbound list-issues by default, surface truncation, bind cursor workspace (#15824)
Fixes STA-5076.

list-issues capped at 50 by default and hard-clamped at 250, with hasMore buried
under result.meta and no stderr warning for --json, so a page that stopped early
read as a complete answer. Omitting --limit now walks Linear's pages until they
run out (meta.limit is null), and --limit <n> is the only cap, paging past
Linear's 250-per-request maximum to reach it. result.truncated sits next to
result.issues and is set only when a cap actually held results back; human output
prints "truncated: showing N".

The read still has to fit the CLI's 60s RPC budget, so a 20s wall-clock deadline
and a 200-page ceiling stop the walk early and report truncated with a
continuation cursor rather than failing the command.

Also:
- issued --cursor values bind the resolved workspace, so call -> nextCursor ->
  call works without --workspace; raw Linear cursors still need one and now carry
  nextSteps
- issued cursors whose payload smuggles back `all` or an empty workspace are
  rejected at decode, since either would widen the read past the bound workspace
- JSON issue rows carry priorityLabel (none/urgent/high/medium/low), matching
  orca linear priority set
- truncated and priorityLabel are optional on the wire, so a host that predates
  either is not read as "complete"; readers fall back to meta.hasMore
- the truncation line prints the rows actually rendered, so a remote result with
  no meta.returned cannot print "showing undefined"
2026-08-21 14:28:55 -07:00
Jinwoo Hong 8462fa72da fix(rate-limits): support Codex 0.149 approval policy (#15823) 2026-08-21 14:21:53 -07:00
Jinwoo Hong 1ce2e562b3 fix(skills): isolate concurrent upload staging (#15693) 2026-08-21 13:15:46 -07:00
OrcaWinandBrennan Benson 2a68b78bb3 fix(worktree): let a configured worktree base outrank a built-in visibility source (#15232) (#15430)
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-21 12:20:04 -07:00
github-actions[bot] 4fd93ead19 Update README downloads badge 2026-08-21 12:27:48 +00:00
VincentandOrcaWin 59892a2f13 fix(terminal): preserve Shift+Enter during routing confirmation (#13598)
* fix(terminal): preserve Shift+Enter during routing confirmation

Keep previously trusted CSI-u routing active only while local foreground revalidation is pending. Clear pending authority on inconclusive reads and never promote display-only identity.\n\nRefs #12541\nRefs #13597

* fix(terminal): respect global WSL routing gate

* fix(terminal): bound the retained routing capability to a live read

Two holes in the previous commit let one provider read authorize CSI-u
indefinitely.

First, routingConfirmationPending satisfied its own precondition, so a
pending entry re-published itself on every reconfirmation request. Only
one of the five callers is the Shift+Enter burst timer; the others fire
on accepted submit/interrupt bytes, focus and visibility changes, and
onAgentExited -- the last of which runs precisely when a shell title
proves the agent is gone. On cmd.exe and Git Bash there is no OSC
boundary to publish over the entry, so nothing decayed it.

Second, the flag was published even when no confirmation read was
actually scheduled -- a hidden pane, a command read already in flight, or
a null pty id -- and only the read's inconclusive settle clears it.

Require routingTrusted to grant the capability, and publish the flag only
after sampling reports a read in flight, so it cannot outlive the read
that justifies it. This also makes the canConfirmRouting gate redundant:
the tracker already refuses WSL, SSH and remote pty ids.

* review round 2: bound the retained capability to any in-flight read

onVisiblePtyBound refuses to schedule while a higher-authority command
read owns the pane, so gating the pending publish on it mistook 'a
command-finished read already owns this' for 'no read at all' and
dropped CSI-u for >=350ms — the window #13598 exists to close.

Ask the tracker whether any read is in flight instead, and settle the
visible confirmation from the command-finished branch that publishes
nothing, so the flag cannot outlive the read that justifies it.

* review round 2: tighten the reconfirmation gate and its comments

hasReadInFlight already covers the visible-pty read that
visibleForegroundSamplePending tracked, so the disjunction was
redundant. Collapse the stacked Why blocks into one.

* review round 3: release the retained capability when a read is abandoned

Every outcome path now clears routingConfirmationPending, but the two
abort guards in readForeground return without publishing or settling. A
pty rebind during the multi-second inspection RPC — a detach/remount
emits no onExit, so the store entry survives — therefore stranded the
flag permanently, leaving Shift+Enter on CSI-u with nothing left to
revalidate it. Worse, once sampling is suppressed by a live hook row the
pane routes bytes on hook evidence alone, which the resolver excludes
precisely because PTY output can forge it.

Settle an abandoned read unless a newer generation will settle for it.

* review round 4: release the retained capability at every exit without a successor

Rounds 2 and 3 closed the outcome paths and the aborted-read paths, but
two review lanes independently proved a third: cancelPendingRead bumps
the generation, so a cancel never settles, and dispose plus the three
untrackable early returns schedule no successor to settle for them. The
store entry outlives the tracker — detach/remount emits no PTY exit — so
the flag latched there with no read left alive.

A visible remount self-heals, but a hidden pane does not: the dashboard
card resolves the encoding for any pane key regardless of visibility.

Release the capability from whichever exit ends the read, and say in the
entry's own doc comment that the flag is Shift+Enter-scoped.

* test(terminal): pin the remaining capability-release exits

dispose and the command-finished exit were covered; the visible-bind and
command-start variants are the same three-line pattern and were not.
Neutering the release now kills all four.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-20 21:08:41 -07:00
VincentandOrcaWin a0cc9f35ba fix(terminal): mark captured shortcut input interactive (#13800)
* fix(terminal): mark captured shortcut input interactive

Refresh the interactive-redraw timestamp after a captured shortcut send
succeeds, so the composer redraw that follows takes the low-latency
foreground path instead of waiting out the 1s coalesce fallback.

Orca's captured shortcut path sends directly through the captured pane
transport to preserve pane, PTY and transport identity, and so bypasses
xterm's onData -- the only place that previously stamped the timestamp.
An idle pane therefore scheduled the post-Shift+Enter redraw as ordinary
throughput. Measured in a real Pi pane on Windows: ~1029ms before,
~16-20ms after, at an identical ~2.3KB redraw.

Stale pane bindings and rejected transport sends cannot refresh it.

Fixes #10203
Refs #13598

* fix(terminal): keep captured shortcuts out of the pane-teardown signal

lastTerminalInputAt has two readers, and the previous commit only meant
to change one of them. onExit reads it as "the user never typed into
this pane" to keep a newborn pane mounted when its shell dies on startup
(the failing-.envrc direnv case, pty-connection.ts onExit) so the error
stays visible and the worktree stays active.

Stamping it from a captured shortcut therefore made a single Shift+Enter
before that exit close the tab and bounce the user to Landing -- on every
platform, since captured shortcuts are not Windows-only.

Split the redraw window onto its own timestamp so captured shortcuts open
the fast path without arming the teardown, and leave onExit's behaviour
byte-identical to main.

* test(terminal): pin the captured-shortcut wiring and its staleness guard

Deleting the onAccepted block, or replacing its binding-identity check
with true, both left the whole suite green — so nothing pinned the part
of this PR that actually ships. Cover both against the existing IME
keyboard harness.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-20 21:08:20 -07:00
ppw-stack df7460af41 fix(ui): keep dialog title and footer inside the panel (#15707)
DialogContent used an implicit grid column, which sizes to min-content: an
unbreakable token such as a long filename in the title widened the column past
the panel, so the title overflowed and the justify-end footer buttons rendered
outside the visible surface. Pin the column to minmax(0,1fr) and let long
titles wrap.
2026-08-20 19:34:31 -07:00
Jinwoo Hong a0578641e9 fix(ci): restrict release test token permissions (STA-4970) (#15675) 2026-08-20 19:14:13 -07:00
OrcaWin 8b60a1e458 fix(grok): stop the Windows status hook from spawning two interpreters per event (#15595) 2026-08-20 19:11:51 -07:00
OrcaWin 2c24025e1d fix(worktrees): honor absolute Linux worktree base paths for WSL repos (STA-4772) (#15384) 2026-08-20 19:10:28 -07:00
Jinwoo Hong 012e9f410c fix(runtime): recover adopted tui-idle and pin worker fixtures (#15569) 2026-08-20 15:51:48 -07:00
Jinjing 4e058d4a52 Fix flaky CI tests by adding retry logic and increasing timeouts (#15635)
* Fix flaky CI tests by adding retry logic and increasing timeouts

Add Electron launch retry for CI runners where startup wedges before
reaching 'ready', with fresh profile per attempt to avoid mid-init state.
Increase skill install lock timeout from 100ms to 5s to account for
fsync cost plus retry duration on loaded CI runners.

* shorten comments
2026-08-20 12:53:44 -07:00
JinjingandJinwoo-H 6e25a90085 fix(terminal): keep a quick command queued until its own spawn takes it (#15630)
* Increase shell readiness timeout to match daemon barrier

Slow interactive rc files can take longer than 1.5s to initialize. Raise
the startup command readiness timeout from 1.5s to 15s to match the daemon
barrier and prevent queued commands from executing mid-startup.

* fix(terminal): keep a quick command queued until its own spawn takes it (STA-4876)

Triggering a quick command opened a terminal tab titled with the command's
label, the shell started and drew its prompt, and the command never ran.

TerminalPane snapshots `pendingStartupByTabId[tabId]` in a useState lazy
initializer, and a mount effect deleted the entry immediately. The pane's key
is `${tab.id}-${tab.generation ?? 0}`, so anything that bumps generation before
the command reaches a shell — the stall-recovery remount fired from
`requestTerminalPaneRecovery`, or the allDead activation regeneration — mounted
a second pane that re-read an emptied slot and spawned with no command at all.
The loss was permanent, which is why every scope failed alike: repo, global and
agent-prompt all funnel through the same queue-then-snapshot sequence.

Spend the entry at `onPtySpawn` instead, which is the one point that proves this
pane's own fresh spawn exists. A pane retired mid-connect never reaches it, so
the command stays queued for the next mount; reattach skips `onPtySpawn`, so it
cannot spend a command it never delivers.

Three details are load-bearing:

- Ownership is reference identity (`paneOwnsQueuedStartup`). Setup and issue
  splits borrow the same `deps.startup` field for their own one-shot payload, and
  that payload can be structurally identical to the queued command, so a
  truthiness test would let a split pane spend a command it never runs.
- The consume runs after `bindActivePanePty`. While the tab still has no ptyId,
  the queued entry is the only thing holding its worktree out of the
  retention-budget force-park, so dropping it first unmounts the pane mid-spawn.
- The callback is one-shot. `onPtySpawn` fires on every fresh spawn a pane makes,
  including hibernation wake and the respawn ladder, and a command queued after
  the first launch belongs to that later launch.

Known residual, documented at the call site: the consume tracks "a pty exists",
not "the command ran". Windows embeds short commands in the shell argv, so they
execute before the spawn resolves and a pane retired in that window re-delivers
on remount; on POSIX the write waits for shell-ready, so a pty that dies in that
window loses a command already spent. Closing either needs a delivery signal
from main rather than this callback. Both windows are narrow, and both are
strictly better than losing the command unconditionally.

* fix(terminal): guard the queued-startup wiring the review found untested

Follow-ups from the final review pass on this branch.

- Collapse the ownership + one-shot decision into `createQueuedStartupConsumer`
  so the call site is a single call rather than inline logic no test could
  reach. Two mutants survived the whole suite before this: relaxing ownership to
  a truthiness check, and dropping the one-shot guard. Both now fail.
- Rewrite the throwing-consume test. It asserted `updateTabPtyId` had been
  called, which runs *before* the callback, so it passed with the try/catch
  deleted. It now asserts the throw does not escape into the connect promise,
  which is the invariant the try/catch actually provides.
- Correct the `onQueuedStartupSpawned` docblock. It claimed the callback is "the
  first moment the command is guaranteed to reach a shell"; the diff's own caveat
  says otherwise, since Windows runs an argv-embedded command before this fires
  and a POSIX shell can die before the shell-ready write. It marks a live shell,
  not delivery.

No behavior change: the consumer is the same predicate and the same one-shot,
moved behind one exported seam.

* fix(terminal): roll shell wrapper isolation into a fresh daemon

* Revert "Increase shell readiness timeout to match daemon barrier"

This reverts commit 6ab273ef36.

* Condense queued startup spawn comment

Simplify the multi-paragraph explanation into a concise summary that captures the key points: spawn must wait until after the pane is bound to preserve the worktree from force-parking, and the behavior differs between POSIX and Windows for delivery timing.

* fix(terminal): prevent consuming replaced queued startup commands

When a queued startup is replaced before the pane's first spawn,
the one-shot guard alone still allows consuming the replacement.
Add isStillQueued callback to verify the slot still holds the
originally captured command (STA-4876).

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
2026-08-20 12:15:08 -07:00
e41074cb1f docs(readme): add Android install guide link to README.ko.md (STA-4817) (#15566)
Taken from #15396. Matches the English README's install-guide link added in #14978.

Fixes #15395

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: erishforG <eric.signal@kakaocorp.com>
2026-08-20 10:45:08 -07:00
Jinjing d8e9fa1bb9 Revert "fix(terminal): apply pane padding on all four edges (#15544)" (#15623)
This reverts commit 4b2ed5ddd4.
2026-08-20 09:57:55 -07:00
github-actions[bot] 0f26ff4ad8 Update README downloads badge 2026-08-20 12:28:13 +00:00
Neil feaebabf2c test(terminal): pin the two conditions that keep macOS period substitution out (#15393)
macOS rewrites a double space into ". " and hands the period to the pty.
Orca is not immune to that: a plain Chromium textarea in the Electron
version pinned here does substitute, measured on hardware with real key
events, and spellcheck="false" does not prevent it. What prevents it is
that the forwarder claims a plain space keydown and then empties the
helper textarea, so the text system never sees the word preceding the
space.

Neither half was a decision. Before 01bcc8dca2 the claim predicate
excluded space, letters and digits, the field accumulated, and the
substitution fired on real hardware. That commit widened the predicate
for unrelated reasons - IME commit survival and kitty encoding - and
suppressed this as a side effect it never mentions. The predicate
already declines on modifiers and during composition, so a later
narrowing would return the bug with nothing to catch it.

This does not test the substitution, which a unit environment cannot
produce. It tests the two conditions measured to suppress it. Verified
non-vacuous: narrowing the predicate back toward punctuation fails three
of the four, and removing the blanking fails the fourth.

Refs #11504
2026-08-20 03:08:29 -07:00
NeilandBrennan c92f394cde fix(pty): delete the reply-withholding scheduler (#15578)
* fix(pty): answer a terminal colour query in its own turn

Root-cause follow-up to #15559, which stopped a CPR overtaking a deferred
colour reply but left the deferral itself in place.

Orca answers terminal queries by writing to the PTY master, which a line
discipline in ECHO copies straight back out as junk on a cooked prompt
(#12112). The guard was to withhold the write until an `stty` subprocess
proved ECHO clear — and forking is what forced the decision to be async.
Any deferral, however short, lets a reply written later in the same turn
overtake this one, so the async probe was the bug's root cause.

Read the bit synchronously instead. Linux and the BSDs redirect a
master's mode ioctls to the slave, so a `tcgetattr` on the master fd
node-pty already owns answers for the slave with no fork: measured 0.26us
against 2403us for the subprocess. With a verdict available inline, a
querying program that already cleared ECHO — every raw-mode prober,
including the colour probe behind the `gh auth login` report — is
answered in its own turn and can never be reordered.

The deferral stays for the genuinely cooked case, and the ordering
guarantee stays underneath it: hosts whose node-pty predates this patch
get no sync probe and fall back to the deferred path, which mixed
client/host versions make a live production path.

Reply routing is all-or-nothing: a payload needing neither containment
nor ordering stays on the host's own path, so a CPR answered during shell
startup cannot pass the daemon's post-ready flush gate and splice into
the buffered startup command.

Native side is fail-safe: a kernel that did not redirect would answer
from the master's own termios, whose ECHO defaults set, so the degraded
verdict is "echoing" — never a false "quiet". The JS half ships in the
pnpm patch while the binding needs a source build, so
ORCA_REQUIRE_NODE_PTY_ECHO_STATE=1 makes CI fail rather than silently
skip when it is handed an upstream prebuild.

Co-authored-by: Brennan <brennanb2025@users.noreply.github.com>

* fix(pty): keep the flush ordered under synchronous re-entry

Three defects found in external review of the reply-ordering work.

node-pty delivers onData inside the master write, so a query can be
answered while the queue is mid-flush. `flushPendingWrites` spliced the
array off before writing, so that reply saw an empty queue, took the
same-turn path, and landed ahead of entries the loop had not written yet
— reproduced as 01, 99, 02, 03. It now shifts one entry at a time so a
re-entrant reply queues behind the rest, bounded by the length at entry
so a re-entrant push cannot spin the loop.

An overflow flush can re-enter as far as teardown. `answer` did not
re-check `closed` afterwards, so it queued behind a closed delivery,
returned true, and the reply was never written and never reported.

The payload router's ownership comment overstated its guarantee. The
`any` semantics are deliberate — returning false after a constituent was
already written would have the caller re-write the whole payload and
duplicate it into the child's stdin — so the residual mixed-failure drop
is now documented rather than implied away.

* fix(pty): delete the reply-withholding scheduler

Orca answered a terminal query by withholding the write until a probe
proved the slave's ECHO bit was clear. That was the wrong mechanism, and
it is now gone: replies are written in the caller's turn and their echo
is contained on the output side, where it always was.

Withholding never removed an echo. The wait was bounded and always ended
in a write, so the output-side projections were doing the work the whole
time — including the readline rewrite, which happens with the tty already
raw and which therefore no reading of the ECHO bit can predict. What
withholding did add was an asynchronous write path, and that is what let
one reply overtake another and land in the next program's stdin (#15559),
what produced a re-entrancy inversion inside its own flush, and what four
rounds of regressions have lived in.

The last thing it covered was the verbatim echo of a `stty -echoctl` tty.
That shape is now projected directly. It starts with ESC, so it is
matched only when complete and never held as a partial: holding it would
take a bare trailing ESC from the query parser and an expired hold would
release it raw, so a query torn at its own ESC would never be answered.
Complete-match-only is what makes the shape safe to project at all.

Measured on a real pty: a cooked-mode master write is both echoed AND
delivered — ECHO copies the bytes without consuming them from the slave's
input queue, so a program arming raw mode with TCSANOW/TCSADRAIN (libuv's
setRawMode, hence every Node agent) still reads them. Only a TCSAFLUSH
switcher discards it, which it does on every terminal, none of which
gates a reply on termios state.

Deletes the pending-write queue, the async stty probe, the poll budget
and probe rate limit, the deadline-driven flush, and the answer/
answerInOrder split. Replies now leave in call order by construction.
No packaging, native or CI surface is touched.

* test(pty): restore stty-probe coverage and pin the duplicate-query retry

Archaeology on how withholding got here, and what its tests were really
protecting.

Deleting the ECHO probe took four tests with it that were not about the
probe at all: they cover createSttyProbe, which the shell-readiness
line-editor probe still uses — in-flight sharing, the per-platform stty
flag, and transient-versus-permanent failure latching. Restored against
the line-editor probe, which is now their only caller.

Also pins the property that answers the one case an immediate write
cannot serve. A program that queries while cooked and then arms raw mode
with TCSAFLUSH discards the reply with the rest of its input queue.
Nothing can prevent that from the terminal side, and no terminal tries.
What matters is that such a program re-queries after its own timeout: the
ingress declines to answer an already-answered slot but forwards the
duplicate downstream, so the renderer's emulator answers the retry, by
which point the program is raw. The retry path is the recovery, not
withholding.

* ci(pty): keep the fish real-PTY test in the shell-contracts lane only

Reverting pr.yml to main dropped the exclusion for the fish query-reply
test, which this branch keeps, so it would have run in the sharded lane
as well. Restores it to the shell-contracts include list and the shard
exclude list, and drops the parallelism expectations for the deleted
cooked-querier suite and the echo-state env guard.

---------

Co-authored-by: Brennan <brennanb2025@users.noreply.github.com>
2026-08-20 02:15:42 -07:00
Brennan Benson 21b66197be fix(worktrees): cover WSL distro history and bound the retirement backfill scan (STA-4472, STA-4473) (#14924)
* fix(worktrees): cover WSL distro history and bound the retirement backfill scan (STA-4472, STA-4473)

* fix(worktrees): bound outstanding retirement backfill listings, not just their rate

The scan deadline abandons a listing, it cannot cancel one: an unabortable readdir
keeps its libuv threadpool thread until the OS releases it. The failure backoff paced
retries but never counted the abandoned calls, so a mount that stayed wedged stacked a
new stuck thread on every lapse until the four-thread pool starved every other
filesystem user in the process. UNC reads were already bounded by the WSL gate's
permit accounting; plain SMB/NFS listings were not.

Cap the outstanding listings process-wide and serve the memoized failure once the cap
is reached. Recovery is preserved: a slot frees as soon as an abandoned listing settles.

* fix(worktrees): keep a late retirement listing, and stop deferrals penalising healthy repos

Three review findings on the scan bound:

- A listing that landed after the 15s deadline had its result discarded. Under the WSL gate
  that is the common case, not an edge one: the gate admits a single scan at a time and allows
  it 60s, four times this deadline. The namespace was then left unseeded on exactly the mounts
  this feature exists to cover. The answer is now kept when it arrives.
- A namespace deferred at the outstanding-listing cap never touched the wedged mount, so arming
  its backoff spread one bad mount's outage to repos on healthy disks. Deferrals no longer
  memoize; the next create retries as soon as a slot frees.
- The cap comment claimed it bounds threadpool starvation. It bounds this module's share only;
  the WSL gate and its close lane hold threads of their own. Comment corrected.

Also stub the WSL resolver in the UNC-root unit test, which otherwise shells out to wsl.exe and
boots the developer's distro on a Windows runner.

* fix(worktrees): only count stuck retirement listings, and fence the backoff on a monotonic clock

Two more review findings on the outstanding-listing cap:

- The cap counted every in-flight listing, not just the stuck ones, so it fired on healthy
  machines. Nested workspaces give each repo its own scan key, so a few first-time backfills are
  routinely in flight together; the third was rejected and its create then picked a name against
  an unseeded registry. Only listings that have outlived their deadline occupy a slot now, which
  is what the cap was always meant to bound — healthy scans finish in milliseconds.
- The backoff fence compared wall-clock times. The WSL gate this scan runs under deliberately
  avoids wall time for exactly this ('would misjudge stuckness across laptop sleep or NTP steps');
  a backward step pinned a namespace in its failure memo for the size of the step. Now monotonic.

A third finding — that a late listing writing into an entry a retry has replaced loses the answer —
was investigated and refuted. An entry is only ever read back through the map, and callers hold the
promise rather than the entry, so a write to a replaced entry cannot be observed. No guard added:
the test for it passed with and without one.

* fix(worktrees): gate retirement rescans per namespace, and keep a partial answer usable

Replaces the process-wide listing budget with a per-namespace rule, and stops a refused source
throwing away the sources that did read.

- A global budget was the wrong shape: one wedged mount spends it on its own retries (lapse,
  restack, lapse) and then every other namespace — including repos on healthy local disks — is
  refused for the process lifetime, which is a strictly larger blast radius than the wedge it
  replaced. A namespace now simply may not start a second listing while its own is still stuck,
  so a bad mount costs exactly one thread and nothing else is affected.
- Rethrowing a gate refusal abandoned the whole scan at the first source. For a WSL repo the UNC
  workspace root is listed first, so a stuck 9P route also discarded the plain, readable
  Windows-side bucket scan that needs no distro access — worse than the behaviour before the
  split. Discovery now returns what it read plus a "complete" flag; the names are used, and only
  the memoization is withheld so the hole is retried.
- An I/O failure was reported as a complete empty listing, so a transient EIO on a redirected or
  network home memoized "nothing is retired" for the process lifetime. Only ENOENT/ENOTDIR now
  count as a complete answer.

The recovery tests now release the stalled listing rather than leaving it pending forever: a
retry while the previous call is still stuck is precisely what stacks threads.

* fix(worktrees): keep the scan retryable when a WSL distro home will not resolve

Resolving a distro home shells out to wsl.exe, which returns nothing for a stopped or slow distro
(the call has a 5s timeout) or when wsl.exe is not resolvable from the Electron process. That case
dropped the distro bucket source silently and still reported the scan complete, so the empty answer
was memoized for the whole process lifetime.

That is the STA-4472 defect re-entering through the back door: the distro is exactly where a WSL
workspace's agent history lives, so a workspace whose directory is gone leaves its only surviving
evidence unread, and the next generated create reissues that cwd. It does not self-heal either —
the scan key is derived from the probe path, which is unchanged by a failed home resolution.

An unresolved distro now marks the scan incomplete, which serves the names that were found while
leaving the hole to be retried after the backoff.

* fix(worktrees): trace a WSL repo's Windows-side workspaces into the distro too

Distro discovery keyed only on the workspace root being a UNC path, but a WSL repo can legitimately
own workspaces under C:\. computeWorkspaceRootAsync mirrors the workspace dir into the distro only
when the distro home resolves at create time; when that wsl.exe call fails it falls back to the
drive path, and those workspaces stay on the Windows side.

The agent is still spawned through wsl.exe, so its cwd is the drvfs mirror (/mnt/c/...) and its
bucket lands in the distro's own ~/.claude/projects, where the host-home scan cannot see it. The
scan then reported complete and memoized the empty answer, so the name was reissued and the next
occupant inherited the previous conversation — the STA-4472 defect, in the one configuration the
UNC check does not cover.

Which distro to look in comes from the repo path rather than the workspace root, since that is what
still identifies the distro once the root is a drive path.

* revert drvfs-mirror discovery, and pin the "no agent state" classification

Reverts the previous commit. The drvfs branch traced a WSL repo's Windows-side workspaces into the
distro, but its production wiring cannot be pinned: the distro comes from parseWslPath(repo.path),
which short-circuits off win32, so no assertion on a Linux or macOS runner can reach it — deleting
the wiring line left every test green. Shipping an unpinnable branch is the exact unreached-module
shape this PR exists to close, and it is not worth it here: the branch only pays off in a narrow
race where getWslHomeAsync fails while the workspace root is computed and then succeeds seconds
later during discovery. Whenever the distro home resolves, the root is UNC and the existing path
already covers it; whenever it does not, the scan is already reported incomplete and retried.

Also adds the missing guard on the other side of the same classification: ENOENT and ENOTDIR mean
"no agent state on this machine", which is a complete answer. That is the common case for a fresh
or Codex-only install, and misclassifying it as incomplete would turn the one-time seed into a
60s-interval rescan for the life of the process. The expression had no test; it does now.

* test(worktrees): make the retirement backoff window a real assertion

The test that claimed to cover it settled the stalled listing and re-entered in the same tick, so
outstanding cleared only in a later microtask and the no-restack rule answered first. It was a duplicate
of the test above it, and the backoff clause it was meant to pin had no coverage at all: deleting
the clause left all twelve tests in both files green, so a regression that re-probes a wedged mount
on every generated create would have shipped.

Flush the microtask so the listing is genuinely settled, then assert both directions — the memo
still serves the failure inside the window, and the same call succeeds once the window lapses.

* fix(worktrees): stop trusting a UNC ENOENT, which is what a shut-down distro looks like

Windows reports an unreachable 9P route as ENOENT, so a distro that has merely been shut down is
indistinguishable from one that never held any buckets. wsl.ts already refuses to trust a UNC
ENOENT for the same reason, probing inside the distro instead.

The classification added earlier called ENOENT a complete answer, which is right for a local home
that simply has no agent state but wrong here. After a wsl --shutdown the cached distro home still
resolves, so nothing else marked the scan incomplete: the empty result was memoized for the whole
process lifetime and every later generated create in that namespace reissued names spent inside
the distro. That is STA-4472 again, reached by a different route.

ENOENT now only means "absent" off UNC.

* fix(worktrees): tell an absent distro directory apart from an unreachable 9P route

Distrusting every UNC ENOENT fixed the shut-down-distro hole but overshot: a distro where nobody
has run Claude genuinely has no ~/.claude/projects, which is the common case for Codex-only users
and for anyone running agents from the Windows side. Those namespaces could never report a
complete answer, so the one-time seed became a full rescan every 60s for the life of the process —
each one re-spawning wsl.exe and taking the single process-wide scan slot from transcript
discovery, on a path that runs on every composer open rather than only at create.

Probe the parent instead. If it lists, the child really is absent and the answer is complete; if it
does not, the route is down and the scan stays retryable. Both directions are pinned: reverting to
either of the previous behaviours turns a test red.

* fix(worktrees): walk up to a reachable ancestor, not just one level

The reachability probe checked a single parent, which only disambiguates when ~/.claude exists but
~/.claude/projects does not. The far more common shapes have the ancestors missing too: a distro
where Claude has never run has no ~/.claude at all, and a repo with no workspaces yet has neither
the workspace root nor its parent. In both, the one-level probe also got ENOENT and called the
route unreachable, which is exactly the 60s rescan loop it was added to prevent.

Walk up until a listing succeeds, bounded so a pathological path cannot hold the scan slot. One
reachable ancestor proves the route is up, so the ENOENT below it is real absence.

The test that was supposed to guard this had ~/.claude resolving, so the real shape was never
exercised — which is why the defect shipped green. Its fixture now leaves the whole chain absent
up to the distro home, and reverting to the one-level probe turns it red.

* docs(worktrees): record what gating retirement listings costs

The shared WSL filesystem gate admits one scan task process-wide, and its stuck-task check matches
scan against scan regardless of route. So retirement discovery now queues with — and on a wedged
distro can fast-fail — native-chat transcript discovery, which the ungated readdir it replaced
never could.

Gating is still right: the gate holds the only deadline and permit accounting these UNC reads get,
and without it a hung 9P route keeps a libuv thread outright. A dedicated lane would need a third
priority, which is a change to the gate rather than to this file. Writing the trade-off down so the
next reader does not have to rediscover it.

* perf(worktrees): probe reachability with stat, and record that the gate coupling runs both ways

The ancestor probe only asks whether a directory is there, but it listed it — enumerating a WSL
home over 9P, on the composer-open path, holding the single scan permit while it did. stat answers
the same question; the gate already supports the operation.

Also corrects the trade-off note added last commit, which recorded only the direction where
retirement discovery is the victim. Because the gate stuck-check matches scan against scan
regardless of route, the reverse is now true too and is the part this PR introduces: a retirement
listing wedged on one distro can fast-fail transcript discovery on a healthy one.

* fix(worktrees): drop imports the discovery extraction left unused

The rebase onto main kept main's import block, which still pulled readdir and
homedir for discovery code this branch moved into worktree-retirement-discovery.ts.

* fix(worktrees): drop the last import the discovery extraction left unused
2026-08-20 01:45:17 -07:00
Brennan Benson 3e079debec fix(sidebar): host-qualify discovery notice rows on multi-host projects (#15546)
* fix(sidebar): host-qualify discovery notice rows and collapse one checkout's twins

A project checked out on several hosts emits one discovery-notice row per
checkout, and those rows only named the project. A sidebar with paired remote
hosts therefore showed several identical "N hidden worktrees" buttons under one
project header, with no way to tell which machine each belonged to — or that
one of them was another machine's worktree inbox entirely.

Two causes, both fixed here:

- Notice rows carried no host context, unlike worktree rows, which have been
  host-labelled since STA-4343. Both notice rows now take a host label, applied
  per project (not per rendered section, since a card can land in the pinned
  fallback) and only when that project spans hosts. The label also lands in the
  review, expand, and dismiss accessible names, so the actions that write to a
  specific host's repo record say which host that is.

- One machine registered as a direct SSH target *and* paired as a runtime
  environment gives a single on-disk checkout two repo records with independent
  hidden-worktree state, so it emitted two rows for one directory. Repos now
  resolve to a (hostname, path) checkout key, and twins collapse to the record
  this client persists itself — its visibility state is the user's own and
  survives the paired runtime going away.

The key is deliberately conservative: an unresolved hostname, or a tunnelled
environment answering on loopback, yields no key and never collapses anything.
Renderer-only; no wire or persistence change.

* fix(sidebar): drop the machine-identity collapse, gate notice labels on host ids

Replaces this branch's second change after a plan review found it has no
precedent and eight concrete failure modes.

Deleted: the (hostname, path) checkout key that collapsed two repo records
believed to be one machine. Orca models a direct SSH target and a paired
runtime environment as different execution hosts everywhere else; that change
asserted sameness by resolving strings a user typed in two places. It also
dropped rows (a differing count vanished with the shadowed record), flipped
with the sidebar host filter, ignored port and user so a host and a container
on it could merge, tie-broke on repo-store order, was disabled in the one case
Orca can prove (a tunnelled pairing answers on loopback) and fired only on
coincidence, and left the visibility dialog showing state the sidebar had
hidden. Its module also carried a literal NUL byte, so git classified the file
as binary and the diff was unreviewable.

Kept, with two corrections: notice rows still carry a host label, but the gate
now counts distinct host ids rather than distinct label strings — two hosts
sharing one user-facing label is exactly when the rows are hardest to tell
apart — and membership is read from the unfiltered repo universe rather than
the host-filtered notice candidates, so a label no longer appears and
disappears with the filter.

Two hosts that share a label still render the same label. Disambiguating that
is a shared concern across worktree badges, host headers, and host-filter
options, and needs its own design; three verification passes each found a
different hole in doing it here. Follow-ups: general host-label collision, and
the repo-record duplication that produces the twin rows in the first place.

* fix(i18n): catalog notice host scope copy

* feat(sidebar): show each notice row's host with the project-on-host glyph

Notice rows on a multi-host project already carried a host label, but two
hosts can share one user-facing name, and the label truncates first in a
narrow sidebar. Each row now also carries its host's glyph.

Deliberately the same indicator worktree cards use (worktree-card-header):
a Server glyph, ServerOff when a paired runtime has no live status, and a
"Project on ..." tooltip naming the host — SSH and runtime keep their
distinct tooltip wording. Local hosts draw nothing, as on the cards.

The glyph is shrink-0, so unlike the text label it survives the sidebar
narrowing, and the row keeps an identifying mark either way.

Rows now carry the host id alongside the label, since the label alone cannot
select a glyph or its tooltip. Catalog entries for the new copy ship with the
change rather than relying on inline fallbacks.

* refactor(sidebar): draw notice-row hosts with the shared host glyph

Follow-up to the notice-row host indicator: use the one glyph vocabulary the
app already has instead of a second copy of it.

HostRowIcon — a monitor for this computer, a server for anything remote — was
private to the composer's run-target rows. Moved to a shared home and reused,
so the sidebar and the composer cannot drift apart. The run-target module
re-exports it, leaving its own call sites untouched.

Every notice row now gets a glyph, local included, so no row is the odd one
out; the tooltip still names the host and says when a paired runtime has no
live status. Same size and tone tokens across kinds, so no row reads as
decorated relative to its neighbours.

* fix(sidebar): make notice host glyphs accessible
2026-08-20 01:38:28 -07:00
Brennan Benson 4b2ed5ddd4 fix(terminal): apply pane padding on all four edges (#15544)
* fix(terminal): apply pane padding on all four edges

Move the configured inset onto xterm so the terminal fills its pane while the fit calculation accounts for both sides of each axis. Add a geometry golden that forces cell remainders and verifies dynamic padding without relying on renderer pixels.

* fix(terminal): normalize imported padding for fitting

* fix(terminal): align stored and fitted padding
2026-08-20 01:07:56 -07:00
Brennan Benson 5ca747dad0 docs(ssh): state the SSH execution boundary and pin the liveness vocabulary (#14971)
* docs(ssh): state the SSH execution boundary and pin the liveness vocabulary

Nothing under docs/ described how work splits between the client and an SSH
host, so agents and humans inferred it from error strings and got it wrong:
loss of contact was repeatedly reported as process death, which orphaned live
remote agents and cold-started duplicates over the same worktree.

Pins the vocabulary to the incumbent live/unverifiable/exited verdict from
unstopped-pty-verification so no synonym is introduced, records the one real
discriminator (all of a host's terminals drop together on link loss; one alone
means process exit), and lists the outstanding gaps with citations.

Tracked via the docs allow-list and linked from AGENTS.md, per the convention
in .gitignore.

* docs(ssh): cite the live restoreRequired site after it moved

The throw now lives in reattachSshPtySessionForSpawn; ssh-pty-provider.ts no
longer contains it. Caught by the worker fixing it, against a newer main than
the audit ran on.

* docs(ssh): require host evidence for liveness verdicts

* docs(ssh): keep boundary references stable

* docs(ssh): fence liveness evidence to its host identity

* docs(ssh): state replay and environment boundaries precisely

* docs(ssh): correct replay and platform boundary claims

* docs(ssh): describe headless runtime continuity accurately

* docs(ssh): distinguish authority from client metadata

* docs(ssh): describe pending fixes accurately

* docs(ssh): date the gap list and name the PR that closes each entry

The Known gaps section was accurate when written and becomes actively
misleading as its fixes land: it told a reader to go fix restoreRequired,
the missing unverifiable verdict, and the absent terminal-list host field,
three things now addressed by #14974, #14977 and #14973.

Mark the section as dated, require verification against current code before
acting on any entry, name the PR per entry, and move landed items out. Also
correct the two body claims that the landed fixes invalidated. The rules
above are durable; only this section rots.

* docs(ssh): make the boundary doc a durable ruleset, not an incident record

The Known gaps section was 18 of 93 lines enumerating specific defects from
one investigation, several already fixed by sibling PRs in the same batch. A
reference doc that needs a 'this section rots' warning is telling you the
section belongs somewhere else; those entries belong in issues.

Replace the six-row table of currently-lying signals with the method that
outlands any particular bug: ask whether the owning host produced the signal,
whether every PTY on the target went quiet together, whether the termination
event matches the current incarnation and generation, and whether a returned
status is actually a claim. Same for artifacts - state what ls-remote and a PR
head each do and do not prove, rather than listing which command is currently
wrong.

Nothing here goes stale when the open fixes land.
2026-08-20 00:47:22 -07:00
Brennan Benson 2e895da937 fix(browser): name the requesting frame and the permission in denial notices (#15542)
* fix(browser): name the requesting frame and the permission in denial notices

Two defects in the same notice, both found by the review of #15481 and left out
of it deliberately.

The notice named the wrong site. setPermissionRequestHandler passed
webContents.getURL(), which is the top-level document, so a permission request
from a cross-origin sub-frame was attributed to the embedder. Every
PermissionRequest variant carries requestingUrl, so all three call sites now use
it and fall back to the top-level document only when it is absent.

The notice also showed raw Chromium permission names. humanizePermission mapped
two permissions and returned the raw token for the rest. That now matters more:
#15481 granted ordinary storage-access and left top-level-storage-access denied,
making it the storage denial a user can still hit - rendered as its raw token.

The default still returns the raw token. Inventing prose for a permission nobody
has seen is worse than showing its real name.

Does not change any permission verdict, and does not fix Google sign-in (#15221).

* fix(browser): keep permission denial attribution accurate

Capture fallback URLs before asynchronous media permission handling and treat opaque requesters as unknown rather than blaming the top-level page. Clarify permission descriptions and cover origin normalization, navigation races, and mapped copy.
2026-08-20 00:22:59 -07:00
OrcaWin 471bc9d8ce Ship the WSL transcript helper with the Windows relay (STA-4831) (#15529) 2026-08-20 00:16:04 -07:00
Neil 9d06b3ba93 ci: stop docs-only commits from starting the skill-roundtrip matrix (#15474)
A cancelled Skill update round trip on a README merge painted main red
because push to main had no path filter. Share the PR path list on
push, and skip expensive PR Checks when every changed file is docs.
2026-08-19 23:33:05 -07:00
Brennan Benson 9a898a84cc fix(terminal): keep xterm's render pause latched for a pane with no layout box (#15555)
* fix(terminal): keep xterm's render pause latched for a pane with no layout box

resetWebglTextureAtlas() released xterm's paused-render gate for every pane of
a visible manager, including panes that are display:none (a collapsed sibling
of an expanded pane, a restore that stays display:none for its whole reattach).

forceRepaintThroughRenderPause exists for a pane that is already DOM-visible
while xterm's IntersectionObserver lags a frame. On a pane with no box it
paints the freshly cleared render model into nothing, and because the observer
only fires on a state change it never re-pauses the service. It also clears
_needsFullRefresh, which is the only thing that makes _handleIntersectionChange
repaint on reveal and flush the deferred _pausedResizeTask.

Latch instead for those panes: terminal.refresh() re-arms _needsFullRefresh and
xterm repaints from it on reveal.

* test(terminal): type the render-service repaint mock
2026-08-19 23:25:47 -07:00
Neil 4daace6251 fix(tabs): stop split workspaces multiplying columns on return (#15482) 2026-08-19 22:54:03 -07:00
Neil ef096d539d fix(terminal): refuse a cursor on a screen read, and correct the source docs (#15563)
Review follow-up on #15380.

The RPC accepted `cursor` and `screen` together. The CLI refuses the pair, but
terminal.read is reachable without it, and honoring both answered with rendered
lines carrying the stream's pagination metadata — two frames of reference in one
payload, which is the confusion `source` exists to remove. The guard beside it,
withVisibleSnapshotFallback, already declines to substitute rendered lines when
a cursor is present; the screen path now agrees, at the RPC boundary where every
remote caller passes. Nothing could previously send both, since `screen` did not
exist, so rejecting breaks no existing caller.

The command notes and the runtime comment both still described the fallback as
`source: stream`, left over from renaming that value to `screen-unavailable`
during implementation. The spec text is surfaced through `orca help` and the
agent-context schema, so a caller following it would test for a value the code
never emits. Both now describe all four states, including that an absent source
means the host predates the field.
2026-08-19 22:42:39 -07:00
Jinjing acbcb477a1 Auto e2e tests autofix scheduled ci 1h run 1 20260818T2143 (#15379)
* fix: update E2E tests for API changes and selector robustness

- Improve source control file locator specificity to avoid flakiness
- Fix board test to use correct worktree ID attribute
- Update removeWorktree calls to pass host ID parameter
- Simplify git status polling with timeout expectation

* fix: increase packaged-watchdog launch timeout and await git-status rows

Extract hardcoded 15s launch timeout to a 30s constant for better reliability under load. E2E test now waits for all git-status rows to render before asserting absence of status messages, preventing flaky passes when the list is still loading.
2026-08-19 22:39:48 -07:00
Neil c72a4eecdd refactor(shell): collapse the zsh wrapper to one .zshenv and a precmd hook (STA-4786) (#15391)
* refactor(shell): collapse the zsh wrapper to one .zshenv and a precmd hook (STA-4786)

Orca needs to run code after the user's own zsh startup files. It bought that by
keeping ZDOTDIR pointed at its own wrapper dir for the whole of startup and
sourcing each user file by hand -- four generated files per transport, with a
fake ZDOTDIR live while /etc/zshrc ran. That single decision is the root of a
whole bug family:

- /etc/zshrc assigns HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history unconditionally, so
  history landed inside Orca's own dir (#11044), and an epilogue had to repair it.
- zsh's sourcehome() ignores ZDOTDIR once the shell is in sh/ksh emulation, so a
  user .zshenv or .zprofile ending in `emulate sh` hid every later wrapper file.
  The emulation degrade blocks and their forked $(emulate) probes exist for that.
- One wrapper dir shared by two installed builds could mix files from both, so
  every generated file had to redefine the helpers it called.
- The baked generation-time ZDOTDIR literal is unusable when a Windows-generated
  wrapper is sourced inside WSL via /mnt/c (#8003), so the runtime path had to be
  re-derived from %x.

The wrapper now hands ZDOTDIR back on its first lines and defers Orca's work to a
precmd hook that runs at the first prompt -- after .zprofile, /etc/zshrc, .zshrc
and .zlogin, every one of which zsh reads from the user's own directory exactly
as in an unwrapped shell. Each bug above stops being reachable rather than being
repaired, and their machinery goes with them: eight of thirteen exported blocks in
shell-templates.ts, both drifted discovery bodies (unifying them closes the
"reconciling the two is a follow-up" note the file carried), and the relay's
separate ORCA_USER_ZDOTDIR shape. Generated zsh drops from 819 lines across
twelve files to 143 in three.

Two things the design has to get right, both found by running it rather than
reasoning about it:

- Every function is defined ABOVE the source of the user's .zshenv. A user file
  ending in `emulate sh` puts the rest of the wrapper under sh parsing rules, and
  the first prototype died there with `parse error near '\n'` -- silently, leaving
  the pane unwrapped. Function bodies are parsed at definition time.
- ORCA_ORIG_ZDOTDIR is vetted, not trusted. The launch config only sets it when it
  resolved a usable dir, but a pane inherits its parent's environment too, so a
  stale value from an older build can arrive on its own and would point ZDOTDIR
  back at a wrapper dir. The ownership check Node applies now also runs in the
  shell, where that route is visible.

Orca also stops inventing a ZDOTDIR: where the user has none, ORCA_ORIG_ZDOTDIR is
absent and the pane ends with ZDOTDIR unset, as an unwrapped login zsh does.

Verified on real zsh over a real PTY -- necessary, because a precmd hook never runs
in a shell started with -c, so the existing `zsh -i -c` probes could not have
exercised this design at all. src/main/zsh-startup-hook-pty-harness.ts drives the
shell to a prompt and reports through a file rather than stdout, which a PTY echoes.

* test(shell): cover the relay variant of the zsh hook in a real shell

The relay writes its own variant -- no OSC 133, remote CLI bin dir instead of the
agent-teams shim -- and it had no live coverage. It used to carry a second ZDOTDIR
shape as well, which is how it drifted from the desktop template in the first
place; now the spec flags are the only difference, and this pins that.

* fix(shell): rebase the single-file hook onto content-addressed wrapper trees

#15285 landed content-addressed wrapper roots and a per-transport fileset module
while this branch was in flight. The fileset modules are now the single place the
tree is described, so 'only .zshenv' is stated once per transport and the
required-paths check follows from it rather than repeating the list.

* test(shell): point the mixed-build proof at the relay, the one fixed wrapper path

#15285 content-addressed the desktop and daemon trees, so two builds can no
longer write the same directory there and the scenario this file covers became
unreachable on those paths. The relay still writes a fixed ~/.orca-relay/
shell-ready, so that is where the hazard survives and where the proof belongs.

* fix(test): make the zsh PTY harness survive a startup that stops to ask

Two CI-only failures, both from driving a real PTY where the old probes drove a
pipe:

- A host whose global zshrc runs `compinit` over directories it considers
  insecure stops startup and ASKS. A pipe-backed `zsh -i -c` never saw the
  question; a PTY sits at it until the timeout. The harness now answers it.
  ZSH_DISABLE_COMPFIX does not help -- that is an oh-my-zsh convention and plain
  compinit ignores it, which I confirmed by reproducing the prompt locally.
- The PS1 line was typed at t=0, so on such a host the question consumed it as
  its answer. The harness now waits for the shell to fall quiet first, which
  also stops a slow prompt framework racing the same write.

Also merges a duplicate vitest import the native code-quality audit flagged.

* fix(test): stop the live-shell assertions assuming macOS host behaviour

Two of them hardcoded what my machine does rather than what Orca owes:

- LINEINIT was pinned to 'none'. A host whose global zsh config installs its own
  zle-line-init widget has one either way; the contract is that it looks the same
  wrapped as unwrapped, which the assertion beside it already states.
- The dropped-precmd_functions case asserted HISTFILE was no longer the scoped
  path. Whether the scoped value survives at all is the host's call: macOS
  /etc/zshrc overwrites HISTFILE so it does not, and a host with no such
  assignment keeps whatever the spawn env set. Now compared against an unwrapped
  pane given the same env, which is the real contract on both.

Also notes, where the emulation cases live, that they only discriminate on a host
whose system zshrc clobbers HISTFILE -- on CI's Ubuntu the load-bearing assertion
is ORCA_HISTFILE having been consumed.

* test(shell): re-pin the fixes the four-file wrapper was built for

Archaeology over the removed blocks: each existed for a bug, so each needs the
bug shown to be unreachable rather than just the code gone. Six restored or added,
each naming the change that introduced the behaviour.

- #8003, twice: the wrapper sourced from a relocated root, and from a non-ASCII
  (token-range) one. The old file baked its generation-time path in and had to
  re-derive the runtime one from %x to avoid using it; this one bakes nothing.
  Both runs assert ORCA_SHELL_FEATURES came back consumed, so 'the user's .zshrc
  loaded' cannot pass on a pane that never read the wrapper at all.
- #4667: user startup files must see their OWN ZDOTDIR while they run, or plugin
  and theme lookups resolve into Orca's dir. The old wrapper swapped ZDOTDIR
  around each source; this one never takes it away, and the values now have to
  match an unwrapped pane's.
- #1947: a user .zshenv that returns early.
- #15258: an inherited ZDOTDIR that is an Orca wrapper dir must be refused. CI
  proved this route is live -- the launch config only sets ORCA_ORIG_ZDOTDIR when
  it resolved a usable dir, but a pane inherits its parent's environment too.
- #11044/#11146: a nested Orca inherits neither cross-process channel and no
  ZDOTDIR of Orca's, which is what makes #11044's plain shape unreachable rather
  than repaired. Verified the child-env probe detects a real leak before trusting
  it to report the absence of one.
2026-08-19 22:37:42 -07:00
Neil fdd4091ebd fix(hooks): isolate lint-staged backups per worktree (#15388) 2026-08-19 22:37:02 -07:00
Neil d7a23c84a9 fix(pty): keep a CPR reply from overtaking a deferred colour reply (#15559)
A background-colour probe writes `OSC 11 ;? ST` then `CSI 6n` and reads
exactly one response, using the CPR as its sentinel: a non-OSC first
response means "unsupported" and it stops draining. #13309 routed live
cooked-echo-risk replies through the ECHO-probe deferral while CPR kept
the immediate path, so the CPR overtook the colour reply, the prober gave
up, and the stray `ESC ]` was left in the tty for the next program —
`gh auth login` died on it with an escape-sequence error.

Queue a reply that needs no echo containment behind ones that do, FIFO,
and only while something is actually deferred, so latency-critical
replies stay immediate on every other path. Windows is unaffected: only
posix-pty defers, so the queue is always empty there.

Also: an in-flight echo probe is already the write continuation, so
re-arming the timer for a queued reply would fork a second stty and throw
away the first verdict; and teardown now hands queued uncontained writes
to the pty best-effort instead of dropping bytes the caller was told were
sent.

Known scope limit, pinned by tests and tracked for follow-up: the
guarantee is FIFO among recognised query replies, not over every byte —
a reply coalesced with a keystroke, and ordinary typed input, still
bypass the queue. Both were unordered before this change too.
2026-08-19 20:46:12 -07:00
Jinjing d541982b9c Improve cmd j search usefulness (#15551)
* Show last active time for workspace tabs in cmd+j palette

Adds session-age formatting and activity tracking to help users find
recently-used tabs. Replaces host badge display with last-active timestamps
that reflect either agent activity or worktree PTY activity, whichever is
more recent.

* Improve cmd+j search ranking with direct fields and recency

Prioritize results matching direct fields (titles, content) over
container fields (worktree, branch, repo). Use tab focus time to break
ranking ties. Makes search more useful for quick navigation.

* Extract path flavor logic to cross-platform-path utility

- Remove local pathFlavor function in favor of shared cross-platform utilities
- Simplify buildExcludePathPrefixes to use relativePathInsideRoot and resolveRuntimePath
- Ensures consistent path handling for both local and remote roots

* Improve cmd+j search ranking with recency-based tiebreaking

Track lastFocusedAt on tab creation/focus and use it to break ties between equally-ranked search results. This surfaces recently-used items first, improving search utility. Also fixes hasDirectHit to check field matches directly rather than evidence metadata.
2026-08-19 19:46:45 -07:00
Brennan Benson a76a95d111 fix(terminal): make remote-host take-back release the phone-fit lock (#15473)
* fix(terminal): make remote-host take-back release the phone-fit lock

The remote-desktop branch of reclaimTerminalForDesktop was the one path that
rolled the presence lock back when its reclaim resize did not converge. On a
remote/SSH host that left the phone-fit banner stranded and made every
subsequent "Take back all terminals" click a no-op. Its sibling (active mobile
subscriber) released the lock but still reported the layout's `ok`, so the
desktop renderer skipped its post-take-back refit and focus.

Both now follow the guarantee the method already documents: an explicit desktop
take-back always drops the lock, and the trailing remote layout is best-effort.

* review: pin the driver flip and correct the applyMobileDisplayMode contract

- Tighten the held-branch driver assertion to the exact post-release state so
  it pins releaseDesktopTakeBack's flip rather than merely "not mobile".
- applyMobileDisplayMode's doc claimed reclaimTerminalForDesktop gates its
  transitions on the returned convergence flag. No branch does after this
  change; say so, so the gate is not reinstated.

Hooks skipped (machine load); oxfmt --check and oxlint verified clean manually.

* test(terminal): pin the converging remote take-back resize

The two take-back tests both force the reclaim resize to fail, so nothing
covered a take-back that converges. Deleting the `idle` driver flip left
every suite green while applyRemoteDesktopLayout no-opped on a still-mobile
driver — lock dropped, `true` returned, PTY stranded at the phone grid.
2026-08-19 18:13:57 -07:00