Commit Graph
8727 Commits
Author SHA1 Message Date
Neil c4e397bcdc perf(renderer): index web session editor tab reconciliation (#8098)
Replace the per-editor-tab Array.find over the worktree's unified tabs with a
lazily-built id/fileId map, and swap two O(n^2) includes-in-a-loop scans for
Set membership. The map is only materialized when a snapshot actually carries
a mirrored editor tab, so terminal-only snapshots pay nothing.
2026-08-15 04:27:58 -07:00
Neil e919f4843d perf: coalesce SSH git status reads (#11696)
* perf: coalesce SSH git status reads

* fix(ssh): key status leases by branch-line-total fork point

The request payload carries branchLineTotalMergeBase but the lease key did
not, so a strict refresh asking for the total could join an in-flight poll
that omitted it (blanking the branch-header chip) or reuse a pre-commit
fork point. Mirrors the guard already on the local path in git/status.ts.
2026-08-15 04:16:00 -07:00
Neil 931cb037c5 fix(ci): satisfy restrict-template-expressions in pr-test-loc-summary test (#14755)
#14738 landed a template literal interpolating an untyped fetch url, which
fails audit:code-quality:type-aware. That audit runs in the static analysis
job, so main is currently red and every open PR inherits the failure.
2026-08-15 03:13:08 -07:00
Brennan Benson 5b7f44278a fix(workspace-cleanup): refuse removal when the owning host is not certain (STA-4343) (#14731)
* fix(workspace-cleanup): refuse removal when the owning host is not certain (STA-4343)

* fix(workspace-cleanup): distinguish host collisions

* fix(workspace-cleanup): recheck host at removal boundary
2026-08-15 01:40:59 -07:00
Neil 393c8764e0 ci: post test vs non-test LoC on pull requests (#14738) 2026-08-15 01:34:44 -07:00
Jinwoo Hong 2eb3e11327 fix(terminal): make close and handles incarnation-stable (STA-4327) (#14590) 2026-08-15 01:25:09 -07:00
Jinjing 2b10767d9d fix(e2e): unblock golden file-link hover and Windows worktree activate (#14720)
* fix(e2e): unblock golden file-link hover and Windows worktree activate

Mac/Windows tmp paths wrap across xterm rows, so locateLink never
found the full absolute path. Print ./package.json instead.

createGoldenWorktree used os.tmpdir() (Windows 8.3 RUNNER~1) while
Git listed the long path, so activateGoldenWorktree never matched.
Realpath after worktree add and compare on the Node side.

* fix(e2e): handle realpath failures in golden worktree creation

Ensure half-built worktrees and branches are rolled back when
realpathSync fails, preventing leaks into later test runs. Extract
error handling into rollbackGoldenWorktree() for consistent cleanup.
2026-08-15 01:02:06 -07:00
Neil 9367169888 refactor(tests): split every oversized test file off the max-lines suppression list (#14728)
* refactor(tests): split oversized test files off the max-lines suppression list

Every `*.test.ts`/`*.spec.ts` that carried an `eslint/oxlint-disable max-lines`
directive is now split into focused, behavior-scoped suites that fit the 800-line
test budget, with shared setup extracted into co-located `*-test-harness.ts` /
`*-test-fixtures.ts` modules (300-line budget). 83 files became ~930; the largest
output is 797 effective lines. `orca-runtime.test.ts` is intentionally untouched.

Test bodies were moved by scripted line-range slicing rather than retyped, so
assertions are byte-identical. The only permitted body edits were mechanical
rebinding where a shared value moved into a harness (e.g. `tmpHome` ->
`homes.tmpHome`).

Registries that enumerate test files were updated in lockstep:
- config/max-lines-baseline.txt: pruned 341 -> 258 entries (all 83 removed).
- config/reliability-gates.jsonc: 33 gates repointed at the split files, with
  assertionRefs split per file where a gate's coverage now spans several.
- .github/workflows/pr.yml: the real-zsh lane now lists the 4 split files that
  actually exercise zsh, so they keep running in the dedicated shell lane.

Also renamed agent-hooks `server-test-fixtures.ts` to `server.test-fixtures.ts`
so the global-fetch call-site audit keeps skipping it, and added `.js` extensions
to the CLI suites' dynamic harness imports (node16 resolution) to unbreak
`build:cli`.

Verification: full suite 52,449 passing vs 52,448 at baseline with zero
assertions lost; `pnpm lint`, `pnpm typecheck`, and `pnpm build:cli` all exit 0;
the terminal-pane e2e spec runs 31/31 headless.

* refactor(tests): split hook-idle arbitration suite that oxfmt pushed over budget

The pre-commit oxfmt pass reflowed pty-connection-hook-idle-arbitration.test.ts
to 811 effective lines, 11 over the test budget. Split the hook-completion side
effect and replacement-agent veto cases into their own suite; both files now sit
well under the cap and the 15 tests are unchanged.

* test: port upstream test changes into the split files after rebase

Rebasing onto main surfaced 27 tests that main had added to files this branch
deleted, plus edits to tests that had already moved. Taking the deletion side of
those modify/delete conflicts would have dropped that coverage silently, so each
upstream change is ported into the split file that now owns the behavior — for
example main's six orchestration mailbox tests land across orchestration-runs,
-send, and -check.

Also repoints `orchestration.notification-mailbox-consistency`, a gate main added
after this branch's gate remap, at those same three split files, and re-prunes
the max-lines baseline against main's (257 entries).

Verified: all 27 upstream test titles present; full suite 52,761 passing with the
only diff vs baseline being 12 tests main itself removed and 3 that moved from
skipped to passing; lint and typecheck exit 0.

* fix(test): flush pending continuations before tearing down terminal test globals

CI shard 5/16 failed on both Node 24 and 26 with `ReferenceError: window is not
defined` from pty-connection.ts, surfacing through
pty-connection-daemon-snapshot-replay.test.ts.

The reattach/settle chains `await` a real promise and then touch `window.api`.
Under fake timers those continuations cannot run, so they only become schedulable
once restoreTerminalTestGlobals() switches back to real timers — which previously
happened immediately before `delete globalThis.window`, so a late continuation
threw and failed the whole file. Flush async ticks in that window instead.

This is latent in the source rather than new: the pre-split 25k-line file kept
running other tests after these, which gave the chains time to settle before
teardown. Splitting the file moved teardown directly behind them.

* fix(test): keep an inert window after terminal test teardown instead of deleting it

The async-tick flush was not enough: the reattach/settle chain can resolve after
teardown regardless of how long we drain, so CI shard 5/16 still failed with
`ReferenceError: window is not defined` from pty-connection.ts.

A real renderer never loses `window`, so deleting it was the artificial part.
Swap in an inert proxy whose properties resolve to callables and whose calls
resolve to undefined, making a late `window.api.pty.*` call a harmless no-op.
The next test replaces it wholesale via installTerminalTestGlobals(), and no test
asserts that `window` is absent.
2026-08-15 00:54:20 -07:00
Brennan Benson 66dfdc456f feat(computer-use): support macOS middle click and stop the silent left-click fallback (#14721)
* feat(computer-use): support macOS middle click and gate the AX click path

`--mouse-button middle` already validated end-to-end through the CLI, the
zod schema, and the provider validator, and both the Windows and Linux
providers honored it. Only the macOS provider rejected it outright with
"middle-click is not yet supported", so the flag was a dead end on the one
platform that has no fallback.

Two changes:

- Add `.middle` to the macOS button mapping. macOS has no dedicated middle
  event family, so it rides `otherMouseDown`/`otherMouseUp` with the button
  number carried by `mouseButton: .center`; that constructor argument is
  honored for exactly the `otherMouse*` types, so no extra field write is
  needed.
- Validate the requested button before the accessibility fast path, and skip
  that path for buttons it cannot express. Previously the raw string was read
  unvalidated, and `performClickAction` only special-cased `right`, so
  `click --mouse-button middle --element-index N` (no modifiers, count 1) fell
  through to `AXPress` — a left click — and reported success with
  `path: "accessibility"`. Any unrecognized button string did the same. This
  matches guards the Windows and Linux providers already had.

The button enum moves into `OrcaComputerUseMacOSCore` so it is unit-testable;
`main.swift` keeps only the CoreGraphics mapping.

Also documents `--mouse-button` in the computer-use skill guide, which never
mentioned the flag, so agents on Windows and Linux had no way to discover it.

* test(computer-use): cover macOS middle click in the real-desktop e2e suite

* test(computer-use): prove macOS middle-click delivery
2026-08-15 00:41:45 -07:00
Neil cceaea1296 perf(usage): index repos for worktree metadata (#8077) 2026-08-15 00:38:13 -07:00
Neil a663f1bd00 fix(terminal): hold a cursor chord until the composing syllable commits (#14730)
* fix(terminal): hold a cursor chord until the composing syllable commits

The composed glyph reaches the pty from the composition session-end handler,
which runs after the chord's keydown. Only Enter was held for that, so every
other chord went straight out on the transport and overtook the text it was
typed after: with 가나 on the line, typing 가나다 and pressing Cmd+Left left
다가나, the composing 다 landed at the cursor's destination.

Defer any sendInput chord while a composition is live or its session has not
yet flushed. Korean 2-Set shows the shape most clearly — the platform replays
the chord unmarked after keyup, so isComposing is already false while the
session is still pending.

No fallback timer on this path. A newline arriving late still arrives, which is
what that timer is for; a chord arriving mid-preedit is the corruption the wait
exists to prevent, and a conversion can hold its candidate window open for
seconds. Dropping the chord costs one keypress, firing early costs a line.

Pane commands are unaffected: they are not sendInput actions.

Fixes #12871

* test(e2e): pin the composing-chord order at the pty

The unit coverage asserts the handler's ordering against a synthetic transport.
This asserts it where it is actually observable: the committed glyph and the
chord reach the pty by two different routes, and only their merged order is
visible to the shell.

Verified to discriminate — against keyboard-handlers.ts from main the same spec
reads 01 eb8ba4 0a, the chord ahead of the syllable, which is the reported
corruption byte-for-byte.

* refactor(terminal): add the composing-chord deferral without touching the Enter path

Nesting the new branch inside the Enter condition re-indented the whole Enter
block, which is the kind of diff that can silently change it. Keeping them as
sibling conditions leaves the Enter path out of the diff entirely.

* test(e2e): pin the renderer to macOS for the Cmd+Left chord

Cmd+Left resolves to \x01 only under the macOS branch of the shortcut policy, so
on a Linux shard the chord produced no byte and the spec passed by measuring
nothing — it failed in CI for that reason, not for the behaviour under test.
Pinning the platform is the established pattern for these specs, and
expectImePlatformPolicy fails loudly if the override does not take.
2026-08-15 00:13:27 -07:00
Neil b849099045 fix(deps): bump transitive nanoid to 3.3.18 (GHSA-2v37-7h3g-55p8) (#14723) 2026-08-14 23:58:20 -07:00
Brennan Benson 54645250e1 fix: keep remote visibility hydration off local startup path (#14674)
* fix: defer remote visibility defaults after startup

* fix: preserve deferred visibility hydration

* fix: retain hydration on no-op runtime selection
2026-08-14 23:18:16 -07:00
Brennan Benson 375b735e9c fix(agent-launch): preserve cold Codex startup drafts (#14688)
* fix(agent-launch): preserve cold Codex startup drafts

* fix(agent-launch): honor startup draft readiness budgets
2026-08-14 23:16:29 -07:00
Brennan Benson 68ca17e46c fix(mobile-native-chat): retire pending bubbles glued into one transcript row (mobile half of #14262) (#14665)
* fix(mobile-native-chat): retire pending bubbles glued into one transcript row

Mobile's native chat retires an optimistic pending bubble only when a
transcript user turn matches its normalized text at the expected ordinal.
When two rapid sends collapse into a single glued user row neither key
matches, so both bubbles pin below every newer reply for the rest of the
session — mobile has a parallel implementation with no glue handling at all.

Trim the send body once at the send seam so the bytes the host writes
verbatim and the reconciliation key describe the same message on every send
path, then add a bounded glue matcher: a greedy cursor walk that may only
consider transcript turns strictly AFTER each send's captured tail, so an
older turn that happens to read like the concatenation can never retire a
newer queued send.

Refs #14262

* fix(mobile-native-chat): harden glued pending retirement

* fix(mobile-native-chat): preserve pending image previews

* fix(mobile-native-chat): bound glue to loaded transcripts
2026-08-14 23:09:23 -07:00
Brennan Benson 0f7707b146 fix(browser): align anti-detection identity signals (#14685)
* fix(browser): align anti-detection identity signals

* fix(browser): hide native Chrome global for Firefox
2026-08-14 22:58:48 -07:00
Brennan Benson 34eca1ecc7 Update mobile 0.0.43 Android download links (#14696)
* Update mobile 0.0.43 Android download links

* Sync French and Portuguese APK labels to 0.0.43
2026-08-14 22:47:00 -07:00
Brennan Benson 1e80c779ad fix(macos): stop reporting every app update as severed TCC attribution (#14655)
Removes the appVersion!==packagedAppVersion check that returned 'severed' after every update, and re-expresses stale-daemon retirement under its own stale_bundle reason.

Field-validated on a machine in the failing state: real ShipIt update cycle, both daemons still version-mismatched, no toast, 200 terminals preserved.
2026-08-14 22:45:16 -07:00
Brennan Benson ab9d1a29a9 fix(worktree): never reissue a generated workspace name (#14350)
* fix(worktree): never reissue a generated workspace name

Generated workspace names were deduped only against currently-live
worktrees, so deleting a workspace returned its name to the pool. A later
workspace could draw the same name, land on the same directory path, and
inherit the previous occupant's agent conversation history — coding-agent
CLIs key their prompt history and transcripts by cwd.

Names are now retired permanently per repo. The registry is written in
main with the name Git actually used (the create loop can advance past a
requested name on collision), and seeded once per run from workspace
directories and surviving agent transcript buckets so already-spent names
are excluded from the start. Suggestions degrade to -2, -3 variants
instead of recycling, and those variants retire too.

User-typed names are untouched: retirement filters suggestions only.

* fix(mobile): honor retired workspace names, on one shared implementation

Mobile hand-duplicated the desktop name-suggestion algorithm and deduped
only against live workspaces, so a phone could still be offered a name
whose deleted workspace left agent conversation state behind at that path.

Both platforms now call one shared selector in src/shared, so the two can
no longer drift. The host publishes retired names as an optional field on
the existing worktree.list response, and mobile fetches them per selected
repo while the create sheet is open — mirroring the desktop hook.

Mobile never calls worktree.list for its catalog (it uses worktree.ps,
which carries rows only), so this is a targeted request rather than a
change to the catalog or its cache. Hosts predating the field omit it and
mobile falls back to live-only dedupe, which is the pre-change behavior.

* fix(worktree): close retirement consistency gaps

* test(worktree): cover retirement runtime contracts

* fix(worktree): retire generated collision names

* fix(worktree): enforce retired names at creation

* refactor(ai-vault): extract the Claude project-dir encoder

The bucket-name encoder and its scope-boundary check were private to the
session scanner, so a second consumer had to reimplement them — and got the
per-character encoding wrong. Move both to a shared module with direct tests.

* fix(worktree): make the retirement seed scan actually match buckets

The bucket encoder collapsed runs of non-alphanumerics while the real one
emits a dash per character, so every dot-path bucket missed and the Windows
default workspace root (C:\...) matched nothing at all. Reuse the shared
encoder and its boundary check, which also stops a repo absorbing a sibling
whose path merely shares its prefix.

Also:
- Derive the workspace leaf by stripping the known encoded parent instead of
  guessing from trailing dash segments, which retired the parent directory's
  name whenever a workspace was named numerically.
- Reuse isAutoGeneratedCreatureBranchName so the -10 and -100 tiers retire.
- Drop the .codex/sessions root: Codex keeps the cwd inside the transcript
  rather than in a directory name, so the scan could only ever see a year
  folder. Reading transcript contents is not a trade this feature justifies,
  so the gap is documented instead.
- Honor CLAUDE_CONFIG_DIR, which relocates the bucket root.
- Delete the unused retirableLeafName export.

Tests write buckets with the real per-character encoding against a fake home,
covering POSIX, dot-directory, Windows drive and WSL UNC roots; all three
platform cases fail against the previous encoder.

* fix(worktree): retire only generated names, keyed by cwd namespace

Two problems in the host-side registry.

Retirement fired for every create, including names the user typed. The
creature pool contains ordinary words — orca, runner, sole, molly, oscar — so
typing a retired 'nautilus' silently produced directory and branch
'nautilus-2' and burned the name for good. Creates now carry an explicit
nameWasGenerated flag; both the skip and the retire are gated on it, and it
defaults to false so CLI and automation callers are unaffected.

The registry was keyed by repo id, but both readers already discarded the id
and unioned by the cwd collision key, because the collision this prevents is
on the path. Keying by that namespace directly fixes several things at once:
entries no longer orphan when a repo is removed, remove/re-add no longer loses
every retirement for an unchanged path, the missing removeProject prune is
moot, and the backfill promise no longer merges into only the first repo id it
saw. The feature is unreleased, so no migration is needed.

Also:
- Memoize the collision key. It runs computeWorktreePath, which for a WSL repo
  is a blocking execFileSync('wsl.exe') whose failure path is uncached, and
  the previous code recomputed it once per repo on every create and every
  listRetiredNames call.
- Drop retiredNamesByRepo from the worktree list result. It had no readers and
  leaked onto 'orca worktree list --json', and its awaited backfill sat on CLI
  selector resolution. The dedicated listRetiredNames RPC keeps its consumers.
- Make the three RuntimeStore methods required. RuntimeStore is file-private
  with two constructors, so the 'older embedders' the optionality protected do
  not exist, and the optional chain silently returned no retirements.
- Revert the unrelated forceDeleteBranch rewrite, and make room under the
  file's line budget by extracting the create-args mapping instead.

* fix(worktree): send name provenance and stop gating Create on the fetch

Desktop and mobile now mark a create as generated-name only when the user
typed nothing and the composer fell back to the suggestion, so the host knows
which names it may retire.

Remove the retired-names loading gate from every create path. The host already
skips retired candidates before doing any git work, so the client gate bought
nothing while it could disable Create for the length of a full mobile
reconnect ladder (the wait had no timeout) and blank the desktop button
between queued creates. The suggestion still waits; the button never does.

Also make the web client call worktree.listRetiredNames instead of hardcoding
an empty list — the method is registered and mobile-allowlisted, so the
comment claiming no wire call existed was wrong — and filter the mobile
response to strings so a malformed row cannot throw during normalization.

* fix(worktree): key retirement by repo id and prune it with the repo

Reverts the collision-key storage key. It was a function of workspaceDir,
nestWorkspaces, worktreeBasePath and repo.path, so toggling any one of those
orphaned every retirement for every affected repo at once — trading a rare
churn (remove/re-add) for a common one. The read path already unions by cwd
namespace at query time, so cross-repo sharing never depended on the storage
key.

Instead, address the growth and orphaning directly:
- Drop the registry in removeProject, and in removeProjectForHost once the last
  host's copy of the repo id is gone, alongside the sparse-preset deletes that
  already follow this convention.
- Bound each repo's registry. The cap sits far above the 552-name pool because
  evicting inside it would reissue a name whose agent state is still on disk;
  only -2/-3 tier accumulation can ever reach it.
- Carry retirements through profile transfer, re-keyed to the destination repo
  id and dropped from the source, mirroring sparsePresetsByRepo.

Separately, fix the backfill merge: the scan promise is cached per cwd
namespace, but it closed over the first repo id that triggered it, so a second
repo in the same namespace received nothing. The scan stays shared; the merge
moves out of the cached promise and runs for whichever repo asked.

Local repos re-seed on re-add through that backfill. SSH repos do not — the
scan cannot see the execution host — which is now stated in the module.

* docs(worktree): spell out why the retirement bound sits above the pool

Names the trap directly: the neighbouring 50/200 bounds cap histories, so
lowering this one to match them would silently start reissuing names whose
agent state is still on disk. Also states that oldest-first eviction is a
deliberate least-bad choice rather than a neutral one.

* fix(worktree): send name provenance from the web runtime client

This client hand-enumerates worktree.create params, so the new optional field
was silently dropped and typecheck could not see it. On web and paired-desktop
the host therefore never received it: generated names were never retired, and
the host-side skip that backstops a stale suggestion was disabled too. The same
client does fetch retired names for suggestions, so it was filtering against a
registry nothing ever wrote to.

The test asserts both directions, and fails without the fix.

* fix(worktree): retire names that took more than one collision suffix

isAutoGeneratedCreatureBranchName strips exactly one trailing -N, which is
right for auto-rename eligibility but wrong here. Once the pool is spent the
suggester emits nautilus-2, and a collision on that yields nautilus-2-3 —
which a single strip leaves as nautilus-2, not a pool name, so retirement
no-opped at exactly the tier where every base name is already gone. Strip
repeated suffixes locally rather than moving the auto-rename predicate.

* perf(worktree): keep the retirement backfill off the blocking WSL probe

The backfill runs on composer repo-select, not just at create time, and it
derived the probe path synchronously — which for a WSL repo with a mirrored
workspace dir reaches getWslHome and its blocking execFileSync('wsl.exe').
A stopped distro froze the main process for up to 5s on composer open.

Adds an async twin of computeWorktreePath and uses it for the probe. Resolving
the home there also warms the shared cache, so later sync callers are free.

Also stops memoizing the collision key when the WSL home is still unresolved:
only the success path is cached upstream, so caching the fallback namespace
would strand the repo there for the rest of the session.

* fix(worktree): hold retired names across a refresh instead of blanking

refreshKey changes on every workspace-list mutation, so create-multiple
refetches after each create and the hook returned an empty list until the
refetch landed — precisely the window in which resetForNextCreate clears the
name field and a fresh suggestion is drawn. Keep the previous answer while
revalidating and reset only when the repo changes; a failed refresh keeps what
was already loaded rather than un-retiring everything.

Also makes the returned array referentially stable, so the suggestion memo
downstream stops rerunning on every refetch.

* refactor(worktree): put the retired-name cache rules on one implementation

The desktop and mobile hooks that fetch retired names had already drifted
four ways. The transports genuinely differ (IPC vs RPC), but the caching
rules must not, and mobile's copy reset to [] on any error -- which
un-retires every name for the rest of the sheet session, the one outcome
retirement exists to prevent.

Moves the rules into src/shared/worktree/retired-name-cache: response
normalization, the never-leak-across-repos rule, and the hold-previous-on-
failure rule. Pure, no React, because src/shared is on the main process's
import graph. Each platform keeps its own transport and effect.

Mobile moves up to desktop's behavior: it now holds the previous answer
through a failed refresh, and refetches when the workspace list changes
instead of never refetching after mount.

Also drops the unused `loading` return. Neither platform consumed it; its
only consumer was the Create-button gate reviewed out earlier, and removing
it makes that regression unexpressible.

* fix(worktree): import shared types from their real modules

Main dropped the src/shared/types barrel, so the retirement module's import
resolved locally but not against the PR's merge base.

* refactor(worktree): bound the retirement registry by tier compaction, not eviction

Retirement is a correctness guarantee — a spent name's directory may still hold
agent conversation state keyed by that cwd — so the 2000-entry cap was the wrong
shape: reaching it handed a name back. At the owner's measured rate (~6.6 pool
names retired per day in one repo) the cap was ~9 months out.

Names come from a fixed 552-entry pool and the suggester only reaches tier N+1
once every tier-N name is taken, so a completed tier is exactly a set that no
longer needs listing. A row is now a watermark plus the names above it: reads
answer at-or-below the watermark with no lookup, and compaction drops the 552
entries the watermark now covers. Bounded at one pool per repo forever, with no
eviction and nothing un-retired.

Tiers can complete out of order (a create-time collision can spend `nautilus-2`
while tier 1 is open), so compaction loops and higher-tier names simply wait.

The RPC result carries the watermark beside the names as a new field; a client
predating it reads the names only and under-retires the compacted tiers, which
degrades to the pre-retirement behavior rather than breaking.

* fix(worktree): preserve generated name retirement across failures
2026-08-14 22:18:36 -07:00
Brennan Benson aaa877d2a2 fix(native-chat): stop glued rapid sends from pinning queued bubbles (#14663)
* fix(native-chat): stop glued rapid sends from pinning queued bubbles

Trim the draft at the send boundary so the PTY body and the optimistic
echo's match key agree, and bound the glue matcher to rows after the
oldest open echo's send boundary.

Fixes #14262

* fix(native-chat): preserve exact prompt payloads
2026-08-14 21:59:43 -07:00
Neil a275f5ad85 fix(skills): cancel and bound abandoned skill discovery scans (#14670)
* fix(skills): cancel and bound abandoned skill discovery scans

A root on a stalled network mount never settles its readdir, so after 30s
the coalescer starts a replacement walk. The abandoned walk kept running
with no cancellation, and nothing counted it, so live filesystem work
accumulated for the life of the process.

Superseding a scan for age now aborts it, and `findSkillFiles` plus the
candidate tasks bail on the signal — that cannot unblock a syscall already
in the kernel, but it stops an abandoned walk issuing more of them. A
budget caps how many abandoned scans may be live at once; past it a
replacement is shed rather than started, and the stalled entry is left in
place so the root recovers on its own once the mount answers.

The budget counts abandoned scans rather than live ones on purpose: one
discovery legitimately walks a dozen-plus roots at once, so a cap on live
scans would shed healthy roots and empty the picker.

A `refresh` still does not abort what it supersedes — on a healthy root
that scan is about to finish and its callers want an answer, and the
existing publish fence already stops it writing a pre-mutation result.

Shed roots report the new `unavailable` skipped reason so they stay
distinct from roots that genuinely are not there.

* fix(skills): propagate a walk abort thrown through a symlinked directory

The broken-link `catch` around the symlink branch also wrapped the nested
`visit`, so an abort thrown from inside a symlinked subtree was swallowed.
When that link was the last entry, nothing afterwards re-checked the
signal and the walk returned a truncated listing as success — the exact
outcome the abort path exists to prevent.

Only the `stat` is guarded now. The test pins the narrow window by
aborting inside the symlink's stat, after the entry loop's check and
before the nested visit's.

* fix(skills): degrade an aborted root instead of failing the whole discovery

Aborting a scan abandoned for age gave the coalescer a way to reject that
it did not have before: every error inside the walk and the candidate
tasks was caught locally, so the task effectively could not fail. Callers
already waiting on that scan now see it reject, and `scanRootShared`
re-threw anything that was not a shed — so one slow root failed the
entire discovery and emptied the picker for every healthy root beside it,
which is exactly what the shed path was written to avoid.

Both endings now mean the same thing to a caller that can degrade a
single root, behind one predicate: shed before the walk began, or aborted
after it was abandoned.

`discoverSkillsOnTarget` runs a second coalescer over the whole target,
where there is no partial answer to degrade to, so it converts both into
a retryable error rather than leaking the internal class to IPC/RPC. It
must not answer with an empty result — zero skills reads as "nothing
installed" and re-offers installs for skills that are present.

Also drops the scan key from the shed message, which carried an absolute
workspace path to a paired client and the renderer's error string, and
corrects the budget comment: it is global and per-abandonment, so one
wedged root can spend it alone, one replacement per 30s window.

* fix(skills): keep the original error as the cause of a stalled-target error

The target layer replaces the internal error with a user-facing one, which
discarded what actually went wrong. Attaching it as `cause` keeps that in
logs while the message stays free of the host path.

Also records why name-matching is narrow rather than broad: the walk and
the candidate tasks catch every filesystem error locally, so the only
AbortError that can escape a scan is the one its own signal raised.

* test(skills): pin that a real abort produces the name the predicate matches

`isSkillRootUnavailableError` decides on `error.name`, and the discovery
and target tests fabricate that shape rather than driving a real abort. So
nothing pinned the linkage: if `throwIfAborted()` stopped producing an
`AbortError` that is an `instanceof Error`, every test would still pass
while a stalled root began failing whole discoveries again.

The walk test already drives a genuine abort, so it asserts the name, the
Error subclassing DOMException relies on, and the predicate itself.
2026-08-14 21:46:09 -07:00
Neil 3a4da06ec3 fix(repos): forget remote-identity deadlines for removed repo locations (#14672)
* fix(repos): forget remote-identity deadlines for removed repo locations

`probeRetryAfterByLocation` is keyed by connection and path and was
written for both resolved and unresolved probes, but never pruned — a
removed repo or a retired SSH host kept its deadline for the life of the
process. `isIdentityRefreshDue` also seeds an entry for every resolved
repo on first sight, so the map grew with repository and host churn even
with no probe activity.

The candidate sweep already enumerates every live repo, so it now
reconciles the deadline map against those location keys. `getRepos()`
reads a hydrated in-memory array, so a repo is never transiently absent
mid-sweep and cannot lose its startup delay or its backoff. Locations
with a probe still in flight are kept, since that probe re-adds its own
key when it settles.

* refactor(repos): drop the no-op in-flight exemption from the deadline prune

The exemption's own comment named the reason it was unnecessary: a probe
re-adds its deadline when it settles, so skipping its key produced the
same map state as deleting it, and only live repos are ever candidates so
nothing read the entry in between.

Removing it also stops a probe that never settles from pinning its
deadline forever — the location's `git remote -v` can already hang with
no timeout, and the exemption turned that one stranded entry into two.
2026-08-14 21:36:09 -07:00
Neil 2252de0f7f fix(workspaces): gate the Jira palette match on the issue's tenant (#14671)
* fix(workspaces): gate the Jira palette match on the issue's tenant

Pasting a Jira issue URL matched any worktree whose linked item carried
the same `jiraIdentifier`, with no comparison of the site it came from.
Jira issue keys are per-project, not per-tenant, so every tenant with a
PROJ project has a PROJ-123 — pasting one tenant's URL could jump to a
worktree tracking a different tenant's issue entirely.

The stored linked URL is the only tenant evidence available here, so
where it exists it now decides, comparing origin and site path the way
the fallback already did. This also covers path-scoped Jira Server
installs sharing one host. The bare identifier still matches when no URL
was stored, since it is then the only evidence there is.

This mirrors the check `isWorkspaceLinkedItemSourceContextMatch` already
makes on the same fields.

* test(workspaces): cover the reachable Jira identifier fallback

The fallback fixture used a blank url, a shape `normalizeWorkspaceLinkedItem`
rejects outright, so it pinned a state that cannot reach the palette. The
reachable way to have no tenant evidence is a url that is present but is not
a Jira browse link, which is now what the test uses.

Also covers a pasted url carrying Jira's `atlOrigin` tracking query, since
the matcher compares pathname only.
2026-08-14 21:36:00 -07:00
Jinjing 3908978ba4 preserve agent badges when searching for tabs (#14677)
* fix: preserve agent badges when searching for tabs

A searched-for tab is exactly when its agent status matters—the map
covers all open tabs, not just recent ones.

* test: scope recent-tabs search test assertions to matched tab

- Make setCommandQuery checking explicit to prevent skipped assertions
- Scope badge verification to the specific queried tab instead of global query
- Add check that only the searched tab appears in results
2026-08-14 20:04:03 -07:00
Brennan Benson e570cade3c fix(mobile): keep polling session tabs while an active terminal is pending-handle (STA-4256) (#14623)
A terminal tab published as `status: 'pending-handle'` renders the session
screen's spinner. Leaving it requires a snapshot that carries the materialized
handle, but a certified-live tabs stream parks `poll()` unless
`hasRecoveryNeed()` says otherwise — and that predicate never considered a
pending terminal. A host that mints the handle without republishing therefore
stranded the pane on its spinner forever: measured live, zero further
`session.tabs.list` calls over 90s while `terminal.list` kept firing every 2s.

Mirrors the existing native-chat recovery-need pattern. Client-only; no wire
change.
2026-08-14 19:20:05 -07:00
Brennan BensonandKaylee Williams 7558fb064a fix(browser): keep browser guests painting when the workbench is hidden (#14599)
* fix(browser): keep browser guests painting when the workbench is hidden

Chromium never paints inside a display:none subtree, so an Electron <webview>
stops emitting CDP screencast frames the moment any ancestor is parked that way.
Orca already models this per pane (browser-page-paintability.ts) and per worktree
surface, using opacity:0 so a phone- or agent-driven page keeps compositing — but
three ancestors above those layers still used `hidden` unconditionally:

  - the App-level terminal workbench container, hidden whenever activeView is not
    'terminal' (opening Settings froze every mobile browser pane),
  - Terminal's root, hidden when there is no active worktree,
  - the split-surface wrapper, hidden when the active worktree has no layout.

A pane-level escape hatch cannot override an ancestor, so all of them have to
agree. Share one predicate across the chain and swap `hidden` for an out-of-flow
transparent layer while a remote controller needs frames.

The predicate ORs automation visibility with the mobile driver, matching the
per-worktree gate. That term is load-bearing, not symmetry: agent-browser
commands acquire a visibility lease and then capture, so gating on the mobile
driver alone left automation from a non-workspace view capturing a blank surface.

Mobile: a stream can report `ready` and then deliver no frames, which cleared the
loading indicator and left an unexplained black rectangle. Key it off actually
having pixels. That also retires the `ready` state and its ref.

Co-authored-by: Kaylee Williams <65376239+KayleeWilliams@users.noreply.github.com>

* fix(browser): keep paint retention off store hot paths

---------

Co-authored-by: Kaylee Williams <65376239+KayleeWilliams@users.noreply.github.com>
2026-08-14 19:13:12 -07:00
73bbe20ca7 fix(i18n): correct Chinese translations (#14299)
* fix(i18n): correct Chinese translations

* fix(i18n): keep startsIn a location label, not a countdown

'Starts in:' renders immediately before request.initialCwd, a directory
path, so the string labels a LOCATION. The new value read as a time delay.
Peer locales agree: ja 開始場所, ko 시작 위치, es 'Se inicia en:'.

* fix(i18n): keep zh terminology aligned with the rest of the locale

- thinking: revert 思考中 -> 思考; it names the Thinking session option
  (alongside Model/Effort/Fast mode) and feeds 切换{{value0}}, so a
  progressive-aspect status reads as ungrammatical there.
- review pills/filters/fixture: revert 审查 -> 评审. English "Draft review"
  was rendering as 草稿审查 on the dashboard card but 草稿评审 in the PR-checks
  row summary; zh.json uses 评审 for the review artifact ~70x vs ~6x 审查.
- notAgent.subtitle: revert 编程智能体 -> 编码智能体, matching 16 other
  "coding agent" keys.

* fix(i18n): translate drop folder prompt correctly

* fix translation

---------

Co-authored-by: DaQun <1.404848e+07+DaQun@users.noreply.github.com>
Co-authored-by: Brennan Benson <brennan@stably.ai>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-08-14 18:58:37 -07:00
fsdwenandBrennan Benson 190d153194 fix: detect external git init on folder projects and upgrade to git repo (#11480)
* Remove scheduled triggers from E2E and README badge workflows

* fix: detect external git init on folder projects and upgrade to git repo

Closes #11477

Three root causes fixed:
1. buildWorktreeBaseDirectoryWatchTargets continued for folder repos
   - now register parent dir as base watch target so poller sees .git creation
2. No path re-evaluated kind after registration
   - add tryUpgradeFolderRepo, checks .git on structural change, calls
   store.updateRepo(repoId, { kind: 'git' })
3. No IPC signal after store update
   - emit repos:changed so frontend git polling re-evaluates

* revert: drop the base-watch-target approach to folder-project git detection

Registering dirname(repo.path) as a base watch target makes the existing poller readdir
the parent directory and stat every sibling, so a project under the home directory scans
the whole home directory on every poll. Replaced by a per-repo .git poll in the following
commits.

* fix(folder-projects): upgrade to a git repo when an external git init lands

Folder projects were registered once as kind: folder and never re-evaluated, so
running `git init` outside Orca left them without any git affordances until a
restart (#11477).

Poll `<repo>/.git` for each local folder project on the base-watcher cadence and
flip kind to git when the marker appears, matching a freshly added git project
(explicit externalWorktreeVisibility, prepared worktree root) before notifying the
renderer and resyncing the base watchers.

One stat per folder project per tick, parked while the window is hidden, backed off
to 30s while no folder project exists.

* fix(folder-projects): reuse the shared repo-change notifier and stop reading the store at attach

Perf audit follow-ups: reading getRepos() synchronously in attachMainWindowServices
broke every test in that file and put O(repos) hydration on the startup path, and a
bare repos:changed send skipped the paired-client broadcast (#11994). Also invalidate
the authorized-roots cache the way the runtime's own folder->git path does.

* fix(folder-projects): keep the project's workspace visible when git's root differs from the stored path

Electron QA found the golden path breaking for a folder project whose path traverses
a symlink: Add Project stores git roots as rev-parse reports them, folder projects
keep the raw path, so after the upgrade the root checkout reads as an *external*
worktree and externalWorktreeVisibility: 'hide' hid the project's only workspace.
Only set 'hide' when git's toplevel matches the stored path.

Also gate the upgrade on isGitRepo so a stray .git file cannot flip a project, and
switch the tests to real git init so both guards are exercised against real git.

* fix(folder-projects): refuse non-root folders and stop re-probing git for a rejected marker

Review round found three real defects:
- A folder project inside another repo's work tree upgraded with repo.path pointing at a
  non-root subdirectory, because git accepts any path inside a work tree. Refuse unless
  git's toplevel resolves to the project directory itself.
- A .git git keeps rejecting re-ran two synchronous git spawns every 2s forever. Cache the
  verdict against the marker's stat signature and re-probe only when the marker changes.
- The poll kept probing after the window was destroyed (macOS keeps the app alive with no
  window), so idle out there instead.

Tests: build the symlink explicitly instead of relying on macOS TMPDIR being one, so the
spelling-mismatch case runs on Linux and Windows CI too; count real git probes; assert
per-project stat counts instead of a modulus; make the idle-backoff test observe the
interval it names.

* fix(folder-projects): refuse the upgrade when it would destroy the project's workspaces

Reproduced in the app: a folder project with extra workspaces went from three sidebar
rows to one within ~2s of an external git init, and their lineage was pruned.

A folder project's extra workspaces are worktreeMeta rows keyed
repoId::path::workspace:<uuid>, and only the folder branch of the worktree listing knows
those keys. Flipping kind moves the repo onto the git branch, which lists git worktree
list (one path) and prunes every lineage id under the repo that is not in it.

Migrating that meta belongs to the listing code that owns both shapes, not to this watch,
so refuse the upgrade for those projects. They keep working exactly as they do today.

* test(folder-projects): wait for the stat count instead of a fixed number of ticks

A tick that spawns git can outrun a fixed wall-clock wait on a loaded machine, so the
rejected-marker test failed roughly one run in six. Poll for the stat count with a
deadline; the load-bearing assertion (git probed exactly once) is unchanged.

* fix(folder-projects): wake git upgrade checks on catalog changes

* docs(folder-projects): align upgrade polling rationale

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-14 18:47:14 -07:00
Jinjing 8b31910ac1 Split worktrees slice (#14643)
* refactor: split worktrees.ts under 400 lines

Move the worktree slice into dest modules under store/slices/worktrees/ and leave a thin public barrel so existing imports keep working.

* refactor: nest worktrees dest modules by domain

Group the extracted slice files into catalog, refresh, create, remove, and related domain folders instead of a flat dest dump.

* refactor: group worktrees dest modules by lifecycle

Collapse the 12 noun folders into listing, create, teardown, metadata, and session so dest layout matches the slice methods.

* rm duplicated mport
2026-08-14 18:44:44 -07:00
Jinwoo HongandJinwoo-H 1d8eaa81c7 fix(orchestration): preserve mailbox delivery identity (#13717)
* test(orchestration): reproduce mailbox pointer mismatch

* fix(orchestration): align pointers with actionable mailboxes

* fix(orchestration): harden pointer reservation lifecycle

* fix(orchestration): bound mailbox reconciliation

* fix(orchestration): make pointer staging restart-safe

* test(orchestration): expect scoped dispatch index

* fix(orchestration): guard skewed inbox indexes

* refactor(orchestration): extract mailbox notification lifecycle

* fix(orchestration): settle mailbox pointer writes

* fix(orchestration): bound mailbox recovery work

* fix(orchestration): fence inactive mailbox snapshots

* test(orchestration): cover mailbox notification boundary

* test(orchestration): pin STA-4325 delivery identity

* fix(orchestration): preserve mailbox delivery identity

* fix(orchestration): harden mailbox settlement

* test(orchestration): make mailbox gates self-contained

* fix(orchestration): preserve paged mailbox ownership

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-14 18:36:51 -07:00
Brennan Benson 78d5920446 fix(orchestration-cli): point dropped mutations at --retry-request (#14586)
* fix(orchestration-cli): guide dropped mutations to idempotent retry

* test(orchestration-cli): preserve read-only drop message

* fix(orchestration): harden mutation replay identity

* fix(orchestration): preserve replay across remints

* fix(orchestration): defer local mutation identity
2026-08-14 18:11:12 -07:00
9cb04b5a35 fix(orchestration): stop fencing fresh-run callers and accept revoked coordinator after takeover (#11582)
* fix(orchestration): stop fencing fresh-run callers and accept revoked coordinator after takeover

LegacyCoordinatorAuthority.resolve was forcing the adopted legacy run for
every orchestration preflight and throwing legacy_read_only at any caller
that could not prove legacy-coordinator identity — including fresh-run
coordinators with no connection to the legacy system. Now only callers
previously known to the legacy run are fenced; unknown fresh-run callers
fall through to the normal current-run handler.

isLegacyCoordinatorHandle returned only the committed principal's handle,
so after a takeover that revoked the principal, worker_done/escalation to
the new coordinator was rejected with "not a retained coordinator". Now
both the retained legacy handle and the current run binding's handle are
accepted, so workers can deliver lifecycle mail to either coordinator.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(orchestration): deliver legacy lifecycle mail to the replacement coordinator

After `run-use --takeover-legacy` revokes the old coordinator principal, a
retained legacy worker addressing worker_done/escalation/ask at the new
coordinator was rejected with "not a retained coordinator", so the dispatch
stayed open forever.

Add a recipient-side permit, isLegacyCoordinatorDeliveryTarget, that also
accepts the current Run binding's coordinator handle. Both handles already
route to run:<id> via resolveLegacyWorkerCoordinatorDelivery.

isLegacyCoordinatorHandle stays narrow: #11745 reused it as the caller-side
fence jurisdiction, where widening it fences MORE callers and replaces an
actionable run_required with dead-end legacy_read_only guidance.

Co-Authored-By: Leonardo <leonardo.marciano@toolzz.me>

* fix(orchestration): keep the delivery permit in step with the takeover router

Round 1 review fixes on top of the recipient-side permit.

isLegacyCoordinatorDeliveryTarget accepted any handle bound as the Run's
coordinator, but resolveLegacyWorkerCoordinatorDelivery only promotes to
run:<id> once the legacy principal is no longer committed. bindRun leaves a
committed principal alone when it rebinds without a takeover over live legacy
work, so a coordinator restarting inside the legacy pane produced a permitted
send that routed legacy_direct to a handle no reader can see: current-contract
inboxes require current_delivery, and legacy mail requires a principal on that
handle. Gate the binding branch on the same takeover test the router uses, so
that send goes back to request_mismatch instead of vanishing.

Tests: cover the ask call site (it had none — reverting question.ts alone
failed nothing), assert the takeover bind landed inside the helper rather than
two assertions downstream, and replace the not-legacy_read_only assertion on
the fence guard with the concrete outcome it means to protect.

Drop the fresh-run coordinator tests: they guard #11745/#11802, already on
main and already covered by orchestration-legacy-fence-jurisdiction and
orchestration-11745-regression-verification, and they pass with this fix
reverted. Rename the file to what it now contains.

* refactor(orchestration): drop the unreachable pane-key clause from the delivery permit

The permit's takeover branch must mirror resolveLegacyWorkerCoordinatorDelivery,
which tests only the principal status. Every runs-table write sets
coordinator_handle and coordinator_pane_key together, so the extra pane-key
term never fires — and if it ever did it would deny mail the router would have
promoted to the readable run mailbox.

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-14 18:03:07 -07:00
Brennan Benson 83d79f3846 fix(workspace-cleanup): make the Filters panel scrollable (#14629)
* fix(workspace-cleanup): make the filter panel scrollable

The height cap sat on the ScrollArea Root, but the Radix viewport inside is
h-full — under an indefinite root height that collapses to auto, so the
viewport grew to full content height with nothing to scroll while the root
clipped at 420px. The last facet groups (Archived, and the tail of Workspace
status) were unreachable behind the footer.

Move the cap to the viewport, which is what the ScrollArea wrapper's
viewportClassName exists for. Adds a regression test that pins the cap to the
viewport; it fails against the previous markup.

* fix(workspace-cleanup): keep filter footer visible
2026-08-14 17:59:25 -07:00
Neil a3b472d050 refactor(renderer): decompose App.tsx into an app-shell module (#14607)
* refactor(renderer): decompose App.tsx into an app-shell module

App.tsx was 2831 lines behind an `eslint-disable max-lines` and a
grandfathered entry in the max-lines ratchet baseline. It is now 92 lines:
a root element, the shared providers, and three children.

The body is split by concern into src/renderer/src/app-shell/:

- use-app-chrome-layout — titlebar/sidebar/workbench layout derivations
- use-floating-workspace-panel — overlay open state, persistence, return focus
- use-app-startup-hydration — the boot chain (order preserved verbatim)
- use-app-session-persistence — session writer + shutdown checkpoint
- use-persisted-ui-writer / use-document-appearance /
  use-runtime-graph-sync / use-window-visibility-effects
- use-onboarding-and-feature-tips — first-run education gating
- use-global-keybindings + app-command-handlers — window shortcut dispatch
- use-app-shell-services — app-level subscriptions that outlive any surface
- AppWorkspaceShell / AppRootSurfaces / AppBackgroundServices / titlebar parts

Two startup branches that no longer needed to sit inline moved to
src/renderer/src/startup/: startup-ssh-connection-restore and
startup-degraded-recovery.

No behavior change. Sibling order of root overlays and modals is preserved
so stacking is unchanged; the sidebar's virtualized scroll refs still live
above the sidebar's remount boundary.

The source-assertion tests in app-startup-routing.test.ts follow the code to
its new files. The one that claimed to check "first-window startup services
before terminal reconnect" was matching the degraded-recovery block, not the
success path; it now asserts the success path, and the degraded ordering
keeps its own dedicated test.

Removes the disable comment and drops App.tsx from
config/max-lines-baseline.txt.

* refactor(renderer): move app-shell ref writes out of render

React Doctor's changed-lines purity gate flags three render-phase ref
writes in the new app-shell files. The patterns predate the split, but
moving them into new files brings them into the gate.

- use-app-chrome-layout: the terminal-workbench latch becomes state set
  during render (the pattern use-lazy-modal-mounts already uses). The
  `canMountTerminalWorkbenchNow ||` term keeps the current render correct,
  so the latch only has to be visible to the next one.
- use-app-startup-hydration: the onboarding callback ref syncs in an effect
  declared before the boot chain, so it lands first on mount. The callback
  is a `useCallback([])`, so its identity never actually changes.
- use-global-keybindings: the shortcut-state mirror syncs in useLayoutEffect,
  which commits before any key event can read it. Key events are discrete,
  so handlers always observe committed state.

All three now hold committed state only, so nothing can leak from a render
React discards — the behavior the gate is protecting.

pnpm run check:react-doctor:changed: 0 errors (was 3).
2026-08-14 17:46:44 -07:00
Brennan Benson e12b22c435 fix(terminal): bracket agent-pane pastes so a pasted newline can't submit the draft (#14456)
* fix(terminal): bracket agent-pane pastes so a pasted newline can't submit

A paste of <=64KB is handed to xterm's term.paste(), which rewrites newlines to
CR and wraps in ESC[200~/ESC[201~ only when its parser observed DECSET 2004.
Windows ConPTY never forwards that mode, so an unbracketed pasted newline
reaches a TUI agent as Enter and submits the draft parked in its composer.

The existing force-bracket guard keyed on isWindowsUserAgent() - the client's
platform - but the ConPTY can be on a remote host, so the guard was off in
exactly the configuration that needs it. Gate on the pane's own TUI agent
instead, applied to all four paste entry points; middle-click primary selection
had no force flag at all.

The agent status row is retained deliberately and is not lifecycle-driven, so
it can outlive the agent. Veto on shell-confirmed foreground (OSC 133;D) and on
a row rehydrated across an app restart. The freshness TTL and a state check are
both unusable here - an idle-but-live agent sits at done and still needs
bracketing.

Refs STA-4294

* fix(terminal): key agent-paste bracketing on live evidence, not shellForeground

Measured against a real pane: shellForeground is republished only at OSC 133
boundaries, so a shell without 133 integration leaves it latched true while an
agent owns the foreground. Vetoing on it silently reinstated the submit bug the
parent commit fixes - the parked draft was sent on paste with the gate in place.

Prefer process-confirmed agent identity when present, keep the restart-rehydrated
veto, and drop the shellForeground veto. Erring toward bracketing costs a literal
ESC[200~ in a non-2004 program; erring the other way sends the user's draft.

Refs STA-4294

* docs(terminal): record why paste bracketing keys on the agent, not the mode bit

Measured in real ptys before taking the obvious alternative. A tri-state on
DECSET 2004 (observed-on / observed-off / never-observed) does not work: zsh 5.9,
fish 4.8.1 and bash >= 5.1 announce and withdraw the mode cleanly, but macOS
/bin/bash 3.2, /bin/sh, bash 4.4 and any shell with bracketed paste disabled emit
nothing at all, byte-identical to a bare `cat`. Silence cannot be read as consent.

Agent identity disambiguates it in the one direction that matters: agents always
enable the mode, so silence on an agent pane means the announcement was lost in
transit, never an opt-out. Also measured: bracketing a program that never
negotiated is worse than useless - the markers land as literal payload bytes and
ICRNL still turns the CR into a submit - so the gate stays narrow.

Refs STA-4294

* fix(terminal): guard the paste pane key and document the evidence policy

Readiness review follow-ups, none behaviour-changing for reachable inputs.

makePaneKey throws on a malformed leaf/tab id. It is unreachable today (pane.leafId
is a minted UUID and the same pair is already called unguarded from a hotter site),
but the failure mode was bad: the throw escapes before the paste helper's catch is
attached, so the paste would be a silent no-op with no error surface. Degrade to the
pre-fix path instead, with a test.

Also record two things a future reader needs: the process-confirmed branch is dead
for remote-runtime and SSH panes because foreground tracking is disabled there, so a
remote pane's status row is its only evidence; and why this resolver deliberately
omits the shellForeground/routingRevoked/routingTrusted gates its two siblings
enforce - they route input bytes, this only wraps a paste whose payload is
ESC-sanitized downstream.

Refs STA-4294

* fix(terminal): encode Windows agent paste newlines as input records

* fix protected paste handling in dashboard previews
2026-08-14 17:18:14 -07:00
Wooseong KimandBrennan Benson c0f9dcc8f4 fix(git): allow bounded override of worktree-add timeout (#12823)
* fix(git): allow bounded override of worktree-add timeout

Keep the 180s OneDrive stall guard as the default floor, but accept
ORCA_WORKTREE_ADD_TIMEOUT_MS up to 30 minutes for legitimately slow
checkouts (large repos, git-crypt).

Preserves a closed upper bound; never removes the timeout.

Fixes #12696

* review: read the worktree-add timeout override at the call site

Keeps WORKTREE_ADD_TIMEOUT_MS meaning the 180s default instead of
silently becoming an env-resolved value, drops the redundant third
export, and folds three parse guards into the clamp Number() already
covers.

Adds the missing coverage that addWorktree actually passes the raised
timeout to git — reverting the call-site wiring previously failed no
test.

* review: clamp an infinite override to the max and warn on a discarded value

Number.isFinite sent ORCA_WORKTREE_ADD_TIMEOUT_MS=Infinity — the natural
way to say 'stop killing my checkout' — back to the 180s default, handing
the operator the exact failure they set the variable to escape. Reject
only NaN and let the clamp handle magnitude.

Every discarded or clamped value was silent, so the '=300' seconds/ms
mixup the floor exists for produced an identical 'git timed out.' with no
signal. Warn once, naming the accepted range.

Also pins both bounds as literals and refreshes two comments that no
longer described the code.

* review: name the real problem in the override warning

An unparseable value took the range branch, so ORCA_WORKTREE_ADD_TIMEOUT_MS=600_000
— the literal style this file itself uses — reported a bound violation that had not
happened. Split the two cases and quote the value so trailing whitespace is legible.

Uses the file's [git/worktree] log prefix, drops a #7225 citation that describes a
startup/UI-freeze report rather than a large checkout, and states why the ceiling
is 30 minutes.

* review: give the resolver a contract and stop splitting the timeout block

Moves resolveWorktreeAddTimeoutMs below the constants so the module's five
timeouts read as one group, and replaces the edge-case JSDoc with the actual
contract — what it reads, what range it clamps to, when it warns.

Comments the NaN-comparison the unparseable-value warning depends on, since
'fixing' it with an isNaN guard would silently delete that warning. Test
spy now matches the file's local-spy idiom, the bound literals get their own
test, and the env stub deletes the key instead of setting an empty string.

* review: pin the clamp-up warning text

Every warn assertion covered a value clamped DOWN to the floor, so swapping
the discriminator back to !Number.isFinite passed all 74 tests while telling
an operator that ORCA_WORKTREE_ADD_TIMEOUT_MS=Infinity 'is not a number;
using 1800000ms' — naming the number it just used, and misdirecting exactly
the person this override exists for. That mutation now fails.

Restores the STA-1292 rationale the call-site comment had dropped, and puts
the env var name and issue back on the ceiling constant.

* review: correct two comment claims about the warn path

The JSDoc promised a warning 'whenever the value is not used verbatim',
but trimming and fractional truncation deliberately stay silent — the
suite asserts exactly that for '300000.9', so the contract contradicted
the tests below it.

The condition comment named 'NaN !== NaN', a comparison that never runs:
resolved is the default whenever requested is NaN, so the live comparison
is 180000 !== NaN. Same warning, right mechanism.

* review: fix the ceiling arithmetic and name the default/floor coupling

30 min against a 3.5 min worst case is ~8x, not ~10x — the comment's only
job is justifying that number. States the actual cost too: a genuine stall
now blocks a create for up to 30 min instead of 3.

WORKTREE_ADD_TIMEOUT_MS silently serves as both the default and the clamp
floor, so tightening it to fail faster would also re-admit the
'=300 means seconds' mistake the floor exists to catch. Now said out loud.

Widens 'git-crypt' to 'a slow content filter' so an LFS or large-monorepo
reader does not conclude their case is different, drops a call-site clause
that restated the constant's comment, and corrects a test comment that
claimed an idiom the code does not use.

* review: pin the warning's prefix and variable name

Deleting the [git/worktree] prefix left the suite green — all three warn
assertions started matching after it. A diagnostic nobody can grep for is
not a diagnostic, so one assertion now pins the whole line.

* review: correct the last three comment claims

A blank value is clamped (Number('') is 0) and stays silent, so 'warns when
a value is rejected or clamped' had an exception the test below it already
exercised. Now says non-blank.

The floor-coupling note claimed lowering the default re-admits the
'=300 means seconds' mistake; it does not — a 60s floor still clamps 300.
The actual cost is that the minimum any override can request drops with it.

Drops the spy comment rather than rewriting it a third time; beforeEach and
afterEach say it themselves.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-14 17:00:16 -07:00
Brennan Benson 0e8d52912c fix(source-control-ai): stop duplicating singleton CLI flags in agent argv (#14585)
* fix(source-control-ai): stop duplicating singleton CLI flags in agent argv

Recipe CLI arguments and agent command overrides were appended on top of
Orca's generated flags, so a user-supplied --model produced a repeated
flag. yargs collapses a repeated flag into an array, crashing OpenCode
with "j.split is not a function"; clap rejects it outright for Codex.

Declare the at-most-once option groups per agent spec and fold every
user-supplied occurrence into the generated slot, with recipe args
outranking a command-override prefix.

Fixes #12305

* fix(source-control-ai): preserve command override argv order
2026-08-14 16:53:43 -07:00
Jinwoo Hong 19bc1ccf7e Keep other-client workspace filter visible during catalog hydration (#14595) 2026-08-14 16:12:49 -07:00
Jinwoo Hong 500b72d8ef fix(vm): harden provisioned root ownership and cleanup (#14477)
* fix(vm): verify provisioned root ownership

* test(vm): retry transient removal menu

* test(vm): stabilize provisioned root teardown

* fix(vm): clarify recipe-owned cleanup

* fix(vm): pin provisioned root source commit

* fix(vm): make runtime cleanup user-cancellable
2026-08-14 19:04:55 -04:00
Jinwoo Hong 32f46f9a24 fix(vm): keep failed cleanup retryable (#14476) 2026-08-14 19:04:54 -04:00
Jinwoo Hong 8b22f044f5 fix(vm): preserve runtime sidecar rollback compatibility (#14444)
* test(vm): reproduce runtime store rollback poisoning

* fix(vm): keep runtime sidecar rollback-readable

* fix(vm): harden rollback-compatible runtime persistence

* fix(vm): publish rollback lifecycle authority first

* test(vm): harden rollback compatibility coverage
2026-08-14 19:04:54 -04:00
Jinjing 3a212584ec feat(native-chat): Extra high grok effort per model (#14577)
* feat(native-chat): offer Extra high grok effort per model

Slice grok's reasoning-effort menu by each model's advertised ceiling so
4.6 can reach xhigh while 4.5 stays at high, and keep the untouched
default at high so launch argv does not silently escalate.

* fix(grok): parse dashed rows in grok models listing

Grok stars only the default model and dashes the rest. A star-only
bullet dropped 4.5 from the picker once discovered models were
authoritative.
2026-08-14 15:52:09 -07:00
Brennan Benson 9bb8836bb6 fix(agent-launch): wait longer for cold-boot Codex composer before dropping prompt (STA-3367) (#12853)
* fix(agent-launch): wait longer for cold-boot Codex composer before dropping prompt (STA-3367)

Continue-in-new-session pastes the handoff prompt once Codex renders its
composer glyph, gated on an 8s readiness budget. A cold/first-run Codex can
take longer than 8s to mount its composer, so the wait timed out and the
prompt was silently dropped into an empty terminal.

Marker-gated ready signals (Codex glyph, opencode show-cursor) are positive
proofs: the paste fires only when the marker actually renders, so a longer
budget can never paste prematurely — it only tolerates slow cold boots. Give
those signals a 20s budget while the markerless quiet-window signal keeps 8s.

* fix(agent-launch): share the composer-readiness budget across all three delivery owners (STA-3367)

The cold-boot fix was correct but landed as a single-path exception, and it
double-spent its own budget. Three follow-ups so the behavior is a system rule:

1. Split the PTY-spawn wait from the composer wait in pasteDraftWhenAgentReady.
   Both were handed the same budget, so a codex tab took up to 41s to report a
   dropped prompt. "Tab has a PTY" and "composer accepts input" are separate
   states: spawn keeps a fixed 8s, and the readiness budget now starts once the
   PTY exists, so a slow spawn can't shorten a cold composer's window.

2. Move the per-signal budget to draftPasteReadyBudgetMs() beside the shared
   readiness scanner. The budget is a property of the ready signal — only that
   module knows which signals are marker-gated — so all three delivery owners
   (renderer tab paste, renderer startup paste, main runtime startup paste)
   consume one policy instead of three hardcoded 8s constants.

3. Give the main-runtime startup paste the process-ownership fallback both
   renderer paths already have. It resolved null on budget expiry, silently
   dropping the prompt on worktree-create / CLI / remote-host delivery — the
   same STA-3367 failure, on the path the original fix didn't reach.

Adds coverage for the main-runtime waiter, which had none.

Test: vitest src/main/runtime src/shared src/renderer/src/lib
      src/renderer/src/components/terminal-pane — all green; tsc clean.

* test(agent-launch): consume the shared readiness budget instead of restating it

Hardcoding 20000 in the runtime waiter test meant it would keep passing if
OrcaRuntimeService stopped consuming draftPasteReadyBudgetMs — the exact drift
this PR exists to prevent. The literal values stay pinned once, in the scanner
test.

* refactor(agent-launch): collapse the readiness budget to one flat timeout

The per-signal budget (marker 20s / quiet-window 8s) tied the timeout to how
readiness is DETECTED. The budget is really a property of how slowly an agent
can boot — a marker, a quiet window, and a process check all wait out the same
cold start — so one number covers all three signals.

Replaces draftPasteReadyBudgetMs() with DRAFT_PASTE_READY_TIMEOUT_MS: drops a
constant, a branch, and two tests, and removes the only reason a delivery path
needed to know which signal class it was using.

Cost: a launch that never emits DECSET 2004 now surfaces its 'prompt not sent'
toast at 20s instead of 8s. That is the failed-launch path only; successful
markerless delivery still resolves on the 1.5s quiet window as before.

* fix(agent-launch): constrain cold Codex readiness budget

* fix(agent-launch): observe Codex readiness from PTY bind

* fix(agent-launch): anchor early Codex prompt to TUI screen
2026-08-14 13:33:05 -07:00
Brennan Benson e2d309e9cd fix(agent-map): commit slider changes once (STA-4281) (#14442)
* fix(agent-map): commit time slider changes

* fix(agent-map): cancel invalidated slider commits

* fix(agent-map): reconcile collapsed slider drags
2026-08-14 13:10:51 -07:00
Brennan Benson cbed44410a STA-4276: preflight Codex in Command Prompt and Git Bash (#14441)
* fix(terminal): preflight Codex in Windows cmd and Git Bash

* test(terminal): run Windows preflight through ConPTY

* test(terminal): isolate cmd harness exit status

* test(terminal): allow slow Git Bash ConPTY startup
2026-08-14 13:10:29 -07:00
Brennan Benson b614908ccc fix(codex): resolve the launch preflight to a verified absolute Orca CLI path (STA-4270) (#14458)
* fix(codex): resolve the launch preflight to a verified absolute Orca CLI path (STA-4270)

The Codex launch preflight carried a bare command name ('orca' / 'orca-dev') in
ORCA_CODEX_LAUNCH_PREFLIGHT. The codex() wrapper that invokes it is emitted after
the user's profile scripts are sourced, and those routinely rewrite PATH, so the
name was resolved against a PATH Orca neither controls nor can predict.

Resolve and verify the shipped CLI's absolute path instead, and return null when
no path verifies so the preflight is skipped rather than run against an
unidentified program.

* test(codex): align bundled launcher fixture across CI hosts
2026-08-14 13:09:50 -07:00
Jinwoo Hong 552dedd26b Keep remote workspace filter visible (#14579) 2026-08-14 13:06:57 -07:00
Jinwoo Hong ff9bc0f079 fix(orchestration): wait for Codex composer render (#14575) 2026-08-14 12:56:35 -07:00
Brennan Benson ebb60a0757 fix(devin): preserve JSONC comments when writing hook config (#14199)
Devin documents config.json as JSONC. Installing hooks parsed it with
jsonc-parser and then reserialized with JSON.stringify, silently dropping
the user's comments, key order, and formatting on every install.

Edit the original text with modify/applyEdits one hook event at a time so
untouched entries keep their attached comments, and let both writers accept
pre-serialized text so the shared atomic write and rolling backup are reused.

The two existing tests asserted with JSON.parse, which could only pass once
the comment had been stripped; both now parse as JSONC and assert the
comment survives.
2026-08-14 12:50:35 -07:00
Brennan Benson b3c4ba9ed3 fix(daemon): harden checkpoint admission after per-session isolation (#14385)
* fix(daemon): bound checkpoint overlay fanout

* fix(daemon): retain checkpoint admission through deadlines

* docs(reliability): refresh checkpoint gate evidence

* fix(daemon): prevent checkpoint admission starvation

* docs(reliability): clarify checkpoint bounds

* fix(daemon): distinguish checkpoint admission warnings

* fix(daemon): bound checkpoint admission diagnostics
2026-08-14 12:21:55 -07:00