Repository navigation
fix(worktree): a create does one fetch and one checkout, and background work waits for it - #23699
brennanb2025 wants to merge 32 commits into
Conversation
Review status: ready for reviewVerdict: ready. Head What changed during review, and why
User-facing effect: creates in a repository are no longer competing with Orca's own conflict checks there. While a create runs, the conflict details on that repository's pull request cards are checked against the base Orca last fetched, and they catch up on the first check after the create. Nothing the user asks for waits. Validation
Known gaps (unchanged by this PR)
Not covered: the QA ran only the build with this change. The comparison without the change came from the same build, with no create in flight, rather than from a separate main build. |
|
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (16)
💤 Files with no reviewable changes (2)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe change removes prepared-checkout creation and routes local worktree creation through regular add operations. It adds a timed activity hold for local creates and applies that hold to selected background work. Create-time remote base refreshes share fresh or in-progress fetches and use priority-aware Git locking. Startup now sweeps retired preparations. Created-worktree resolution checks the worktree directly before using a full listing. Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to Local worktree creation now uses a direct fetch and checkout, and selected background work is held during creates. The supplied evidence shows no concrete merge-blocking defect, though Windows, WSL and SSH scenarios have not been run in the app. Security Architecture ReviewSecurity architecture risk: 🟡 Moderate · up to Direct creation simplifies the workflow, and background waiting is bounded. However, automatic cleanup can act on an ownership check made before a delay. Concurrent adoption or modification of an old checkout may therefore put local work at risk. The demonstrated exposure is local checkout data. Retained concerns
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
ℹ️ No critical issues — one rough edge inline.
Reviewed changes
- Hold registry for in-flight creates (
worktree-create-repo-activity.ts, new): ref-counted map keyed bynormalizeRuntimePathForComparison(repo.path), with an idempotent release and an app-widehasWorktreeCreatesInFlight(). - Create entry points wrapped (
worktree-create-git-executor.ts,worktree-remote.ts:2331,runtime/runtime-local-worktree-create.ts:51):runLocalWorktreeCreatecomposes the existing interactive git executor with the hold, so the interactive-tier span is unchanged and the hold is released infinallyon success, failure, and cancellation. - Background base-fetch skip (
conflict-summary-base-tip.ts, new): with the repo held and effective tierbackground, base resolution reuses the last tip if under 5 minutes old, else reads the local remote-tracking ref on a cold cache without storing it. The skip runs outsidededupeBaseOidResolve, so interactive callers still fetch and never inherit a skipped fetch. - Expired base-tip entries retained (
conflict-summary-cache.ts):readFreshBaseTipResolutionno longer deletes expired entries, and the newreadLastBaseTipResolutionexposes them at any age; the 64-entry cap still bounds the map. - Idle ref packing vetoed during creates (
repo-maintenance-idle-gate.ts): the app-wide gate now also consultshasWorktreeCreatesInFlight(). - Tests: hold accounting, per-repo isolation, interactive vs background split, cold-cache read, the 5-minute cap, dedupe isolation, and the card lookup path; plus the touched idle-gate suite.
ℹ️ Nitpicks
- The test titled
still fetches for the create's own lookup, which inherits the interactive tierwould also pass if the create's lookup resolved to thestatusdefault rather thaninteractive, since both differ frombackground. The inheritance itself is pinned inworktree-create-repo-activity.test.ts, so only this test's name overstates what it asserts. PAST_CREATE_TIP_CAPhardcodes5 * 60_000 + 1_000while the production cap is an unexported module constant; exportingWORKTREE_CREATE_BASE_TIP_MAX_AGE_MSwould keep the test honest if the cap changes.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
Opening the new-worktree composer used to start a full background checkout of the repo (a "spare"), and a create claimed it, waiting with no time limit if it was still being built. On a busy disk that wait reached 209 s, and the spare's checkout slowed the create's own even when it was not claimed. Delete the spare-checkout preparation. A create runs one plain `git worktree add`. A startup sweep reclaims every spare an older build left on disk: locked registrations whose owning process is gone, and orphaned directories under .orca-preparing.
…etches A create waits for its base fetch so the new worktree starts from the latest origin/<base>. That fetch used to join the shared per-remote refresh chain, where it could sit behind any queued background or speculative fetch of the same remote; one create waited 82 s there. The create now runs its own targeted fetch at interactive priority. It reuses a fetch that completed in the last 30 s, but never joins or queues behind the shared chain. The in-process FETCH_HEAD lock also lets interactive git go ahead of queued background git in the same repo; a fetch already running still finishes first, because two fetches updating one ref would fail each other.
After `git worktree add`, a create listed every registered worktree to find the one it just made. On a repo with ~800 worktrees that walk took up to 57 s under disk load, on every create's critical path. Ask Git about the new worktree directly first; fall back to the full listing only when that read cannot confirm it. A create recovered this way already authorizes just the new worktree's folder.
While a local create checked out, Orca kept running background work on the same disk: listing every worktree after each watcher event, fetching and merge-tree-ing for PR conflict badges, and deleting removed worktrees' folders. Each extra full checkout-sized job roughly doubled the create. A create now marks "a local create is in flight" for its whole run. Only background producers consult it, at their entry: the .git watcher holds structural changes and delivers them once when the create settles, the PR refresh queue holds local background refreshes, and trash deletion waits. Each resumes after two minutes even if a create never settles. Nothing a client awaits ever waits on it. A card poll during a create answers its conflict summary from cache without running git; manual refreshes and SSH repos are unaffected. A ratchet test fails if the wait is used anywhere else.
4e83d07 to
f5a1052
Compare
…th spelling Concurrent creates of the same base, or a create while the composer's prefetch is running, now share one fetch instead of fetching again behind the FETCH_HEAD lock. The confirmed worktree row takes its path from Git's own gitdir record so its id matches later scans on case-insensitive disks.
…locks the rest The two-minute deadline now belongs to the stretch of overlapping creates, so no consumer re-waits a fresh deadline per batch or entry. The PR refresh queue skips held entries entirely, always re-arms for the next runnable one (SSH, rate-limited manual refreshes) and never substitutes a held one. The .git watcher holds only structural changes; status and head identity go out at once. The wait-site test also covers the indirect waiters.
The startup sweep reads .orca-preparing and each repo's worktree lock files instead of listing every worktree on every launch, so it works on Git 2.25-2.30, reclaims spares that were never locked or whose repo was removed, unlocks moved spares, and spawns no Git once nothing is left.
…ore each deletion An unlocked leftover spare with an index was listed in the sidebar and may now hold the user's work, so the startup sweep leaves it alone. Only a spare that was never checked out and holds nothing but HEAD's files is removed, judged by a status that lists untracked and ignored files whatever the user's config. A directory whose repo moved away is kept, and each deletion waits for in-flight creates instead of only the sweep's start.
The composer's base prefetch is the create's own fetch started early, so a create that joins it keeps interactive priority at the fetch lock. A created worktree whose path Git recorded as relative is confirmed from the listing, so its path matches later scans. Also drops an unused create-activity export and corrects two comments.
Git drops a worktree's registration once its path moves and anything runs a prune, so a spare the user had adopted could lose its registration and then be deleted whole at launch. The startup cleanup now deletes an unregistered directory only when it has no .git at all.
Review status: ready for reviewThe problem. Creating a worktree sometimes sat on "Creating worktree…" for two to five minutes, although Git's own copy takes 5 to 15 seconds even on a busy Mac. Orca was competing with itself for the disk: while a create ran, it was also preparing a spare checkout for the next create, running background fetches, rescanning What changes for you. A local create now does only its own work: one fetch of the base branch and one checkout. Orca's background disk work waits until the create is done, for at most two minutes. The create card, composer and sidebar look the same; "Creating worktree…" should just end sooner. Opening the composer no longer leaves Round 3 (2026-09-30). This round merged current
Round 3b (2026-09-30): merged #23837, which changed how worktrees are deleted. On
How the review ran. Round 1 reviewed the whole PR. Round 2 reviewed the fixes and included a separate agent that challenged the architecture. The fixes to the startup cleanup then got two narrow reviews. Round 2 found no serious (P0/P1) issue caused by the PR, which ends the loop. A final readiness check over the whole diff found nothing new. Fixed during review (the notable ones):
Deferred (low severity, not fixed here):
On splitting the PR. The architecture challenger suggested splitting the background hold (part B) into a stacked follow-up. I kept it in one PR because this step was scoped as A and B together, and A on its own leaves the create contending with the same background work. Verified
Not verified
Update after review. CI on the reviewed head failed 4 runtime create specs in |
There was a problem hiding this comment.
ℹ️ No critical issues — the create fast path, the in-flight signal, and the fetch-lock changes read correctly and their new suites pass. Two exports are now dead after the preparation subsystem was deleted.
Reviewed changes
- Spare-checkout subsystem removed: the pool, claim, burst, stale-cleanup, discard-retry, git-side preparation, and divergence-bound modules are gone; a local create now runs a plain
addWorktree/addSparseWorktree, and thepreparedCheckouttiming/telemetry andworktreeBaseRefFamilyare dropped. - Machine-wide create signal (
git/local-worktree-create-activity.ts, new): ref-counted hold with an idempotent release, a 2-minute deadline counted once per episode, andwhenLocalWorktreeCreatesSettle/createLocalWorktreeCreateDeferral.runLocalWorktreeCreatecomposes it with the interactive git executor for both the desktop and runtime create paths. - Background producers gate at entry only: watcher structural deliveries, the PR refresh queue, trash deletion, the retired-spare startup sweep, and idle ref packing;
conflict-summaryanswers background lookups from cache without running git, so no client-awaited path waits. - Create-owned base fetch (
runtime-remote-fetch-controller.refreshRemoteTrackingBaseForCreate): bypasses the shared refresh chain, shares one fetch across concurrent creates, and the fetch lock gained a priority tier (git-operation-lockwaiter queue,git-fetch-head-lock,git-exec-file). - Direct create confirmation:
describeCreatedWorktreereads the worktree's recordedgitdirso the row keeps Git's path spelling, andresolveCreatedWorktreetries it before the full listing (fallback), returningworktrees: []/listingComplete: false. - Startup reclamation of older builds' spares (
retired-worktree-create-preparation-sweep.ts) wired intomain-process-ready-runtime.ts, plus the wait-sites source ratchet and the hold/priority/fetch tests.
ℹ️ Exports orphaned by the preparation removal
Two public functions lost their only callers when the preparation subsystem was deleted but were left behind, so the removed feature still has live scaffolding.
Technical details
# Orphaned exports after removing the prepared-checkout subsystem
## Affected sites
- `src/main/git/worktree-base-ref-probe.ts:52` — `resolveLocalWorktreeBaseRef` has no callers repo-wide; its only caller was the deleted `worktree-create-preparation.ts`. `hasLocalWorktreeBaseRef` (which shares the probe) is still used.
- `src/shared/worktree/create-preparation.ts:6` — `createWorktreePreparationLockReason` is referenced only by `create-preparation.test.ts`; production now only parses the lock reason (`parseWorktreePreparationOwnerPid` used by the sweep and listing).
## Required outcome
- Delete the now-unused exports (and the test's use of `createWorktreePreparationLockReason`), or keep them deliberately with a note that they are retained for external callers.
## Suggested approach (optional)
- Remove `resolveLocalWorktreeBaseRef` outright.
- Have `create-preparation.test.ts` build the lock reason via the prefix constant (`WORKTREE_CREATE_PREPARATION_LOCK_PREFIX`) so it no longer pins a production-dead helper.DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…fetch and bounded listing The create's base fetch now runs at interactive priority and the created-worktree listing carries a time budget; two runtime specs matched the old call shapes exactly. The first mismatch also left an unconsumed one-shot listing row that failed the next two specs in the file.
…can-idle-gate # Conflicts: # src/main/runtime/runtime-local-create-rearm-ordering.test.ts
Comments Outside DiffThese findings could not be posted inline.
|
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
The delta since the prior pullfrog review (5f0b035) is a merge of origin/main plus one fix.
- Merged
origin/main— absorbed #23837 (git deletes removed checkouts in the background) and reconciled the create hold with it: the PR's legacy trash-deletion hold (scheduleWorktreeTrashDeletion/whenWorktreeTrashDeletionsSettled) is gone, replaced by main's startup-onlysweepStaleWorktreeTrash, which still waits for creates to settle before each delete; the wait-sites ratchet dropped the removed export; the idle ref-packing veto now also consultshasPendingWorktreeRemovals(); andmain-process-ready-runtime.tsruns main'sfinishInterruptedWorktreeRemovals()in the same first-window deferral as the PR's sweeps, after the awaitedloadWorktreeRemovalRecords. - The spare sweep respects a user's in-flight delete (
retired-worktree-create-preparation-sweep.ts,worktree-background-removal.ts) —reclaimSpareDirectorynow skips any path with a pending worktree removal (isWorktreeRemovalPendingAt), leaving it to that delete's own startup finish, so the sweep can no longer race the user's delete of an adopted spare. A real-Git test covers the interrupted-delete shape (checkout.gitremoved, registration still present); without the guard the no-.gitbranch wouldremoveHostTreeand the test would fail.
The added check is sound: the pending-removal record is loaded before the sweep runs, the path comparison uses areWorktreePathsEqual, and the user-adopted spare is the unlocked case, so the folder pass is the only sweep pass that can reach it. Focused suites pass (retired-worktree-create-preparation-sweep-real-git 12/12, plus the wait-sites, worktree-trash, and repo-maintenance-idle-gate suites 24/24).
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
The delta since the prior pullfrog review (5af61e3) is one fix.
- Match a pending delete through a symlinked workspace root (
retired-worktree-create-preparation-sweep.ts:172-175) —reclaimSpareDirectorynow resolves the spare path withrealpath(falling back to the literal path when the resolution fails) and treats the path as owned when either the literal or the resolved path has a pending worktree removal. Git records the worktree path resolved, so with a symlinked or junctioned workspace root the previous single-pathisWorktreeRemovalPendingAt(sparePath)check missed the user's in-flight delete, and the no-.gitbranch wouldremoveHostTreea directory that delete still owns. The new real-Git test builds a junctioned root and asserts{reclaimed: 0, removedDirectories: 0}with the file surviving; it fails without the resolution.
The change is narrowly scoped and correct. The lstat directory check still runs after the ownership test, so a spare path that is itself a symlink is not deleted, and the resolved path is used only for the pending-removal comparison. The focused real-Git suite passes (13/13, run this session). No previously flagged threads remain open (the two conflict-summary-base-tip.ts threads are resolved and the file is gone).
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
The delta since the prior pullfrog review (4535758) is the merge of origin/main (12b8ef8, #23698).
- Merged
origin/main(#23698) — absorbed the local-base-refresh rewrite (worktree-base-refresh.ts,worktree-add.ts,shared/worktree/local-base-branch-fast-forward.ts) and its SSH/relay counterparts, so a local create fast-forwards the base branch overlapped with the checkout and waits at most 30s for it. - Resolved the two modify/delete conflicts by deletion —
src/main/git/worktree-create-preparation.tsandsrc/main/git/worktree-preparation-base-oid.test.tsare gone. Both are prepared-checkout files this PR removes, and #23698 had modified them; #23698's only edit to the former was thependingLocalBaseRefRefresh/branchadaptation insidefinalizePreparedWorktree, the exact function this PR deletes, so nothing live is lost — the survivingaddWorktreepath already carries that adaptation from main.
git diff against the clean auto-merge (git merge-tree) shows exactly those two deletions and nothing else, so no other file needed a manual resolution. pnpm tc:node passes; the focused base-ref-refresh, create-wait-sites, and prep-sweep suites pass (31/31). The whenLocalWorktreeCreatesSettle wait-sites ratchet is unaffected because main's new code never mentions it, and no prior thread remains open.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…eep skips this process's spares Listings now hide a spare by Orca's lock reason or by an id this process owns, which covers the moment between worktree add and lock and Git 2.25-2.30 listings that carry no lock reason. The startup sweep skips an owned spare and re-checks ownership after every wait, before each git command or tree delete. Also adds the hook-run Git capability predicate the spare handover uses.
…t hook run (2.36) boundary The real-binary contract now runs a spare's build and handover in the order a create uses, checks that rev-parse --git-path hooks/post-checkout honors core.hooksPath, and pins git hook run as supported from Git 2.36 and recognized as missing before. The 2.25.5 and 2.38.1 lanes span it.
A create claims a spare only when it is completely ready for the create's base commit; it never waits on one. Every unfinished spare on the machine is stopped the moment a local create starts (its recorded git child is killed; on WSL it is left to finish) and removed later as background work under the create hold. A spare is built only after the composer's base refresh, on an idle machine: not while a create is in flight, not for 3 minutes after a slow create, only after the base has been stable for 2 s, and replaced for another base at most once per 30 s per repo. Nothing builds a spare after a create. The handover moves the locked spare, attaches the branch with branch + symbolic-ref, runs post-checkout with a plain create's arguments through git hook run (Git 2.36+; older Git gets no spare where a hook would run), and goes through the same pending-removal refusal and local base refresh skip as a plain add. A failed handover puts the spare back and falls back to a plain add; nothing on the create path deletes a tree.
Covers never waiting on an unfinished spare, the base-moved miss, a reset that exits after its abort, the machine-wide stop at create start and the ready spare it spares, the WSL abandon without a kill, the handover's hook arguments, put-back and unlock retry, the pending-removal refusal, the start gate (in-flight, slow-create cooldown and baseline, debounce, abandon window), background discard, the hook gate on old Git, quit, ownership in listings and the sweep, the spare commit matching a plain add, and the WSL workspace-root agreement. Existing create tests now expect the spare opt-in on plain new-branch adds.
… their rules The abort-after-exit case now stops the build through quit, which leaves the state alone, so only the reset's own abort check can refuse the spare. The gate tests wait for each spare request to finish before asserting, through a test-only settle hook on the request module.
Each spare is a full checkout, so a fourth repo's spare replaces the oldest one, as before the rework.
…observability - A spare hit runs post-checkout from the hooks directory the repo resolves (a relative core.hooksPath such as husky's gitignored .husky/_ exists only in the main checkout) and with the new worktree as the work tree, so the hook's git calls see what a plain add gives. - The handover moves the spare only to a path that does not exist, and checks that the moved .git names the spare before attaching a branch, putting it back or removing it, so it can never switch the main checkout or take over another create's worktree. - A spare request takes a ticket when the composer's prefetch arrives: a later pick supersedes it, and a create since it arrived refuses it (no build after a create). Quit is checked first. Each refusal is logged with its reason, and a create reports not_ready when its start stopped a building spare. - The slow bar's baseline is the median of a repo's last five non-slow checkouts, and a slow checkout that then failed still starts the cooldown. - A WSL spare abandoned before its checkout never starts it; the checkout skips submodule recursion, as worktree add does; an unregistered spare is never removed with git; and builds still running count toward the maintenance veto.
Husky-style hooks with a relative core.hooksPath and the hook's view of its work tree, a target that already exists or appears during the move, submodule.recurse, a request a create started inside, slow-then-fast picks, the baseline ladder, quit before the debounce, the refusal log, a failed slow checkout, a WSL spare abandoned while registering, an unregistered spare, and the sweep test reading lock reasons from Git's admin files so it passes on Git 2.25.
… not the async build
…t_ready to the create's repo, and name the target_exists miss A failed create's checkout feeds the cooldown only when it was slow; a fast failure (an existing branch, a bad base, a race at the target) no longer drags the baseline median down to the floor. The not_ready label is reset at every create start, so a spare stopped for one create never labels a later create in another repo. A create whose path already exists reports target_exists.
… repo's idle spare builds A create's plain git worktree add (never a spare hit or a no-checkout add), success or failure, is slow when it takes at least max(15 s, twice the fastest of the repo's last five completed spare builds), and starts a 3-minute cooldown from the moment it ended. Spare builds run only on an idle machine, so they are the repo's own idle checkout time; creates never feed the baseline. A fast failure, a hit's handover or a big repo's normal 20 s checkout therefore no longer move the bar or trip the cooldown, and an overlapping fast create can no longer restart it.
With the workspace folder inside the repo, .orca-preparing/<id> holds an untracked full copy of the repo. Quick Open's hidden-directory blocklist (shared with the SSH relay and the readdir walk), rg text search and the git grep fallback now all skip it.
…pares on RPC creates A spare build checks its stop flag before the add, the lock and the checkout, so a WSL spare abandoned before it registered runs no git at all. RPC and CLI creates now record the same worktree.create span attributes for spare hits and misses as IPC creates.
|
Closing. The create-timing data from #24483 shows the spare checkout makes creates substantially faster (median 1.0 s with a spare vs 2.6 s without) and that waiting on an unfinished spare is usually not slower than a fresh checkout, so limiting or removing the spare is not supported. The pieces still worth doing landed or are moving separately (#23698, #24483, #24093, #22356). |




ELI5
Creating a worktree sometimes sat on "Creating worktree…" for two to five minutes. The copy of the repo it makes is quick on its own (5 to 15 seconds even on a busy Mac), but Orca was doing other heavy disk work at the same moment and making the create wait for it. This PR makes a create do only its own work, and makes Orca's background disk work wait until the create is done. It affects local repos only: SSH repos and folder workspaces are unchanged.
Where this fits
Creating a worktree could take one to five minutes because Orca competed with itself for the disk: it built a spare checkout and waited on it with no time limit, ran background Git (fetches, status sweeps, PR conflict checks) during the create, and re-ran
git worktree listover and over, including for repos whose folders were deleted. The fix is split into these PRs:git worktree list; repos whose folders are gone stop being polledmainis updated safely, once per repo and branch, alongside the checkout, with no false "Local main was not refreshed" warningThis PR is step 1.
What Changed
The problem. On 2026-09-28, creates in the
orcarepo took 268 s, 193 s, 115 s, 85 s, 170 s and 336 s. Across five days of traces, the median create took 12 s but one in ten took 115 s or more.Why it happened (measured from Orca's own traces). Across the 14 creates that took 30 s or more:
The factors behind those numbers:
What is different for users:
How (mechanism):
worktree.create.prepared_checkout*trace attributes. This supersedes the preparation code fix(worktree): serialize worktree admin writes per repo and attribute git to its create #22342 edits..orca-preparingfolders and Git's worktree records directly (no Git process once nothing is left). It removes spares locked by an Orca process that is gone (a running older Orca keeps its own), spares that were never checked out and hold only the repo's own files (checked with a status that lists untracked and ignored files whatever the user's settings), and folders that never became a Git checkout. A checked-out spare without the lock, a checkout Git has stopped tracking, and anything else it cannot prove is Orca's are left alone. Every deletion waits for in-flight creates.origin/<base>so the new worktree starts from the latest commit. It now runs its own targeted fetch at top priority instead of waiting in the shared per-remote fetch queue, where it could sit behind queued background fetches (82 s in one trace). Concurrent creates of the same base share one fetch, and a fetch that finished in the last 30 s still counts. The in-app fetch lock now lets top-priority git go ahead of queued background git in the same repo. A fetch that is already running still finishes first, because two fetches updating one ref would fail each other.git worktree add, the create now asks Git about the new worktree directly, using the path spelling Git recorded so it matches later listings on case-insensitive disks. It falls back to listing every worktree only if that fails or Git recorded a relative path. The full listing is what took up to 57 s under load..gitwatcher holds structural changes and delivers them once when the create settles; status and branch changes still go out at once;The spare checkout: why Orca had it, what it cost, and why this PR removes it
Why Orca had it. The spare checkout landed in #17290, "perf(worktree): prepare checkouts while the composer is open" (merged 2026-08-30), to make Create feel instant. While the new-worktree composer was open, Orca built a full copy of the repo in a hidden folder under
.orca-preparing(git worktree add --no-checkout, thengit reset --hard), and on Create it moved that folder into place. On an idle machine, #17290 measured the median create dropping from 6.9 s to 0.4 s on macOS (20k files), from 8.3 s to 0.75 s on Windows (16.6k files), and from 1.0 s to 0.33 s on Linux.It hurt creates in two separate ways.
git reset --hardhit Git's 180 s timeout, and then did a normal checkout anyway. On its own, this part is a fixable bug: "never wait on a spare that isn't finished" would remove it.git worktree addat once took 10.6–11.6 s each, against 5–8 s one at a time. This is built into having spares, and no scheduling change can remove it. It is also why spares were rarely fast in practice: of 43 creates that used a spare, only 10 finished within 5 s, 11 took over 20 s, and the slowest took 218 s. Over five days Orca built 71 spares for 44 creates, so much of that disk work served creates that never happened.The timeout was a symptom, not the cause. The spare's
reset --hardtimed out because the disk was saturated: three checkouts at once, Orca's background PR conflict checks, and many other agents' work on the same machine.It is not a threading problem. Orca does not run Git on a thread. Every Git command, the spare's included, is its own operating-system process; Electron's main thread only starts it and waits for it to exit. During both incidents, quick Git commands started by the same process (
rev-parse,config,symbolic-ref) finished in a median of 20–60 ms, whilegit worktree addin the same minutes took 55–174 s. Orca's Git queue was not in the way either: create commands already run at the highest priority and never waited more than 2.2 s to start. The Git processes themselves were slow because they were competing for the disk, so moving Git to another thread or process would not help.Why delete it rather than only fix the wait. Fixing the wait removes harm 1 but keeps harm 2: on a busy machine, which is exactly where agents create worktrees in bursts, every spare still slows the real create. Keeping spares would also keep their bookkeeping: a pool of up to three spares, a five-minute expiry, locks, claiming, re-arming after each create, and cleanup after crashed Orca processes. That needed 17 follow-up fixes and still left 14 stray folders after the incident. The common pattern is a plain
git worktree addwhen the user clicks.Alternative considered, not tested. Keep spares but build them at the operating system's background disk priority, so a spare yields the disk to the real checkout. This was not measured. A low-priority spare would often not be ready when Create is clicked, so the create would fall back to a normal checkout anyway. It is worth revisiting only if the idle-machine speed below turns out to matter to users.
What removing it changes for users.
.orca-preparing.Performance study
Method. Both builds ran on the same M4Air (10 cores; Git 2.50.1), one at a time: main at 6f2a7d0, which still has the spare checkout, and this PR at 4535758. Each run used an isolated profile and the same fixture, a copy of the orca repo with 30,996 files and 100 other registered worktrees. The fixture was reset before every run, the app was relaunched, and Orca waited until it had been quiet for 15 s. The order of the two builds was randomized per pair. A script clicked through the real composer and measured the time from the Create click to the card leaving "Creating worktree…". Orca's own trace supplied the phase breakdown. There were 15 creates per build for S1, S2 and S4, and 5 bursts of 3 creates (S3) and 2 sequences of 5 (S5) per build.
Main is clearly faster only when the spare had time to finish (S1: 1.5 s vs 4.8 s median). In the other scenarios the medians are within about 2.5 s of each other, in both directions. Each of the 45–75 s dots ran during a machine-wide load spike, described below the table.
When Create is clicked before the spare is finished (S2, S4, S5), main does not skip the checkout: it waits for the spare to finish writing (green), which takes about as long as the plain
git worktree addthe PR runs (orange). Main's create took the spare in every composer run (15/15 in S1, S2 and S4, and 10/10 in S5). In S3, creates made without the composer had no spare to take, so both builds ran the same checkout.Outliers. The M4Air is shared with other agents' builds and tests: the 1-minute load average when Create was clicked ranged from 4 to 155. Of the 8 creates that took more than 3x their median (6 on the PR, 2 on main), 7 ran while the machine's load average was 32–132 at the start or end of the run, and in 5 of those free disk jumped by 3–8 GB during the run. In none of them does the trace show other Orca work overlapping the create. The eighth (PR, S1, 18.6 s) has no such sign. A spare hit writes almost nothing to disk, which is why main's S1 creates were not hit by these spikes.
The slowest create on main is the incident's mechanism. Main's worst create (S4, busy disk, 73.4 s) spent 67.9 s waiting for its unfinished spare, the same wait that took 209 of 268 s in the 2026-09-28 incident. The PR's slowest creates (45–65 s) were its one
git worktree addrunning during a load spike from other work on the machine; this PR removes Orca's own competing disk work, not the machine's.Checkouts written. For each create, main wrote one full checkout (the spare), and the PR wrote one (its own add). In S5, main also left one unused re-armed spare in
.orca-preparingat the end of each 5-create sequence. The PR left no.orca-preparingfolders in any run.S1 is the honest cost of this PR. On an idle machine where the composer was open long enough for the spare to finish, main creates in about 1.5 s and this PR takes one normal checkout, about 4.8 s here. The PR accepts this cost for three reasons. That speed comes from writing a full checkout in the background every time the composer opens. It also needs the spare pool's bookkeeping. And main's create waits for an unfinished spare with no time limit, which is what made one create in the 2026-09-28 incident spend 209 of its 268 s waiting. The follow-up #24093 shows the checkout's progress while it runs.
Not measured. This study did not reproduce the incident's multi-minute wait on a spare. It also did not measure creates whose base differs from the one the spare was prepared on, Windows, Linux, SSH, or a repo much larger than 31k files.
Why
checkout.workers=0). In a randomized benchmark it was about 25% faster alone (5.1 s against 6.9 s), but twice as slow when two creates overlap (19.9 s against 9.3 s each). Agents create worktrees in bursts, so Orca keeps Git's default. Acheckout.workersvalue the user set is still honoured, because Git reads its own config.This is step 1 of a three-part plan to stop Orca competing with itself for the disk:
.git/worktreesis added or removed, instead of after every watcher event and on short timers, and stop polling repos whose folders are gone.Differences from the common pattern
This PR matches the common pattern for creating a worktree: one
git worktree addwhen the user clicks, no checkout built in advance, a single-branch fetch of the base before the checkout, and background Git that waits, with a time limit, while a create runs.Linked Issue
N/A (internal).
Visual Proof
N/A: no UI changes. The create card, composer and sidebar look the same; what changes is how long "Creating worktree…" lasts. See the timings above and in Testing.
QA screenshots from a running build of this branch (M4Air, macOS, Git 2.50.1, scratch repo, isolated profile):
Composer open for about 50 seconds: no

.orca-preparingfolder and no background checkout.A create finished (733 ms) and the new worktree opened.

During a create slowed on purpose, the other worktrees keep rendering.

A worktree added from a shell during that create is found once the create finishes.

Delete: the card and folder are gone about 3.5 seconds after clicking.

Startup cleanup: a locked spare from a dead Orca and an orphan folder were removed; an unlocked spare holding a user file was kept (shown as a detached-HEAD card) and its file is unchanged.

Testing
vitestover every test file this PR adds or changes (30 files): 265 passed, 17 skipped (real-Git cases that need a newer Git or another platform). Earlier in review, every test file that imports a changed module also passed.tscpasses forconfig/tsconfig.node.json;oxlintand the changed-code quality gate pass.Review
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
preparedCheckoutfield that no client reads. No RPC or stream changes.worktree remove --force --force,worktree unlock,read-treeandstatus --untracked-files=all --ignored, all available in 2.25. A relative worktree record (an option added in 2.48) falls back to the full listing.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)