The Kotlin compile fix replaced the android.net.Uri origin check in
MobileWebBridgeDocumentUrl with a JVM-testable host projection and
hoisted activeSessionId into a local; the source-parity assertions still
named the old spellings.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
native.alert raised an OS-authority modal on demand, with only a 4-token bucket
between a compromised page and a sustained stream of system-looking dialogs.
It now requires a recent gesture.
The gesture is witnessed, not spent: page-side callers open a confirm dialog and
then run the gated action it confirms, and consuming here would deny that action.
A denied alert degrades to the page's own modal through the existing fallback,
so error surfacing after a slow async failure still reaches the user.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The recent-gesture predicate was rebuilt at six call sites and the throw was
copied into three helpers plus five open-coded conditions, so a gate could be
dropped in a refactor without anything failing. mobileWebUserGestureConsumer
and requireRecentUserGesture now own both halves, and a census test derives the
gated set from the sources, pins it, and drives denial and pass-through for
every operation its executor can reach directly.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Re-derive the 36 stage hooks and the presentation modules from the inlined
Tasks screen so app/h/[hostId]/tasks.tsx is an 80-line route again, with the
hybrid host-operations delta threaded through the composition instead of an
RPC client.
- New use-mobile-tasks-host-operations resolves the 15 injectable operations
objects, connection metrics, and route params once; every stage hook reads
them off the model.
- mobile-tasks-dependencies no longer re-exports expo-clipboard, Linking,
haptics, or the RPC client, keeping native modules out of the hosted bundle.
- Statement-for-statement and render-token parity with the inlined screen was
verified before the parity digests were re-frozen.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
blockNetworkLoads and shouldInterceptRequest never see a WebSocket, and CSP
connect-src is the only fence a compromised page had to defeat there. iOS has
installed a document-start blocker since the shell landed; Android now installs
the byte-identical script through WebViewCompat.addDocumentStartJavaScript
scoped to the session origin, including on the in-place onRenderProcessGone
reload, and fails closed when DOCUMENT_START_SCRIPT is unsupported.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Add a broker-only harness and shared page-message builders to the bridge
roundtrip fixture, then migrate the eleven suites that hand-rolled their own
postMessage glue, native-authority stubs, and envelope constants.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
A process termination below the crash-loop threshold bumps viewEpoch and keeps
the shell session id, so the native view loads a fresh document while the old
MobileWebCapabilityBroker survived: its subscriptions, terminal streams, speech
authority, pending requests, replay window and rate limiter competed with the
new page against maxConcurrent grants.
Move the broker lifecycle into useMobileWebCapabilityBroker keyed on viewEpoch,
retire page-scoped shell state (gesture timestamp, route handoff, page-ready,
health deadline) on the same key, and dispose synchronously from
onProcessTerminated so Android's in-place onRenderProcessGone reload cannot
inherit the previous page's broker.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
mobile/README.md named three of the twelve hosted journeys and pointed at
package.json for the rest; the two native store suites appeared nowhere. Both
docs now carry the full list with the prerequisite that actually blocks each one
(simulator, adb device, release-signed install, Docker, POSIX signals), and say
which suites CI runs.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
audit:dead-code:mobile and config/knip.mobile.json have existed with no workflow
calling them. A first run reports 101 unused files, so the step is
continue-on-error: the count lands in the checks list without gating mobile PRs
on a cleanup nobody has scheduled.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Page request clients erased operation names to bare `string`, so an
unregistered operation compiled and only failed at runtime. Key the one-shot
request signature on the capability's registry union and thread that type
through every request-client helper.
Adds a census asserting that direct capability/operation pairs in page request
clients are registered, that no request client re-widens an operation parameter
to `string`, and that every registered operation is named by a shell module
outside the grant tables.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Under pnpm 12 the patch authority for the mobile workspace is
pnpm-workspace.yaml, not package.json, so the patchedDependencies block
added to mobile/package.json was inert: the three new patches never
applied and the lock carried no hash for them. The versions they named
were stale too, since pnpm resolves @expo/dom-webview to 55.0.6,
@expo/log-box to 55.0.13, and react-native-webview to 13.16.2.
Drop the dead pnpm block, declare the patches in pnpm-workspace.yaml
against the installed versions, and re-resolve each patch against the
real sources. The react-native-webview Android hunks fold into the
existing 13.16.2 patch, since pnpm allows one patch per package version.
Every hunk keeps its original intent: setWebContentsDebuggingEnabled is
gated on ApplicationInfo.FLAG_DEBUGGABLE.
The CDP session test read these patches by version-stamped filename,
which would break again on the next bump. Resolve them through
pnpm-workspace.yaml instead, so the test also fails when a patch is
declared nowhere.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The page invoked `workspace.creationRetiredNames` and the shell implemented
it, but the operation was in neither the bridge registry nor the production
grant table, so every hosted new-workspace name lookup was rejected with
unsupported_capability before it left the page.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The spec test.skip()s itself off darwin, and every e2e lane is ubuntu, so
changed-e2e handed it a runner that could only produce a green skip. It also
needs an iOS simulator and a Docker daemon at once, which no GitHub-hosted
runner provides, so there is no lane to move it to.
Dropped from the changed-e2e filter with the reason, matching the native IME
spec already excluded there. It stays runnable from a macOS checkout via
`pnpm test:e2e:hosted-mobile-webview:ssh`.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Fold the iOS scheme-handler session check into one origin call, name the
native.alert cancellation exemption, and drop the Android-shaped
activation-*.tmp sweep iOS never writes.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The hybrid WebView branch's strongest tests were invoked by no workflow: 1,753
lines of Kotlin store tests behind a Gradle task nothing called, and 2,253 lines
of Swift store tests behind `pnpm --dir mobile test:native:ios-web-store`.
Both land in a workflow of their own because GitHub has no per-job `paths` and
neither toolchain belongs on mobile.yml's ubuntu verify job. The filter is the
module directory plus src/shared/mobile-web, so unrelated mobile PRs pay
nothing.
- android-unit-tests (ubuntu-latest): expo prebuild, then
:orca-expo-mobile-web-shell:testDebugUnitTest. Autolinking names the project
after the npm package, and the task is finalizedBy the process-interruption
drill, so that runs too.
- ios-store-tests (macos-26, Xcode 26.5 to match mobile-ios-release.yml): the
swiftc runner, which needs no simulator, no pods, and no node_modules.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The branch pinned 16 packages below main (expo 55.0.27 vs 55.0.30, xterm
6.1.0-beta.285 vs beta.303, vitest 4.1.9 vs 4.1.11, and friends), so
merging would silently downgrade them. Restore main's specifiers and
align the branch's new @xterm/addon-fit with the beta the rest of the
xterm packages ship from (0.12.0-beta.300, same publish batch and the
same pin the desktop root already uses).
Drop buffer and @types/react-test-renderer from the desktop root: no
source, test, or config file imports either, and mobile/package.json
declares both itself.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The Kotlin sources for the hybrid WebView shell had never been built by any
workflow, so five compile errors and one boundary escape shipped unnoticed:
- MobileWebShellView shadowed ExpoView.appContext and used `return` inside
three expression-bodied functions.
- MobileWebBridgeDocumentUrl passed a java.net.URI to the android.net.Uri
origin check; it now compares against an Android-free host projection so the
JVM unit test can exercise it.
- beginStage called mkdirs() before checking its ancestors, creating the stage
through a symlinked `staging` directory and leaving it behind when the later
check rejected it (MobileWebCacheWriteBoundaryTest).
43 tests now pass under :orca-expo-mobile-web-shell:testDebugUnitTest.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The page had two competing splits: an Aug-25 folder split that the Aug-30
oversized-surfaces split stranded, and the 47 flat files that replaced it. The
orphaned tree had no non-test importers, yet eight ratchet files still asserted
against it, so their invariants stopped constraining shipping code -- which is
how six regressions reached main unnoticed. Those ratchets were repointed and
the regressions fixed earlier; this removes the tree they were guarding.
Moves the live files into task-page/{github,gitlab,jira,linear} and drops the
now-redundant prefix, matching the new-workspace sibling.
Makes the source-family walker recursive first: it listed a single flat
directory, so moving the files under it would have emptied the family and turned
every ratchet built on it into a no-op without failing.
The oversized-UI-surfaces split was cut from a stale branch and reverted merged
work. getClientCreationActionPolicy entered Terminal.tsx in #13909 and left in
the split, taking six call sites with it, so every action-time creation gate in
the terminal and floating surfaces was gone. Restores those and the other
behavior the split dropped, each ported from the pre-split reference:
- Cmd/Ctrl+S dispatched a bare Event with no detail, so the only listener always
bailed on detail?.fileId and the chord never saved. Its resolver had been left
orphaned, imported by nothing but its own test.
- Terminal and floating create actions lost their availability gates, their
toasts, and their catch handlers; one path throws on unavailable, so it was a
silent unhandled rejection.
- Both outermost workbench wrappers lost the browser guest paint retention
branch, and the census entry covering them was deleted in the same commit.
- The Space Analyzer header counted omitted items the list no longer rendered,
and a worktree whose items were all omitted showed the empty state.
- The terminal root lost its tab topology projection, so every tab-title update
re-rendered it.
- The titlebar tab bar stopped being passed clientHostedBrowserRows, leaving
client-hosted pages uncloseable before a worktree has a layout.
- Parking diagnostics lost their exempt-route counts and crash breadcrumb.
- A suppressed inherited-terminal frame began buying a freshness scan the
pre-split early return skipped.
Adds regression tests for each, all verified to fail against the pre-fix code.
Restores three deleted assertions whose invariants are still live, and replaces
a concatenated source-boundary fixture with per-module pinning so a symbol is
again asserted against the module that must own it.
Deletes three orphaned trees the splits stranded: a duplicate ResourceUsage
surface, cmd-j-match-relevance, and an agent-session claim-key module whose
logic the record store already owns. Makes two non-recursive test walkers
recursive, one of which silently skipped every nested CLI handler group.
Renames seven -helpers modules for the concept their functions operate on, and
splits three that were genuine grab-bags -- each had a clean cleavage along its
importers, which is the signal AGENTS.md describes for a file holding more than
one responsibility.
Leaves keybindings/definitions-core-1..4 alone: definitions.ts spreads them in
order, so their concatenation order is the command palette order and regrouping
them thematically would be a user-visible change. Records that reasoning in a
comment so it is not re-litigated.
fragment-01..10 were arbitrary line-count slices of one template literal. Two
seams fell mid-expression -- inside buildMouseClickInput and inside the touchmove
listener -- so those pieces had no identity to name. Re-splits at real statement
boundaries and names each for what it holds.
The composed output is byte-identical: sha256 42cc000f..., 729776 bytes, verified
before, after the regroup, and after formatting. Also fixes two ratchet tests that
read fragment paths directly, one of which duplicated the composer's file list.
The split named these -part-N, which says nothing. Renames each for the group of
bridge methods it actually exposes and folds the single-method window-reveal
module into the window-controls module it belongs with.
Verified by walking the composed contextBridge surface before and after: 1060
keys, identical nesting and value types, zero delta. The bridge modules carry no
satisfies annotation, so a dropped key here is a runtime error in the renderer
rather than a typecheck failure.
The split left this logic inline in use-task-page-github-detail.ts at its
pre-STA-5949 shape: a 5s deadline whose expiry overwrote the remembered offset
with the committed 0 -- the permanent-loss bug the extracted module's header
says was removed -- observing only the children rather than the container, and
with no MutationObserver, so late-mounting rows never retriggered a retry. The
list scroll handler also blanket-returned while a restore was pending instead
of classifying echoes, dropping a user's scroll during an unreachable restore.
Promotes the module to a live path and wires the hook and scroll handler to it,
which also gives the ten behavioral cases in the scroll-restore suite something
live to assert against.
Five regressions shipped in the oversized-UI-surfaces split because the ratchet
tests guarding them still pointed at an earlier, orphaned split of the same file:
- GitLab rows lost the target/currentTarget guard, so Enter on the nested
open-in-browser button also opened the task detail.
- GitLab row keys dropped the repo prefix; work-item ids collide across hosts.
- GitHub Enterprise avatars lost their onError fallback in three slots, so an
unauthenticated avatarUrl rendered a broken image (#8784, #13976).
- Linear new-issue popovers lost the viewport-aware scroll container and
reverted to a fixed inner scroller the outer cap clips.
- Jira issue creation lost its catch, so a transport failure told the user
nothing.
Retargets the eight ratchet files at the live modules so these stay guarded.
probeRequiredNativeDeps mapped any thrown error to available:false, which
both triggered the repair and fed resetDeps — so one dropped exec channel
rm -rf'd node_modules/node-pty on a healthy relay and forced a node-gyp
source build. Verdicts are now ok / blocked / unverifiable; only an
answered probe may repair, and only an answered probe may name reset deps.
GitHub reports a release's createdAt as the date of the commit its tag
points at. Every adhoc tag is cut against orca-adhoc's single seed commit
(ff9ca5b6, 2026-08-02T09:46:58Z), so all of them share that one createdAt.
The 30-day cutoff crossed it today: the 06:53 run logged "Nothing to
prune", and the 10:34 run marked the entire channel expired and deleted
40+ releases -- including the one it had published two minutes earlier.
The picker had nothing newer than Aug 13 left to offer.
Age on publishedAt instead, keep any release missing one rather than
guessing, exclude the tag the run just shipped, and prune only after a
live publish. Hourly and daily already moved to publishedAt for the
adjacent sort bug; adhoc was the last one still on createdAt.
Two main-process `resolveGitDir` call sites dropped the WSL distro their caller
already held, so they could only resolve a gitdir pointer whose spelling carries
its own translation: a `//wsl.localhost/<Distro>/...` base, or a `/mnt/<letter>`
drvfs pointer that maps to a drive letter on its own.
The layout that needs the third case is a repo inside the distro's filesystem
with its worktrees on the Windows drive. `git worktree list` reports the worktree
as `/mnt/c/wt/x`, which the listing translates to `C:\wt\x` — a base that no
longer names a distro — while the `.git` gitfile beside it points at
`/home/me/repo/.git/worktrees/x`, which has no drive to derive. Win32 then treats
that pointer as absolute and reads a path that names nothing:
- `detectSparseCheckout` stats `info/sparse-checkout` under the fabricated path,
always misses, and reports the worktree as non-sparse — no sparse badge, and
the file list claims files that are not on disk.
- `readWorktreeDiffStamp` reads HEAD under the same path, gets nothing, and
returns null. Null is the safe answer ("cannot prove unchanged"), but it
retires the settled-diff cache for every file in that worktree, so each diff
respawns Git.
Both callers already have the distro: the listing threads its
`GitWorktreeExecOptions` to `annotateSparseCheckoutStatus`, on through
`detectSparseCheckoutCached` (the annotation cache added by #17859) and its
background revalidation probe, and finally to `detectSparseCheckout`; and
`file-diff` already forwards its `GitRuntimeOptions` to `readWorktreeDiffStamp`,
which now forwards it to `resolveGitDir` as well.
`resolveGitMetadataPath` still prefers a UNC base's distro and still tries drvfs
before the caller-named distro, so nothing that resolved before resolves
differently.
The cache hop matters twice over. It is the only remaining caller of
`detectSparseCheckout`, so without threading it the fix would not reach the
probe at all. And the cache is where the bug turns sticky. #17859 keyed entries
on `repoPath` + `worktreePath` alone, on the reasoning that the distro is a
property of the repo and so every read for a given `repoPath` carries the same
one. That invariant does not hold. `listRepoWorktrees(repo)` is called with no
options at all from the filesystem-auth root rebuild
(`registered-worktree-roots-cache.ts`, reached from `ensureAuthorizedRootsCache`
on any auth check with a dirty cache) and from the local worktree-ownership
check in `filesystem-worktree-helpers.ts`. Both land on the *same* key as the
distro-carrying listing, because `translateWslOutputPaths` derives the distro
from the cwd spelling before falling back to `options.wslDistro`, so a
UNC-spelled repo path yields the identical `C:\...` worktree row either way.
Measured on Windows in one process, branch build: a distro-less read followed by
a distro-carrying read reported the sparse worktree as non-sparse both times.
So `wslDistro` now joins the cache key -- trimmed and lowercased, matching how
the rest of the codebase compares distro names, and appended last so the
repo-scoped prefix delete still matches every variant. The per-path invalidate
becomes a prefix delete for the same reason, dropping every distro variant of a
removed or moved worktree.
Keying on it closes both halves of the defect. A correct caller can no longer be
served an answer derived without the distro it supplied. And because the entry a
reader reaches is now selected by the same distro it would re-probe with,
`revalidateInBackground` can no longer re-derive a warm entry under weaker
options -- which mattered on its own: a distro-less reader crossing the
five-minute window would otherwise flip a correct `true` to `false`, and the
resulting change notification runs the registered invalidator, clearing the
whole repo's cache and re-probing every worktree cold, on a five-minute loop.
Cost of the extra key dimension is bounded by the number of distinct distros a
given repo is actually read under: one where a distro is threaded everywhere,
two while the distro-less callers above still exist. Entries are still
repo-scoped, and both clears already sweep by prefix.
Per-platform delta:
- macOS/Linux: no change. Guest-pointer translation is gated to win32 and a
caller-named distro is ignored off Windows; no caller supplies one there, so
the cache keys and probes exactly as before.
- native Windows, no WSL: no change. `wslDistro` is undefined, so the resolver
takes exactly the branches it took before and every read keys on the same
empty distro component, so the cache behaves exactly as it did.
- Windows + WSL, UNC-spelled worktree: no change. The base already names the
distro and outranks the caller's.
- Windows + WSL, drvfs-spelled worktree with a drvfs pointer: no change. The
drive-letter derivation still runs first.
- Windows + WSL, drvfs-spelled worktree with a non-drvfs pointer: the sparse
badge appears and the settled-diff cache starts hitting. Both previously
failed toward "not sparse" / "do not cache", so neither can now serve a stale
answer, and the distro-less listings no longer share the badge's cache entry.
- SSH/relay: none. Those paths return through the provider branch before
reaching either function.
- folder workspaces, GitLab: none. Neither is on these code paths.
Not in this change:
- `readRepoCommonDirFromDisk` (worktree-listing). Passing the distro there is
inert: a repo root's `.git` is a directory, so the gitfile-pointer branch never
runs, and when `repoPath` itself is guest-spelled the preceding `stat` already
fails — which no `resolveGitDir` option can fix.
- The two `findExistingWorktreeSymlinkPaths` calls on the removal paths. Both
receive `registeredWorktree.path` from `listWorktreesStrict`, which already
translates every row out of the guest namespace, so the distro would be a
no-op. The `removeWorktreeLinkedPaths` unlink beside them is untranslated too,
so a half-threaded fix would only move the refusal from Orca's preflight to
`git worktree remove`.
- An absolute `commondir` payload, which `resolveGitCommonDir` still resolves
untranslated. Git writes that file relative in the layouts above, and the
failure direction is unchanged.
- Giving the two distro-less `listRepoWorktrees(repo)` callers a distro. Neither
reads `isSparse` -- both use only `worktree.path` -- so the distro would buy
them nothing they consume, while resolving a project runtime inside the
filesystem-auth rebuild would put a call that throws on `repair-required`
behind a catch that skips the whole repo's authorized roots. The cache key
makes their reads harmless; skipping the annotation for callers that never
read it is a separate, larger change. The third no-options call in
`hosted-review.ts` is inside the `repo.connectionId` branch and returns
through the SSH provider, so it never reaches this cache.
Agent Session History exceeded its 130-second deadline on large local Codex
histories. Three costs combined: excluded worker transcripts were recognized on
their first line but still drained to EOF (1,011 files / ~18.3 GiB on the
reported corpus), large ignored records were fully decoded and JSON.parsed, and
the persisted parse cache was discarded on every app update.
- Stop resumable reads the moment `session_meta` marks a worker transcript.
- Skip decode + `JSON.parse` for records the parser only feeds to the timeline.
The skip set is the complement of what `consumeCodexRecordLine` reads, and
applies only above the bounded prefix limit, so a long opening prompt (which
is the session title) still takes the exact parser.
- Prove cross-volume rollout aliases from a bounded `session_meta` read routed
through the WSL transcript FS gate, carrying the scan's AbortSignal, fanned
out across contested candidates with bounded concurrency.
- Make parse-cache schema 2 the semantic compatibility boundary so an update no
longer forces a multi-gigabyte cold scan, fenced by a build-time ratchet on
the persisted session shape.
- Report early-stopped transcripts as their own `aiVault.scan` attribute.
Verified on macOS, Ubuntu over SSH, and Windows: read volume drops 336 -> 49.5
MiB identically on all three; the Windows failing-test set is byte-identical to
main. Reported corpus: 130s timeout -> 58.6s, 244 sessions, 0 issues.
Fixes#17888.
* 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.
* perf(git): cache sparse-checkout annotation on worktree listing
`git worktree list` never reports sparse-checkout state, so every listing paid a
per-worktree fs.stat + config read to detect it -- measured at ~9x the cost of
the `git worktree list` call it decorates on a 1000-worktree repo. Cache the
result per worktree path, invalidated by the existing worktree-change
invalidator registry plus explicit remove/move hooks, with a 5-minute
reconcile window bounding the one unwitnessed edge case (external
`git sparse-checkout` toggle with extensions.worktreeConfig off), matching the
precedent already accepted in readRepoWorktreeAdminFingerprint.
* perf(git): normalize/scope sparse-checkout cache keys, add SWR
Address independent-review follow-ups on the sparse-checkout annotation
cache (#17859):
- Extract canonicalWorktreePath() from areWorktreePathsEqual and key/invalidate
the cache through it on both read and write, closing the disclosed
path-spelling P2 outright instead of leaving it as a residual risk.
- Scope cache entries and clears by repo path (derived from the invalidator
registry's repoId via a store lookup, falling back to a full clear when the
repo can't be resolved), so churn in one repo no longer evicts a sibling
repo's warm cache.
- Replace the hard 5-minute cutoff with stale-while-revalidate: past the
window, callers get the cached value immediately while a deduplicated
background probe corrects it and, on a flip, drives the existing
worktrees-changed notification -- collapsing visible staleness from the
full window to one refresh cycle at zero added listing latency.
Also corrects a stale claim in the original PR description: newer Git does
emit a `sparse` porcelain line (which annotateSparseCheckoutStatus already
skips), but Orca's Git 2.25 compatibility baseline predates it, so the
fallback detection this caches remains necessary.
* fix(git): stop background sparse-checkout revalidation resurrecting invalidated entries
Readiness-loop finding: a stale-while-revalidate probe in flight when a
worktree is removed/moved (or a repo's cache is cleared) would still write
its result back afterward, resurrecting an entry that was deliberately
dropped. Guard the write with a presence check so an invalidated key stays
absent until the next real read.
* fix(git): identity-check the sparse-checkout SWR write-back guard
The has()/presence guard from the previous commit only proved some
entry existed at the key, not that it was the one this revalidation
started from. A worktree removed and re-created at the same path while
a background re-detect was in flight would repopulate the key with a
fresh cold read, and the stale in-flight result would then overwrite
it -- exactly the race greptile (P1) and pullfrog both flagged as
still open. Compare the map's current entry by reference to the entry
captured when the revalidation began; a mismatch means something else
(invalidate, clear, or a fresh cold read) replaced it, and the stale
result must not be written back.
Added a regression test that fails against the old has() guard and
passes with the identity check: invalidate and repopulate the key with
a different value mid-flight, then let the stale revalidation settle
and assert the fresh value survives.