Skip to content

fix(worktree): a create does one fetch and one checkout, and background work waits for it - #23699

Closed
brennanb2025 wants to merge 32 commits into
mainfrom
brennanb2025/create-scan-idle-gate
Closed

brennanb2025 wants to merge 32 commits into
mainfrom
brennanb2025/create-scan-idle-gate

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 57 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​3271 $\color{#cf222e}{\Huge{\mathbf{−}}}$​3791 $\color{#cf222e}{\Huge{\mathbf{−}}}$​520
Prod 56 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2438 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1784 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​654

Parked (2026-10-01): waiting for real-world create timing data. Draft; do not merge.

Why it's parked. This PR first deleted the spare checkout Orca builds when the new-worktree composer opens. A performance study (below) showed that deleting it makes an idle create slower (about 1.5 s with a finished spare against 4.8 s without), while the spare's unbounded wait is what made the 2026-09-28 creates take minutes. So the branch was reworked to keep the spare under three rules: a create never waits on an unfinished spare; no spare is built while a create is running or for 3 minutes after a slow one; and no new spare is prepared after a create. The decision now is to collect real-world create timings before choosing between the two designs.

State of the branch (head 0481b7c). The three-rule rework is built and has had two full review rounds. Fixes from round 5 are partly in: the simpler "slow create" cooldown, spare folders hidden from Quick Open and text search, the WSL fix, and hit/miss recording for CLI and mobile creates. Two small round-5 items are not fixed yet: a WSL spare stopped mid-checkout can still feed the cooldown baseline, and a user's include filter in text search can bring a nested spare back into results.

Open question (not decided). On a spare hit, Orca has to replay the repo's post-checkout hook, and every way of replaying it runs the hook in a slightly different environment than a plain create does. The recommended option (option 1) is to build no spare in repos that would run a post-checkout, reference-transaction or post-index-change hook, with a fail-safe exception for husky's generated stub when the repo has no post-checkout script. The hook-replay code (git hook run, Git 2.36+) is still on the branch until that is decided.

The rest of this description still reflects the earlier design that deleted the spare, and will be rewritten if this PR resumes.

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 list over and over, including for repos whose folders were deleted. The fix is split into these PRs:

Step PR What it does
1 #23699 (this PR) A create does one fetch and one checkout (the spare checkout is removed); background disk work waits for a local create, at most two minutes, and never inside a request someone is waiting on
2 #24085 (stacked on this PR) One list of worktrees per repo, read from Git's own files and checked for changes, instead of repeated git worktree list; repos whose folders are gone stop being polled
2b #24093 (stacked on this PR) The create card shows Git's checkout progress (deferred from this PR)
3 #22356 PR conflict checks stop writing throwaway objects into your repo
separate #23698 Local main is updated safely, once per repo and branch, alongside the checkout, with no false "Local main was not refreshed" warning

This PR is step 1.

What Changed

The problem. On 2026-09-28, creates in the orca repo 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:

Where the time went Share
Waiting on an unfinished "spare checkout" 28% (older traces hide it inside "add", so it is higher)
The checkout itself, while other Orca checkouts, deletions and background git shared the disk 51%
Waiting on the base fetch 13%
Listing all ~800 worktrees to confirm the new one 7%

The factors behind those numbers:

  • Spare checkouts. Opening the new-worktree composer started a full background copy of the repo, and a create claimed it. If it was still being written, the create waited with no time limit: 209 s in the worst case, when the spare timed out and the create then did a normal copy anyway. 71 spares were built for 44 creates.
  • Checkouts slow each other down. In a controlled test on this repo, two checkouts at once took 10.6–11.6 s each, against 5–8 s one at a time. So every spare, and every extra background job on the disk, roughly doubled the create.
  • Nothing made background work step aside. It ran during creates on the same disk: listing all ~800 worktrees after file-watcher events, fetches and merge checks for PR conflict badges, and deleting removed worktrees (which take a median 11 s, 440 s at the 90th percentile).

What is different for users:

  • Clicking Create does one fetch of the base branch and one checkout. It never waits on a hidden spare or on the queue of background fetches, and it lists every worktree only as a fallback.
  • Opening the new-worktree composer no longer starts a background copy of the repo. It still starts the base fetch early, now at the same top priority as the create that will use it.
  • While a worktree is being created on this machine, Orca's background disk work pauses: re-listing worktrees after file-watcher events, automatic PR card refreshes and conflict checks for local repos (including the active worktree's and the one after a push), deleting removed worktrees, and ref maintenance.
  • That work catches up as soon as the create finishes. If creates keep overlapping, it resumes two minutes after the first one started, so a stuck create can never block it for long.
  • Never paused: a manual PR refresh, anything in SSH repos, branch and file-status changes in the sidebar, and every request the app, phone or CLI is waiting on (those answer from cache instead).
  • Leftover spares from older builds are cleaned up at launch. A spare that was checked out without Orca's lock showed in the sidebar and may now hold your work, so it is left for you to remove.
  • On an idle machine, a create that used to finish in about half a second, by claiming a ready spare, now takes one checkout: a few seconds on a large repo. The worst case drops from minutes to one fetch plus one checkout.

