Custom agents now work across launch, display, recovery, and mobile paths:
- Resolve to base harness for execution; preserve requested identity for display
- Headless automation dispatches agent launches after worktree creation (so
variables can resolve), not at create time
- Vault deletion forgets all correlated session records when transcripts are
removed
- Mobile UI displays custom agent labels and icons via base harness
- Resume and recovery paths track pending launches with sleeping records
- Orchestration federation and worker topology route custom ids through the
host agentLaunch boundary
- Optimize display lookups with O(1) settings index instead of per-render scans
- Custom agents route through agentLaunch contract for dynamic args/env instead of legacy fields
- Mobile UI projects available custom agents based on detected base harnesses
- SSH Windows hosts resolve default shell for correct startup command quoting
- Capability probe errors now distinguish permanent (old relay method-not-found) from transient (connection) failures
- Background launch recovery routes to the execution host where the attempt occurred, not the repo owner
- Kimi agent removed from resumable agent list
- When contact with the execution host breaks during spawn, loss of contact is
not evidence the spawn failed; the host may have spawned the agent. Retain
the pending operation and admission reservation for reconciliation to settle on
real host evidence instead of settling the operation failed, which would let
plain Retry cold-start a duplicate beside a live remote agent.
- Rename availabilityCheck from 'host-preflight' to 'launch-reported': no
executable-existence preflight runs anywhere; availability is established only
by the launch itself on the execution host. Clients must not gate such rows on
baseline detection.
- Fix custom agent ID resolution in terminal pane operations (paste bracketing,
ctrl-enter input encoding): these can be owned by custom IDs, but TUI_AGENT_CONFIG
is keyed by built-in IDs. Resolve to base agent before indexing, falling back
to baseline behavior when the catalog is orphaned.
- Resolve custom agents to their base agent (e.g., command-code) when
determining prompt seeding behavior, while preserving the custom id.
- Add persistHostSessionBinding option for headless sessions to persist
session bindings server-side.
Custom agent IDs (custom-agent:base:uuid) must resolve to their base harness
for identity comparisons against built-in-keyed registries. Without this,
custom agents were silently excluded from session continuation, usage tracking,
account-switch restart logic, source-control actions, and agent availability
checks. Unified resolution through new agent-base-identity accessors and added
a ratchet test to prevent raw identity comparisons from drifting. Custom
agents now work throughout the app with proper labeling and icons.
- Enable resume of sleeping agent sessions with captured configuration
- Fix platform-specific command quoting for WSL, SSH, and Windows shells
- Ensure proper account selection in vault-backed agent launches
- Coordinate session identity across host creation and reconnection
- Add infrastructure for launch token echo and identity migration
- v36 daemon properly echoes launch tokens from listSessions/listProcesses; v34/v35 accepted but never stored/echoed them
- Worktree creates and PTY spawns now accept host-owned `agentLaunch` request, superseding client-determined command/config
- Custom agent catalog support expanded throughout agent launch, native chat, and telemetry paths
- Settings.get opts-out of 512 KiB catalog projection for CLI preflights
- Windows shell override now beats global terminalWindowsShell at agent spawn for proper quoting (#12320)
- Database schema bumped to v30 for dispatch termination reasons
Replace separate restore dialog with inline collapsible section. Show multiple custom agent examples in the picker. Update i18n to support the new disclosure pattern. Remove unused imports and make quick-open-filter.ts browser-compatible by replacing node:path with pure JS path calculation.
Detect when persisted agent catalog schema exceeds what this build supports and mark the profile as read-only. Add previewSettingsUpdate for safe settings inspection and updateSettingsDurable for checked mutations with rollback.
- Distinguish unreachable relay (null) from absent agents ([]) to prevent launch gates from misreading connectivity loss as "base agent unavailable"
- Fix data recovery dismissal to persist for both null and known timestamps
- Guard against blank host names in destructive dialogs
- Add fallback messages for unknown error codes in mixed client/host versions
- Catch dismissal rejections to prevent unhandled promise rejections
- Resolve and seed native chat session options for agent launches
- Rename custom-agent-platform job to custom_agent_platform (snake_case) to
fix env var generation and property access in workflow gates, since env vars
cannot contain `-` and `needs.<job>.result` with kebab-case parses as
subtraction
- Distinguish between absent and unreadable persisted state in launch stores
to prevent data loss: a file that exists but cannot be read (locked keychain,
EACCES/EBUSY) is not empty, so treating it as such and writing back would
overwrite intact bytes. Write-back is skipped until recovery merge runs.
- Extract shared test render harness and host-state helpers to focused modules
(new-workspace-composer-card-test-render, quick-commands-pane-host-state)
to reduce file size and improve reusability
- Move orchestration and AI vault scanning logic into dedicated files to reduce
coupling and improve discoverability
- Various bug fixes: vault resume now carries transcript paths for Pi/Prime-Agent,
tilde expansion defers to remote shell when home is unavailable, agent launch
identity is negotiated on remote runtimes, and background worktree creation
no longer auto-activates.
Why: CLI's offline settings writes need AGENT_CATALOG_SCHEMA_VERSION but
cannot import from out/main (electron-vite removes it during rebuild).
Extract pure schema logic to src/shared, FS-bound backup operations to
src/main. Add CLI require-resolution verification to catch MODULE_NOT_FOUND
at build time instead of runtime. Fix hydration-time purge to defer
incomplete repos: was deleting all local owners on remote-only hydration
(incident 2026-07-19).
Add recovery-point inventory and atomic restore, data-recovery IPC surface,
and app-level migration-blocked notice that persists until retry succeeds or
a pre-v1 backup is restored. Offline CLI writes are schema-transparent: they
refuse pre-v1 file downgrades and never stamp v1 fields without the pinned
backup contract. Mobile surfaces the blocked state in the agent picker.
Web clients probe host identity-launch capability and degrade to legacy paths
on pre-identity hosts, failing fast for custom agents or stored defaults that
cannot resolve.
space-y margins use :where() (zero specificity), so m-0 on nested
fieldsets collapses the gap. flex flex-col with gap-8 maintains
proper spacing between sections.
- Custom agents appear in mobile new-worktree agent picker with catalog snapshot
- Add tombstone GC that strips suppressed same-id rows during prune
- Echo launchToken through daemon, relay, and orchestration PTY layers
- invalid_launch_snapshot keeps pending for recovery when conflicting terminal exits
- Lock form inputs during edit seed and on failed seeds to prevent overwrites
- Save source control agent recipe before launch so host resolves current args
- Add stranded gate for orchestration dispatchForget (parity with automation)
- Validate commandOverride is a string instead of throwing in draftToDefinition
- Refetch catalog on custom agent mutations in all mount points
- Handle pre-spawn agentLaunch failures (disabled agents, capacity exceeded) with
typed failure codes in mobile and worktree-create paths, not generic errors
- Track worktreeId and principal through admission so per-worktree caps count
correctly; rebuild capacity on boot from durable pending snapshots
- Add background-agent-launch store persistence for unattended failure survival
- Validate catalog mutations (malformed payloads, tombstone prunes) and reference
writes (repo action overrides, quick commands) against live agent identities
- Fix spawn-in-flight detection to skip tokens mid-spawn during reconcile passes
- Improve locked-keychain recovery for session resume records via deferred merge
- Add accessibility tooltips for disabled catalog actions (no native title attr)
- Fix cmd metacharacter escaping for env-only variable references
- Improve orchestration dispatch forget idempotency and settlement ordering
Introduces a shared decision function that enforces the stale-reference
write rule across quick commands, commit-message, and Source Control
recipe mutations: unrelated edits preserve stale references untouched,
while any actual agent change must resolve to a currently enabled,
live built-in or custom agent identity.
- Fail-closed catalog/reference mutations while a pre-v1 profile backup is pending, and settle stranded orchestration dispatches with parity to worktree/background forgets.
- Harden env/argv handling against prototype pollution, reserved-name spoofing, and cmd-shell metachar injection; validate rehydrated session/operation records before use.
- Fix admission accounting (per-worktree cap on reservation commit, bounded retained launch records), reconciliation host-authority scoping to actually re-listed connections, and CLI wire-shape negotiation for pre-identity remote hosts.
- Recover terminal panes more precisely on undeliverable input (re-validate liveness before retrying/remounting) and keep the agent catalog search box usable in read-only mode.
Extracts the built-in/custom agent merge logic from NewWorkspaceComposerCard
into buildWorkspaceAgentOptions, so the composer card and the selection
resolver share one source of truth for which agents are choosable. Adds
e2e coverage for custom-agent launch surfaces (composer, annotations,
git actions, settings authoring, SSH) to close gaps left by the earlier
seededCustomAgents harness truncation bug.
- MAJOR: worktree.create now returns agentTerminalHandle on the SSH-remote and
folder-repo agentLaunch paths (were local-git only), by setting startupTerminal
from the launched outcome's terminalId — restores the --agent --json dispatch
contract on all three create paths (SSH is first-class per AGENTS.md).
- db.ts v7: run pane-column ALTERs unconditionally in the <7 step so a DB stamped
v6 by the pre-rebase branch build (identity cols, no pane cols) still converges.
- dismissLaunchNotice uses the validated worktree-id selector (repo-id rejection)
like its 10 siblings, matching main's tightened selector contract.
Co-authored-by: Orca <help@stably.ai>
- MAJOR: bump dispatch_contexts SCHEMA_VERSION 6->7 — main already shipped v6
(pane cols); reusing v6 for the U6 rebuild skipped it on main-built DBs,
crashing orchestration on upgrade. Split into <6 (pane ALTERs) and <7
(identity/forgotten rebuild, preserving pane keys); regression test added.
- draftPromptEnvVar win32 launches now honor the 24K env-block ceiling and
fall back to post-ready paste (matched main's dropped guard).
- worktree.create returns agentTerminalHandle on the host-resolved agentLaunch
path (not just legacy startupAgent), as the CLI --agent --json help promises.
- Remove dead terminalWindowsShell settings Pick (shell resolved host-side).
Co-authored-by: Orca <help@stably.ai>
Restore main's dropped onTerminalQueryReply mobile wiring (take-ours regression),
and split 3 files pushed over max-lines by the merge into cohesive named modules
(mobile-session-diff-line-row, host-client-context-contract,
orchestration-handler-test-harness). No max-lines disables or baseline bumps.
Co-authored-by: Orca <help@stably.ai>
Host-owned agent-launch subsystem, orchestration U6 identity/forget,
resolver #7862 Windows-shell + Hermes native-query parity, shared launch
contracts, preload/relay/CLI surfaces. One rebase landing split for review;
only the branch tip is expected to build/test green.
Co-authored-by: Orca <help@stably.ai>
* 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.
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.
Terminal sessions now report startup command delivery details (whether written, presence, length, and delivery method) without logging the command text—preventing credential leakage and distinguishing missing commands from lost ones in diagnostics.
Setup scripts now announce completion on both POSIX and Windows before executing the startup command, so healthy setups don't appear stuck in the UI with "Waiting for setup..." as the last visible line.
Diagnostics failures are caught and ignored so they never break session creation.
* Move workspace search toggle to floating button
Extract the search bar into a separate component and move the search toggle button from the toolbar to a bottom-left floating action button, positioned above the new workspace FAB. This consolidates phone-only floating actions in one location.
* Remove SearchWorkspacesFab component
Consolidates search functionality into bottom-left floating action button as part of mobile search button repositioning.
* 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.