Commit Graph
10162 Commits
Author SHA1 Message Date
Neilandinnocarpe f400f8fd5f fix(macos): opt out of press-and-hold so held keys repeat (#14746) (#15589)
* fix(macos): opt out of press-and-hold so held keys repeat (#14746)

macOS routes press-and-hold to the accent picker unless an app sets
ApplePressAndHoldEnabled=false for its own bundle, so holding j in vim
inserted one character instead of repeating. Orca never set it.

Written at most once, and never over an explicit value: `defaults read`
is domain-scoped and exits 1 when the key is absent, which is the only
way to tell "unset" from a deliberate false — Electron's
systemPreferences.getUserDefault reports false for both. A recorded
decision in userData keeps a later launch from re-clobbering a user who
deletes the key to get the accent picker back.

* docs(macos): record the revert hazard and CI's macOS test gap

Two things a reader of this module cannot otherwise know.

A revert leaves the key written in every user's domain forever. AppKit reads
the plist, not this file, so removing the code alone keeps press-and-hold
disabled for everyone who ran an affected build. The sibling period-substitution
module carries the same warning because that fix was already lost once this way.

And the real-binary test file that pins the defaults(1) exit-code semantics this
design rests on never runs in CI: the e2e workflow and both unit-test jobs are
ubuntu and windows, and the only macOS runners in the repo are build and
packaging jobs that run no tests. Those six tests plus the real-bundle e2e case
pass on a developer Mac and execute zero times in a green PR, so the comment
should not imply enforcement that is not there.

Refs #14746

* feat(macos): let users turn the accent menu back on (#14746)

Orca disables press-and-hold for its own preferences domain so held keys
repeat. That is the right default, but the way back was a `defaults write`
buried in a source comment: nothing in docs/ or the README mentioned it, and
the preference is per-application, so it silently takes the accent picker
away from the Markdown editor and every other text field too.

Terminal -> Advanced now carries a "Character Accent Menu" switch, macOS and
desktop only. A web client cannot write a macOS preference for the machine the
user is looking at, so the control and its search-index entry are both gated on
that, not on the client's platform alone.

Precedence, which is the part that is easy to get wrong: the setting is
`undefined` until the user touches it, which is what keeps a hand-run `defaults
write` in charge for everyone who never opens the toggle. Once used, Orca owns
the key and writes exactly what the switch asks for -- `ApplePressAndHoldEnabled`
*is* the accent-menu switch, so it maps straight through with no inversion. The
choice is compared against `appliedSetting` in the existing decision record
rather than against the domain, so a `defaults write` made *after* using the
toggle is still the newer choice and survives the next launch. Re-asserting the
value every launch would have reintroduced the clobbering the record exists to
prevent.

The write lands for the next launch, since AppKit reads the preference as the
process starts, so the toggle shows the same restart banner the window-blur
setting uses. That banner is now a shared component, keeping its original
translation keys.

docs/reference/macos-press-and-hold.md records the precedence rules, the
`defaults read` rationale, the revert hazard, and the fact that none of this
executes in CI: every macOS job builds or packages and runs no tests, so the
real-binary and e2e coverage here passes only on a developer Mac.

* docs(macos): stop asserting when AppKit re-reads the press-and-hold key

Five places stated "AppKit reads the preference as the process starts" as
fact. That is the reason given for requiring a relaunch, and it is not
something this change ever measured.

Evidence points the other way: terminal emulators that register this key
after their process has started get key repeat in that same launch, which a
read-once-at-startup model cannot explain.

The relaunch requirement itself still looks right, but for a different and
verifiable reason: the write goes out through a separate `defaults` process,
so this app's own cached copy need not observe it. That is what the comments
now say, with the AppKit question left open rather than answered.

Refs #14746

* docs(macos): correct the startup comment's launch-timing claim

The comment said this call site is "the last point that can still matter for
this launch", which contradicts the rest of the module: the write is assumed
to land for the next launch because it goes out through a separate `defaults`
process. Reported on the PR by @innocarpe, who also supplied the replacement
wording.

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

* refactor(macos): probe press-and-hold through the shared spawn chokepoint

`src/shared/child-process/child-process-import-boundary.test.ts` forbids a
direct `node:child_process` import outside its allowlist, and the allowlist only
shrinks — so this module moves to `runProcessSync`, which exists for callers
that genuinely cannot await. This one runs before `app.whenReady()`.

`runProcessSync` returns a non-zero exit instead of throwing it, so the
three-way read decision is re-expressed against `ProcessResult`: exit 0 is an
explicit value, exit 1 is a missing key, and a timeout, a signal kill, any other
exit, or a child that never started all stay 'unknown'. The throw path is now
inside `interpretDefaultsRead` so a spawn failure is reachable from a test
rather than hidden in an untested catch, and the write checks the exit code —
a refused `defaults write` no longer looks like success.

Both boundary-test failures were the same import: with it gone the offender
count returns to 155, so no ratchet baseline is bumped.

* Revert "feat(macos): let users turn the accent menu back on (#14746)"

This reverts commit cc5669f306.

---------

Co-authored-by: innocarpe <innocarpe@users.noreply.github.com>
2026-08-27 13:44:03 -07:00
Jinjing 58232cd797 Allow drag to reorder tabs on floating workspace (#16828)
* Enable drag-to-reorder for floating workspace tabs

- Wrap tab bar with FloatingWorkspaceTabDragContext to reuse workspace
  tab-drag-split logic and gesture model
- Disable sensors while panel is closed to avoid DndContext conflicts
  with main workspace
- Extract isFloatingTerminalDragTarget to separate module for clarity
- Add tests for reorder behavior and titlebar drag-target detection

* Add client-hosted row support to floating panel drag-to-reorder

- Include `[data-client-hosted-browser-row-id]` in no-drag selectors alongside other tab types
- Broaden type check from HTMLElement to Element for better SVG support
- Add test coverage for client-hosted rows and SVG icon interactions

* Improve floating terminal drag target detection with Element safety chec

Handle undefined Element in non-DOM contexts by explicitly checking for its
existence before type-checking. Clarify the logic by replacing double negatives
with explicit null comparison, making the intent clearer and the code more
resilient.
2026-08-27 13:41:01 -07:00
Jinjing 3179a5761f Allow select-all shortcuts in quick command dialogs (#16851)
Add isSelectAllShortcut() utility to detect Cmd+A (Mac) or Ctrl+A (Linux/Windows). Use it to preserve native select-all behavior in editable fields instead of being intercepted by dialog keyboard handlers.
2026-08-27 13:32:09 -07:00
Jinjing 61c8f85d52 Always enable Tasks button and provider shortcuts (#16838)
* Always enable Tasks button and provider shortcuts

Allows the task page to show an empty state explanation when no git
repos are available, instead of disabling the button entirely.

* Make task provider shortcuts keyboard-accessible sibling buttons

Convert task provider shortcuts from non-semantic spans to proper button
elements and position them as siblings of the Tasks button rather than
children. This keeps them in the keyboard tab order while using opacity
instead of display for visibility, ensuring they remain discoverable by
keyboard navigation alongside the main Tasks button.
2026-08-27 13:28:52 -07:00
Brennan Benson 3558cf943f fix(codex): heal WSL hooks before typed launches (#16535)
* fix(codex): heal WSL hooks before typed launches

* test(codex): keep launcher fixture type-safe on Windows

* fix(build): list codex-home-wsl-env in the CLI typecheck project

`managed-home-shell-preflight.ts` is already in the CLI project's include list and now imports
`wslCodexRuntimeHomeForGuestHome` from `src/main/pty/codex-home-wsl-env.ts`, which the list did not
cover — TS6307, so the CLI typecheck failed on every push.

Added the single module rather than a `src/main/pty/**` glob: it is a 31-line leaf with no imports
of its own, so it does not widen what the CLI bundle can reach.

* fix(codex): converge the two WSL hook install lanes onto one writer

Two independent readiness reviews agreed the Orca-terminal boundary holds, but Codex Sol found a
P1 the other rated P2: the new just-in-time repair raced the existing relay installer and the two
produced DIFFERENT hook and trust representations for the same managed home. Two unserialized
writers emitting different formats is worse than the bug this PR fixes, because it fails
intermittently rather than cleanly — a pane works or does not depending on which lane won.

- Relay Codex installs now delegate to the runtime-home writer, so there is one canonical
  representation instead of two. Redirected scripts use the runtime path, the readable wrapper,
  and the prepended group.
- `installForRuntimeHomeSerialized` puts every asynchronous WSL caller for a given home on one
  queue (`wslInstallQueues`), so concurrent panes cannot interleave writes.

Also rewrites the stale pin test the new `-x` guard broke. It asserted the defect —
"would run the impostor if the preflight carried an unqualified command name", expecting the
hijack marker to exist. The guard is a security improvement, so the test now asserts the contract:
an unqualified preflight is skipped and the marker is never written. Rewritten to the new
behavior, not loosened or deleted.

818 tests pass across the affected suites; typecheck clean. The changed-file quality gate could
not run locally — its pnpm engine-warning JSON parser fails under Node 26 — so CI covers it.

The boundary both reviews verified is untouched: paired/relay/mobile clients stay hard-blocked
from the RPC, params remain shape-locked to the managed home suffix with traversal rejection,
nothing is written outside the managed home, and macOS/Linux stay inert.

* fix(codex): serialize resolved WSL hook homes

* fix(codex): recover managed WSL homes after restart

* fix(wsl): translate Codex preflight through WSLENV

* fix(cli): cover bounded WSL Codex repair

* fix(codex): coalesce duplicate WSL hook repairs

* fix(codex): verify reconstructed WSL homes
2026-08-27 13:05:12 -07:00
Jinjing 9635e6822f Normalize project catalog rows defensively (#16826)
* wip: normalize untrusted project catalog rows at load boundaries

* fix(catalog): make ProjectHostSetup field types true at the ingest boundary

Crash 3bcc5be3: a setup row whose repoId arrived null reached Settings'
projectByRepoId memo and threw on .trim(). The type said `string`; persisted
JSON and remote hosts on other versions can disagree.

Normalize project/setup rows where untrusted data enters typed code — the
persisted-state load (marking dirty, which is the migration), profile
transfer reads, the repo-derived projection, and the renderer's IPC/RPC
ingest and adoption steps — instead of re-guarding each consumer. Also
covers `setup.path`, whose identical crash is on the sidebar render path.

Coercion only: never drops a row, never adds or removes an optional key, and
returns input references when a row already conforms, so selector and useMemo
identity is unchanged.

* refactor(catalog): drop the `as` casts the normalizer introduced

A change whose thesis is "stop the declared types from lying" should not use
`as` to paper over types.

The four source casts all came from the row normalizers returning `readonly`
arrays into mutably-owned fields. Take and return mutable arrays instead, and
copy at the one caller that holds a readonly projection — where identity is
not load-bearing, unlike the persistence path, whose dirty check compares it.

Tests built deliberately malformed rows by casting a literal. Build a valid
row and `Reflect.set` the bad value onto it, which says outright that the
fixture violates its type; parse the non-array case from JSON, which is how
it actually arrives. Fixtures that were merely incomplete needed no cast at
all — `Repo` requires only the five fields they already had.

Also narrow normalizeLoadedProjectCatalog to the two fields it reads.

* refactor(profiles): validate untrusted profile JSON instead of asserting it

Removes the last introduced cast and the unsound pre-existing ones in the
file this change already touches.

The test cast is gone because narrowing normalizeLoadedProjectCatalog to the
two fields it reads made `{}` assignable on its own.

arrayOrEmpty and recordOrEmpty checked the shape and then asserted the
element type, which is the same "declared type is a promise the data does not
keep" problem this change exists to fix. Array.isArray already narrows on its
own, and a generic isRecord narrows the value part, so both assertions delete
outright. JSON.parse returns any, so annotating the binding beats asserting
its result.

The two remaining `as const` in project-host-setup-actions are untouched and
deliberate: a literal assertion narrows a type rather than overriding it.

* Make project catalog normalizers handle null rows defensively

Instead of crashing when corrupt or null catalog rows are encountered,
the normalizers now gracefully repair them with default values. Refactored
helper functions for clarity and added type predicates to improve type
narrowing.
2026-08-27 12:35:03 -07:00
Mark XianandBrennan Benson cc384c5a3d fix(agent-hooks): post posix payloads as json (#11292)
* fix(agent-hooks): post posix payloads as json

* fix(agent-hooks): mark header merged envelopes

* docs(agent-hooks): describe header merge envelope

* fix(agent-hooks): encode posix metadata headers

* test(agent-hooks): update WSL JSON hook assertions

* fix(agent-hooks): negotiate raw JSON transport

* fix(agent-hooks): preserve packed metadata in POSIX shells

* test(agent-hooks): include hook envelope in relay boundary inventory

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-27 12:32:29 -07:00
Brennan Benson fecdf0bde8 fix(daemon): recover concurrent spawns after disconnect (#16818) 2026-08-27 11:13:32 -07:00
Brennan Benson 7c3bfe72d7 fix(windows): stop a wedged process-table reader retaining a callback per cooldown (#16696)
* fix(windows): stop a wedged process-table reader retaining a callback per cooldown

The vendored reader pushes every callback onto a module-global queue and drains
it only when the request holding its `requestInProgress` latch completes. When a
Toolhelp32 snapshot never comes back, that latch is stuck for the life of the
process, so the 30 s cooldown -- which let one probe through per window --
bounded the rate of new callbacks but not the total: one more closure retained
every 30 s, forever, plus a full 3 s deadline block on whichever caller drew the
probe.

Gate on the outstanding read instead. Once a read misses its deadline and has
not called back, every further read is refused until that read's callback fires,
which bounds retention at exactly one callback. Nothing is given up on recovery:
a probe queued behind the latch could never have observed the drain anyway,
whereas the stuck callback firing IS the drain, so the reader now resumes the
instant it recovers rather than up to 30 s later.

It matters more on a relay, which binds the bare addon with no JS queue to
absorb the retries. Each read there is a `Napi::AsyncWorker`, so a wedged one
holds a libuv threadpool slot for good and one probe per window would have
pinned all four default threads inside ~2 minutes -- hanging every async `fs`
and DNS call in that process, not just the process table.

A wedge still does not engage the PowerShell fallback, and a wedged read still
rejects rather than resolving empty, so "unavailable" stays distinguishable from
"nothing is running" on every host.

Fixes STA-5499.

* fix(windows): invalidate stale reader deadlines on reset
2026-08-27 10:52:14 -07:00
Brennan Benson 928d306b53 fix(agent-hooks): stop the Windows hook launcher spelling the AV-denied flag pair (STA-5237) (#16739)
* fix(agent-hooks): stop the Windows hook launcher spelling the AV-denied flag pair (STA-5237)

`-WindowStyle Hidden` + `-EncodedCommand` is denied at CreateProcess by
Kaspersky on Windows 11, whatever the payload decodes to. Bash reports it as
`Permission denied` and every managed hook event fails, so agent status never
arrives; the parent shell also briefly cannot spawn anything afterwards, so a
denied hook can take the user's next command down with it.

Measured on the reporting host (#16003), with a harmless `exit 0` payload:

  -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand  126
  -NoProfile -WindowStyle Hidden -EncodedCommand                          126
  -WindowStyle Hidden -EncodedCommand                                     126
  -NoProfile -EncodedCommand                                              0 (5/5)

#16576 removed `-ExecutionPolicy Bypass`, which is the one flag of the three
NOT in the signature, so hooks kept failing after that fix. The pair that has
to stop being spelled is `-WindowStyle Hidden` + `-EncodedCommand`.

Because the change is to the shared switch constant, it covers every site that
spells the denied pair in one edit: Claude via `wrapWindowsPowerShellEncodedCommand`,
gemini/cursor/droid/command-code/copilot via `wrapWindowsHookCommand`, the
`runtime-home-hook-command` unsafe-HOME fallback, and the spaced-path fallback
for codex/grok/devin/antigravity. Only a flag is removed, so parser and payload
compatibility is unchanged for every executor: the string is still a PowerShell
command line, still one self-contained token, still base64-shielded.

The tradeoff, recorded rather than hidden: `-WindowStyle Hidden` was the shipped
fix for #14815 (+#14828, #15117, #15447, #15767), and this removes it. Its
suppression was never measured — #14825 confirmed it visually, #16576's author
stated it "remains unverified on a real box", and #15506's author argued it
cannot help a `.cmd` child with no console to inherit. The console is allocated
by the parent chain, not by this command line. A live window measurement is
still outstanding and is called out in the PR.

Also adds `windows-hook-payload-delivery.test.ts` to the PR CI Windows leg,
which had never run it.

* test(agent-hooks): keep launcher token out of source grep
2026-08-27 10:48:53 -07:00
Brennan Benson 2c07e7c1d5 fix(workspace-cleanup): color review pills by PR/MR state (#16726)
* fix(workspace-cleanup): color review pills by PR/MR state

The inactive-workspace review dialog rendered every linked review pill in
one of two flat tones, so a merged PR, a closed PR, and an unlinked row all
looked alike. Reuse the state colors the PR page and item dialog already
use (purple merged, rose closed, slate draft, emerald open) and give the
pill the matching state glyph.

The mapping lived in two byte-identical copies; both now delegate to a
shared review-state module, as does the sidebar's state-icon picker.

* fix(workspace-cleanup): expose review state in confirmation rows

* fix(workspace-cleanup): don't repeat the review number in the pill's sr-only text

The confirmation row's screen-reader span read the whole tooltip, so the PR
number was announced twice. Announce only what the color carries — the
translated state label and title.

* refactor(github): collapse the duplicated work-item state badge

The PR page and the item dialog each carried their own copy of the badge:
identical markup, identical base classes, identical open-state tone. Only
the closed-ISSUE tone genuinely differs, so that becomes a parameter and
the rest moves to one component.

Also drops a one-line tone wrapper in workspace-cleanup and folds the
review tooltip onto the screen-reader text it already duplicated.
2026-08-27 10:46:42 -07:00
Jinjing 642607bfa7 feat(diagnostics): name the code driving a React commit cascade (#16730)
* feat(diagnostics): name the code driving a React commit cascade

React #185 reports blame whichever component dispatched after the
root-global counter tripped. react-update-depth-attribution already tells
the report that boundary_id names a bystander; nothing recorded what the
real driver was.

Count commits through react-dom's devtools commit hook — the only
per-commit seam that survives minification. Profiler's onRender is
compiled out of the production bundle, and a dependency-less root layout
effect fires per render of its own component, not per commit (measured: a
root effect saw 1 of 11 commits a leaf drove).

Mirror React's own reset rule rather than a time window: a commit that
leaves no sync lanes pending ends the cascade, and a different root
restarts it. The steady-state cost is a mask, a compare and an increment,
with no clock read and no allocation. Stack sampling arms only once a
cascade is already deep, so ordinary work never pays for it.

* fix(diagnostics): remove the install-order trap and guard the write path

Adversarial and perf review of the cascade diagnostic:

The install-order ratchet guarded the wrong thing. The observer self-installs
at the bottom of its own module, so it only ran after its transitive graph
evaluated — one new import reaching react-dom would have killed the
diagnostic in production with every test green. The entries now import the
import-free shim instead, which only has to make the global exist; wrapping
the callback is timing-independent because react-dom re-reads it per commit.

The store write probe called the sampler unguarded, so a throw there dropped
the write on the app's universal write path. Guarded; the try/catch measured
free at +0.005ns.

Report the frames that name the driver instead of capturing eight and
reporting one, arm the self-check on the paths where install fails, bind the
sample cap to the write count rather than a V8-only API, and stop defining
the devtools global for every test file to serve one.

The cascadeRoot comment claimed a strong reference cannot retain; a WeakRef
probe disproved it. It is still not a leak — the next non-cascading commit
clears the slot — so the comment now says that instead.

* test(diagnostics): close the ratchet holes guarding the cascade hook

Adversarial review loop 2:

The install-order ratchet only saw imports whose `from` shared a line with
the keyword, so a multi-line `import { createRoot } from 'react-dom/client'`
in the shim passed it — and that is the one edit that kills the diagnostic in
production. 43% of files in this directory use the multi-line form. Scan the
shim source directly as well as walking the graph.

The 4000-char budget for the driver frames is bought by the key ending in
`stack`, but the only test asserting that emitted its own literal key, so
renaming the real one truncated the frames with the suite green. Assert the
name the renderer actually emits.

Also correct the comment on the `installed` placement: the self-check never
reads that flag, it arms because it sits outside the try.

* test(diagnostics): stop the shim ratchet firing on prose

Adversarial review loop 3 caught two flaws in the guards added last commit.

The source-scan regex used an unbounded `[\s\S]*?` after an anchor that also
matched the shim's own `export type`, so it degenerated to "does the word
`from` appear later in the file" — rewriting a doc comment to say "reads the
hook from the global" failed the ratchet. A guard that fails on prose is a
guard someone deletes, and this one is what stands between a reshuffled
import and a silently dead diagnostic. Require a quote after `from`, tolerate
comment obfuscation, and catch `await import(...)`, which makes the shim
async so react-dom evaluates before the hook is installed.

The 4000-char budget assertion matched `/stack$/i` against the raw key, but
the real rule camel-splits first — so `driverstack` would pass while shipping
truncated frames. Assert through sanitizeCrashReportDetails, resolving the
key from the payload rather than hard-coding it.
2026-08-27 09:45:56 -07:00
Jinjing 92536346bd Add terminal unavailability exports to parity test (#16810)
New runtime exports for handling terminal unavailability: RuntimeTerminalUnavailableReason type and related error codes and messaging constants.
2026-08-27 09:08:07 -07:00
Jinjing 07b7e9e68d Open target=_blank links and unnamed popups in new Orca tabs (#16720)
* feat(browser): open target=_blank links and unnamed popups in new Orca t

- Treat target=_blank as a new-tab request matching browser behavior
- Route unnamed, featureless window.open() calls to Orca tabs instead of native popups
- Add rate limiting to prevent page-initiated tab loops
- Inherit session profiles when opening links to maintain isolation boundaries

* fix(browser): deny new-tab window.open when renderer is destroyed

Move deny action outside conditional to ensure new-tab intents are
safely rejected even if renderer vanishes mid-open, preventing native
popup fallthrough. Add test coverage and simplify comments.

* Share page-initiated tab budget across opener popup tree

Prevent pages from bypassing the new-tab rate limit by chaining popup
windows. The page-initiated tab quota is now shared by all popups in
an opener tree (root + named children), so child windows inherit their
root's budget instead of each getting a fresh allocation.
2026-08-27 08:49:02 -07:00
Jinjing 026389a3bc Suppress default quit on window-all-closed in DNS probe (#16683)
* Suppress default quit on window-all-closed in DNS probe

Destroying the probe window awaits stopLogging, which yields to the event
loop long enough for the default window-all-closed exit (non-macOS) to
trigger before the result can be written. Preventing this default behavior
allows the result to complete and exit cleanly.

* Preserve Japanese Skills UI labels; fix test flakiness and deps

- Add skipKeyPrefixes filter to Japanese phrase fixes to prevent automatic
  translation of UI labels (e.g., keep エージェント in Skills components)
- Wrap timing-dependent tests in vi.waitFor to eliminate race conditions during
  reconciliation and scheduler boundary ticks
- Complete useEffect dependency arrays to resolve React hook warnings

* Fix updater startup scheduling test flakiness

Set last update check to 23h ago instead of null to make timing deterministic. The startup check arms its own 24h timer; by pre-setting the last check time, only the result handler's re-arm can produce the expected check 24h later, eliminating race conditions.
2026-08-27 00:26:55 -07:00
Brennan Benson f352e3e27d fix(cursor): emit Cursor-contract JSON from managed hooks
Merge rebased conflict repair after exact-head tests, typecheck, lint, format, and all required GitHub checks passed.
2026-08-27 00:25:31 -07:00
Neil 5631aa00dd feat(orcad): items 2–7 — degradation, natives, daemon, ops, deploy (#16398)
* fix(ports): stop joining an undefined resourcesPath on a non-Electron host

`resolveWorkerEntryPath` branched on `isPackaged` alone and joined
`process.resourcesPath`. orcad reports `isPackaged` true — correctly, it is a
production build, and ~15 consumers read it that way to gate HTTPS-only skill
downloads and the real CLI name — but `process.resourcesPath` is Electron-only
and `undefined` under plain Node.

So the packaged branch threw
`TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string`
where a clean "worker unavailable" was the honest outcome. The type said
`resourcesPath: string`, which is how it went unnoticed; it is now
`string | undefined`, so the compiler carries the fact.

A host with no Electron resources tree has no asar to look in, so it falls back
to the module directory and lets the caller report a missing worker.

Found by the item 1 agent while auditing the same `isPackaged` defect class in
the watcher. Verified in both directions: reverting the guard reproduces the
TypeError.

* feat(orcad): prove node-pty loads before anything requires it

Of the two ways node-pty fails, only one is catchable. A missing module throws
MODULE_NOT_FOUND. A module built against the wrong libc or Node ABI is refused by
the dynamic loader, and in the worst case takes the process down before any handler
exists — that is #9902, which crashed the desktop app on Ubuntu 20.04 before a
window appeared. There was no libc or ABI precondition anywhere in the tree.

So orcad now proves the load in a CHILD process, from main.ts, before anything
requires node-pty. Whatever the child does — throw, abort, die on a signal — is data
rather than our own death, and the operator gets a sentence naming the host's libc,
Node ABI and prebuild slot plus the command to run. Proven-unloadable exits 78
(EX_CONFIG), so a supervisor does not restart an unequippable host forever. A probe
that never answered is unverifiable, not blocked: refusing to boot on an inconclusive
signal would take down hosts that work.

The child dlopens the file node-pty would have chosen, before requiring the package.
node-pty's loader walks several directories and rethrows only the LAST error, so a
refused binary reads as "Cannot find module ./prebuilds/..." — which sends the
operator to install a module that is already there. It also reports through stdout:
node echoes the whole -e source above a stack trace, and matching tokens against
stderr made the probe's own source text answer for the verdict.

Verdicts reach clients as a terminal_unavailable degradation alongside the existing
browser_unavailable one, through the same cause-registry shape. degradations[].code
is now an open vocabulary; clients already render only `message`.

Prebuilds are compiled from PATCHED sources — the patch IS the glibc-floor fix, so an
upstream tarball reproduces #9902 — into linux-{x64,arm64}-{glibc,musl} and
darwin-{x64,arm64} slots. libc is in the slot name because node-pty's loader falls
back to prebuilds/<platform>-<arch> and cannot tell glibc from musl. orcad installs
the matching slot at boot, so a host with no compiler serves terminals.

The relay's five pure toolchain-diagnosis functions moved to a transport-free module
so the Node bundle can reuse them without dragging ssh2 in behind them; the relay
keeps its API by re-export. macOS gets `xcode-select --install` rather than the
cross-distro apt/dnf/pacman/apk menu, every line of which is wrong there.

* test(orcad): pin the node-pty precondition to ground truth, not a prepared host

CI's test shard runs `vitest` directly, so `ensure-native-runtime --runtime=node`
never prepares node-pty for the Node ABI — `degraded` is the correct verdict
there, and asserting 'ok' encoded an environment the shard does not have.

Asserting whatever it returned would be vacuous, so the expectation is now
derived from an independent require() of node-pty. Verified it still bites:
forcing the precondition to always report 'ok' fails the suite.

* feat(orcad): run the terminal daemon, and the ops contract around it

orcad declared `canRecoverPersistentLocalPtys: () => false` because it did not
run the terminal daemon, so every restart, update and rollback SIGKILLed every
running terminal — on the host whose selling point is that work survives the
client going away. That is the one property `ssh-execution-boundary.md`
recommends the peer model for.

Item 4 — the daemon:

- Port the launch path off electron: `daemon-init.ts`,
  `daemon-host-relocation.ts` and `observability/logs-directory.ts` now read
  the `AppEnvironment` port. Relocation additionally asks whether the app root
  is an asar archive rather than whether the build is packaged, so a Node host
  answering `isPackaged() === true` no longer walks into an Electron-only
  NSIS-escape path (same precedent as `parcel-watcher-entry-path.ts`).
- `build-orcad.mjs` emits `daemon-entry.js` beside `orcad.js`, scans the
  forked children's metafiles for electron/node:sqlite, and load-checks the
  child under plain Node.
- orcad spawns and adopts the daemon; shutdown disconnects and never kills it.
  `canRecoverPersistentLocalPtys` now reads the live provider and is false
  under degraded routing, where fresh terminals would die with the process.

Item 3 — the ops contract (docs/reference/orcad-operations.md):

- Bind policy: `--bind`, default loopback, pinned so neither `orca serve`'s
  wide default nor the connected-device widen can override it, and so a paired
  client cannot rebind the listener from outside.
- Instance lock on the data root before profile load, scoped to the runtime
  role so it never refuses a restart that a live daemon makes worthwhile.
- Supervision: exit codes a supervisor can act on (78 = do not retry),
  second-signal escalation, a shutdown deadline, and crash-loop containment on
  daemon respawn.
- Health in the readiness payload: build hash, Node ABI, and a PTY self-test
  that spans both processes — the daemon spawns a real PTY in its own process
  and the verdict crosses its socket.

Both bundle load-checks now assert on exit codes: these bundles are minified
onto one line, so Node's uncaught-exception report echoes every string literal
in the bundle and the previous message match passed against a bundle that
never loaded.

* feat(orcad): deploy, activate and roll back a versioned orcad install

Plan items 6 and 7 from docs/design/shipping-orcad.html.

Install reuses the relay's transaction verbatim — per-version lock, staged
SFTP write, .install-complete sentinel, stale-lock recovery — under a
parameterized namespace, so orcad-<v>/ sits beside relay-<v>/ permanently
(§06). Parameterizing GC is the trap that creates: each model now collects
only its own directories, enforced twice (prefix-scoped remote listing plus
a local ownership re-check), and a client picks its model from how the host
is registered, never from what it finds on disk.

Activation is separate from installation, because a versioned directory
selects nothing. A candidate is launched, publishes orca_server_ready, and
only becomes active if its cross-process health payload passes: right build
hash, listening, daemon live, PTY self-test green. A rejected candidate is
stopped and the incumbent restarted, so a careful deploy cannot cause the
outage it was being careful about.

Update and rollback are shaped by the daemon. An update restarts orcad, the
daemon outlives it, and the surviving daemon was forked from the outgoing
bundle — so live terminals defer the update rather than proceed, and GC pins
the active version, the rollback target and the live daemon's bundle. Orca's
persisted state carries no schema version, so rollback restores a
pre-activation snapshot rather than trusting backward-readability; the point
past which it is unsafe is the first terminal created after activation,
which the snapshot cannot describe and the surviving daemon still owns.

Running the generated shell for real found two bugs the text assertions
missed: tar members re-quoted inside a shell variable captured nothing, and
kill -0 reports a zombie as alive.

* test(orcad): assert the precondition is self-consistent, not environment-shaped

The real-host case cannot predict a status: CI's shard runs vitest directly, so
node-pty is never built for the Node ABI and 'degraded' is correct there, while a
prepared checkout gives 'ok'.

The previous attempt used require('node-pty') as ground truth, which resolves the
JS wrapper while the native binding loads lazily — it proved strictly less than
the precondition checks, and failed CI for exactly that reason.

What is invariant on a host with node-pty installed: never 'blocked', and never a
degraded verdict carrying an unestablished reason. The injected-input tests keep
the logic coverage.

* fix(orcad): drop an eslint-disable the rule no longer needs

* test(orcad): separate slot placement from the load verdict

Both remaining CI failures were the same shape: tests reaching into node_modules
for a pty.node that only exists after `ensure-native-runtime --runtime=node`,
which CI's shard never runs because it invokes vitest directly.

Slot *placement* is the logic worth checking on every host, so it now uses a
synthetic payload and asserts the verdict stays honest about not loading. The
three assertions that genuinely need a Node-ABI binding are gated on it existing.

Verified: breaking slot installation fails both placement tests; with the real
pty.node hidden the file is 17 passed / 3 skipped instead of ENOENT.

* test(orcad): gate the load-dependent cases on a real load, not on the file existing

CI ships a pty.node built for Electron's ABI, so existsSync was true while require
still failed — the gate ran exactly the tests that host can never satisfy. It now
probes the binding in a child process, so a bad one cannot take the runner down.

The self-consistency assertion also allowed too little: 'blocked' is the honest
verdict for a corrupt binding, alongside 'ok' on a prepared host and 'degraded' on
an unprepared one. What stays invariant is that anything other than 'ok' names an
established cause, so a terminal is never declined for a reason nobody worked out.

Verified against all three host states: prepared (19 passed), unprepared, and a
corrupt binding (17 passed / 3 skipped, no failures).

* test(orcad): gate on the whole premise — binding AND spawn-helper

CI has a loadable pty.node but no spawn-helper, and a slot without the helper is
legitimately 'degraded'. So the previous gate let a test run whose premise ('a
complete slot yields ok') that host cannot satisfy.

Verified in both states: with the helper present 19 pass; with it removed the
load-dependent cases skip (17 passed / 3 skipped) instead of failing.

* fix(orcad): preserve degradation types after rebase
2026-08-27 00:18:51 -07:00
Neil 9062494f9b fix(ai-vault): stop a whole opencode.db failure reading as one skipped transcript (#16587)
* fix(ai-vault): stop a whole opencode.db failure reading as one skipped transcript

#15036 reported "1 transcript skipped / database is locked" with both Agent
Session History scopes empty. Two separate defects.

The panel counts every unkinded scan issue as a skipped transcript, so a
failure that lost an entire *source* was reported as one lost *file*. The
whole-database failure is now kinded `scope`, and an unknown `kind` from a
newer host degrades to `scope` instead of failing validation and coming back
unkinded — a mixed-version remote host previously turned a source-level
failure into a phantom skipped transcript.

The read also inherited sqlite3's 0 ms busy timeout, so a genuinely contended
open failed in ~1 ms. It now opens once with a bounded timeout. No retry loop:
sqlite's own busy handler already blocks and retries internally for the whole
timeout, and WAL readers do not block on a writer at all (measured: 547/547
cross-process reads at timeout=0 while a writer held open transactions).

Measured against a real Ubuntu-24.04 distro, Windows cannot take SQLite's file
locks over \\wsl.localhost at all: an idle, never-WAL, nothing-attached
database still answers SQLITE_BUSY, a 5 s busy timeout does not change it, and
the identical bytes open fine once copied to local disk. So a lock-family error
on that share never means "a writer holds it" and no timeout can help. The copy
says so rather than sending the user after a write-ahead log that is not the
problem. Restoring those sessions needs an in-distro read; that is a follow-up,
and this PR no longer pretends a timeout will do it.

immutable=1 is deliberately not used as a workaround: over the same share it
opens and returns 100 of 150 rows, silently dropping everything still in the
uncheckpointed -wal — in a history panel, exactly the newest sessions.

* skip the provably futile busy wait on \\wsl.localhost paths
2026-08-26 23:37:16 -07:00
Lesley Murfin de6fe8b7ea fix(worktrees): resolve id: worktree selectors by path equivalence (#16243) (#16494)
* test(worktrees): cover id: selector path-spelling parity with path: (#16243)

The renderer can only address a workspace by id (toRuntimeWorktreeSelector always
emits id:<repoId>::<path>), and the runtime matches that id byte for byte while a
path: selector has always compared through normalizeRuntimePathForComparison. A
stored id that spells its path differently from `git worktree list` therefore
resolves for the CLI and answers selector_not_found for the UI, which reads that
as a stale local mirror, calls forgetLocal, reports success, and lets the row
return on the next catalog refresh: a silent delete.

These tests fail on both resolution sites -- the fleet `id:` branch of
resolveWorktreeSelector and the scoped resolveScopedWorktreeIdRow a
host-qualified removal takes -- and pin what must stay closed: an exact repo id
(STA-4343), host qualification, dot segments neither selector canonicalizes, and
a refusal rather than a guess when two rows spell one path.

13 failing, 35 passing.

* fix(worktrees): resolve id: worktree selectors by path equivalence (#16243)

worktreeIdComparisonKey names one repo, one filesystem location, and one
folder-workspace instance, folding exactly the path spellings
normalizeRuntimePathForComparison already folds for a path: selector -- and
nothing more, so dot segments stay unresolved for both shapes. Both id:
resolution sites consult it only after an exact match finds nothing: the fleet
branch of resolveWorktreeSelector and resolveScopedWorktreeIdRow, which a
host-qualified removal takes. runtimeWorktreeIdsEqual now derives from the same
key so the runtime has one normalizer rather than a parallel one.

Not a pure refactor at that last site: runtimeWorktreeIdsEqual used to
normalize-compare ids that parse but carry an empty repoId or an empty path
('::/p', or 'repo::' against 'repo::/'), and worktreeIdComparisonKey returns
null for those, so across its call sites (PTY identity, refresh, mutation
queue) such ids now compare byte-exact instead. That narrows matching rather
than widening it, no real worktree carries such an id, and it is the behavior
#15616 guarantees for malformed ids -- but it is a behavior delta, not just a
tidy-up.

Perf (#14399): the exact match is still tried first and still wins outright, so
a resolvable id costs exactly what it did before. Neither site adds a scan --
the fleet branch re-filters the array it had already listed, the scoped lookup
re-filters the single owning repo's projected rows -- so an explicit id still
never scans every repo.

Fail-closed behavior is unchanged: the repo id compares exactly (STA-4343), host
qualification is untouched, the folder-workspace instance suffix stays part of
the path, and a scoped lookup with two equivalent rows refuses instead of
guessing. The bare unprefixed selector branch keeps byte-exact id matching,
since only the id: shape reaches a renderer caller.

Shares src/shared/worktree/id.ts with the open #15616, which introduces
worktreeIdComparisonKey for the same divergence in lineage pruning and
authoritative-scan purging; this adopts that helper rather than adding a second
one. Complementary to the open #16295, which makes the miss visible; this
removes the miss.

* chore(worktrees): satisfy oxfmt and oxlint on #16243 tests

oxfmt --check flagged both new test files and oxlint's
unicorn/no-useless-fallback-in-spread flagged the store mock; the full
lint and format gates now match the pre-change baseline.

* test(worktrees): pin Windows spellings and malformed-id exactness (#16243)

Review found two axes the first pass left unproven at the two id: resolution
sites. Both are the invariants the open #15616 guarantees for the shared
worktreeIdComparisonKey it introduces for #15598, so violating either here would
break a contract a sibling PR depends on.

Windows: #15598's whole defect is that one checkout is recorded under both
`D:\Agentic\game2` and `D:/Agentic/game2`. The fleet branch, the scoped removal
lookup, and the key itself now each resolve the backslash spelling against the
forward-slash spelling git reports, and fold drive-letter case -- while a
backslash inside a POSIX path stays a filename character and a POSIX root stays
case-sensitive, exactly as normalizeRuntimePathForComparison already decides for
a path: selector.

Malformed ids keep exact matching at both sites: an id with no repo boundary or
an empty path still refuses, and the scoped lookup still refuses it without
scanning.

Four of these fail without the production change (three fleet/removal Windows
cases and the scoped one); the malformed-id and POSIX-backslash cases are
invariant guards that hold either way.

Verified: 58 passed in the three files; 18 fail with the production hunks
reverted; orca-runtime.test.ts and worktree-teardown-unstopped-pty.test.ts green
(1270 passed | 1 skipped); pnpm tc:node clean.

* test(worktrees): pin Windows id: spelling folds and fleet ambiguity refusal (#16243)

The Windows backslash spelling now rides the ID_SPELLINGS rows, so it is driven
through both id: sites -- resolveWorktreeSelector and the scoped removal target --
and compared against what the same workspace's path: selector resolves, rather
than only through worktreeIdComparisonKey. That is the spelling #15598/#15616
found in the wild and the one the owner's Windows client produces.

The fleet path's ambiguity refusal had no test: two same-repo rows spelling one
directory, an id: matching neither exactly, must reject selector_ambiguous. It is
the fail-closed guard on a delete-capable resolver, and the property a later
refactor is most likely to turn into a silent pick.

Also records two limits at the source instead of leaving them to be rediscovered:
a UNC or WSL root never folds into a drive-letter location (while Windows' two
WSL UNC aliases do name one location), and a folder-workspace id keeps a trailing
slash placed before the ::workspace:<uuid> suffix, so that spelling stays
exact-match-only. Neither behavior changes here.

The file docblock overclaimed parity. path: collapses duplicate same-host
registrations to the first row while a folded id: refuses them; the contract this
file pins is path-spelling parity, not dedup parity, and the divergence is
deliberate because this resolver also serves delete.

Non-vacuity, verified by temporarily reverting the production hunks: neutralizing
both id: fallbacks turns 10 of these tests red, including both new Windows rows
and the ambiguity refusal (it degrades to selector_not_found). Making the fleet
fallback pick the first folded match instead of collecting all of them turns the
ambiguity test red on its own. The remaining cases -- malformed ids, dot
segments, the POSIX backslash, the folder-workspace slash -- pass against the
pre-fix code too: they guard against future widening rather than proving this
fix.

Drops the two Windows cases the ID_SPELLINGS row subsumes.

The drive-letter case test asserted only the Windows half its name promised; it
now also pins that a POSIX root does NOT fold case, since an unconditional
lowercase would merge /data/Foo with /data/foo on the platform CI runs on.

Fixture paths use the upstream-attested /srv/projects prefix (and a neutral
plugin-host leaf) instead of a local install root; the spelling variations the
tests exist to pin -- doubled separator, dot segment, trailing slash, uppercase
POSIX, cafe NFC/NFD, and the Windows D: rows -- are unchanged in form.

* docs(worktrees): trim the id: selector test header and document the comparison key (#16243)
2026-08-26 23:35:18 -07:00
Brennan Benson 8d0156ae0b fix(terminal): route Enter to focused split pane (#16705)
* fix(terminal): route shortcuts to focused split pane

* fix(terminal): synchronize IME fallback pane
2026-08-26 23:30:30 -07:00
Brennan Benson aaef5e8c9f fix(agent-hooks): deliver hook events that fire while Orca is restarting (STA-5329) (#16685)
* fix(agent-hooks): correct durable spool delivery

* fix(agent-hooks): spool curl failures after retries

* fix(agent-hooks): keep replay out of runtime observations

* test(agent-hooks): pin managed hooks inert outside an Orca terminal

* fix(agent-hooks): address review findings on the durable spool

- claude: pass the literal source; options.agent does not exist (typecheck)
- kimi: the windows-local ordering runs its guard pre-stdin and before the
  function exists, so it no longer spools there (printed command-not-found)
- writer: require a readable endpoint file before creating a spool tree
- antigravity: carry its out-of-band event name into the record and filter on it
- drain: truncate only the bytes consumed, preserving concurrent appends and a
  torn trailing line

* fix(agent-hooks): ignore spool events without pane attribution

* fix(agent-hooks): make spool replay and appends robust

* test(agent-hooks): type spool replay records

* fix(agent-hooks): defer unterminated spool records

* fix(agent-hooks): replay spool events through relays

* fix(agent-hooks): preserve Codex prompt across child replay

* fix(relay): keep startup alive when spool replay fails

* fix(relay): simplify spool replay startup guard
2026-08-26 23:17:34 -07:00
Jinjing 9c01e09ecc Revert "fix(codex): launch WSL accounts from direct homes" and "refactor(codex): remove WSL runtime mirror machinery" (#16722)
This reverts commit ebcd637db9 (#16504) and dependent commit 673842db35 (#16505).

Launch-blocker rationale (findings-counsel validated):
- P0 Data Loss (F06-1): The WSL legacy auth drain deleteSource=1 path deletes intact source auth without re-validating the destination after concurrent destination rewrites, permanently corrupting auth credentials on upgrade.
- P1 Workflow Regression (FC-01): Pre-upgrade sessions under ~/.local/share/orca/codex-runtime-home/home are not linked into direct homes, breaking /resume in the Codex CLI for upgrading WSL users.
2026-08-26 22:42:37 -07:00
Neil 6e28c9a9b7 fix(editor): constrain image previews before load instead of laying out at natural size (#16686) 2026-08-26 22:33:01 -07:00
Jinjing 762fb05caa fix(agents): one-shot submit-retry Enter for codex (STA-5379) (#16689)
Codex silently discards Enter for ~75-150ms after its composer glyph first
renders, and the boundary widens with prompt size and machine load, so no
fixed first-Enter delay is provably safe on slow hosts. Submit success is not
verifiable from PTY output, but a redundant Enter is a measured no-op on codex
in both post-submit states, so send one blind retry after the first Enter.

- tui-agent-config: new optional submitRetryDelayMs knob, set to 1200 on codex
  only; every other agent is byte-identical to today.
- agent-paste-draft: after the post-paste '\r', wait the configured gap and
  send exactly one more '\r' inside the same PTY input transaction, so a
  concurrent paste cannot interleave. The retry is best-effort and never
  downgrades the first Enter's result.
- Retry tests live in a new file to keep agent-paste-draft.test.ts under the
  max-lines budget.

active-agent-note-send is deliberately exempt: its Enter rides the
terminal.send RPC (different transport, server-side sendable guard), has no
local agent identity to read the config from, and only fires on an
already-running agent, where the codex cold-boot submit gate cannot occur.
2026-08-26 22:31:57 -07:00
Neil 3d8b9eeeb0 fix(crash-reporting): stop discarding the whole module list on POSIX dumps (#16687) 2026-08-26 22:31:39 -07:00
Jinwoo Hong 0f522c35e5 fix(remote): gate empty session inventory on host authority (#16546) 2026-08-26 22:30:48 -07:00
Brennan Benson 673842db35 refactor(codex): remove WSL runtime mirror machinery (#16505)
* refactor(codex): remove WSL runtime mirror machinery

* test(codex): drop allowlist entries the mirror removal made stale

runtime-home-service.ts no longer spawns wsl.exe or imports child_process;
both boundary guards fail closed on a stale entry so the goalpost keeps moving.

* fix(codex): drain legacy WSL auth before restart

* fix(codex): await WSL auth drain before restart
2026-08-26 22:24:32 -07:00
Jinjing 9fb5220239 Prevent duplicate file renames when input unmounts after Enter (#16719)
When Enter is pressed to confirm a rename, the input unmounts and its onBlur
handler fires as it detaches from the DOM. Without consuming this event, a
second commitRename call would attempt to rename against the old path. Setting
the cancel flag after capturing the new name causes the trailing onBlur to
return early, preventing the duplicate operation.
2026-08-26 22:17:59 -07:00
Brennan Benson ebcd637db9 fix(codex): launch WSL accounts from direct homes (#16504)
* fix(codex): launch WSL accounts from direct homes

* fix(codex): coalesce WSL auth drains and validate distro homes

* fix(codex): preserve legacy WSL account home metadata

* fix(codex): retain marked WSL home compatibility

* fix(codex): verify the bytes the WSL drain promotes, not an earlier read

The apply script validated the source hash and then re-read it with cp, so a
legacy pane rotating in that window put bytes freshness never judged over a
valid account home. Codex rewrites auth.json in place, so that read can be torn.

Covers it by running the real guest script under sh with a sha256sum shim that
rotates the source between the two reads; without the guard it exits 0.

* fix(codex): harden WSL auth drain races
2026-08-26 21:22:05 -07:00
Jinwoo Hong d60a3c900b Reset stale terminal modes after dead TUI replay (#16379) 2026-08-26 19:49:26 -07:00
Brennan Benson 80246238fc fix(mobile): reset recycled paragraph layout before reuse (#16692)
* fix(mobile): quantize chat pinch font scale so a zoom stops re-measuring the list every frame

A user bubble on a 390pt iPhone painted five lines inside a frame that
reserved six, with the last painted line cut through a glyph at the content
edge and "no longer needed." gone.

The paint is React Native's: a `<Text>` with no `numberOfLines` gets a text
container whose `lineBreakMode` is `NSLineBreakByClipping`
(RCTTextLayoutManager.mm). Measure lays out into `{width, CGFLOAT_MAX}`, paint
lays out into the mounted content frame — so a frame one line short does not
re-wrap, it dumps the remainder onto the last fitting line and clips it, with
no ellipsis. Reproduced on-device against the real component with the message
text held constant, so five painted lines can only be truncation.

Two conditions are each necessary, and removing either makes it vanish over
~6000 measured bubble renders: a pooled `RCTParagraphComponentView` carrying a
shorter row's content frame (`prepareForRecycle` clears `state` but not
`_textView.layoutMetrics`), and whole-list re-measure churn while rows enter
and leave that pool.

The churn was ours. `renderItem` closes over `fontScale`, and the pinch handler
committed a new scale on every gesture frame, so one zoom drove hundreds of
full-list re-measures. The pinch is composed `Simultaneous` with the list's own
scroll, so a stray second finger during a scroll started that storm at scales
the user cannot see — matching the report, whose glyph metrics are `fontScale`
1.0 exactly.

`quantizeFontScale` snaps commits to a 5% grid. React bails out of a same-value
`setState`, so gesture noise now commits nothing and a full-range pinch commits
at most ~20 times. Under the churn that produced 67 defects in 6636 bubble
renders, the quantized build measured 0 in 6064 — with a forced-defect control
bubble flagged in 100% of frames of both runs to prove the detector was live.

This removes the trigger we own; it does not close the RN recycling window
itself. That needs a one-line reset in `prepareForRecycle`, which cannot land
here without refreshing the `patchedDependencies` hash under `mobile/`.

* fix(mobile): reset recycled paragraph layout before reuse
2026-08-26 19:17:31 -07:00
github-actions[bot] 3c79779ae0 Update README downloads badge 2026-08-27 02:00:49 +00:00
Brennan Benson cd5be48a34 fix(workspace-cleanup): show git status for every row (#16690) 2026-08-26 17:23:04 -07:00
Brennan Benson f6e16ba3f8 fix(native-chat): anchor an unmatched chat echo where it was sent (#16117)
* fix(native-chat): anchor an unmatched chat echo where it was sent

The reported symptom was old user messages replaying below every new turn, so the
conversation read as scrambled. The cause was not that the echo failed to match a
transcript row. Claude consumes a mid-turn send through a `queued_command`
attachment and writes no `type:"user"` record for it, so some echoes can never
match, and no amount of matching will change that. The cause was WHERE an
unmatched echo rendered: buildMobileNativeChatTransientData appended every pending
item after the entire transcript, so it re-read below each turn that landed
afterwards.

Render each echo directly after the transcript row it was sent against, using the
baseline the send already captures. An unmatched echo is then at worst a duplicate
in the right position rather than a scrambled one, and it stays visible. Echoes
sharing an anchor keep send order; a send with no baseline, or one whose anchor
folding dropped, still falls back to the tail.

Deliberately NOT fixed by deleting the echo. Inferring from send ordering that an
echo can never match, then removing it, loses the user's own text for a message
the agent did receive, and it cannot fire in the common case anyway - measured
drain groups are 1,017 of size 1 against 55 larger. It also escalates an existing
gap: the count pass has no baseline-tail guard, unlike the glue pass, while
`messages` is a 40-row window that head-trims, resets on reconnect and grows at
the front on loadEarlier, so a false landing there would license deleting a
DIFFERENT outstanding message.

That count-pass gap is real and left for a separate change; anchoring makes its
worst case a duplicate in place rather than a scrambled conversation.

* fix(native-chat): preserve folded echo anchors

* fix(native-chat): preserve forward-folded echo anchors

* fix(native-chat): keep leading folded echoes in place
2026-08-26 17:02:09 -07:00
Brennan Benson 07f2e14c08 refactor(codex): make WSL account surfaces direct-home aware (#16499)
* refactor(codex): make WSL account surfaces direct-home aware

* fix(wsl): keep Codex relay hooks on managed runtime home

* test(wsl): assert relay hooks use managed Codex home
2026-08-26 17:00:21 -07:00
Neil 88ecc739e3 fix(browser): bound the agent-browser daemon's life instead of hoping teardown runs (#16367) (#16588)
* fix(browser): bound the agent-browser daemon lifetime (#16367)

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

Three fixes, in order of how much they cover:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Two invariants that held only because the lane blocked:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Follow-up to f9f973c on two review findings.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix(orchestration): close nested depth readiness gaps

* fix(settings): defer nested depth translations

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

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

* fix(settings): label nested worker depth select

* fix(settings): move nested depth to orchestration

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

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

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

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

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

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

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

Part of #15036 (source-control latency).

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

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

Three changes:

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

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

Part of #15036 (source-control latency).

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

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

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

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

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

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

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

Fixes the source-control half of #15036.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Review follow-ups on the v4 backfill marker:

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

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

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

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

* fix(runtime): centralize pending driver cancellation

* fix(runtime): complete pending driver cancellation extraction

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

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

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

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

The Windows command-line cap and its libuv-aware length estimate move out
of the WSL runner into src/shared/windows-command-line-budget.ts, shared by
both callers.
2026-08-26 14:37:15 -07:00