Skip to content

fix(worktree): a create never waits for a spare checkout that isn't ready - #29115

Closed
brennanb2025 wants to merge 3 commits into
mainfrom
brennanb2025/create-skips-unready-spare
Closed

brennanb2025 wants to merge 3 commits into
mainfrom
brennanb2025/create-skips-unready-spare

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​63 $\color{#cf222e}{\Huge{\mathbf{−}}}$​888 $\color{#cf222e}{\Huge{\mathbf{−}}}$​825
Prod 13 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​132 $\color{#cf222e}{\Huge{\mathbf{−}}}$​210 $\color{#cf222e}{\Huge{\mathbf{−}}}$​78

ELI5

Creating a worktree could hang for minutes.
Orca builds a "spare" checkout in the background so the next create is fast.
If you clicked Create while that spare was still being built, the create sat and waited for it, with no time limit.
Now the create only uses a spare that is already finished. If the matching spare is still being built, Orca stops it and makes the worktree the normal way right away.

What Changed

Before: a create could pick a spare that was still being built and wait for it to finish, however long that took. On 2026-09-28 one create took 268 s, 209 s of it waiting on an unfinished spare while Orca's own background Git work competed for the disk.
After: a create never waits on a spare. A finished spare is used as before. If the only matching spare is still being built, the create stops it and immediately runs the same git worktree add it already uses when there is no spare. The stopped spare is cleaned up in the background, and the create does not wait for that either.
A spare is now built once and not touched again. Previously, each time the new-worktree dialog reopened after a fetch, Orca moved the existing spare to the newest commit in the background. That refresh made the spare unusable at exactly the moment a create (which waits for the same fetch) arrived. Now the create claims the spare right away, and the existing finishing step moves it onto the requested commit, as it already did whenever the base had moved.
Both create entry points get this through their shared path: the new-worktree dialog and the runtime API used by the CLI, agents, the phone app and paired clients.
Telemetry: the existing prepared-checkout outcome gains one miss reason, not_ready. prepare_failed and the prepared_checkout_wait phase are no longer produced; they stay in the event vocabulary so the event shape does not change. Expect more hits to report base_moved, because the move to the newest commit now happens at claim time.

Why

The structural problem was that a user action depended on speculative background work: it either waited for it with no deadline or ran its own checkout beside it. A timeout on the wait would still make the create sit behind disk work for the length of the timeout.
Now readiness is one fact, owned by the spare's build: finished or not. A create takes only a finished spare. An unfinished match is taken out of the pool and stopped, so the create's own checkout is not sharing the disk with a second full checkout of the same tree. Prefetches for the same base wait until the create finishes, through the claim mechanism that already exists.
Removing the background refresh removes the only way a finished spare could become unfinished again, so readiness never flips back. The finishing step already resolved the requested commit and reset the spare onto it, so nothing it did is lost.
On WSL, stopping a build used to kill only wsl.exe while Git inside the distro kept writing. The spare's checkout now uses the existing process-group termination that other long WSL Git operations use, so cleanup starts only after it has really stopped.
The common pattern is that a create runs its own fetch and git worktree add and never depends on speculative work. This change makes Orca's spare purely opportunistic.

Differences from the common pattern:

  • Intended: Orca still keeps background spare checkouts. Safe because a create now only uses one that is already finished, and stops an unfinished one instead of waiting on or competing with it. Whether to keep spares, the automatic re-arm after a create, or a cooldown is a separate decision for the post-telemetry data.

Trade-offs, stated plainly:

  • A create clicked while a large spare is almost finished now does a full checkout of its own instead of waiting out the last few seconds. That is bounded by one normal create, never the unbounded wait.
  • If the dialog sits open long enough after a fetch, the move to the newest commit used to be done before the click. It is now done after the click by the finishing step: a reset limited to the files that changed.

Unchanged: ownership checks, the fetch barrier a spare's first build waits on, local base refresh, recovery, host routing (local and WSL), and the create timing telemetry.
Scope: this does not remove spares, change when they are re-armed, add cooldowns, defer conflict checks, or gate creates.

Linked Issue

Old context: #23699 (parked; its broader scheduling changes are not part of this PR).

Visual Proof

N/A: no UI change. The difference is how long Create takes while a spare is building; see Testing.

Testing

  • Tests for the preparation pool, claim, timing, cancellation, contention, fetch barrier, WSL root, discard retry, runtime create, and the real-Git preparation suites pass.

  • New/updated tests pin: a create while the spare is still building gets not_ready, stops the build, and returns while its cleanup is still running; a prefetch for the same base during that create waits until the create finishes; a finished spare is still used, including after the dialog reopens and fetches again, with no second build; selection prefers a finished spare over a building one and names a building match.

  • Removed: tests for the background tip refresh (the mechanism is gone) and tests for a create waiting on or joining an unfinished build (that state can no longer happen).

  • Real-Git probe (not committed): stopping a build while it waits on the fetch and 30 ms into its reset of a 20,000-file repo, with a cold git worktree add running at the same time; the stop returns in under 50 ms, the cold add succeeds clean, and the partial spare and its Git registration are gone.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Cross-platform: no path logic changed. A stopped build's Git now runs under the existing termination barrier: on WSL it is terminated inside the distro before cleanup; on macOS, Linux and Windows the background cleanup of a stopped spare starts after Git has fully exited (about 2 s later than before). The user's create never waits on either.
SSH: SSH repos never get spares (the remote Orca server runs its own create path), so nothing changes there.
Folder workspaces: never get spares; unchanged.
Mixed versions: telemetry only gains a value in this build's own closed list; nothing crosses the client/host wire.

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)

…eady

A create reserved a prepared spare checkout that could still be building
and then awaited it with no deadline; under disk contention one create
waited 209 of 268 s. Only a spare whose latest build or tip refresh has
finished is now claimable; otherwise the create takes the plain add at
once and records the miss as not_ready. The skipped spare keeps building
for the next create.
A tip refresh made a built spare unclaimable exactly when a create that
awaits the same fetch arrived, so the create did a full cold add; and a
skipped spare kept checking out beside the cold add. A spare is now built
once (finalize already resets it onto the create's base), readiness comes
from its build alone, and a create that finds only an unfinished match
cancels it under a claim and cleans it up in the background.
Cancelling an unfinished spare is now the common path, and on WSL an abort
killed only wsl.exe while the guest reset kept writing the tree. Opt the
materializing reset into the termination barrier so cleanup starts after
the guest git exits.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Closing. Real create-timing data (14 days, ~582k creates) shows that when a create finds a spare checkout still building, finishing it is usually as fast as or faster than a fresh checkout (larger repos: median 19 s remaining vs 39 s fresh), and cancelling adds deletion work plus a second checkout while the disk is busy. The one independent correctness fix found here (stopping git inside WSL when a spare's reset is cancelled) is moving to its own small PR.

@brennanb2025
brennanb2025 deleted the brennanb2025/create-skips-unready-spare branch October 16, 2026 01:28
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