How (mechanism):

  • Spare checkout removed. The pool, the claim, the re-arm and the composer trigger are deleted, along with the 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.
  • Startup cleanup of leftover spares. Each launch reads the .orca-preparing folders 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.
  • The create's own base fetch. The create still waits for 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.
  • Confirming the new worktree. After 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.
  • One "a create is in flight" signal. Both create paths (the app and the CLI/runtime) hold it for the whole create. It is machine-wide, not per repo, because what a create competes with is the disk.
  • Only background producers consult the signal, and only at their entry:
    • the .git watcher holds structural changes and delivers them once when the create settles; status and branch changes still go out at once;
    • the PR refresh queue skips held local entries, runs everything else on time, and wakes itself when the creates settle;
    • trash deletion and the spare cleanup wait before each deletion;
    • repo ref maintenance does not start during a create.
  • Nothing a client awaits ever waits on it. A PR card poll during a create answers its conflict summary from cache and runs no git. A test scans the source and fails if the waiting call is added anywhere a client could be waiting.
  • The two-minute limit is counted once per stretch of overlapping creates, so a stuck create lifts the pause after two minutes instead of starving background work.

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, then git 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.

  1. A create waited on an unfinished spare with no time limit. In the 2026-09-28 incident, one create spent 209 of its 268 s waiting on a spare whose git reset --hard hit 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.
  2. A spare is a second full checkout on the same disk. It writes every file in the repo (about 30k here) at the same time as the real create. In a controlled test, two git worktree add at 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 --hard timed 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, while git worktree add in 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 add when 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.

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.

Create worktree time per scenario, main vs this PR, every create a dot with the median marked, log time axis

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.

Where the time goes in the median create of each scenario and build, as stacked phases

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 add the 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.

