* fix(git): narrow fork-remote fetch refspecs to tracked branches
git remote add with no -t writes the wide +refs/heads/*:refs/remotes/<name>/*
refspec, so any later plain `git fetch` (user, agent, or Orca's own Fetch
action) re-imports a fork's entire branch set and its tags -- one real
machine had ~50 leaked/wide fork remotes producing 59,716 remote-tracking
refs. Mint and reuse now pin -t <branch> --no-tags; a rate-limited sweep
narrows and cleans up remotes minted before this fix; gitFetch self-heals
when a narrowed remote's tracked branch is later deleted upstream.
Refs #17828
* fix(git): soften narrow fork-remote refspec against deleted upstream branches
A bare `git fetch` in a worktree checked out on a fork-PR branch resolves to
the pr-* remote via branch.<name>.remote -- not origin -- making it the
dominant fetch shape in Orca's terminal-centric, agent-driven usage. The
previous literal-refspec design hard-failed that fetch ("couldn't find
remote ref") the moment the tracked branch was deleted/renamed upstream,
which is not the narrow edge case it was first described as.
Switch to a trailing-`*`-suffixed refspec source/destination
(refs/heads/<branch>*:refs/remotes/<name>/<branch>*). Verified against real
git: this restores wildcard zero-match tolerance (silent no-op instead of a
hard failure) and lets plain `git fetch --prune` reclaim the stale ref once
the branch disappears, at the cost of also matching sibling branches that
share the literal name as a prefix -- a materially smaller widening than the
original unbounded-import bug.
Also close a race with #17842's orphaned-pr-remote reconciliation sweep:
both sweeps read the same worktree-metadata store to pick candidate remotes,
so reconciliation can `remote remove` a remote this migration is
concurrently narrowing. `ensureRemoteTracksBranchNarrowly`'s plain `config
--add` would silently resurrect a url-less config section in that case;
re-check `remote.<name>.url` (via the new `remoteHasUrl`, plumbing rather
than porcelain `remote get-url`, which falls back to echoing the remote name
as a bogus URL) after the narrowing writes and remove the section if it's
gone.
* fix(git): update stale fork-remote mint assertions for -t/--no-tags and wildcard-suffix refspec
Four test files still asserted the pre-#17828 remote-add shape or the
literal (non-wildcard-suffixed) fetch refspec from before the
deleted-upstream-branch softening commit, so CI went red on that HEAD:
- worktree-push-target-refspec-real-git.test.ts: the migration fixture
asserted a hardcoded tracked-ref count before narrowing. Under git
>= 2.44, `followRemoteHEAD` auto-creates a `refs/remotes/<name>/HEAD`
symref on the first fetch matching the full wildcard refspec, adding
one untracked ref. Made the count/assertions robust to that ref's
presence instead of hand-tuning the constant per git version.
- worktrees-wsl-runtime-routing.test.ts: assertions predated both the
`-t <branch> --no-tags` mint change and the wildcard-suffix refspec
change; updated to the full, correct call sequence and confirmed the
WSL routing options (cwd, wslDistro) are threaded to every call.
- worktrees-create-metadata-persistence.test.ts and
orca-runtime-tests/worktree-removal-and-reconciliation.spec.ts: same
class of staleness, found via CI job log cross-referencing rather
than being explicitly flagged.
Verified out of scope: the SSH fork-remote mint path
(prepareWorktreePushTargetSsh) is untouched by this PR -- it never
persists a `remote.<name>.fetch` refspec at all, using
provider.fetchRemoteTrackingRef for a targeted per-branch fetch
instead -- so worktrees-ssh-fork-push-target-remote.test.ts needed no
change.
* fix(git): migrate pr-* remotes with zero worktree-metadata trace too
The migration sweep's candidate discovery was purely metadata-driven
(store.getAllWorktreeMeta()), so a pr-* remote whose every referencing
worktree was removed outside preserve-on-delete (metadata purged, not
just the worktree) was permanently invisible to it and stayed on the
wide default forever.
Field data from a manual migration run against a real user's repo (31
pr-* remotes, 34,637 tracking refs, only 18 actually needed) found
exactly this: 15 of 31 remotes had no branch pinning them at all.
Widen discovery to every pr-* remote git reports on disk, in addition
to metadata-derived candidates. For a remote with no branch provenance
from either metadata or surviving branch.*.remote/.pushRemote config,
there's nothing to narrow *to* -- clear its fetch refspec entirely
instead (stays pushable, imports nothing on a plain fetch), gated on
it still carrying the untouched stock wide default so a user's own
custom pr-*-named remote isn't touched. Removing the remote outright
stays #17842's job.
Adds clearForkRemoteFetchRefspec (fork-remote-refspec.ts), 3 new
mocked-exec tests, and a real-git integration test proving a
subsequent plain `git fetch` on the cleared remote imports nothing.
Git running inside a WSL distro writes `.git` gitdir pointers, and answers
`status --porcelain`, in the guest namespace. Node reads both back in the
Windows main process, where `/mnt/c/repo/.git` resolves to `C:\mnt\c\repo\.git`
and `/home/me/wt` names nothing at all. Four fs probes were built on those
fabricated paths and always came back "absent":
- `detectConflictOperation`'s four marker probes, so merge/rebase/cherry-pick
badges silently went missing.
- `parseUnmergedEntry`'s compat existence check, so every `deleted_by_us` /
`added_by_them` conflict rendered as 'deleted' regardless of the working tree.
- `findExistingWorktreeSymlinkPaths`' `lstat` from status, so Orca's own shared
symlinks (node_modules and friends) showed as user changes.
- the same `lstat` from the hosted-review dirty preflight, which fails closed:
an unreadable shared symlink read as uncommitted work and blocked PR/MR
creation outright.
`resolveGitDir` computes the host spelling of the worktree once and uses it for
both the gitfile read and the pointer resolve, so a guest-spelled worktree path
is reached at all, and a relative pointer (`worktree.useRelativePaths`, git
2.48+) resolves against a spelling Win32 understands. The pointer itself now
goes through the already-landed `resolveGitMetadataPath`, and the function gains
an optional `{ wslDistro }` for a caller whose base path does not encode a
distro. `detectConflictOperation` forwards it, and the three callers that reach
it -- status-read, the runtime RPC, the `git:conflictOperation` IPC -- pass the
git options they already hold. The return type stays `Promise<string>`.
`resolveWorktreeHostPath` is the same rule applied to a worktree path, used by
status-read for the two working-tree probes and by the review preflight. Both it
and `resolveGitMetadataPath` now treat only a single-leading-slash path as guest
namespace: `//wsl.localhost/...` is already a host UNC spelling, and translating
it prepended a second share prefix.
`readWorktreeDiffStamp` needed the same one-namespace guarantee, since moving
translation inside `resolveGitDir` would otherwise make its HEAD and index real
while the working-tree stat stayed fabricated, letting a settled diff survive
every edit. #17896 landed that change first, so it is no longer in this diff;
its version is a superset and all four components already resolve from one
`hostWorktreePath`. What remains here is the `resolveGitDir` gitfile-pointer
fix that #17896 explicitly deferred, which `worktree-diff-stamp-host-paths.test.ts`
pins.
`getConflictCompatibilityStatus` moves from `existsSync` to async `access`, for
the same reason `detectConflictOperation` did: once these paths are real they
are `\\wsl.localhost\...` shares, and a sync probe per asymmetric conflict
blocks the Electron main thread for a 9p round trip on every status poll.
Per-platform delta:
- native Windows, no WSL: no behavioral change. Nothing here starts with a
single `/`, so no path is translated. An absolute pointer is now returned
verbatim rather than separator-normalized; every consumer re-joins or
normalizes it before use.
- macOS/Linux: no change. Guest-pointer translation is gated to win32, and a
caller-named distro is ignored off Windows.
- Windows + WSL: drvfs pointers and drvfs-spelled worktrees now resolve to their
drive spelling instead of `C:\mnt\...`; a non-drvfs guest path resolves
through the named distro's UNC share, or stays verbatim (ENOENT -> existing
fail-safe) when none is named.
- SSH/relay: none. Those paths return before any of this via the provider
branch; `src/relay/git-handler-status-ops.ts` keeps its own resolveGitDir.
- folder workspaces, GitLab: none. Neither is on these code paths.
Doubles the maximum UTF-8 bytes accepted for manually shared artifacts,
enabling users to share larger content while maintaining recovery and
transport constraints.
* perf(worktree): make head-identity refresh incremental
Head-identity refresh re-read `gitdir` + `HEAD` + a loose ref for every
linked worktree on every watcher burst. On a 973-worktree checkout that is
~2,800 metadata reads (~1.0s of main-process fs I/O) per event, and the
debounced pipeline fires on every commit in any worktree — so fleet-wide
agent activity degenerated into a continuous scan loop.
Watcher events already name the admin dir that changed. Classify each event
into a head-identity scope, memoize per-entry identities, and re-read only
the scoped entries. Refs resolved during a pass are replayed onto cached
entries that share the same branch, so `git worktree add --force` siblings
stay current without extra reads.
Invalidation stays conservative: an absent scope (watcher failure, event
overflow, cold start) means a full re-read, `packed-refs` writes invalidate
every entry, misses are never memoized, and one refresh per minute is
promoted back to a full re-read to bound the window where a ref moves with
no event under any admin dir.
Measured on the reported 989-entry checkout (macOS/APFS): one-worktree
commit 2,816 -> 2 file reads, 61ms -> 0.5ms p50 with an identical page
cache; a 20-worktree debounce burst costs 57 reads / 9.7ms; an external
`git worktree add`/`remove` costs one readdir / 1.0ms.
Refs #17828
* fix(worktree): harden incremental head-identity invalidation
Two holes found in self-review:
- An admin entry name removed and immediately reused inside one debounce
window coalesced into a listing-only scope, so the reused entry kept
serving the removed worktree's cached head. Name the entry alongside the
listing on every `worktrees/<name>` create/delete.
- A non-ENOENT `readdir` failure on `worktrees/` collapsed the memo to the
primary row, which then re-emitted every identity on recovery. Mirror
worktree-git-common-polling: only a genuinely absent dir means empty; any
other error keeps the previous listing.
* fix(worktree): let empty-scope bursts still take the head re-baseline
Adversarial review found the 60s full-rebaseline promotion was unreachable
whenever the triggering burst had an empty head-identity scope: the skip
guarded on the raw caller scope and returned before `resolveScope` ran, so
`lastFullReadAtMs` was never re-evaluated. A repo whose only churn is
`git worktree lock`/`unlock` or a sparse toggle — Orca's own prepared-checkout
flow locks and unlocks on every create — could starve the promotion forever
and hold a stale head indefinitely. Resolve the scope first and skip on the
effective scope.
Also stop deferring an add/remove that arrived while the `worktrees/` listing
was transiently unreadable: forget the memoized listing so the next refresh
re-enumerates whatever its scope, instead of waiting for another listing event.
Both fixes carry a test verified to fail without them.
* fix(worktree): return head-read completeness instead of sniffing the memo
Adversarial review round two. Six fixes, each with a test verified to fail
without it.
- `readGitCommonHeadIdentities` now returns `{ identities, listingComplete }`.
The refresh layer was inferring "enumeration failed" from `cache.entryNames
=== null`, a reader-owned field whose null also means "cold start" — fragile
in production and impossible to express in a mock.
- A read discarded by teardown, or one that could not enumerate `worktrees/`,
no longer arms the 60s freshness clock.
- A queued refresh whose re-run met a destroyed window (macOS recreates the
window while the watch lives on) was cleared and dropped. It now stays armed
and is folded into the next request.
- An incomplete listing carries forward the baseline rows it could not observe,
so recovery does not report every linked worktree as changed.
- The baseline advances after notifying, so a send into destroyed chrome leaves
the move to be retried instead of diffing it away.
- A scope naming an entry the memoized listing does not know now forces a
re-enumeration instead of resolving to zero work — this removes an unstated
dependency on `diffGitCommon` emitting a dir-level create for new entries.
- Overflow states FULL at its construction site rather than relying on a
downstream `?? FULL` for an absent field.
Also documents the load-bearing invariant behind the empty-scope skip (an empty
scope only reaches the refresh from a structural burst, which forces
`emit: false` and is always paired with a catalog notification for every repo
on the watch), and strengthens two tests that could not distinguish the
behaviour they claimed.
* fix(worktree): bound head-identity staleness with a one-shot catch-up
The previous re-baseline was opportunistic: it rode the next refresh, so a ref
that moves with no watched write (`git update-ref refs/heads/x` from a sibling
worktree) stayed stale until an event happened to arrive after the interval.
Pre-PR the very next event anywhere in the repo corrected it, so this was a
real narrowing of correctness, not just a pre-existing gap.
Arm a one-shot, unref'd timer when a SCOPED pass completes, firing one full
re-baseline an interval after the last full read, then disarming. A full pass
disarms instead of arming, so it never becomes a background poll, and the timer
only exists after an event — an idle repo still schedules nothing and reads
nothing. Cost is O(1) timer per active repo and at most one full read per
interval: the same operation the old code ran per event, 60x rarer.
This also converts "stale until some later event" into "stale at most one
interval, period", which is what bounds the blast radius of any invalidation
bug in the scoping itself.
Cleared on watch disposal. Three tests, each verified to fail without its fix:
the catch-up runs with no further events; a quiet repo issues no background
reads and the timer disarms after firing; disposal stops it.
* fix(worktree): treat an unreadable head as unknown, not absent
Reported independently by two PR reviewers. `readTrimmedFile` collapsed every
errno to `null`, so an EIO/EACCES/ENFILE on a `gitdir`, `HEAD`, loose ref, or
`packed-refs` read was indistinguishable from the file being absent — and the
caller deletes the cached identity on `null`. Same conflation AGENTS.md forbids
for the SSH verdict vocabulary: loss of contact is not evidence of absence.
Reads now report three outcomes, and an unknown:
- keeps the entry's last verified identity instead of evicting it,
- is never replayed onto siblings sharing the branch as "this ref is gone",
- marks the entry unverified so the very next pass re-reads it whatever its
scope, and
- reports the pass incomplete, so it cannot arm the freshness clock.
The reviewers' stated consequence — that an evicted entry stays evicted until
the next full pass — did not hold, because `!cache.entries.has(name)` already
forced a re-read. The real cost was that one EMFILE evicted every entry it
touched and the next pass re-read all of them, which is exactly the full scan
this PR exists to remove, plus a spurious re-publish of every row.
Renames `listingComplete` to `complete`: it now covers entry reads too.
The speculative warm-up that runs while the create composer is open resolved
refs and fetched with host Git even when the project's runtime is a WSL distro,
while both the checkout preparation it feeds (`prepareWorktreeCreateForRepo`,
which already resolves `{ wslDistro }` itself) and the real create path run
inside the distro.
The concrete cost was a discarded fetch: `getCanonicalFetchKey` namespaces the
runtime's remote-fetch cache `wsl:<distro>` vs `local`, so the warm-up's fetch
landed in a namespace create never looks at, and create fetched again. On a
Windows host with no usable host-side Git the probes also failed outright, so
that cohort got no warm-up at all.
Thread the project's worktree Git options through the prefetch (resolved by a
non-throwing helper, because an optimistic warm-up must not surface a
repair-required runtime as a failure) so every probe and fetch runs where create
runs. `gitOptions` is a required argument, so a caller cannot drop the routing
silently. Host-routed calls keep their original arity, so macOS, Linux,
native-Windows-host projects, SSH repos and folder workspaces are unchanged.
Narrower than it looks: for a repo under \\wsl.localhost\<distro>\... the probes
were already routed by cwd, and for a repo on a Windows drive letter host Git
and WSL Git read the same on-disk repository, so the answers were already
correct there. What those cohorts gain is a fetch create can reuse; what they
pay is that the probes now run inside the distro (over /mnt/c for drive-letter
repos, which also newly arms the linked-worktree routing probe) and the
speculative fetch now shares create's per-remote fetch queue, as it always has
on native platforms.
Also collapse the three byte-equivalent copies of `hasLocalWorktreeBaseRef`
(create, prefetch, remote-repo create) into one in
git/worktree-base-ref-probe.ts, drop the host-only `hasLocalCommitObject` that
caused the routing bug, and add the first routing assertions on the create-path
consumers of the now-shared probe.
* refactor(runtime): split OrcaRuntimeService into focused modules
* test(runtime): cover admission tiers and strict worktree reconciliation
* fix(runtime): preserve owner and structured session visibility
* fix(runtime): port post-extraction compatibility fixes
* fix(runtime): preserve skill-share cancellation barrier
* test(runtime): update identity inventory after extraction
* fix(runtime): preserve hook transport environment cleanup
* fix(runtime): consolidate idle probe imports
* test(runtime): retire split file process allowlist entry
* fix(runtime): route child process types through shared boundary
* test(runtime): preserve worktree host metadata precedence
* fix(runtime): update extracted test seams
* fix(runtime): gate the split's ts-nocheck set and restore the stop-confirmed contract
Audit follow-ups for the OrcaRuntimeService split:
- Freeze the 171 @ts-nocheck files behind a ratchet so no new file can disable
type checking. The split's linear mixin chain cannot express forward
references yet, so the existing suppressions are grandfathered; the baseline
may only shrink.
- Drop the stray @ts-nocheck at the end of orca-runtime-get-status.ts. It sat
after the first statement, where TypeScript ignores it, so the module was
already checked.
- Restore `retireRejectedPty(ptyId, stopConfirmed: boolean)` as a required
argument. The split widened it to optional and patched the resulting error
with `stopConfirmed === true`; an omitted argument would have silently taken
the unverified-stop path instead of failing to compile.
- Guard that every orca-runtime-tests fragment is imported by the compatibility
entrypoint. The fragments are .spec.ts, which no Vitest include glob matches,
so one left out of the list would silently stop running.
* fix(runtime): restore four behaviors the OrcaRuntimeService split dropped
Audit findings against the refactor's true base (ad5ba2572e):
- retirePtyAgentLaunchAuthority collected pane keys after deleting the
restored-authority receipt instead of before it. collectPaneKeysForPty reads
that receipt, so a receipt-only pane lost its key and never had its agent-hook
compatibility authority retired. on-pty-exit.ts already carried a comment
naming this exact invariant.
- The PTY-exit path kept orchestrationMailboxNotifications.retirePty but lost
the loop that schedules a debounced mail-pointer repoint for the dead pty's
terminal handle and any run bound to its panes. Restores the schedule call
count to 7, matching base.
- subscribeToPtyExit lost isPtyKnownExited's leaf fallback and its
post-registration lifecycle-generation recheck. leavesByPtyId is rebuilt from
the renderer graph independently of ptysById, so a leaf can outlive its pty
record; without the fallback a caller waiting on an already-dead pty never
gets released.
- The chain root declared `[key: string]: unknown`, which base had nowhere. It
leaked through the exported runtime type into every consumer, so any misspelled
member access typechecked as unknown instead of erroring, and it accounted for
957 of the suppressed errors. Removing it costs zero type errors.
* fix(runtime): restore escalation prose and unscoped automation publication
Two more behaviors the split dropped, each with a regression test that fails
against the pre-fix code:
- The worker-exit escalation stopped deriving its title through
buildOrchestrationTaskDisplayMetadata and inlined `task.spec` instead. That
ignored an explicit task_title, dropped the single-line normalization and the
80-character bound, and turned the no-spec case into a quoted, duplicated id.
A multi-paragraph spec landed verbatim in the coordinator's banner. The
existing 11 tests all use short single-line specs, where the derived title and
the raw spec are identical, so none of them could see it.
Also reverts an added `if (!handle) return` guard: the dispatch lookup is
deliberately keyed on the pane as well, because a reminted handle no longer
matches the row while the pane identity outlives the remint.
- updateAutomation stopped going through automationChangePublications and
published `source` unconditionally while gating the fallback on a non-null
destination. A destination the store can no longer name then published only
the stale source, so subscribers scoped elsewhere kept rendering a row that
had left them — the exact case the helper documents. The helper had been left
with zero callers; all three sites use it again.
* fix(skills): stop swallowing lookup errors and hard-erroring on non-ssh hosts
Follow-ups from auditing the skill install path against the refactor's base:
- resolveWorktree wrapped showManagedWorktree in `.catch(() => null)`, so a
transient git or IO failure surfaced to the user as
skill-install-workspace-not-found with the real cause discarded. Errors
propagate again; a genuine id mismatch still returns null.
- resolveSkillSshTarget threw skill-install-workspace-host-unavailable when the
execution host was neither local nor ssh, on both the repo and folder
branches. Base gated these on connectionId, so a runtime-owned repo simply
was not an SSH install and fell through to the local path. Both return null
again, and the error code the split invented is now unreferenced.
- listManagedSkillInstalls awaited the receipt walk and the worktree resolve in
sequence. They are independent and either can hit disk, WSL, or an SSH scan,
so Promise.all is restored.
Deliberately unchanged: resolving the worktree through listResolvedWorktrees
rather than showManagedWorktree, which disambiguates a worktree id colliding
across hosts and is covered by its own test, and the SSH-folder
skill-install-ssh-dispatch-required throw, which matches the repo branch.
* fix(runtime): merge duplicate worktree-logic imports
The #17448 port added a third import from ../ipc/worktree-logic, which the
code-quality oxlint config rejects under --deny-warnings. Plain oxlint does not
flag it, so it only surfaced in CI's static analysis job.
* ci: run the ts-nocheck ratchet in PR checks
pr-workflow-lint-parity requires every leaf command in `pnpm lint` to have a
matching step in pr.yml. The ratchet was wired into lint but not the workflow,
so PR CI would not have enforced it.
* Merge remote-tracking branch 'origin/main' and retry the paired-host launch evaluate
main advanced 9 commits; none touch the orca-runtime.ts this branch splits, so
nothing needed porting.
CI failed twice on `Execution context was destroyed` thrown from
headless-paired-runtime-host's first `evaluate` after launch — a different spec
each run, which is the signature of the flake #17780 describes rather than a
regression. That commit added retryTransientMainEvaluate and adopted it in five
helpers but not this call site, even though its docblock names exactly this
case: the first evaluate after electron.launch() resolves, before the app is
ready. Wrapped it the same way.
Restart-survival polls treated a recycled renderer as a hard failure.
Wrap those evaluates so "Execution context was destroyed" is a pending
miss. Windows package-lane teardowns after a force-kill used rmSync
with force:true only, which does not absorb EPERM; put them on the
shared maxRetries:8 policy.
* perf(git): bound ref and worktree scans
* fix(repo-search): clamp oversized ref limits
* fix(worktree): keep strict worktree listing unshared
The shared-scan re-export flipped every `listWorktreesStrict` caller from an
isolated subprocess to the coalesced scan. `git worktree prune` in the removal
recovery path does not bump the scan generation, so a post-prune verification
could join a pre-prune scan, see the stale row, and report a successful removal
as a stale registration. The same gap defeats the post-archive-hook rechecks
that exist to catch an external Git client locking the row.
Restore the unshared export and make coalescing opt-in via
`listWorktreesSharedStrict`, which existing callers already use deliberately.
* fix(git): separate a proven absent ref from a failed probe
`show-ref --verify --quiet` exits 1 for a missing ref, but so does `wsl.exe`
when its own launch fails, so reading any exit 1 as absence collapsed
`unverifiable` into `exited`. A genuine miss prints nothing while a wrapper
failure always explains itself, so require empty stderr alongside the exit
code; a runner that reports no stderr at all keeps its exit-code contract.
That same signal removes a spawn regression: `show-ref` is a direct-git read
under WSL, and the runner retried any numeric exit through the user's
interactive login shell. The replaced `for-each-ref` exited 0 on a miss, so
absence never retried; every absent probe now would. Treat a quiet exit 1 as
Git control flow and skip the fallback.
Also narrow the hosted-review suffix fallback: the replaced
`refs/remotes/*/<base>` could not cross a slash, but `show-ref -- <base>`
matches at any depth, so `origin/feature/main` answered a query for `main`
and submitted a review against a base the provider rejects.
Refresh the real-binary compatibility contract to the shipped excludes, and
assert exact probe concurrency rather than an upper bound so a regression to
serial probing fails.
* fix(worktrees): preserve user workspace names across branch changes
* test(worktrees): cover pinned rename metadata
* fix(workspaces): address display-name review edge cases
* fix(workspaces): keep automatic names fresh across refreshes
* fix(workspaces): preserve legacy CLI labels
* fix(workspaces): preserve display-name provenance across hosts
* fix(workspaces): honor legacy display-name provenance
* fix(workspaces): fence display-name refresh races
* fix(workspaces): accept peer renames from provenance-less hosts
The old-host preserve fence kept a pinned local label on every refresh,
which also suppressed a legitimate rename another client persisted
through the same host until app restart. Narrow it to labels the host
re-derived itself (branch short name, or path basename when detached);
any other changed label in a mode-less response is explicit meta a peer
wrote there. Stale prior-label responses stay covered by the downstream
staleness fence, in-flight writes by the pending fence.
* refactor(workspaces): unify display-name pin derivation
Three call sites (renderer optimistic update, local IPC updateMeta
handler, remote worktree.set handler) each restated the same formula;
a future edit to one would silently skew provenance between paths.
* perf(terminal): let a worktree's terminal spawns run concurrently
The per-worktree terminal mutation guard was a FIFO mutex, so activating a
multi-tab worktree made each tab wait for every predecessor's whole spawn.
Measured with ORCA_PTY_SPAWN_TIMING=1 on a 4-tab worktree, the `options`
phase was a pure-queueing staircase: 0 / 125 / 212 / 291ms.
The invariant that guard protects is spawn-vs-sleep exclusion, never
spawn-vs-spawn. Replace it with a writer-preferring shared/exclusive lock:
spawns share, sleep still excludes, and a queued sleep blocks later spawns
so a stream of spawns cannot starve it into its 12s deadline.
Same worktree after: options = 2 / 3 / 12 / 34ms, and stable_adoption
flattens from 65/398/398/312ms to a steady ~253ms.
The existing folder-workspace control assertion asserted the FIFO behavior
this removes, so it is re-based on sleep-vs-spawn (which still queues) and
joined by a case asserting concurrent same-worktree spawns.
* perf(terminal): memoize the headless snapshot per mutation epoch
Attaching a viewer serializes the session's whole headless buffer
synchronously on the daemon event loop, so every reattach of a quiescent
session re-serialized identical bytes. Measured with ORCA_PTY_SPAWN_TIMING=1,
stable_adoption was 253-281ms per session on reattach.
Move snapshot assembly into HeadlessSnapshotCache and memoize its expensive
parts (the serialize, the OSC link walk, the frame-restore fields) on a
mutation epoch that every emulator state mutation bumps, so a cache hit is
byte-identical by construction rather than merely fresh-enough. The async
write path bumps on entry and again in the parse-completion callback, so a
snapshot taken mid-parse can never be retained.
Reattach of a quiescent session after: stable_adoption 12ms. Sessions with
output since their last snapshot re-serialize exactly as before.
Retention is capped: an entry is held for the session's lifetime once it goes
quiescent, and a renderer may request 50k scrollback rows, so oversized
payloads serve normally but are not retained. Cache hits clone nested values
so a caller mutating its snapshot cannot corrupt later ones.
* perf(terminal): keep the snapshot cache warm across zero-byte parse fences
Review follow-up. flushParsedWrites() is write(''), used purely as a parse
fence, and every getSettledSnapshot runs one — so the epoch bump on an empty
write evicted the attach entry on each checkpoint read, defeating the cache
for any session that gets checkpointed.
Zero bytes cannot mutate the buffer: the OSC and mouse-mode scans are no-ops
on '' and the partial-escape tail is idempotent, so skip the bump for empty
data. Real writes still bracket themselves, and any write a fence orders
behind has already bumped on its own completion.
Also invalidate on dispose, so a post-dispose read can never be served a
pre-dispose entry, and freeze the emulator's public method surface in a test:
the cache's correctness rests on every mutator calling markMutated(), which is
convention rather than a type, so a new method should be a deliberate decision
about invalidation instead of a silent stale-snapshot bug.
* refactor(terminal): apply elegance review to the attach-latency fixes
Reuse: waitForMutationGrant hand-rolled the deadline race that
settleBeforeDeadline (same directory, four existing callers) already owns.
Using it also picks up the timer.unref() the local copy lacked, which was
keeping the Node event loop alive for up to the 12s sleep deadline.
Simplify: drop the waiter `abandoned` flag. The timeout path sets it and
splices the waiter out in the same synchronous block, so no queued waiter can
ever be observed abandoned and both reads were unreachable. The splice is what
actually does the work; the comment now carries why that makes a
grant-after-timeout unrepresentable. drain() then collapses into its loop
condition and reuses markActive() instead of inlining it twice.
Extract markWritten() so the zero-byte parse-fence rationale lives at one
mutation gate instead of being restated at three call sites.
Match the daemon's byte-accounting convention: the retention cap is now
expressed in bytes with code-unit sizing, like MAX_COLD_RESTORE_CACHE_BYTES
next door, so the two retention budgets read in one unit. Same effective cap.
The public-surface guard test caught markWritten on the first run, which is
the behavior it was added for.
* perf(terminal): stop discarding the snapshot cache on non-memoized fields
Second elegance round, and it found the same class of bug as the parse-fence
one: cwd and lastTitle are read fresh on every build and were never memoized,
yet setCwd/setLastTitle bumped the epoch — discarding a whole serialize to
update a field the cache does not hold. OSC 7 cwd updates land on every `cd`,
so this was a live cost on exactly the busy sessions the cache targets. The
invariant is "every mutation of a memoized part", not "every state mutation";
both docblocks said the latter and are corrected.
Drop the dispose bump too. A post-dispose getSnapshot re-serializes the
disposed terminal to byte-identical content, so the bump bought nothing and
only reached into a disposed xterm — verified by probe, not assumed. Its test
asserted zero serializations after dispose, which no implementation could
violate; it passed with the bump deleted.
Also memoize rehydrateSequences (a string, so no clone needed) instead of
rebuilding it on every hit, derive the frameRestore type from
buildFrameRestoreSnapshotFields so a new field cannot flow through at runtime
while the type omits it, inline the single-caller resolve() into build(), and
move the write() entry bump after the sync early-return so the three bumps map
1:1 onto sync / async-pre-parse / async-post-parse.
The surface guard is sorted in source and renamed to say what it freezes: the
prototype, TS-private members included.
* fix(terminal): correct the fence justification and key the cache by window
The markWritten docblock claimed "zero bytes cannot mutate the buffer". That
is false, and I verified it: `_core.writeSync('')` drains xterm's pending queue
and applies it. The exemption is still correct, but for a different reason —
a fence cannot introduce an *unattributed* mutation, because any bytes it
drains belong to a queued async write whose own completion callback bumps
first. The two write regimes are exhaustive: with writeSync present nothing
can queue, without it every write is async and self-bumps. A comment asserting
a false invariant is worse than no comment, since the next change may rely on
it, so it now states the real one.
Key the cache by scrollback window instead of a single slot. Consumers ask for
different windows against the same emulator — attach passes the full window
while agent/text reads pass 0 — so one slot thrashed to a 0% hit rate whenever
they alternated, silently removing the benefit on runtime-side emulators. Two
entries cover every caller pair in the tree.
Drop the epoch counter: markMutated already nulls the retained entry and an
entry is only ever stored under the current epoch, so the comparison could
never fail. Invalidation is simply "clear the cache".
* refactor(terminal): name the lock sides shared/exclusive for a third caller
Rebasing onto main surfaced a semantic conflict the merge applied cleanly:
main added runWorktreeTerminalMutation (terminal orphan adoption, #17159) as a
third caller of the guard this PR changed. It needs the exclusive side —
adoption reconciles a worktree's terminal records, so it must not interleave
with a spawn registering a pty or with a sleep, which is exactly the semantics
it was written under when the guard was a plain mutex.
With three operations, naming the sides after two of them no longer fits, so
the kinds are now `shared` (spawn) and `exclusive` (sleep, adoption) — what
they do rather than who calls them.
* perf(terminal): do not invalidate the snapshot cache on a no-op resize
Re-measuring the final rebased build caught the cache barely working on the
path it exists for. Every attach re-asserts the pane's dimensions, and resize()
bumped unconditionally, so a reattach of a fully idle session missed its own
cached snapshot and re-serialized.
Measured on the same 4-tab worktree, reattach of sessions verified quiescent
(no buffer change over 4s), stable_adoption per session:
before this commit: 98 / 382 / 395 / 418 ms
after: 16 / 67 / 78 / 87 ms
A resize to the size already applied changes nothing the snapshot reads, so it
now returns early — which also stops it clearing restoredOscLinks, correct
since no rows shifted.
* fix(ssh): fence stale kills and retired pane replay
* fix(ssh): support cancellable interactive authentication
* fix(ssh): await remote catalog before snapshot adoption
* fix(pty): contain Windows ConPTY input failures
* fix(power): avoid redundant macOS display blocking
* perf(editor): narrow markdown override subscriptions
* fix(quick-open): close directory handles after reads
* refactor(linux): remove unused proc socket scanner
* fix(usage): apply flat Sonnet 4.6 pricing
* ci: prime Node next native test cache
* docs(skills): resolve snapshot cleanup data path
* fix(ssh): recover install locks after host reboot
* test(ssh): recognize boot-aware install locks
* test(ssh): prove previous-boot lock recovery live
* test(wire): pin pre-metadata release coverage
* fix(terminal): preserve remote tab ownership through recovery races
* test(runtime): fence replaced terminal handles in agent guard
* fix(ssh): preserve remote snapshot authority across polls
* fix(pty): contain late ConPTY output EPIPE
* test(pty): register Windows exit watcher before kill
* fix: close SSH and tab readiness race gaps
* fix(tabs): retain headless order and placeholder titles
* fix(build): avoid parallel electron-vite config race
* test(windows): avoid MSYS temp path rewriting
* test(windows): avoid killing exited PTY
* fix(pty): avoid late ConPTY input teardown race
* fix(terminal): sync reconnect error ownership after commit
* fix(runtime): use canonical worktree identity comparison
* test(ssh): assert complete cold-hydration baseline
* test(windows): invoke quoted retention fixture via PowerShell
* test(windows): read ConPTY grid through mode con
* fix(terminal): publish PTY replacements atomically
* fix(terminal): infer stale identity on reattach
* fix(terminal): fence stale pane PTY callbacks
* fix(terminal): fence stale pane binds after rebind
* fix(terminal): reject stale pane transport callbacks
* fix(terminal): fence mirrored reattach spawn callbacks
* fix(terminal): replace stale pane PTYs on remount
* fix(ci): size the Windows launcher-compile test budget from measurement
`native-smoke (windows-latest)` fails ~4.5% of runs on
`preserves a multiline argument through the compiled remote launcher`
with "Test timed out in 15000ms" — on unrelated PRs, for reasons that
have nothing to do with them. Across 176 sampled attempts it is the only
red that job produced, and it hit seven different PRs in two days:
#16900, #16904, #16915, #16955 (twice), #16979, #17014, #17085.
The test is six process creations: powershell.exe forks csc.exe, then
the freshly compiled orca.exe forks node.exe, twice. Hosted Windows
runners periodically slow process creation down, and this test amplifies
that far harder than anything else in the job. Comparing the 80 attempts
where it ran under 3s against the 12 where it ran over 12s, its own
median goes 2198ms -> 15917ms (7.2x) while the same file's
powershell-only test moves 556 -> 686ms (1.2x), the cmd.exe and Git Bash
process tests in the neighbouring file move 1.4x, and the other 35 files
put together move 1.5x.
Measured across those 176 attempts: 1881ms to 35438ms, p50 4264ms,
correlation +0.881 with the job's total Vitest duration. 8 of 176 (4.5%)
exceeded the 15s cap; 2 of 176 (1.1%) also exceeded the shared 30s
testTimeout, so deleting the override and inheriting the config is not
enough on its own. 60s clears all 176 with 1.7x headroom on the worst.
This is slow, not hung. Every body here is synchronous spawnSync, so
Vitest cannot interrupt one — the timer fires only after the body
returns and the reported duration is real elapsed time. That is why a
failure reads `× ... 22464ms` under `Test timed out in 15000ms`. The
work finished; the stopwatch was short. Seven reruns at one identical
head measured 2053 / 4680 / 5551 / 8732 / 13506 / 14868 / 21937ms — the
last of those would have been red on code that had not changed.
The 15s came from #8897, which raised this test off Vitest's built-in 5s
default because the job then ran bare `pnpm vitest run`. #8909 landed
3h27m later and pointed the job at config/vitest.config.ts, which is the
real fix for that. The constant stayed behind and has been the binding
budget ever since.
* fix(terminal): fence stale remount reattach ownership
* fix(terminal): reconcile mounted pane identity after replacement
* fix(terminal): fence stale reattach fallback ownership
* fix(terminal): fence deferred SSH reattach ownership
* fix(terminal): fence stale split pane ownership callbacks
* fix(terminal): keep stale spawns from consuming startup
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(worktree): gate agent activation on the live surface census, not renderer state (STA-5701)
* fix(worktree): seed a pane when the surface census cannot prove ownership (STA-5701)
Failing closed must not also fail silent. When the census is unverifiable
the sweep adopts nothing and mints nothing, yet the gate still reported
'adopted' — and both callers suppress their own seeding on any outcome but
'empty', so the workspace ended with zero surfaces. The sweep now reports
whether any live PTY holds a surface and the gate hands the caller its seed
when none does. Also folds equivalent workspace-path spellings in the census
index and in exact-surface binding, so a host row spelled differently is
neither dropped (mint a duplicate) nor unbindable (no pane).
* fix(worktree): name the live PTYs the surface census declined (STA-5701)
The adoption sweep can leave a live PTY without a surface — an unreadable
census, two host surfaces claiming one PTY, or a host-named leaf the
persisted layout does not have. The gate already stops reporting 'adopted'
in that case so the caller seeds a shell, but the decline itself was mute.
- adoptLiveWorkspacePtySurfaces now returns { surfaced, declinedPtyIds }
and the gate warns with the workspace and the PTY ids left unsurfaced.
- Pin the host-named-leaf decline, which had no test either way.
- Pin the superseded-inventory race in terminal.list: a concurrent refresh
makes hostScope.hostIds empty, which is what makes the renderer's
'unverifiable' verdict reachable on a plain local machine.
* perf(rpc): compile Zod request schemas lazily
* chore(deps): pin zod 4.5.4 and except it from the release-age gate
4.5.4 is the first release fixing isRecursiveSchema (upstream 84e416f, #6500),
which compile() calls on every schema — on 4.5.0 it fired .default() factories
at compile time. Verified: compile-time factory calls 0 on 4.5.4, 1 on 4.5.0.
Review follow-ups on the create-field shaping:
- Seed only `reporter`. isVisibleJiraCreateField matches every required
non-system field, so the previous filter also pre-filled required custom
user pickers (Reviewer, Requested by) that Jira never defaults.
- Skip the seed when the target project is on another site. The viewer
comes from the active site, and the host shapes against the target's
client, so a foreign accountId is rejected on Cloud and can silently
resolve to a different person by username on Server/DC.
- Render JiraUserPicker only for scalar user fields. It holds one user, so
an array-of-user field collapsed to a single member; those keep the
existing comma-separated text path, which still reaches toUserFieldValue's
array branch.
Adds docstrings across the touched Jira functions to satisfy the
docstring-coverage pre-merge check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdMzQN6T3jRVahCK2sNyur
- Jira rejects a bare string for reporter/user-picker fields on issue
create, so shape customFields values into {accountId}/{name} objects
for keys the caller flags via userFieldKeys.
- Seed required user fields with the authenticated viewer by default
and add a searchable user picker (jira.searchUsers) so users aren't
forced into free text for reporter/custom user fields.
* fix(agents): keep OMP identity and forward ask/approval events (STA-4130)
A live OMP pane was re-owned as Pi because the generic pi-compatible
fallback always won, ask events blocked without a question payload, and
OMP suppressed tool_approval_* unless an extension registered handlers.
Mark Pi as the title-group fallback so a specific OMP identity is not
downgraded, publish OMP ask input as the existing questions envelope, and
forward tool_approval_requested/resolved onto blocked/working.
STA-4130
Related to #14278
Co-authored-by: devatnull <59279509+devatnull@users.noreply.github.com>
* fix(agents): keep launch Pi ownership over OMP wrapper frames (STA-4130)
The pi-compatible fallback treated every generic Pi owner as inferred, so an
explicit launch-Pi pane (and a launchless Pi pane with an OMP-shaped title)
was re-owned as OMP. Launch provenance now stays authoritative; only an
inferred status-frame owner yields to a specific sibling, and same-group
titles no longer count as reuse.
STA-4130
* fix(agents): drop Pi wrapper idle titles while OMP hook is active (STA-4130)
Title-completion suppression compared pick-a-winner ownership, so a Pi ready
frame looked like a different agent than a live OMP hook and fired a spurious
task-complete notification. Reuse checks now use the title-identity group.
STA-4130
* fix(agents): restore OMP approval forwarding after merge
* fix(agents): restore title-owner API after merge
* test(agents): update identity inventory ratchet
---------
Co-authored-by: devatnull <59279509+devatnull@users.noreply.github.com>
Co-authored-by: Merge Sim <sim@local>
* Fix orchestration CLI recovery, settled-Dispatch mail, and guide defects
Five reported orchestration CLI defects, verified individually before fixing.
Two were real code defects, one was a docs error, one was correct as-is, and
one was correct on both ends except for its recovery wording.
- Mail addressed to a settled `dispatch:<id>` was accepted and silently dropped.
Local sends bypassed the settlement check the federated branch already had, so
the caller was told success for a delivery no worker would ever read. Reject
with `dispatch_inactive` and name the Run mailbox to use instead.
- A lost mutation response offered no read-only way to ask whether it took
effect. `--retry-request` does dedupe correctly, but the recovery guidance
emitted a query command only when the payload carried a dispatch id, which is
exactly what a lost response lacks. Add read-only
`orca orchestration request-show --request <id>` over the durable receipt
ledger, and always emit a read-only step before the keyed retry.
- The bundled `orca-cli` guide documented `check --unread --inject`, a flag the
parser rejects. Correct it to `--format` and add a ratchet that runs every
orchestration invocation in the bundled guides through the real CLI parser.
- `check --json` is one stdout document and its keepalives are stderr-only; the
reported `Extra data: line 2` came from merging the streams. Document the
contract rather than changing the wire.
- A rejected lifecycle message is loud on both ends already, but the rejection
never named the flag that supplies the missing capability. Name it.
* Harden orchestration mutation recovery guidance
On a Windows host the runtime stores a WSL worktree as the UNC path Windows
sees, but a user inside the distro types the Linux spelling, so every `path:`
selector missed: `worktree show`, `terminal list --worktree` and
`worktree rm --worktree` all reported selector_not_found for a directory Orca
manages.
Translate once in the CLI, which is the only side that can prove which distro
the typed path belongs to — from its own UNC cwd, never from WSL_DISTRO_NAME,
which a Linux-native CLI also sets. The runtime's `path:` branch stays
exact-spelling-only for the same reason: this resolver feeds delete, so a
tail-only match would remove another distro's copy.
* fix(worktree): complete a create Git can confirm but cannot list
`worktree.create` verified against `listWorktrees`, which softens every git
failure to `[]`. Any listing failure therefore failed a create whose worktree
and branch `git worktree add` had already written, orphaning both, and reported
only 'Worktree created but not found in listing' — the real cause reached the
main-process console and never the user.
Verify against the error-propagating listing instead, and when that fails or
omits the row, rebuild the row by asking Git about the worktree itself. The
direct read returns nothing unless Git resolves the path into this repo's
object store with the expected branch checked out, so an unrelated or half-made
checkout still fails the create.
Fixes#16520
* fix(worktree): authorize a recovered create and reject an unreadable HEAD
Review follow-ups on the create-verification fallback:
- register the recovered worktree's own root, additively, so the create the
user just made is not rejected by filesystem/git-status IPC
- treat an unreadable HEAD as no recovery instead of a blank OID
- keep the direct read's failure when the listing merely omitted the row
- skip the symlink cases on Windows and reset the new harness mock
* fix(worktree): bound the create-recovery disk read and keep WSL paths case-sensitive
Readiness-scan follow-ups:
- deadline the filesystem common-dir read; a .git on a hung mount left the whole
create IPC pending where it used to fail after the Git deadline
- offer no disk candidate for a bare repo instead of a fabricated <repo>/.git
- compare POSIX common dirs case-sensitively, so two WSL repos differing only in
case are not accepted as one object store on a Windows desktop
- move toGitOutputSpace to shared/wsl-paths as toWslExecutionSpace, next to the
parseWslUncPath callers that already open-code it
* fix(worktree): share one budget for create verification and keep recovered roots
Three follow-ups from review of the create-recovery path:
- The recovery no longer starts a fresh 30s deadline after the listing already
burned one, so worst-case create verification stays at ~30s instead of ~60s.
A 5s floor keeps the direct read a chance to answer when the listing spent
the whole budget.
- rebuildAuthorizedRootsCache now carries a repo's previously registered roots
forward when its listing throws. A rebuild running while Git is still broken
could otherwise un-authorize the worktree a create just recovered.
- Corrected the scan-cache doc comment: it claimed strict and lenient listings
coalesce, but the cache key includes the runner name precisely to keep them
apart, so a strict joiner can never inherit a lenient scan's softened [].
Each change has a negative control: reverting the hunk fails exactly its own
test and nothing else.
* fix(worktree): keep a recovered worktree authorized across roots-cache rebuilds
The previous approach registered a recovered create into the same per-repo set
the rebuild recomputes from `git worktree list`. That set is derived from the
very listing that failed, so a rebuild would re-deny the worktree — either by
overlapping the registration, or by simply listing again and omitting the row.
Carrying old roots forward on a thrown listing did not cover either case.
Recovered roots now live in their own additive layer that rebuilds union in
rather than replace. The layer is retired on evidence, not on a timer:
- the listing can see the worktree again (Git recovered), or
- the listing succeeded and the directory is gone (worktree removed).
A repo whose listing threw is left untouched, because a dead mount fails both
the listing and the stat, and treating that as "removed" would revoke the
worktree in exactly the outage this layer exists for. The layer is capped so it
cannot grow unbounded, and survives cache invalidation deliberately: repo
mutations are frequent and would otherwise re-deny a recovered worktree.
Three tests cover the healthy-rebuild-omits-the-row case, the in-flight rebuild
race, and retirement once the listing sees it again. Removing the union fails
exactly the two keep-tests and nothing else.
* perf(worktree): only read the repo's .git from disk when Git's own answer disagrees
The disk read is a second opinion on Git's reading of the common dir, but it ran
unconditionally as part of the same Promise.all. A deadline bounds the IPC, not
the syscall: Promise.race cannot cancel an in-flight fs operation, and a `.git`
on a hung mount (dead NFS/SSHFS, stalled WSL 9p) pins a libuv threadpool thread
that no timeout can reclaim. AbortSignal would not help either — fsPromises.stat
takes no signal, and a blocked syscall is not interruptible from userland.
So stop paying it on the happy path: read from disk only when Git's own reading
did not already confirm the common dir. Same accept/reject outcome, but the
threadpool exposure now requires both a failed listing and Git disagreeing about
the repo, instead of every recovered create.
* fix(worktree): compare the disk common-dir witness in Git's execution space
Exercising the fix on a real Windows host against WSL Ubuntu-24.04 found the
filesystem second opinion is inert there. Node reads `.git` in the caller's
space and answers `\\wsl.localhost\<Distro>\home\...\.git`, while Git-in-the-
distro answers `/home/...`. isSameCommonDirPath refuses to compare a POSIX path
against a Windows one, and canonicalizeLocalPath cannot bridge them because
realpath on a Linux path from a Windows process is ENOENT.
So the candidate could never match, and the one case that depends on this
witness alone — a symlinked repo root on the Git 2.25 fallback — declined a
worktree Git had already confirmed. Run the disk result through
toWslExecutionSpace, the same translation readRepoLocation already uses.
This is a false reject, not a false accept: it made recovery give up, never
adopt the wrong repo. Verified on awin; the modern --path-format=absolute
branch was unaffected because Git answers both sides itself there.
* fix(worktree): retire a recovered root only on proof, never on a stalled probe
The prune ran an unbounded stat and read every failure as removal. Two consequences, both in
the outage the recovered layer exists for: a hung mount stalled the rebuild that gates
filesystem auth, and a transient EACCES/EIO revoked a live worktree. The listingFailed guard
did not cover either, because listWorktrees softens Git failures to [] and never throws.
Prune now retires on definitive ENOENT only, probes in parallel under a deadline, and treats a
stall as inconclusive. The capacity bound refuses a new root instead of evicting an authorized
one, so an over-cap create is merely unauthorized rather than a live worktree being revoked.
* perf(git): bound git subprocess execution with an atomic admission scheduler
Field traces (#16038, #11363) show Windows freeze storms driven by unbounded
concurrent git children (12+ at once, 50-65s status convoys for 25+ minutes).
Admit every main-process git child against atomic per-budget base+headroom
counters (general / network / per-route), with reserved interactive capacity,
ordering-only aging, close-bound permit release, a 120s fail-safe read timeout
that feeds scheduler backoff, tier plumbing through every option carrier, and
coalesced+jittered visibility pollers. Killswitch: ORCA_GIT_ADMISSION_DISABLED=1.
Storm harness A/B: max concurrent children 65 -> 6, interactive p95 791ms -> 88ms;
output-parity battery byte-identical with admission on vs off.
* test(git): run the admission output-parity battery on every platform
Parity needs real git, not the storm harness's PATH stub, so it must not share
that file's POSIX gate - Windows is the platform where parity evidence matters.
* fix(git): preserve interactive admission invariants
* perf(git): keep admission queue drains linear
* fix(git): close final admission gaps
* perf(git): bound eligible route selection
* fix(merge): remove unrelated stale snapshot changes
* fix(git): preserve refresh lifecycle authority
* test(git): align admission lifetime contracts
* fix(git): harden admission across runtime paths
* fix(git): restore freshness for bulk status reads
* test(git): repoint delete-dialog source pins after admission plumbing
The hydration effect now orders its targets through
orderDeleteWorktreeStatusHydrationTargets and passes includeLineStats
alongside the abort signal, so both literal anchors stopped matching.
The invariants are unchanged and still pinned: dropping the signal, the
main-worktree/folder filter, or getState-instead-of-subscribe each
still reddens this test.
* Fix git admission tier propagation and lock ordering
Decode optional Git status tiers permissively and default runtime RPC status reads to the status lane while preserving renderer caller intent.
Acquire the FETCH_HEAD mutex before atomic admission so same-repository fetch waiters hold no global or route permits.
Preserve automatic pull-request refresh reasons, keep explicit hosted-review refreshes interactive, remove the dead candidate tier, and keep relay scheduling unchanged.
Use tier-aware status lease keys because a shared lease cannot be safely promoted after its admission request is queued or granted.
* test: align expectations with admission plumbing
* refactor(child-process): move the process contract types to process-spec
run-process.ts crossed its line cap after gaining the termination observer;
the public types and defaults move out with re-exports so no caller changes.
* chore: restore pnpm-lock.yaml to main (unintended local drift)
---------
Co-authored-by: Merge Sim <sim@local>
* fix: make PR unlink hide auto-detected reviews
* Type the empty-content test double against the real model
The literal narrowed suppressedGitHubPR to number and typed the callback
as Mock, so neither direction was comparable and tsconfig.tc.web.json
failed on TS2352. Keeping the 'as' cast preserves checking of the fields
the double does supply.
* Add localization keys for the unlinked checks-panel state
The unlinked title, relink action, and the remote-runtime upgrade notice
introduced untranslated keys that static analysis requires in en.json.
* Advertise PR suppression capability in the transport test
The client capability list is pinned by websocket-transport.test.ts, and
adding WORKTREE_GITHUB_PR_SUPPRESSION left the expected list stale.
* Fix stale PR suppression in Checks
* fix: harden PR unlink suppression state
* refactor: extract PR unlink state handling
* fix: show PR relink recovery in source control
* fix: add unlinked PR localization
* Clarify workspace-scoped PR unlinking
---------
Co-authored-by: Merge Sim <sim@local>
* fix(browser): apply the app-wide HTTP proxy to embedded browser sessions
The proxy setting was only ever written to `session.defaultSession`, but browser
guests run on their own `persist:orca-*` partitions. Any host reachable only via
the configured proxy failed to load in an embedded tab, landing on
`chrome-error://chromewebdata/`, while the same setting worked everywhere else.
Adds a per-session applier alongside the existing defaultSession path, keyed by a
WeakMap so one session's applied config can't suppress another's, and applies it
to every browser partition through the single installer they all pass through.
Startup awaits an explicit sweep so the first guest navigation can't race the
installer's fire-and-forget write, and a settings change re-sweeps so toggling
the proxy takes effect without a restart.
Env-var fallback and the system-proxy probe mirror the defaultSession behaviour,
so a browser partition resolves the proxy the same way the rest of the app does.
Fixes STA-4779
* fix(browser): await per-session proxy readiness
* fix(proxy): preserve loopback and authenticate
* fix(proxy): settle browser partition update races
* fix(proxy): close partition policy races
* fix(proxy): order settings and release removed sessions
* test(browser): await partition proxy readiness
* fix(proxy): cancel removed partition retries
* refactor(proxy): keep OpenCode rate limits out of scope
* fix(proxy): preserve sessionless host policy
* fix(proxy): gate requests on policy readiness
* fix(proxy): retire deleted browser sessions
* fix(proxy): close retired browser guests
* fix(proxy): retain retired session guards
* fix(proxy): retain retired partition policies
* fix(browser): retry transient proxy application failures
* fix(browser): release deleted partition installer state
* fix(proxy): retry delayed transient failures
* fix(proxy): preserve route session authority after rebase
* fix(proxy): clear retired session credentials
* fix(proxy): retire failed browser profiles
* fix(proxy): harden failed session cleanup
---------
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
* fix(cli): name PowerShell when it strips quotes from JSON flags
Windows PowerShell 5.1 does not escape inner quotes when building a native
command line, so `--options '["a","b"]'` reaches orca.exe as `--options [a,b]`.
The value is correct when printed and damaged by the time argv is parsed, so the
resulting "invalid JSON" error blamed the user's input rather than the shell.
#16743 recovered this for `--deps`, which is safe only because generated task IDs
have a fixed 12-hex grammar. The same mangling hits `--options`, `--payload` and
`--result`, and those are NOT safely recoverable: `["1","2"]` and `[1,2]` arrive
at argv identically, so a general repair would silently turn strings into numbers.
Detect instead. `getOptionalJsonFlag` rejects the damaged shape up front with an
error that names the shell and shows the workaround. It fires only when the value
is bracketed, quote-free, fails JSON.parse, AND consists entirely of bare tokens
that quoting would rescue, so valid JSON is untouched.
Also share the generated-id contract: `task-deps-flag` hardcoded
/^task_[0-9a-f]{12}$/i, which silently diverges if `generateId`'s byte count
changes. It now calls `isGeneratedId`, with a test pinning the two together.
Verified on a Windows host. Measured argv, which the new test pins as a fixture:
PS_VALUE=["task_b2a580db74d8","task_c3b691ec85e9"]
ARGV=["--deps","[task_b2a580db74d8,task_c3b691ec85e9]"]
Before: Invalid --options: must be a JSON array of strings
After: --options arrived as [a,b], which is not valid JSON.
Windows PowerShell 5.1 strips the inner quotes ...
* fix(cli): scope JSON-flag detection to genuinely JSON flags
Review found the detector wired to two flags that are not JSON:
- `orchestration ask --options` is documented `<csv>` and the runtime splits it
on commas, so `--options [a,b]` was a legitimate value being rejected.
- `task-update --result` is stored verbatim and reused as dispatch failure text;
existing tests pass free text, so a bracketed `[ok]` was being rejected.
Both revert to `getOptionalStringFlag`. Only `gate-create --options`
(`<json_array>`) and `send --payload` (`<json>`) are JSON-parsed and keep it.
Three further review fixes:
- Objects now require a `key:value` pair per entry. `{a,b}` and `{a:b,c}` were
reported as quote-stripped although quoting them cannot produce valid JSON.
- The raw value is no longer echoed. A `--payload` can carry secrets and this
message reaches `--json` output; the flag name and guidance are enough.
- The message hedges the shell attribution. Detection inspects only the value's
shape, so it also fires when a macOS/Linux user forgets to quote, where
PowerShell is not involved.
Verified against a Windows host, all six cases: both JSON flags fire on the
mangled shape and pass valid JSON through to the runtime; both non-JSON flags
now reach the runtime again; and the secret in `{token:hunter2}` appears zero
times in the error output.
* fix(remote): focus host-delegated split panes
Return the authoritative leaf identity from terminal.split, record viewer-local focus intent behind the captured pairing revision, and replay the mirrored layout before focusing the exact pane. Preserve old-host fallback and prevent delayed split responses from stealing focus after the viewer moves away.
Add deterministic runtime, renderer, concurrency, compatibility, and headed paired-Electron coverage for Cmd+D, header splits, and immediate PTY input routing.
Fixes#16510
* fix(remote): preserve split focus across tab groups
Resolve the initiating source tab and leaf from the remote PTY, while keeping the viewer's current focus as a separate anti-steal baseline. This lets context-menu/header splits from non-focused group tabs focus their result without allowing delayed responses to override a later navigation.
* test(remote): drive split focus with key events
* test(remote): use the platform split shortcut
* fix(remote): fence concurrent split focus intent
* fix(remote): harden split focus ordering
* fix(remote): preserve split focus after runtime refactor
* fix(remote): fence stale split focus gestures
* test(remote): keep split focus regression within line budget