* 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.
Git can execute inside a WSL distro against a raw Linux worktree path while Node,
on the Windows side, reads the same files back through Win32. `path.join(
'/home/me/repo/feature', 'src/file.ts')` on win32 produces the drive-relative
`\home\me\repo\feature\src\file.ts`, which resolves against whatever the current
drive happens to be and almost always ENOENTs. The same mis-spelling hits the
drvfs form, where `/mnt/c/repo` should read as `C:\repo`.
Two consequences, both on the Node side only (git already works, because it gets
the Linux path as its cwd and resolves it inside the distro):
- getDiff's unstaged working-tree read missed, `readWorkingTreeFile` mapped ENOENT
to `exists: false`, and an existing file rendered as DELETED in the diff view.
- `readWorktreeDiffStamp` could not find `.git`, so the stamp was null, the settled
diff cache neither hit nor stored, and every diff respawned `git show` - two
`wsl.exe` spawns the cache exists specifically to avoid.
Both now spell the worktree directory for the reading host first, via a new
`resolveWorktreeHostPath` wrapper around the resolver that landed in #17804.
The wrapper exists because `resolveGitMetadataPath` trims: a gitfile payload
carries a trailing newline, but a directory name may legally begin or end with
whitespace on POSIX, so the wrapper keeps the caller's spelling whenever the
resolver only trimmed it. The stamp's opaque `value` still embeds the caller's
original `worktreePath`, so settled-cache identity is byte-identical and no cache
key moves.
`readWorktreeDiffStamp` was already `Promise<WorktreeDiffStamp | null>` with one
caller that treats null as a cache miss, so no new nullability enters the type
system and the resolver's never-null-for-a-non-empty-pointer contract is
untouched. The only unspellable input is an empty worktree path, handled locally
as "not provably unchanged" in the stamp and as a read *failure* (not a proven
deletion) in file-diff.
What changes for users
| Platform | Delta |
|---|---|
| macOS | No change. An absolute POSIX path is returned verbatim, including one whose directory name carries leading or trailing whitespace. |
| Linux | No change. Same reason. |
| Native Windows (no WSL) | No change. A `C:\...` or `\\server\share\...` path is already absolute for win32 and passes through verbatim. |
| Windows + WSL, UNC worktree path (`\\wsl.localhost\Ubuntu\...`) | No change. Already absolute for win32; passes through verbatim. This is today's common case. |
| Windows + WSL, drvfs worktree path (`/mnt/c/repo`) | Fixed. Reads as `C:\repo` instead of the drive-relative `\mnt\c\repo`. Needs no distro name. |
| Windows + WSL, Linux worktree path with a named distro (`/home/me/repo`) | Fixed. Reads as `\\wsl.localhost\Ubuntu\home\me\repo`. The deleted-file misrender goes away and the diff cache starts hitting. |
| Windows, POSIX path, no distro and not a drvfs mount | No change. Passes through verbatim, same ENOENT, same existing fallback. |
| SSH | No change. `runtime-git-diff-commands.ts` and the `git:diff` IPC both route to `provider.getDiff` for a connection, so this local code is never reached. |
| Relay / remote | No change. No RPC param, wire field, stream opcode, or published content is touched; the relay host runs the same local code and gets the same fix. |
| Folder workspace (non-git) | No change. `.git` is absent either way, `resolveGitDir` returns the same fallback, and the stamp stays null exactly as today. |
| GitLab / other providers | Not applicable. No provider-specific or review code is touched. |
What this does NOT do
- It does not fix `resolveGitDir` itself. For a drvfs repo whose worktree Orca
already spells `C:\repo\feature`, the gitfile payload `gitdir: /mnt/c/repo/.git/
worktrees/feature` is still mis-resolved by `path.resolve` to
`C:\mnt\c\repo\.git\...`, so the stamp still returns null in that shape. Separate
change, separate PR; this one neither fixes nor regresses it.
- It does not touch submodule path resolution. `resolveSubmoduleWorktreePath` is
the path-escape guard and has a near-identical twin in the relay; changing it
without escape tests on both is out of scope.
- It does not change `readHeadComponent`'s `commondir` resolution. The relative
`../..` git actually writes takes the identical `path.resolve` branch, and an
absolute POSIX `commondir` under a WSL UNC `gitDir` already resolves correctly
because the UNC root is `\\wsl.localhost\<distro>\`.
- It does not reorder drvfs-before-UNC inside the shared resolver. That changes the
identity of returned strings and needs a real Windows+WSL box.
- It does not add any Git command, option, or version dependency.
Costs and residual risk
- One extra pure function call per diff read. No I/O added or removed on the
unaffected paths.
- Translation still trims. `resolveWorktreeHostPath` preserves whitespace only when
no translation happened; a guest directory named `/home/me/repo ` loses its
trailing space on a Windows reader. Reachable only on win32, where such a name is
not addressable anyway, and the previous behavior for that shape was a
drive-relative miss.
- A relative worktree path (no caller passes one) is now resolved against the
process cwd instead of joined relative to it. Same file in every case except a
relative name that itself ends in whitespace.
- `UNSPELLABLE_WORKING_TREE_READ`'s `exists`/`failed` fields are correct but not
observable today: the stamp is null for the same input, so nothing can be cached
and `reusable` cannot be read back. They are there so the branch stays right if
`loadDiff` ever gains a second caller. The test pins the observable part - that no
read lands on a cwd-relative path.
- Every test here mocks `node:fs/promises` and spoofs `process.platform`. They prove
which path string reaches `stat`/`readFile`, which is the right assertion, but
none of this has executed against a real 9p mount on a Windows+WSL box and this
repo's CI has no such runner.
- Honest framing of the trigger: I could not demonstrate a mainline path that hands
`getDiff` an untranslated POSIX worktree path on Windows today -
`translateWslOutputPaths` UNC-translates worktree paths whenever a distro is
known, `getWslHome` returns the UNC spelling, and `resolveWslRepoWorktreeBasePath`
normalizes a configured Linux base. The drvfs case is the most plausible live one.
Treat this as defense-in-depth that is a strict no-op on every configuration above
except the two marked Fixed.
Verification
- `npx vitest run src/main/git src/shared/git-metadata-path.test.ts` -> 196 files /
2241 tests passed, 2 files and 5 tests skipped. One failure,
`git-admission-storm-measurement.test.ts > reports bounded-concurrency before and
after measurements` (ENOENT scandir on its own temp state dir), is pre-existing
and environmental: it fails identically in isolation and spawns real git children
without touching any changed module.
- `npx vitest run src/main/git/status-diff-settled-cache.test.ts` -> 21/21 (16
pre-existing, 5 new). `npx vitest run src/shared/git-metadata-path.test.ts` ->
25/25 (19 pre-existing, 6 new cases across 3 new tests).
- `npx oxfmt --write` then `npx oxlint` on all five changed files -> clean.
Mutation checks - all eight production substitutions were reverted one at a time
and the suite re-run. Each fails at least one test, and no new test survives its
own mutation:
| Reverted | Failing test |
|---|---|
| file-diff working-tree read -> `worktreePath` | reads the working tree through the host spelling instead of reporting a deletion; invalidates when the working tree file is edited under the host spelling |
| stamp working-tree component -> `worktreePath` | invalidates when the working tree file is edited under the host spelling |
| stamp `.gitmodules` stat -> `worktreePath` | invalidates when .gitmodules appears under the host spelling |
| stamp `resolveGitDir` -> `worktreePath` | stamps through the host spelling so the second read does not respawn git |
| `options` threading at the `readWorktreeDiffStamp` call | stamps through the host spelling...; invalidates when .gitmodules appears... |
| wrapper's untrimmed preservation -> return the resolver's value | keeps whitespace that belongs to the directory name (both cases) |
| `UNSPELLABLE_WORKING_TREE_READ` -> a cwd-relative `readWorkingTreeFile` | reads nothing relative to the cwd when the worktree path has no host spelling |
| stamp's null early return -> `hostWorktreePath ?? worktreePath` | reads nothing relative to the cwd when the worktree path has no host spelling |
The settled-cache tests seed the fake filesystem through the platform-bound `path`
module rather than `path.win32`, so they assert real behavior on a POSIX CI host as
well as on Windows and are not gated on the host platform.
Co-authored-by: Neil <79079362+brennanb2025@users.noreply.github.com>
Untracked line counts, and the untracked share of the branch line total, come
from direct lstat/open calls rather than from git. When git executes inside a
WSL distro the worktree path can be a guest path, which Win32 reads as
drive-relative (`/home/me/repo`) or as a literal `C:\mnt\c\repo`. Every lstat
then fails, countFileAdditions swallows the error into `{}`, the file renders
with no +N, and the branch total silently undercounts it.
Route only those two filesystem reads through resolveWorktreeFilesystemPath,
which translates a guest-rooted path via the shared resolveGitMetadataPath.
Both call sites produce the same string, so the stat-keyed untracked cache
still hits across them. resolveGitMetadataPath itself is not modified, so its
other callers are untouched.
The wrapper translates only when the host is win32 and the path is a guest
spelling: exactly one leading slash, and unchanged by trim(). Each condition
is load-bearing.
- `//wsl.localhost/...` and `//wsl$/...` are UNC spellings that also start with
`/`, and translating one prepends the distro root a second time, so a
worktree that reads fine today would ENOENT on every untracked file. The same
single-leading-slash guard is used, for the same reason, by
resolveWslRepoWorktreeBasePath in src/shared/wsl-paths.ts.
- resolveGitMetadataPath returns `rawPath.trim()`, so a worktree directory name
with leading or trailing whitespace (legal on ext4) would be re-spelt onto a
different directory. Leaving those verbatim keeps them exactly as they read
today.
- Off win32 the resolver is already an identity for these inputs; the platform
check makes the macOS/Linux no-op structural rather than derived.
Per platform:
- Windows + WSL, worktree spelled as a guest path: untracked +N appears and the
branch total includes it.
- Windows + WSL, worktree already spelled `\\wsl.localhost\...`,
`//wsl.localhost/...` or `//wsl$/...`: unchanged, returned verbatim.
- Native Windows: `C:\...` is unchanged. One shape does change: a
`/mnt/<drive>/...` worktree now resolves to `<Drive>:\...` instead of being
passed through to a guaranteed lstat failure. Native Windows does not produce
that spelling, and if it were reached the new result is the correct file.
- macOS / Linux: unchanged; not win32, returned verbatim, whitespace included.
- SSH / relay: unchanged. Remote status runs in the relay, which builds its own
branch-total input and does not pass filesystemWorktreePath.
- Folder workspaces / GitLab: unaffected, no workspace-kind or provider
behavior is touched.
No fail-closed degradation: attachLineStats still returns
`stagedStats !== null && unstagedStats !== null`, and createBranchLineTotalInput
still has no early return. The wrapper returns `string`, never null, so an
unmappable worktree keeps its old spelling and its old (missing) untracked
counts rather than dropping the staged/unstaged counts as well.
The `filesystemWorktreePath` field on computeGitBranchLineTotal is optional and
does not touch the lease key, so the coalescing/cooldown identity is unchanged.
Co-authored-by: Neil <neil@orca.local>
The fetch lock key is the worktree's resolved common Git directory. On a Windows
host two derivations split one repo into several lanes, so sibling fetches on the
same repo race the FETCH_HEAD write the lock exists to serialize.
- Windows aliases \\wsl$ to \\wsl.localhost and folds the distro name and any
drvfs tail case-insensitively, so two spellings of one repo produced two keys.
The finished key now goes through foldWslUncPathCaseInsensitiveParts. This is a
pure function of the finished key, so equal keys stay equal: it can only merge.
- Under a drive-spelled base, git-in-WSL's `/mnt/c/repo/.git` gitfile and
commondir pointers are read by path.resolve as the non-existent
C:\mnt\c\repo\.git. The commondir read then fails and every linked worktree got
its own dead-end key while the main checkout keyed on C:\repo\.git. Such a
pointer now goes through toWindowsWslDrivePath.
- realpath and stat take no AbortSignal, so cancelling a fetch still blocked
behind a hung 9P/UNC lookup. Both are wrapped in waitForPromiseWithSignal. The
rejection keeps today's synthetic AbortError shape, including when the caller
aborts with its own reason, because callers classify on error.name.
- The hand-rolled gitfile regex is replaced by the shared
parseGitdirMarkerPayload, matching git's own read_gitfile_gently.
A WSL UNC base is deliberately excluded from the pointer translation. win32
path.resolve already carries such a base's distro onto a guest-rooted pointer
(\\wsl.localhost\Ubuntu\home\me\wt + /mnt/c/repo/.git ->
\\wsl.localhost\Ubuntu\mnt\c\repo\.git), and a main worktree's `.git` is a
directory with no pointer to translate, so its key stays on that UNC spelling.
Rewriting only the linked worktrees to C:\... would have split one repo across
two lanes - the opposite of the intent. A test now pins that layout.
Direction of every key change, re-derived over a ten-layout matrix that runs both
this code and an emulation of the pre-change derivation under Win32 path rules:
nine layouts either merge or are byte-identical. The fold is merge-only by
construction; the pointer translation's sole delta is C:\mnt\c\X -> C:\X, and
C:\mnt\c\X is derived from bytes that live at C:\X, so it can only join a
worktree to its own common dir. The tenth layout is the one narrowing: the shared
parser accepts `gitdir:` only at offset 0 where the old regex accepted it on any
line, so a `.git` file with a leading blank line falls through to the parent walk
(C:\repo\.git\FETCH_HEAD -> C:\.git\FETCH_HEAD). Nothing can race there - git
2.44 refuses that same file with `fatal: invalid gitfile format`, so no fetch
runs in such a worktree at all. Native Windows repos with no WSL, SSH, relay and
folder workspaces are byte-identical.
hostPath() returns the same node:path submodule Node itself selects, so it is a
no-op on every real host; it exists so the Win32 derivation is testable off
Windows. resolveGitFetchHeadCommand's argument parsing is untouched, and
--git-dir gitfile dereferencing is deliberately not added: it would make N
worktrees of one repo serialize fetches that run in parallel today.
Speculative create-preparation evicts entries past the 3-entry limit and the
5-minute TTL, and both paths swallowed a failed `discardPreparedWorktree`.
The only other reclaim path, `cleanupStalePreparations`, skips any preparation
whose lock-reason pid is still alive, so a discard that failed inside the
running app stranded its scratch checkout and its locked worktree registration
until restart.
Record the failed discard keyed by host (repo path + WSL distro) and prepared
path, and retry it the next time a fresh preparation starts for that host.
The retry is kicked off before `listWorktreeGraph` but never awaited, so it
runs alongside the stale scan and the `worktree add` instead of sitting in
front of the user's create. It is capped at 3 attempts and warns when it gives
up. Enrolment is unconditional: `prepareWorktreeCreateCheckout` self-discards
on failure, but only best-effort, so a checkout that failed on a busy handle
can strand the same registration.
WSL2 keeps the guest filesystem in a dynamically-expanding ext4.vhdx.
Deleting files inside the distro frees the blocks for ext4 to reuse but
never shrinks the host-visible file, so engineers watching speculative
worktree preparation and mirrored worktrees write into a distro see the
vhdx grow and reasonably ask whether we leak disk.
Records the measurement taken on WSL 2.7.11.0 / Ubuntu-24.04 (a second
fresh incompressible 1 GiB after deleting the first cost zero growth,
measured as size on disk via GetCompressedFileSize), how to locate the
vhdx across all three install layouts, and a complete elevated diskpart
recipe for compacting existing slack. Scopes the sparse-flag observation
to the measured machine and states that peak-tracking is the best case
for block reuse, not a guarantee against drift.
The reclaim steps state their preconditions rather than reading as
directly runnable: --set-sparse needs the distro stopped and WSL 2.5 or
newer. .wslconfig is given as %UserProfile%\.wslconfig -- it lives in
the Windows user profile, not inside the distro at ~/.wslconfig.
Per-platform delta: documentation only, no production code. No behavior
change on macOS, Linux, native Windows, WSL, SSH, relay, folder
workspaces, or any git provider.
* Add browser history search to the new-tab omnibox
- Display matching pages from browser history in the tab entry panel
- Extract address-bar history scoring into reusable `browser-history-match` module
- Rank by match tier (host prefix > substring > title > tail) then frecency
* Replace History icon with ExternalLink for browser search results
* Replace ExternalLink with Globe icon for browser search results
* Fix browser history matching for workspace docs and recency rankings
Promote path-prefix matches to top tier for entries without a host (workspace
docs), and clamp the recency bonus so future timestamps cannot outrank fresh
visits. Includes tests for both path-prefix promotion and recency bonus
clamping behavior.
* Cache browser history by identity and snapshot omnibox entries
- Use WeakMap to cache prepared browser history entries by identity, so
re-parsing is skipped for the same snapshot
- Change omnibox to read a history snapshot at menu open (via getState)
instead of subscribing to live updates, preventing background navigations
from reshuffling results mid-keystroke
- Support fully-qualified URL prefix matching (e.g., `https://github.com`)
to preserve address-bar behavior
- Fix percentile calculation in performance tests (nearest-rank method)
* Break browser history ties with URL for stable snapshot ordering
When browser history entries tie on tier, score, and recency, the sort
order can become non-deterministic, especially when combining browser and
document history that may be reordered in snapshots. Add normalizedUrl
as the final tie-breaker to guarantee consistent ordering.
* fix(mobile-native-chat): retire an image echo glued with the send beside it
A message sent with images could render two or three times over, with the copy
carrying the photos sorting below the reply that answered it — and it never
cleared.
A send issued while the agent is mid-turn is glued onto the agent's input line
with any send adjacent to it, so the pair lands as one transcript row whose text
is the concatenation. Every retirement path then declined the pair:
- The image matcher wanted the whole row to equal the echo's text, so a glued
row never bound. That also stranded the local preview: the phone's photo never
reached the authoritative row.
- The exact-count path skips image echoes by design.
- The glue path excluded image echoes too, which made one a *barrier* — splitting
the run so the text-only send beside it was left alone, and a lone match is
rejected as an ordinary landing.
So neither echo could ever retire, and the unmatched image echo fell through to
the trailing bucket, which is what put it below the reply.
Match a glued row in the image matcher, and let an image echo take part in the
glue pass once its preview has been rebound. It stays a barrier while unbound,
so the existing guarantee is kept: an image echo never retires before its local
preview reaches the transcript row, or the photo would disappear.
* fix(mobile-chat): require image provenance for glued prefix matches
---------
Co-authored-by: Merge Sim <sim@local>
Delivery callbacks and telemetry belong to the completed send operation, not to the
picker instance that launched it. Remove early returns that skipped delivery
acknowledgment and success toast when the popover was already closed.
Documents the pre-existing render-time refs the split relocated onto changed
lines, and drops a ref assignment the split added that the monolith never had.