Scenario main median PR median main worst PR worst
S1 idle, Create 60 s after opening the composer 1.5 s 4.8 s 4.4 s 47.9 s
S2 idle, Create 1 s after opening 5.2 s 6.0 s 18.6 s 64.6 s
S3 burst of 3 creates at once (per create) 14.1 s 13.3 s 17.2 s 20.6 s
S3 burst total (first click to last card done) 14.3 s 13.5 s 17.2 s 20.6 s
S4 busy disk (a bounded background checkout loop), Create 5 s after opening 6.3 s 8.7 s 73.4 s 17.5 s
S5 5 creates in a row from the composer 9.0 s 8.1 s 11.9 s 65.5 s

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 add running 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-preparing at the end of each 5-create sequence. The PR left no .orca-preparing folders 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

  • Why delete the spare instead of "keep it but never wait on an unfinished one". See "The spare checkout" above: a spare that is still being written slows the create whether or not the create waits on it, because the two checkouts share the disk.
  • Why a machine-wide signal at each producer's entry, not a gate inside the git scheduler. The first design put the gate in the scheduler. An independent review found it could deadlock: a create could join a fetch already parked at the gate. Gating before a producer starts, and before it registers shared in-flight work, means a create can only join work that is already running.
  • Why not queue creates one after another. The controlled test shows two checkouts at once finish in about the same total time as two in a row, so queueing would only move the wait around.
  • Why not parallel checkout (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. A checkout.workers value the user set is still honoured, because Git reads its own config.
  • Why not move all git out of Electron's main process. See "It is not a threading problem" above: during the incidents, cheap Git commands from the same process finished in 20–60 ms while the checkout took minutes, so the Git processes themselves were slow.

This is step 1 of a three-part plan to stop Orca competing with itself for the disk:

  • Step 1, this PR: a create does one fetch and one checkout, and background work steps aside while it runs.
  • Step 2, a separate PR: re-list worktrees only when an entry under .git/worktrees is added or removed, instead of after every watcher event and on short timers, and stop polling repos whose folders are gone.
  • Step 3, fix(git): keep merge-tree's throwaway trees out of the real object store #22356: PR conflict checks stop writing their scratch objects into the user's repo.

Differences from the common pattern

This PR matches the common pattern for creating a worktree: one git worktree add when 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.

  • Intended: the pause covers every repo on the machine, not just the one being created in, because what a create competes with is the disk. Nothing the user is waiting on is ever paused.
  • Intended: the two-minute limit counts from the first of several overlapping creates, not separately for each waiting job, so a stuck create can't keep pushing background work back.
  • Intended: a create reuses a fetch of the same base that is running or finished in the last 30 seconds. Orca starts that fetch early when the new-worktree composer opens, and two fetches of one branch would fail each other.
  • Intended: at Orca's per-repo fetch lock, a fetch the user is waiting on goes ahead of queued background fetches; the common lock is first come, first served. A fetch already running still finishes first.
  • Intended: Orca reads back the one new worktree from Git, where the common pattern trusts Git's exit status or lists every worktree. The sidebar row needs the new commit, and listing ~800 worktrees took up to 57 s.
  • Intended: parallel checkout stays off. Measured here, it was 25% faster alone but twice as slow when two creates overlap.
  • Intended: during a create, PR conflict badges answer from the last known result instead of waiting, since a card poll may be serving a request someone is waiting on.
  • Intended: a cleanup at launch removes spares older builds left behind and keeps any that might hold the user's work.
  • Temporary: adding or removing a worktree still triggers a listing of every worktree, only delayed until the create ends; the common pattern re-reads just the entry that changed. perf(worktree): keep one worktree list per repo instead of re-running git worktree list #24085 removes this.
  • Temporary: the create card shows no checkout progress yet. feat(worktree): show checkout progress while a worktree is created #24093 adds it.

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-preparing folder and no background checkout.
Composer open, no spare checkout

A create finished (733 ms) and the new worktree opened.
New worktree open after create

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

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

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

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.
After the startup cleanup

Testing

  • Automated tests: vitest over 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.
  • Typecheck: tsc passes for config/tsconfig.node.json; oxlint and the changed-code quality gate pass.
  • New tests:
    • the create-in-flight signal (settle, one deadline per stretch of overlapping creates);
    • the source scan on where the waiting call may be used;
    • watcher hold, including SSH, status-only and head-change batches;
    • PR queue hold, including manual, SSH and deadline cases;
    • card-poll conflict summary answering from cache with no git, and computing again after the deadline;
    • trash deletion waiting;
    • the create's own fetch not queueing behind the shared chain, sharing one fetch between creates, and keeping priority when it joins the composer's fetch;
    • fetch-lock priority;
    • confirming the created worktree without the full listing, including a letter-case mismatch and a relative Git record;
    • real-Git tests of the startup cleanup: every leftover shape, spares that must be kept (commits, ignored and untracked files, a moved repo), and deletions waiting for a create that starts mid-sweep.
  • Platforms: macOS only (automated). The fetch lock, cleanup and pause are platform-neutral code. WSL repos hold the same signal. SSH repos run their git on the remote host and are not affected.
  • Automated tests added/updated
  • Manual QA in a running app (M4Air, macOS): composer open, create, create during a slowed create, a shell-added worktree appearing after it, delete, and the startup cleanup all passed with 0 renderer console errors. Not covered: PR cards during a create (the QA profile has no GitHub login), Git 2.25, Windows, Linux, WSL and SSH.

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Cross-platform: no platform-specific paths. The spare cleanup skips WSL-path folders, as the trash sweep does; registered WSL spares are removed through the repo's own Git routing.
  • SSH: remote git runs on the remote host. This PR changes nothing there, and SSH creates get no new protection from local background work (a known gap).
  • Folder workspaces run no git and are unaffected.
  • Wire compatibility: the create result's timing drops an optional preparedCheckout field that no client reads. No RPC or stream changes.
  • Git baseline 2.25: the cleanup reads Git's lock files directly (the listing shows lock reasons only from 2.31) and uses worktree remove --force --force, worktree unlock, read-tree and status --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.
  • Not in this PR: a checkout progress bar ("Updating files: N%"). Git prints it into a pipe on 2.50; showing it needs a stderr stream through the shared git runner and a sidebar label, so it is a follow-up.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@brennanb2025 brennanb2025 changed the title fix(worktree): background git waits while a worktree is being created in the same repo fix(worktree): stop PR conflict checks from competing with a worktree create in the same repo Sep 28, 2026
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status: ready for review

Verdict: ready. Head 4e83d073d78. CI is green (all required checks pass, the rest skipped). The node typecheck is clean. Live Electron QA passed.

What changed during review, and why

  • The first version made Orca's background git wait (up to 3 minutes) for a create to finish on the same repository. The waits sat inside shared request paths, and that caused real problems. A pull request refresh that started just before a create paused the whole refresh queue for every repository, and one lookup could wait twice. A manual refresh could join a waiting background lookup and wait with it. The Source Control status and branch compare views could freeze for the length of the create. Paired clients could hit their 30 s request timeout.
  • All waiting is gone. The dominant background work was the pull request conflict check: every time the base branch moves, it fetches the base and re-runs git merge-tree for each conflicting pull request. Now, while a create runs in a repository, background conflict checks for that repository skip the base fetch and reuse the base they already know (up to 5 minutes old). They then find their cached result and run no git merge-tree. Manual refreshes and the create's own lookups still fetch. Idle ref packing waits while any create is in flight.
  • The refresh-queue deferral and the idle-wait/deadline machinery were deleted. Idle ref packing and the conflict check both read the same per-repository record of which creates are in flight.

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

  • Three review loops, plus an independent architecture review that picked this design over waiting in requests, deferring the refresh queue, a skip in the renderer, or a new "deferred" response type.
  • Readiness checklist at the start and at the end: both PASS, no blocking findings.
  • Unit tests cover the new behavior, with each checked against the code without its fix.
  • Live Electron QA on a separate Mac (see Visual Proof in the description). With no create running, background refreshes fetched and ran git merge-tree. During a 131 s create, background refreshes ran neither. A manual refresh still fetched, and fetches resumed after the create.

Known gaps (unchanged by this PR)

  • SSH repositories: the conflict check never runs for them, and SSH creates take no hold.
  • git worktree list and git status volume during a create is not addressed here.
  • Every advance of the base branch re-runs the conflict check for every conflicting pull request all day. That existed before this PR and is a separate follow-up.

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.

@brennanb2025
brennanb2025 marked this pull request as ready for review September 29, 2026 02:14
@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[High risk] Refactors worktree creation sequencing and git operation scheduling.

The PR does not appear safe to merge until the local-base refresh cannot fast-forward a different branch after an owner-worktree switch.

Summary

This PR removes prepared worktree checkouts, gives creates their own base fetch, and defers background disk work during local creates. Changes since the prior review also revise local-base branch refresh, agent presence and child-work handling, structured-chat delivery, and simulator packaging.

  • Local-base refresh now overlaps checkout and uses a shared fast-forward policy for local and SSH repositories.
  • The owner-worktree fast-forward has a branch-switch race that needs attention.

Reviews (7) · Last reviewed commit: "merge: origin/main (12b8ef8c0be, #23698)..."

Comment thread src/main/github/conflict-summary-base-tip.ts Outdated
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 7dff873e-078a-4ba3-a899-079c0db345bb

📥 Commits

Reviewing files that changed from the base of the PR and between 5f0b035 and 5af61e3.

📒 Files selected for processing (16)
  • config/scripts/ci-shard-timings.json
  • src/main/git/command-runner/git-exec-file.ts
  • src/main/git/local-worktree-create-wait-sites.test.ts
  • src/main/git/worktree-mutation-route-invalidation.test.ts
  • src/main/git/worktree-scan-cache-sharing.test.ts
  • src/main/ipc/worktree-remote.ts
  • src/main/ipc/worktrees-local-create-flow.test.ts
  • src/main/ipc/worktrees-test-runtime-stub.ts
  • src/main/repo-maintenance-idle-gate.ts
  • src/main/retired-worktree-create-preparation-sweep-real-git.test.ts
  • src/main/retired-worktree-create-preparation-sweep.ts
  • src/main/startup/main-process-ready-runtime.ts
  • src/main/worktree-background-removal.ts
  • src/main/worktree-trash.test.ts
  • src/main/worktree-trash.ts
  • src/shared/git-binary-compatibility.test.ts
💤 Files with no reviewable changes (2)
  • config/scripts/ci-shard-timings.json
  • src/main/ipc/worktrees-local-create-flow.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The 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 5af61

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 Review

Security architecture risk: 🟡 Moderate · up to 5af61

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

  • Medium · reliability · inferred: The new startup reclamation path separates ownership inspection from destructive execution. After identifying a dead owner's spare, it can wait for local creates and then double-force-remove the registration without re-reading its lock, path, or ownership. The unregistered-directory path similarly waits before recursive removal without checking whether the directory became registered or acquired user content. Concurrent adoption, relocking, or modification can invalidate the original proof and expose user-owned work to irreversible cleanup. The bounded create gate coordinates scheduling; it does not reserve the candidate against ownership changes.
Security review details

Security Blast Radius

  • inferred — The evidenced destructive scope is local preparation directories and linked-worktree registrations reachable through configured roots or repository administration metadata. A party exploiting the identified race would need concurrent control over a candidate path or its ownership metadata; an unauthenticated remote attack path is not established.

Security Findings and Attack Paths

  • inferred — The new cleanup path can convert a stale ownership decision into forced deletion: inspect an abandoned preparation, wait, allow its ownership or contents to change, then delete using the earlier decision. This is an inferred integrity and failure-containment risk, not a verified Security finding.

Trust Boundaries and Controls

  • observed — Initial reclamation checks preserve live owners, mismatched locks, indexed checkouts, pending removals, and user content detected by status including ignored and untracked files. Directory scanning excludes symlinked preparation roots and candidates. These controls constrain selection but are not held atomically through destructive execution.

Hardening Proposals

  • proposed — Couple reclamation authority to destructive execution through a shared ownership claim or equivalent serialization with adoption and removal. Perform waiting before final ownership validation, and preserve or quarantine candidates whose identity cannot be established rather than force-removing them.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.21% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 95 functions across 57 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: one fetch and checkout for local creates, with background work deferred during creation.
Description check ✅ Passed The description is detailed and covers the required explanation, changes, rationale, testing, visual proof, compatibility notes, and checklist. The Linked Issue is marked N/A for this internal PR, whi…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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 by normalizeRuntimePathForComparison(repo.path), with an idempotent release and an app-wide hasWorktreeCreatesInFlight().
  • Create entry points wrapped (worktree-create-git-executor.ts, worktree-remote.ts:2331, runtime/runtime-local-worktree-create.ts:51): runLocalWorktreeCreate composes the existing interactive git executor with the hold, so the interactive-tier span is unchanged and the hold is released in finally on success, failure, and cancellation.
  • Background base-fetch skip (conflict-summary-base-tip.ts, new): with the repo held and effective tier background, 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 outside dedupeBaseOidResolve, so interactive callers still fetch and never inherit a skipped fetch.
  • Expired base-tip entries retained (conflict-summary-cache.ts): readFreshBaseTipResolution no longer deletes expired entries, and the new readLastBaseTipResolution exposes 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 consults hasWorktreeCreatesInFlight().
  • 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 tier would also pass if the create's lookup resolved to the status default rather than interactive, since both differ from background. The inheritance itself is pinned in worktree-create-repo-activity.test.ts, so only this test's name overstates what it asserts.
  • PAST_CREATE_TIP_CAP hardcodes 5 * 60_000 + 1_000 while the production cap is an unexported module constant; exporting WORKTREE_CREATE_BASE_TIP_MAX_AGE_MS would keep the test honest if the cap changes.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/github/conflict-summary-base-tip.ts Outdated
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.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/create-scan-idle-gate branch from 4e83d07 to f5a1052 Compare September 29, 2026 06:51
@brennanb2025 brennanb2025 changed the title fix(worktree): stop PR conflict checks from competing with a worktree create in the same repo fix(worktree): a create does one fetch and one checkout, and background work waits for it Sep 29, 2026
…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.
@brennanb2025

brennanb2025 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Review status: ready for review

The 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 .git folders, refreshing PR cards, deleting old worktree folders in the background and repacking refs. The create had to wait behind all of that.

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 .orca-preparing checkouts behind, and on first launch Orca removes the spare checkouts older builds left there, keeping any spare that might hold your work. SSH repos and folder workspaces are unchanged.

Round 3 (2026-09-30). This round merged current main and answered an open bot finding. It changes no app behavior, so the earlier app testing still stands.

  • Merged main in a381210. The one conflict was a test that only checked when the removed spare-checkout pool was refilled, so it stays deleted. The runtime create path keeps main's newer per-repo refresh of the allowed-folders list. A separate check compared the merge against main's own changes file by file: nothing from either side was dropped, and no newly merged code waits on a create inside a request someone is waiting on.
  • The bot finding said the startup cleanup deletes a spare folder that has no .git without looking inside, so files could be lost. We checked every way such a folder can appear. Older builds made spares only with git worktree add --no-checkout, which writes .git first. Such a folder is left only when a git worktree remove that Orca or the user had already started stops partway (a Windows file lock, a timeout, quitting); Git then drops .git and its record but leaves the checkout files. We reproduced that with Git 2.25 and 2.50. A spare you may have worked in still has .git and is kept. So the deletion stays, and the reply is on the thread. The test's hand-made fixture (a folder with one file.txt) is what made the code look like it deletes user files; in 5f0b035 it is now what Git really leaves behind.
  • A final readiness check over the whole diff after the merge found nothing.

Round 3b (2026-09-30): merged #23837, which changed how worktrees are deleted. On main, git worktree remove now deletes the folder itself, under its own limit, and a delete a quit interrupted is finished at the next launch.

  • Deleting a worktree never waits for a create. You are waiting on a delete, so holding it would leave a "Deleting" card stuck for up to two minutes. The only deletion work that still waits for a create is the startup cleanup of trash folders older versions left behind, which nobody waits on.
  • Every create path still goes through main's "cleanup pending" check, which refuses a create at a path or branch that is still being deleted. main's old spare-checkout path skipped that check, and this PR removes that path.
  • Fixed in this round: at startup, the spare cleanup and main's finish of an interrupted delete could both act on the same spare the user had asked to delete. Your files were never at risk, but the delete could fail for no reason. The cleanup now leaves any folder with a delete in progress to that delete (5af61e3), including when the workspace folder is reached through a symlink (4535758). A real-Git test for each fails without the fix.
  • A separate review of the merge found nothing dropped from either side. CI passes on 4535758.

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):

  • The composer's early fetch and the create's own fetch ran twice; a create now joins the fetch already running and keeps its priority for Git's fetch lock.
  • PR card refreshes for SSH repos, and ones already due, could be starved for up to two minutes behind a held local entry; only local, non-manual entries are held now.
  • Folder-watcher notices during a long create arrived at most once every two minutes; the wait is now one deadline per stretch of overlapping creates, and status and branch changes go out immediately.
  • The startup cleanup could delete user work in three shapes introduced while fixing it in this loop: an unlocked spare that had been checked out, untracked or ignored files hidden by a user's Git settings, and a spare whose Git record was pruned after its repo moved. Each now stays on disk, with a real-Git test for each.
  • The cleanup's deletions now wait for any create that starts mid-cleanup, so the first launch after upgrading does not slow the first create.
  • A worktree confirmed with a relative Git record (a Git 2.48+ option) now falls back to the full listing instead of reading as a different worktree.

Deferred (low severity, not fixed here):

  • The startup cleanup's own git worktree remove of dead spares runs under the normal background Git limit, not the new limit for deletes.
  • The PR-card refresh hold partly overlaps the card's cached conflict summary; the hold is in the scope set for this step, so it stays, and a later step can drop it.
  • A Merge click during a create gets the cached conflict summary, so a refusal may not list the conflicting files until the next refresh.
  • The composer's early fetch now runs at create priority; flipping four or more repos quickly can briefly fill the fetch budget so a push or pull waits (bounded by a 30-second freshness window and a 60-second timeout).
  • The test that guards where the waiting call may be used only sees export function declarations, not exported arrow functions.
  • The cleanup skips orphan folders under WSL paths; a spare with a corrupt HEAD is kept and logs a warning each launch.
  • A disk-contention benchmark of git worktree add under each held background job needs your approval to run.

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

  • Round 3b, on 4535758: tsc for the node project is clean; 41 test files pass after the merge (1668 tests), plus the new symlink test; all CI checks pass.
  • Round 3, on 5f0b035: tsc for the node project is clean; 33 test files pass (1618 tests passed, 18 skipped), including the runtime create specs, the real-Git cleanup tests and the test that keeps waits out of request paths; oxlint is clean.
  • Automated: all 30 test files this PR adds or changes pass (265 passed, 17 skipped for newer Git or another platform); tsc for the node project, oxlint and the changed-code quality gate pass.
  • In a running build of this branch on a second Mac (M4Air, macOS, Git 2.50.1, scratch repo, isolated profile), with 0 renderer console errors across three launches:
  • Opening the composer for about 50 seconds created no .orca-preparing folder and ran no background checkout.
  • A create finished in 733 ms and the new worktree opened.
  • During a create slowed on purpose, the other worktrees kept rendering; a worktree added from a shell during it was found after it finished.
  • Delete: the card and folder were 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 with a user file was kept and the file unchanged; user worktrees were untouched.

Composer open, no spare checkout
Sidebar during a slow create
After the startup cleanup

Not verified

  • PR cards during a create: the QA profile has no GitHub login, so this part was blocked.
  • Git 2.25 in a running app (the tests cover the commands; the QA Mac has 2.50.1).
  • Windows, Linux, WSL and SSH in a running app.
  • The cleanup of an unlocked spare that was never checked out (covered by a real-Git test only), and spares made by an actual older build (the QA spares were made by hand).
  • A kept spare shows as a "Detached HEAD" card in the sidebar, as it did before this PR.

Update after review. CI on the reviewed head failed 4 runtime create specs in src/main/runtime/orca-runtime.test.ts: two expected the create's old fetch and listing call shapes exactly, and the first mismatch left a one-shot listing row that failed the next two. Test-only fix in d6656c1; all 40 checks pass on it.

@brennanb2025
brennanb2025 marked this pull request as ready for review September 29, 2026 08:46
Comment thread src/main/retired-worktree-create-preparation-sweep.ts

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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 the preparedCheckout timing/telemetry and worktreeBaseRefFamily are 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, and whenLocalWorktreeCreatesSettle / createLocalWorktreeCreateDeferral. runLocalWorktreeCreate composes 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-summary answers 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-lock waiter queue, git-fetch-head-lock, git-exec-file).
  • Direct create confirmation: describeCreatedWorktree reads the worktree's recorded gitdir so the row keeps Git's path spelling, and resolveCreatedWorktree tries it before the full listing (fallback), returning worktrees: [] / listingComplete: false.
  • Startup reclamation of older builds' spares (retired-worktree-create-preparation-sweep.ts) wired into main-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.

Pullfrog  | Fix it ➔ | View workflow run | Using 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
@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Comments Outside Diff

These findings could not be posted inline.

  • P1 Deletion can lose recovery record src/main/worktree-background-removal.ts:304 ▶

    If writing the removal record fails or takes more than two seconds, the accepted delete proceeds without a durable recovery record. A quit or crash during deletion can then leave a partly removed checkout that startup does not know to finish.

  • P1 Deletion overlaps worktree creation src/main/worktree-background-removal.ts:251 ▶

    When a local worktree create is in flight, an accepted background removal starts git worktree remove without waiting for the create to settle. Up to two deletes can run during the checkout and compete for the disk, recreating the contention this PR is meant to prevent. The detached delete should wait before starting without delaying the request’s acknowledgement.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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-only sweepStaleWorktreeTrash, 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 consults hasPendingWorktreeRemovals(); and main-process-ready-runtime.ts runs main's finishInterruptedWorktreeRemovals() in the same first-window deferral as the PR's sweeps, after the awaited loadWorktreeRemovalRecords.
  • The spare sweep respects a user's in-flight delete (retired-worktree-create-preparation-sweep.ts, worktree-background-removal.ts) — reclaimSpareDirectory now 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 .git removed, registration still present); without the guard the no-.git branch would removeHostTree and 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).

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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) — reclaimSpareDirectory now resolves the spare path with realpath (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-path isWorktreeRemovalPendingAt(sparePath) check missed the user's in-flight delete, and the no-.git branch would removeHostTree a 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).

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.ts and src/main/git/worktree-preparation-base-oid.test.ts are gone. Both are prepared-checkout files this PR removes, and #23698 had modified them; #23698's only edit to the former was the pendingLocalBaseRefRefresh/branch adaptation inside finalizePreparedWorktree, the exact function this PR deletes, so nothing live is lost — the surviving addWorktree path 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.

Pullfrog  | View workflow run | Using 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.
…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.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant