Skip to content

chore(worktree): include create timing and spare outcome in the workspace create events - #24483

Merged
brennanb2025 merged 10 commits into
mainfrom
brennanb2025/worktree-create-step-timing
Oct 2, 2026
Merged

brennanb2025 merged 10 commits into
mainfrom
brennanb2025/worktree-create-step-timing

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 14 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1888 $\color{#cf222e}{\Huge{\mathbf{−}}}$​18 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1870
Prod 27 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1539 $\color{#cf222e}{\Huge{\mathbf{−}}}$​385 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1154

ELI5

Creating a workspace sometimes takes minutes instead of seconds, and we can't tell from one machine whether the checkout Orca prepares in advance (the "spare") speeds creates up or slows them down for real users. This PR adds a few numbers to the event Orca already sends when a workspace is created (or fails to be), so that question can be answered. Nothing changes in what you see or do.

What Changed

The problem. On 2026-09-28 worktree creates took 80-268 s instead of 5-15 s. To save time, Orca builds a spare checkout in the background (when the new-workspace dialog opens, and again automatically after a create in a burst) so the next create can reuse it instead of checking out from scratch. That background work may also slow down creates running at the same time. A study on one machine and one repo size could not settle whether the spare helps or hurts on balance, because the answer depends on repo size, disk, platform, how many worktrees exist, and what else is running. #23699 (remove the spare, or keep it under tighter rules) is parked until we have real numbers.

User-facing change: none. Creates behave exactly as before. No new Git process runs on the create path, nothing new is waited on before the create answers, and every new value comes from data the create already had, plus one small file read of the repo's .git folder after the create has already returned.

What is added. The existing workspace_created and workspace_create_failed events get optional fields. They are numbers or fixed categories only: never a path, repo name, branch name, ref, or error text. Consent and opt-out work exactly as they already do for these events (same track() call, same consent check, same validator and rate caps).

On a successful create:

  • total_ms, and unattributed_ms (wall-clock time no timed step covers).
  • One duration per timed step, left out when the step didn't run: resolve_name_ms, refresh_base_ref_ms (the wait for the base fetch), git_worktree_add_ms, prepared_checkout_claim_ms (picking and reserving a spare, including its base and drift checks), prepared_checkout_wait_ms (waiting for an unfinished spare), prepared_checkout_finalize_ms (handing the spare over), prepared_checkout_discard_ms (removing a spare whose hand-over failed), list_created_worktree_ms, persist_metadata_ms, create_symlinks_ms, resolve_shared_directories_ms, resolve_worktreeinclude_ms, create_shared_directories_ms, copy_worktreeinclude_ms, prepare_setup_ms, spawn_startup_terminal_ms.
  • Spare outcome: prepared_checkout = hit or miss.
  • On a hit: prepared_checkout_reset (none, base_moved: the base moved since the spare was built or last refreshed, or retargeted: it was reset onto a sibling ref such as main vs origin/main), prepared_checkout_origin (prefetch: built when the dialog opened, rearm: built automatically after an earlier create, rearm_then_prefetch: built automatically and then also asked for by the dialog), prepared_checkout_build_ms (from when Orca started building the spare, including any wait for the base fetch it is built on, to it being ready; a later refresh doesn't restart it) and prepared_checkout_idle_ms (from its latest ready, after its build or a refresh, to this create taking it; 0 if the create waited for it).
  • On a miss: prepared_checkout_miss_reason, a fixed reason (none_armed, repo_mismatch, base_mismatch, prepare_failed, finalize_failed, sparse_checkout, and so on).
  • Context: create_entry_point (app for the Orca window, runtime for the CLI, phone app, agents, paired clients, orchestration and server automations), execution_host (local, wsl or ssh: where the create's Git ran), worktree_count_bucket (1, 2-5, 6-20, 21-100, 101-300, 301-1000, 1001+), repo_file_count_bucket (<1k, 1k-10k, 10k-100k, 100k-500k, 500k+ tracked files), concurrent_creates (the most other creates running at once during this one), concurrent_preparations (the most spare builds and spare clean-ups running at once during this create, not counting the spare it used), post_checkout_hook (present, absent, custom_hooks_path, unknown; the hook's content is never read): whether the repo has a post-checkout hook; every create runs it once, a normal checkout inside git worktree add (part of git_worktree_add_ms) and a reused checkout at hand-over (part of prepared_checkout_finalize_ms), so a slow hook can be told apart from a slow checkout.

On a failed create: failed_phase (the step it failed in, or untimed), total_ms (time until the failure), the same spare outcome fields as above plus prepared_checkout_wait_ms, and create_entry_point, execution_host, concurrent_creates, concurrent_preparations.

The public privacy page (docs/site/content/docs/telemetry.mdx) gets one sentence so its description of the workspace event stays accurate.

Mechanism.

  • Both ways a create can start (the app's create handler and the runtime API used by the CLI, phone app, agents, paired clients, orchestration and server automations) begin one shared sender. It owns the step timer and sends exactly one event per create, on success or failure. Before this PR only the app path sent workspace_created; runtime creates used the spare too but sent nothing.
  • The step timer is the one the create already used for its trace span. failed_phase is matched by the error itself: each timed step remembers the error it let escape, and the sender looks up the error it caught (following cause if it was wrapped), so a failure that was recovered from can't be mistaken for where the create died.
  • The spare's own steps are timed separately inside git_worktree_add, so on a miss the plain checkout time is git_worktree_add_ms minus the prepared_checkout_* steps.
  • An in-memory counter tracks creates in flight and spare work in flight (builds until they are ready, and background removals of expired or evicted spares). Each create records the most it saw at once, leaves out the spare it used, and stops counting when it finishes, so the replacement spare it starts at the end isn't counted against it.
  • The spare pool already knew whether a spare was asked for by the dialog or built automatically; it now also notes when the dialog asks for an automatically built one, when its first build started and finished, and when it was last ready (after its build or a later refresh), all on the same monotonic clock as the step timer.
  • After the create has returned, and only for local and WSL repos with telemetry on, a capped (2 s) file check reads .git/config and .git/config.worktree, stats .git/hooks/post-checkout, lists the .git folder once (to spot a split index), and reads the 12-byte header of .git/index for its entry count (the number of tracked files). The count is left out for a split index or whenever sparse checkout is on, where it isn't the file count. The same check lists .git/worktrees once and reads each entry's small locked file to count the worktrees, leaving out Orca's prepared checkouts the way the worktree listing does, because the create itself no longer lists worktrees (SSH creates still use their own listing).
  • Sending the event never affects the create: if sending fails, the error is logged and the create's own result or error is returned unchanged.

What is recorded where.

  • Every local, WSL or SSH create sends these fields, from the Orca window (dialog, in-app automations, work-item launches, session forks) and from the runtime API.
  • SSH: there is no spare, the hook and file count aren't read (that would need a remote round trip), and name resolution and the base fetch land in unattributed_ms.
  • SSH provisioned-root recipes still send their events without the new fields; adopting a provisioned root is a registration, not a create Orca times.
  • Folder workspaces: the event is unchanged, since a folder "create" is a registration with nothing to time; runtime folder creates still send nothing.

Event counts change. workspace_created and workspace_create_failed counts rise from this release, because CLI, phone, agent, orchestration and server-automation creates now send them. Creates from a paired client run on, and are counted under, the host install. Filtering to create_entry_point missing or app reproduces the earlier series.

Reading the numbers.

  • Step fields nest: the prepared_checkout_* steps sit inside git_worktree_add_ms, so never sum step fields; unattributed_ms already accounts for overlap.
  • Compare the Orca window and the runtime on git_worktree_add_ms and its sub-steps, not on total_ms: runtime creates also spawn their terminals inside the create, and that time is in their unattributed_ms.
  • The existing per-event rate cap (30 per event name, refilling at 0.5 per second) can drop the tail of a very large agent burst, and every event also counts toward the existing per-session ceiling of 1,000.

Compatibility. Every new field is optional, so payloads from older builds still validate, and additive optional fields are safe on an existing event. CreateWorktreeResult.timing gains optional fields that nothing on either side of the wire parses.

What this will tell us

  • Whether reusing a checkout Orca prepared in advance makes creating a worktree faster or slower, and by how much for a typical create and a slow one, compared with creates that did a normal checkout.
  • How long creates wait for a prepared checkout that isn't finished yet, how long one takes to build, and how long it sits ready before it's used.
  • Whether prepared checkouts being built or cleaned up in the background slow down other creates running at the same time, for example when agents create several worktrees in a row.
  • How often a prepared checkout is reused, whether it came from the dialog, from the automatic replacement after a create, or from that replacement which the dialog then asked for as well, what reset it needed before hand-over, and when it isn't reused, the fixed reason why.
  • Where the rest of a create's time goes (updating the base branch, the checkout, copying files, starting terminals), and for a failed create, which step it stopped in and what the prepared checkout had done before it.
  • How all of this differs by platform, by local, WSL or SSH repos, by how many files the repo tracks, and between the Orca window and the CLI, phone and agent interfaces.
  • Whether the repo has a post-checkout hook; every create runs it once, a normal checkout inside git worktree add (part of git_worktree_add_ms) and a reused checkout at hand-over (part of prepared_checkout_finalize_ms), so a slow hook can be told apart from a slow checkout.
  • Together, these decide fix(worktree): a create does one fetch and one checkout, and background work waits for it #23699: keep the prepared checkout under tighter limits, or remove it.

Why

We need real-world numbers before deciding what happens to the spare checkout. Extending the existing events, rather than adding a new one, keeps one row per create with its outcome, source and timing together, and inherits the existing consent and validation path unchanged.

Alternatives I considered:

  • A new sibling event: a second row per create that has to be joined back to the first, and it would still leave the existing count missing CLI and agent creates. The registry calls for a new name only when a field is renamed or changes meaning, and nothing here does that.
  • Duration buckets instead of milliseconds: buckets would hide the distribution we're trying to see (for example whether the spare wait is 200 ms or 20 s). A plain number carries no identifying information.
  • Repo size from the .git/index byte size: the compact index format Git can use in large repos (feature.manyFiles) is about 40% smaller per file, which would skew exactly the repos we care about; the header's entry count is the same in every format.

Where this fits

The problem: creating a worktree took 80-268 s instead of 5-15 s on 2026-09-28. The plan to fix it:

Step PR What it does
1 #23699 A create does one fetch and one checkout (the spare checkout is removed); background disk work waits for a local create. Parked until this data exists.
2 planned, no PR yet One worktree membership model per repo; no repeated git worktree list
2b planned, no PR yet The create card shows Git's checkout progress
3 #22356 PR conflict checks stop writing throwaway objects into the user's repo
(separate) #23698 Local main is updated safely, once per repo and branch

This PR is the measurement step that #23699 waits on. It records how creates behave on main today, with no behavior change.

Linked Issue

N/A (maintainer).

Visual Proof

N/A: nothing visible changes. The create flow and UI are untouched.

Testing

  • I manually tested these changes locally

  • Automated tests added/updated

  • Schema tests: full valid payloads for both events, old payloads still valid, .strict() rejects unknown keys (repo_path, branch, base_ref, hook content) and success-only fields on the failure event, and wrong types, negatives, fractions and out-of-vocabulary enum values are rejected.

  • Step-timer tests: the failing step is named, the outermost step that rethrew wins, a parallel step that finishes later doesn't hide the one that failed, a wrapped error is traced through cause (with a cycle bound), and finishing the timer stops the concurrency count.

  • Concurrency tests: peaks of other creates and of spare work, the used spare left out wherever its peak fell, and work that starts after the create ends is ignored.

  • Spare tests: a miss's base check is timed as the claim, not as the checkout; a failed hand-over's clean-up gets its own step; reset, origin (including rearm_then_prefetch), build time and idle time on hits, with a controlled clock; a building spare counts against other creates but not the one that used it.

  • File-check tests on a temp dir: hook present, non-executable, sample-only, core.hooksPath, .git as a file, missing repo; index entry count from version 2 and 4 headers; no count for a missing, truncated, foreign, split or sparse index.

  • App create handler tests through the existing worktree IPC harness: the event is sent after the create returns with no further Git call, exactly one event per create, the hook check is skipped with telemetry off, \\wsl.localhost\... reports wsl, SSH reports ssh without a hook check, a failed add reports failed_phase: git_worktree_add with no error text, and a throwing track() still returns the create's result or error.

  • Runtime create tests: one workspace_created with create_entry_point: runtime, one workspace_create_failed naming the failed step, nothing for folder workspaces or requests rejected before the create, the SSH create's timing hand-off (execution_host: ssh and its steps), the create's own replacement spare not counted, a throwing track() doesn't change the create's outcome, and a source scan showing the two create paths only send through the shared sender and never call each other.

  • Ran 90 related files (worktree IPC, runtime create, spare pool, telemetry, instrumentation, orca-runtime.test.ts): 2248 passed, 1 skipped. Typecheck (tsconfig.node, tsconfig.tc.web, tsconfig.cli), oxlint, the changed-code quality gate and the anti-slop audit pass on the latest commit.

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

Security: only numbers and fixed categories leave the machine, under the existing consent check. Cross-platform: the file check handles Windows (no execute bit) and WSL paths; nothing is read on SSH. Mobile and paired clients: no wire change they parse. Performance: nothing new on the create path before it returns; the file check is after, capped at 2 s, and skipped with telemetry off.

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)

…pace-created event

The workspace_created and workspace_create_failed events gain optional,
numbers-and-enums-only fields built from what the create already measured:
total and per-phase durations, the prepared-checkout hit/miss and miss
reason, the execution host (local/WSL/SSH), a worktree count bucket, how
many other creates were in flight, whether the repo has a post-checkout
hook (file existence only, probed after the create returns), and for a
failure the phase it died in plus elapsed time. No new git process runs;
consent and opt-out are unchanged.
… skip the hook check with telemetry off

- failed_phase now names the outermost timed step the thrown error (or its cause) left, so a
  caught failure or a concurrent sibling step can no longer be misattributed; the old-relay SSH
  error keeps its cause so it still reads as git_worktree_add.
- execution_host follows the same rule Git routing uses, so a \\wsl.localhost repo reads wsl.
- The post-checkout hook check does not read the repo when telemetry is disabled.
- Privacy page mentions the miss reason code and the failed step.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

QA screenshots

Tested head a108d233461 in a hidden dev build on a separate Mac, with an isolated profile and home, against a small local test repo. The same failed-create steps were repeated on the base commit 02790c53e88 for comparison.

  • Launch: PASS. The app started and the test repo was added.
  • Successful create from the New Workspace dialog: PASS. The dialog closed, the new worktree appeared in the sidebar and opened, and its terminal started (about 4 s for the first create, 1 s for the second). No error toast and no new console errors.
  • Failed create (a git hook refuses the branch): PASS. The "Couldn't create worktree" error, the sidebar row, and Retry/Dismiss look the same as on the base commit. Only the branch name differs.
  • Name with .. (bad..name): PASS. This build and the base commit behave the same: nothing blocks it before submit, and the create succeeds with branch bad.name.
  • The create event's new timing fields: BLOCKED. Dev builds never send these events and there is no local-only way to capture them, so the field contents are covered by the unit tests only.
  • Teardown: PASS. The app, its terminal daemon, and its pane processes were all stopped. Nothing was left running.

Launch, test repo added
App launched with the test repo added

New Workspace dialog filled in (qa-ok-1, base main, Blank Terminal)
New Workspace dialog filled in

After create: dialog closed, qa-ok-1 active in the sidebar, terminal started
qa-ok-1 created and active

Terminal works in the new worktree
Terminal in qa-ok-1 after echo

Second create (qa-ok-2)
Sidebar with qa-ok-1 and qa-ok-2

Failed create on this PR (qa-fail-1)
Failed create error on this PR

Same failed create on the base commit (qa-fail-2)
Failed create error on the base commit

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status: ready (head 0d197f5bcfc)

The problem. Creating a worktree sometimes took minutes instead of seconds (2026-09-28). A lab study on one machine couldn't tell whether the prepared "spare" checkout helps or hurts real users. So we need to see, across real repos and machines, how long creates take, where the time goes, and what the spare did, before deciding #23699 (parked until this data exists).

What changes for users: nothing. The create works exactly as before, and nothing new is awaited before the worktree appears. The existing workspace_created / workspace_create_failed events gain optional numbers and fixed categories only: no paths, names, branches or error text. Consent and opt-out are unchanged, because the same track() call applies them.

Fixed during review

  • The post-checkout hook check (a file-existence check after the create returns) used to run even with usage data sharing turned off. It now runs only when the event can be sent.
  • execution_host reported local for a repo on a \\wsl.localhost\... path when the project used the default runtime, even though Git runs inside WSL there. It now uses the same path/WSL-distro rule Git routing uses.
  • failed_phase is now read from the error itself: the outermost timed step the error passed through, following cause. The old step-ordering logic could report untimed when two steps ran in parallel.
  • The SSH "older relay" authorization error now keeps the original error as its cause, so it still reports git_worktree_add. The message users see is unchanged, and a test pins this.
  • The public privacy page also mentions the reason code when the prepared checkout isn't reused, and the step a failed create stopped in.
  • The PR description now says correctly which creates send the event. In-app automation runs, work-item launches and session forks go through the same handler and send it. The CLI, mobile, paired web, headless automations, agent-launch and orchestration use the runtime path, which never sent it.
  • CI: a test typing error and the symbol-naming rule (schema constants renamed to *Properties).

Deferred (minor, no user impact)

  • On a WSL repo, a non-executable post-checkout hook still reads present, because the file check runs on the Windows side.
  • A mobile code comment cites line numbers in create-types.ts that this PR moved.
  • The hook-check values are defined in two places.
  • The schema tests for the failed event don't cover unknown keys, negative or fractional numbers, or a bad execution_host.
  • The parallel-step test covers a sibling step that succeeds, not one that fails.
  • [core] hooksPath = x written on one line isn't detected (rare).
  • Concurrency counts don't cover SSH provisioned-root adopts. Those send the events without the new fields, by design, since they are a registration rather than a timed create.
  • The main-process log for a failed create may now also print the inner relay error's stack (log only, unverified).

Verified

  • Two independent review rounds, with no P0–P2 findings left. The readiness checklist passed with no P0/P1/P2 candidates.
  • 56 related test files: 1944 tests passed, 1 skipped. Typecheck passes for the node, web and CLI projects. oxlint, the quality gate and the anti-slop audit are clean. CI is green on 0d197f5bcfc.
  • Electron QA on a remote Mac at a108d233461, compared with the base commit (screenshots in the QA comment above). The create succeeds, the failed-create error is unchanged, and there are no new console errors. The later commits change only tests and constant names.

Not verified

  • The event payload in a running app. Development builds never send events and there is no local-only sink, so field contents rest on unit tests.
  • A real Windows machine with a \\wsl.localhost repo: the tests simulate the platform.
  • Real event delivery. By design, no events were sent during review.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 2, 2026 02:00
@greptile-apps

greptile-apps Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Adds telemetry fields to workspace creation events.

The PR appears safe to merge, with non-blocking inaccuracies in the repository context and previously noted event measurements.

Findings

  1. P2 Inactive worktree config affects telemetry ▶
  2. P2 Success event can be lost ▶
  3. P2 Execution host can be mislabeled ▶
  4. P2 WSL hooks can be misclassified ▶
  5. P2 Hooks path ignores config sections ▶

Summary

The PR adds step timings, prepared-checkout outcomes, concurrency and coarse repository context to workspace-create events, including runtime creates. It also adds a post-create repository probe. The probe needs to respect Git’s activation of per-worktree configuration before using that file to classify hooks or file counts.

Reviews (4) · Last reviewed commit: "fix(worktree): leave out the file count ..."

Comment on lines +57 to +61
const postCheckoutHook =
isTelemetryEnabled() && (timing.executionHost === 'local' || timing.executionHost === 'wsl')
? await probePostCheckoutHookPresence(repoPath)
: undefined
track('workspace_created', {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Success event can be lost If a local or WSL create finishes just before the app quits, workspace_created waits for the hook probe, which can take up to two seconds. If telemetry shutdown starts during that wait, track() drops the event. Previously the event was queued immediately, so this can leave a completed create out of the measurements.

Comment thread src/main/ipc/worktree-remote.ts Outdated
Comment on lines +2219 to +2221
// Why the routing rule: a \\wsl.localhost repo runs Git in WSL even without a WSL project runtime.
timing.recordExecutionHost(
wslDistroForCommand(localGitExecOptions.cwd, localGitExecOptions.wslDistro) ? 'wsl' : 'local'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Execution host can be mislabeled For a Windows-side linked worktree in a WSL-configured project, Git can run on the Windows host. This code still records wsl because the project has a WSL distro, putting that create in the wrong host group for timing comparisons.

Comment on lines +57 to +58
// Git skips a hook without an execute bit, except on Windows where it has none to check.
return platform === 'win32' || (hook.mode & 0o111) !== 0 ? 'present' : 'absent'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 WSL hooks can be misclassified When a WSL repository is accessed from Windows, this check uses the host's win32 platform and marks any post-checkout file as present. Git running inside WSL can skip a non-executable hook, so the event can incorrectly report that the repo has an active hook.

return 'unknown'
}
const config = await readFile(path.join(gitDir, 'config'), 'utf8')
if (/^\s*hookspath\s*=/im.test(config)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Hooks path ignores config sections This search matches hooksPath anywhere in the config, including sections other than [core]. In that case, the probe reports custom_hooks_path even though Git uses the repo's default hooks directory, making the recorded hook context inaccurate.

@coderabbitai

coderabbitai Bot commented Oct 2, 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: 0f19ce71-de15-48bd-9a5e-bc4e20755d5f

📥 Commits

Reviewing files that changed from the base of the PR and between 3667459 and 2660cee.

📒 Files selected for processing (14)
  • src/main/git/create-event-repo-probe.test.ts
  • src/main/git/create-event-repo-probe.ts
  • src/main/git/worktree-create-preparation.ts
  • src/main/ipc/worktree-remote.ts
  • src/main/workspace-create-event-fields.test.ts
  • src/main/workspace-create-event-fields.ts
  • src/main/workspace-create-telemetry.ts
  • src/main/worktree-create-preparation-activity.test.ts
  • src/main/worktree-create-preparation-activity.ts
  • src/main/worktree-create-preparation-claim-registry.ts
  • src/main/worktree-create-preparation-pool.ts
  • src/main/worktree-create-preparation.test.ts
  • src/main/worktree-create-preparation.ts
  • src/main/worktree-preparation-discard-retry.ts

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


📝 Walkthrough

Walkthrough

Workspace creation now records phase durations, failure phases, prepared-checkout outcomes, execution host, worktree counts, and concurrent create and preparation counts. App and runtime creation flows pass timing data into local and remote operations. Telemetry schemas and event builders include available values, with repository hook and index facts probed for eligible creates. Added tests cover timing, concurrency, preparation outcomes, repository probing, event fields, and schema validation.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 2660c

This change adds measurement fields to workspace-create telemetry and should not change how workspaces are created. No blocking issues were found. Real event delivery has not been verified in a packaged build, so check it after release.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 2660c

The additional data is constrained to timings, counts, coarse buckets, and fixed categories. Transmission still checks the current telemetry preference, and the reviewed checkout flows retain ownership safeguards. No material security regression was established, but incomplete coverage prevents a stronger assurance.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • observed — The reviewed exposure is repository metadata collection in the creating process and additional properties in its existing install-identified telemetry stream. The runtime SSH path forwards the recorder to a creation function that marks execution as SSH, excluding that path from the local repository probe.

Trust Boundaries and Controls

  • observed — Repository-derived facts pass through fixed-field construction and strict schema validation. The capture sink also enforces build, shutdown, initialization, burst-rate, and live-consent gates; raw repository paths and configuration contents are not added to these event properties.
  • observed — Consent is checked before starting repository collection and again before transmission, but collection is not canceled when consent changes during an asynchronous read. The final gate prevents capture while consent is disabled; it does not reverse an already-started local read.

Resilience and Maintainability Implications

  • observed — The preparation refactoring retains identity-bearing claims, idempotent release, and finally-based rearm in app and runtime callers. The new reset indicator is removed before returning the existing worktree result, keeping telemetry observation separate from checkout authority.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.32% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 95 functions across 43 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 identifies the main change: adding worktree creation timing and prepared-checkout outcome data to workspace creation events.
Description check ✅ Passed The description follows the repository template and provides complete ELI5, change, rationale, issue, visual proof, testing, compatibility, security, performance, and checklist information. It also do…
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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 new issues found.

Reviewed changes

  • Event fields — workspace_created and workspace_create_failed gain optional timing, spare-outcome, and coarse-context fields, all numbers or closed enums (no path/branch/ref/error text). Additive optional fields keep old payloads valid; the public privacy page is updated to match.
  • Timing recorder — the create handler now owns the recorder and threads it into the local/remote creates, so a failed create can still report failed_phase (matched by error identity, following cause, outermost rethrow wins) plus execution_host and elapsed time.
  • Concurrency counter — new worktree-create-concurrency.ts tracks in-flight creates across the dialog handler and runtime local/remote paths for concurrent_creates, with no double counting.
  • Post-checkout hook probe — new read-only probe reports hook presence without spawning Git, run fire-and-forget after the create returns and only when telemetry is on for local/WSL.
  • WSL host labeling — wslDistroForCommand is exported and reused so execution_host follows the same rule Git routing uses.
  • Tests — schema (strict rejection, backward compat, enum/type bounds), recorder (failed_phase incl. cause chains and cycles), field builders, hook probe on temp dirs, and handler integration through the worktree IPC harness.

I read the full diff and ran the new tests (66 passed) plus related existing suites (create-flow, sparse-checkout, execution-host routing, Windows, preparation, instrumentation, telemetry events, event classification) — all green. track() is non-throwing, so the fire-and-forget call is safe, and the validator delegates to the schemas, so there is no parallel allowlist to update.

Two non-blocking observations, no action expected:

  • probePostCheckoutHookPresence is called without an explicit platform in production, so WSL repos are probed with Windows exec-bit semantics. The PR frames the field as best-effort and Windows stat likely cannot expose Linux exec bits anyway.
  • workspace_created now emits after an async probe (capped at 2s), so a create immediately followed by app quit can lose the event to the shuttingDown gate. A deliberate latency-over-completeness tradeoff.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

…t the spare checkout did

Runtime creates (CLI, agents, phone app, paired clients, orchestration, server
automations) reuse prepared checkouts like the app's own creates, but recorded
no timing and sent no events. Both entry points now start one shared sender
(workspace-create-telemetry.ts), so every create sends exactly one event with
the same fields, plus create_entry_point (app | runtime).

Spare-checkout fields:
- concurrent_preparations: peak prepared-checkout builds and background
  discards running during the create, excluding the one it used; the window
  closes before the create's own re-arm starts.
- prepared_checkout_claim / prepared_checkout_discard phases, so on a miss
  git_worktree_add minus the prepared_checkout_* phases is the plain checkout.
- prepared_checkout_reset (none | base_moved | retargeted) replaces the
  retargeted flag; prepared_checkout_origin (prefetch | rearm) on hits.
- workspace_create_failed carries the spare outcome and its wait.
- repo_index_size_bucket from one stat of .git/index in the existing
  post-create probe (telemetry on, local/WSL only, 2 s cap).
Comment thread src/main/worktree-create-preparation.ts Resolved
@greptile-apps

greptile-apps Bot commented Oct 2, 2026

Copy link
Copy Markdown

Comments Outside Diff

These findings could not be posted inline.

  • P2 Runtime steps remain unattributed src/main/runtime/orca-runtime-create-managed-worktree.ts:202 ▶

    For a runtime create, the recorder includes time spent preparing setup here and starting terminals later, but neither operation records its named timing phase. Both costs land in unattributed_ms, making slow runtime creates harder to compare with app creates.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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 — minor suggestions inline.

Reviewed changes

This run reviewed the newest commit (66c2f43), which reworks telemetry ownership and widens coverage well beyond the previous revision:

  • Single shared sender — workspace-create-telemetry.ts now owns the create events for both entry points. A settled latch guarantees at most one workspace_created/workspace_create_failed per create, and a new create_entry_point field tags whether the Orca window (app) or the runtime API (runtime) ran it.
  • Runtime creates now emit events — trackRuntimeWorkspaceCreate settles the event before the caller fires its re-arm, so CLI, agent, phone, paired-client and orchestration creates send the same fields the dialog does. Folder workspaces and requests rejected before the route resolves still send nothing.
  • Spare-checkout reporting reshaped — prepared_checkout_reset (none/base_moved/retargeted) and prepared_checkout_origin (prefetch/rearm) replace the retargeted boolean; new prepared_checkout_claim/prepared_checkout_discard phases isolate the claim's probes and a failed finalize's cleanup from the plain add; the failure event now carries the spare outcome and prepared_checkout_wait_ms.
  • New context fields — concurrent_preparations (peak other prepared-checkout builds/discards, excluding the one the create adopted) and repo_index_size_bucket (size only). The concurrency store now tracks preparation work in addition to creates, and the recorder threads through the runtime local/remote create paths.
  • Probe consolidated — post-checkout-hook-presence.ts becomes create-event-repo-probe.ts and also stats .git/index; it still runs fire-and-forget after the create returns, only for local/WSL with telemetry on.

I read the full PR diff end-to-end plus the latest-commit delta. Two focused subagent passes on event exactly-once accounting and on the concurrent_preparations peak-exclusion/leak invariants returned no counterexamples — the settle latch holds one event per create, end() is memoized, and every tracked preparation work unit is ended.

ℹ️ The PR description predates the runtime-create change

The body still states that creates over the runtime API "have never sent workspace_created, and still don't," lists prepared_checkout_retargeted, and enumerates only the original 14 steps. The latest commit makes runtime creates send the event and replaces/extends those fields, so the description now contradicts the code it introduces and omits create_entry_point, concurrent_preparations, repo_index_size_bucket, prepared_checkout_claim_ms, prepared_checkout_discard_ms, and the failure event's spare fields.

Technical details
# PR description is stale for the runtime-create change

## Affected sites
- `docs/site/content/docs/telemetry.mdx` is updated, but the PR body's "What is recorded where" bullet still reads "Creates over the runtime API ... have never sent `workspace_created`, and still don't."
- The success bullet lists `prepared_checkout_retargeted` and the original 14-step set, missing `create_entry_point`, `prepared_checkout_reset`, `prepared_checkout_origin`, `concurrent_preparations`, `repo_index_size_bucket`, `prepared_checkout_claim_ms`, `prepared_checkout_discard_ms`.
- The failure bullet omits the added spare outcome and `prepared_checkout_wait_ms`.

## Required outcome
- The PR body describes the behavior this commit actually ships, since it is the primary artifact reviewers read and is what remains in the merge history.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

pendingDiscards.delete(pendingKey(target))
const discard = runDiscard(target, attempts)
// Counted as preparation work: removing a full tree competes with creates for the disk.
const discard = trackPreparationWork(runDiscard(target, attempts))

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.

This counts the retry-path discard as preparation work, but stale-preparation cleanup reclaims trees through discardPreparedWorktree directly (worktree-create-preparation-stale-cleanup.ts:62), so those background removals never enter concurrent_preparations.

Technical details
# Stale-cleanup discards bypass the preparation-work count

## Affected sites
- `src/main/worktree-preparation-discard-retry.ts:61` — retry-path discard is wrapped; `startStalePreparationCleanup` runs `discardNextStalePreparation` in the background and calls the same `discardPreparedWorktree` without the wrapper.
- The reclamation runs after `startStalePreparationCleanup` awaits only `scanned` (not `cleanup`), so it can continue after the owning build's `work.end()` and is invisible to the metric.

## Required outcome
- `concurrent_preparations` reflects the disk work the field documents ("prepared-checkout builds and discards running at once during this create"), or the scope is made explicit.

## Open questions for the human
- Is the stale-cleanup path intentionally excluded, or should it go through `trackPreparationWork` too?

…gin, and a tracked-file count

- prepared_checkout_build_ms / prepared_checkout_idle_ms on hits: from arming
  the spare to ready, and how long it sat ready before the claim (0 when the
  create waited). readyAt is recorded in the pool's existing ready handler.
- prepared_checkout_origin gains rearm_then_prefetch: an automatic re-arm that
  the dialog prefetch then asked for too, so rearm means the re-arm alone.
- repo_file_count_bucket replaces the index byte size: the entry count from
  the 12-byte index header, which is the same in every index version; left
  out for a split or sparse index.
- The shared sender never lets a failed send change the create's result or
  error; it logs instead and still ends the create's concurrency membership.
- Tests pin the runtime SSH create's timing hand-off and the throwing-send
  cases on both entry points.
@brennanb2025 brennanb2025 changed the title chore(worktree): include create timing and spare outcome in the workspace-created event chore(worktree): include create timing and spare outcome in the workspace create events Oct 2, 2026

@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 in the latest commit.

Reviewed changes

This run reviewed the delta since the prior pullfrog review (66c2f43 → 6d37872), which sharpens prepared-checkout telemetry and the repo probe.

  • Index entry count — the repo probe now reads the tracked-file count from the 12-byte .git/index header (DIRC, version, big-endian count) instead of the index's byte size, and leaves it out for split or sparse indexes; repo_index_size_bucket becomes repo_file_count_bucket with file-count thresholds.
  • Prepared-checkout build and idle time — a hit reports prepared_checkout_build_ms (arm to ready) and prepared_checkout_idle_ms (ready to claim, 0 when the create waited), from a readyAt stamp on the pool entry.
  • rearm_then_prefetch origin — an automatic re-arm that the dialog's prefetch then asks for is distinguished from a pure re-arm.
  • Event-send hardening — a shared settleOnce catches a throw from track() (sync and async), so bookkeeping can never change the create's result or mask its error.

One prior thread remains open and is untouched by this commit, so this review is not an approval.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/git/create-event-repo-probe.ts Resolved
… time spare builds monotonically

- The repo probe also reads .git/config.worktree, where git sparse-checkout
  --sparse-index writes index.sparse, and omits the file count whenever
  sparse checkout or a sparse index is on in either file, with Git's boolean
  spellings. core.hooksPath there is honoured too.
- prepared_checkout_build_ms / _idle_ms use performance.now(), like every
  other duration; the build is timed from its own start (buildStartedAt).
- The origin field comment names all three values.
Comment on lines +55 to +59
const [mainConfig, worktreeConfig] = await Promise.all([
readFile(path.join(gitDir, 'config'), 'utf8').catch(() => null),
readFile(path.join(gitDir, 'config.worktree'), 'utf8').catch(() => '')
])
const config = mainConfig === null ? null : `${mainConfig}\n${worktreeConfig}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Inactive worktree config affects telemetry When .git/config.worktree exists but extensions.worktreeConfig is not enabled, Git ignores that file, but this probe still uses its settings. A stale hooksPath can make the event report custom_hooks_path for a hook Git does not use, and a stale sparse setting can omit a valid repo_file_count_bucket. This makes the repository context less reliable for create-time comparisons.

@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

This run reviewed the delta since the prior pullfrog review (6d37872 → 3667459), which tightens the repo probe and moves spare-build timing onto a monotonic clock.

  • Sparse-checkout detection widened — the probe now also reads .git/config.worktree and folds it into the config it inspects, and SPARSE_CHECKOUT_ON recognizes index.sparse / core.sparseCheckout set to any truthy value or written bare (only false/no/off/0 count as off). The tracked-file count is omitted under any sparse checkout, which is the conservative direction.
  • config.worktree hooks path — a core.hooksPath set per-worktree now reports custom_hooks_path instead of absent, since the hook probe reads the combined config.
  • Spare-build timing on the monotonic clock — buildStartedAt, readyAt, and claimedAt now use performance.now() (the same clock as the create's step timer) instead of Date.now(), so prepared_checkout_build_ms / prepared_checkout_idle_ms cannot be skewed by a wall-clock adjustment. createdAt stays Date.now() for TTL/eviction.
  • Tests — new cases cover the config.worktree sparse variants, explicit-off keeping the count, a config.worktree hooks path, and the build/idle tests now mock performance.now().

The one prior pullfrog thread — concurrent_preparations misses the stale-preparation cleanup that reclaims trees via discardPreparedWorktree directly (worktree-create-preparation-stale-cleanup.ts:62) — is untouched by this commit, so this review is not an approval. No new inline comments.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025

Copy link
Copy Markdown
Contributor Author

QA screenshots (v2, head 3667459)

Tested head 366745988250 in a hidden dev build on a separate Mac, with an isolated profile and home, against a small local test repo. The failed-create, ..-name and command-line steps were repeated on the base commit 02790c53e88 for comparison. Every create used the "Blank Terminal" agent.

  • Launch: PASS. The app started and the test repo was added.
  • Create from the New Workspace dialog: PASS. The dialog closed, the new worktree appeared in the sidebar and opened, and its terminal worked (qa-ok-1 showed up within about 1 s, qa-ok-2 in about 5 s). No error toast and no console errors.
  • Spare checkout reuse and quick repeated creates: PASS. With the dialog left open for 15 s first, qa-spare-1 was created normally. Then qa-burst-1 to qa-burst-4 were created back to back. Each one opened with a working terminal, sat on the right commit with a clean status, and was listed by git worktree list.
  • Failed create (a git hook refuses the branch): PASS. The "Couldn't create worktree" message, the sidebar error row, and Retry/Dismiss are the same as on the base commit. Only the branch name differs.
  • Name with .. (bad..name): PASS. This build and the base commit behave the same: nothing blocks it before submit, and the create succeeds with branch bad.name.
  • Create from the orca command line (the runtime path this PR changed): PASS. The CLI was built from each commit without installing anything and pointed only at the test app. A good name succeeded, printing the new worktree as JSON with exit code 0, and the worktree appeared in the sidebar. A hook-refused name failed with the same runtime_error message and exit code 1 on both commits, and the app showed nothing extra.
  • The create event's new timing fields: BLOCKED. Dev builds never send these events and there is no local-only way to capture them, so the field contents are covered by the unit tests only.
  • Teardown: PASS. Every test app, its terminal daemon and its pane processes were stopped, and the test ports were free afterwards.

Launch, test repo added
App launched with the test repo added

New Workspace dialog filled in (qa-ok-1, base main, Blank Terminal)
New Workspace dialog filled in

After create: qa-ok-1 active in the sidebar, terminal started
qa-ok-1 created and active

Terminal works in the new worktree
Terminal in qa-ok-1 after echo

Second create (qa-ok-2)
Sidebar with qa-ok-1 and qa-ok-2

Dialog left open for 15 s, then qa-spare-1 (Blank Terminal)
qa-spare-1 dialog

After qa-spare-1 and qa-burst-1
Sidebar after qa-spare-1 and qa-burst-1

After back-to-back qa-burst-2, 3 and 4 (fresh profile; earlier worktrees are listed as discovered)
Sidebar after three back-to-back creates

Failed create on this PR (qa-fail-1)
Failed create error on this PR

Same failed create on the base commit (qa-fail-2)
Failed create error on the base commit

bad..name on this PR
bad..name created as bad.name on this PR

bad..name on the base commit (branch bad.name-2 only because bad.name already existed)
bad..name on the base commit

Two command-line creates on this PR in the sidebar (both succeed: the test hook refuses only names containing qa-fail, which qa-cli-fail-1 does not)
CLI-created worktrees on this PR

The same two command-line creates on the base commit
CLI-created worktrees on the base commit

After a hook-refused command-line create on this PR: no error row (the base commit looks the same)
App after a refused CLI create

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status: ready (head 3667459)

The problem. On 2026-09-28, creating a worktree sometimes took minutes instead of seconds. Part of that time went to waiting on the "spare" checkout Orca prepares in advance. A study on one machine couldn't settle whether the spare helps or hurts real users on balance, because the answer depends on repo size, disk, platform and what else is running. #23699 (remove the spare, or keep it under tighter limits) is parked until real numbers exist. This PR collects them.

What changes for users: nothing. Creates behave exactly as before. Nothing new is waited on before the worktree appears, and no new Git process runs. The existing workspace_created / workspace_create_failed events gain optional numbers and fixed categories only: no paths, names, branches or error text. Consent and opt-out are unchanged.

What changed in this review round. The earlier version only covered creates from the Orca window, and couldn't show whether spare work slows other creates. Fixed:

  • Creates from the CLI, phone app, agents, paired clients and orchestration now send the same fields, marked create_entry_point: runtime. They use the spare too and are where creates come in bursts. workspace_created counts rise from this release; filtering to create_entry_point missing or app reproduces the earlier series.
  • concurrent_preparations: how much spare-checkout work (builds and clean-ups) was running during each create, not counting the spare it used.
  • On a miss, the spare's own checks and clean-up are timed as separate steps, so the plain checkout time is no longer mixed with them.
  • On a reused spare: where it came from (prefetch, rearm, or rearm_then_prefetch), what reset it needed (none, base_moved, retargeted), how long it took to build and how long it sat ready.
  • A failed create also reports what the spare did before it failed.
  • repo_file_count_bucket: the tracked-file count from the index file's 12-byte header. It is read only after the create returns, with usage data sharing on, for local and WSL repos, within a 2 s cap. It is left out for split indexes and sparse checkouts.
  • If sending the event fails, the create's own result or error is returned unchanged.
  • The description now has a "What this will tell us" section and notes for reading the numbers.

Deferred (data limits, no user impact)

  • none_armed doesn't yet say why no spare was ready (expired, failed before use, taken by another create), and spares that are built but never used aren't counted.
  • concurrent_preparations doesn't split the automatic replacement spare from other spare work.
  • No count of agents working during a create (needs new wiring).
  • SSH creates time fewer steps, and SSH and local creates count toward each other's concurrent_creates.
  • The hook check looks only at post-checkout, not the other two hooks fix(worktree): a create does one fetch and one checkout, and background work waits for it #23699 considers.
  • Very large agent bursts can lose their last events to the existing rate cap (30 per event name, refilling 0.5/s).

Verified

  • Fresh review rounds, each checking its own area: whether the data can answer the question (with the actual queries written out), the runtime create path (one event per create, no double sends), the spare-checkout counters and steps (the moved claim code is identical line by line; two creates racing for one spare still get exactly one hit), and a narrow check of each fix batch. No P0–P2 findings are left.
  • The readiness checklist passed with no P0/P1/P2 candidates. 96 related test files: 2286 tests passed, 1 skipped. The node, web and CLI typechecks, oxlint, the quality gate and the anti-slop audit are clean. CI is green on 3667459.
  • Electron QA on a remote Mac at 3667459, compared with the base commit (screenshots in the QA comment above). Dialog creates, a spare reuse and back-to-back creates, CLI creates (success and a hook refusal), and failed creates all behave the same as on the base commit, with no new console errors.

Not verified

  • The event payload in a running app: development builds never send events, so field contents rest on unit tests.
  • A real Windows or WSL machine.
  • Real event delivery: no events were sent during review.

Resolves the conflicts with #24346 (keep creation fast in large repositories)
in the prepared-checkout code. All of #24346's behavior is kept; the create
timing and spare recording are re-applied onto its new shape:
- finalize keeps main's lock-ownership checks and lock reason, and still
  reports whether the prepared checkout had to be reset;
- the failed-finalize discard is timed only when main actually runs it;
- a tip refresh of an existing spare counts as preparation work and extends
  its build timing (per-entry bookkeeping moved to
  worktree-create-preparation-activity.ts, the claim registry to
  worktree-create-preparation-claim-registry.ts, to keep the pool under the
  line limit; no behavior change);
- the create no longer lists worktrees, so the worktree count now comes from
  one listing of .git/worktrees in the existing post-create repo read.
…nt worktrees by lock reason

- prepared_checkout_build_ms runs from the first build's start (including any
  wait for the base fetch it is built on) to its first ready; a later tip
  refresh no longer restarts it, though it still counts as new preparation
  work. prepared_checkout_idle_ms runs from the latest ready (build or
  refresh) to the claim.
- The worktree count reads each .git/worktrees entry's locked file and leaves
  out entries whose lock reason names an Orca preparation, the way the
  listing does, instead of subtracting this process's spares. That covers
  spares from other processes, crash leftovers and spares being discarded,
  and cannot run one low while a spare's admin dir does not exist yet. It has
  its own 1.5 s cap inside the probe.

@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 in the latest commit — one minor test nit below.

Reviewed changes

This run reviewed the delta since the prior pullfrog review (3667459 → 6e90bda), which corrects how the spare is timed and how worktrees are counted.

  • Worktree count by lock reason — the repo probe now reads each .git/worktrees/<entry>/locked file and drops entries whose reason carries Orca's preparation prefix, replacing the in-process countRegisteredPreparations subtraction. This excludes this process's spare, another process's spare, and a crash leftover the same way; a 1.5 s inner timeout keeps a slow per-worktree read from costing the other repo facts.
  • First-build spare timing — prepared_checkout_build_ms is now the first build's arm-to-ready duration, so a later tip refresh no longer restarts it, while prepared_checkout_idle_ms still runs from the latest ready (build or refresh) to the claim.
  • Tests — the probe test covers this process's, another process's, and a crashed spare plus a missing admin dir; the activity and tip-refresh tests pin the first-build and refreshed-idle numbers.

I read the incremental and full diffs and ran the four affected suites (47 passed). The worktree exclusion reuses the same parseWorktreePreparationOwnerPid predicate the worktree listing uses, so the two agree; the timing change matches the field comments and the PR body. The one open prior thread is untouched by this commit, so this is not an approval.

ℹ️ Nitpicks

  • src/main/git/create-event-repo-probe.test.ts:82 — the case named "does not count a spare whose admin directory Git has not written yet" registers no spare: it only adds feature-a, so it passes under any implementation, including one that never subtracts an unregistered spare. Either create an in-memory spare for the repo or drop the case; the actual exclusion is already covered by the preceding test.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025
brennanb2025 merged commit d6d2795 into main Oct 2, 2026
59 of 61 checks passed
nwparker added a commit that referenced this pull request Oct 3, 2026
* fix: wait for OpenCode worker composer before first dispatch

Reuse captured composer readiness on local and paired execution hosts and revoke launching-shell paste anchors.

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>

* chore(deps): update reviewed dependencies across Orca (#24561)

* chore(deps): update reviewed desktop dependencies and tooling

* chore(deps): update compatible mobile packages and Fastlane

* chore(deps): update cloud transports and enforce release age

* chore(deps): patch documentation dependencies and record review

* chore: remove dependency review reports

* test(linear): smoke-load resolved SDK through CommonJS loader

* fix(deps): keep native rebuilds from reinstalling addon dependencies

* fix(native): invoke installed node-gyp directly for Node rebuilds

* test(cloud): exclude observer probes from row-lock timing budget

* test(mobile): preserve the CSS writer receiver in viewport spy

* test(native): remove obsolete batch-shim fixture exception

* Stream native rebuild output through the process wrapper

* fix(antigravity): discover global skills from the CLI configuration root (#24592)

* fix(antigravity): keep quota probes free and visible without Gemini OAuth (#24593)

Consolidates the reviewed version and visibility work from #24283 with the probe gating and structured error classification from #24296. Reject unsuccessful version probes, preserve diagnostic precedence, and assert that real quota reads start no model turn.

Co-authored-by: Pablo Werlang <19828711+werlang@users.noreply.github.com>

* fix(antigravity): launch POSIX hooks through sh with bounded JSON stdin (#24596)

* fix(antigravity): make POSIX hooks executable with bounded JSON stdin

* test(antigravity): decode SSH hook shell command before assertions

* test: check plugin fixture worktree cleanup (#24779)

* fix(files): ignore nested generated directories on macOS (#24620)

* fix(files): ignore nested generated directories on macOS

* fix(files): filter generated filenames containing newlines

* Check E2E packages where the installer reads them (#24785)

* test: check plugin fixture worktree cleanup

* test: check E2E installer package environment

* Preserve Mermaid exports through mobile bundling (#24661)

* Update README downloads badge

* ci: skip installed glibc tools in SSH host qualification (#24733)

* Fix terminal width cutoff on wide panes (#24687)

* fix(terminal): let wide panes use up to 1024 columns

Adapt the wider viewport limit proposed in #16578 to the current runtime, shared RPC schemas, and preview sizing.

Co-authored-by: innocarpe <innocarpe@users.noreply.github.com>

* test(terminal): wait for probe output after command echo

* test(terminal): align RPC boundary with wider viewport limit

---------

Co-authored-by: innocarpe <innocarpe@users.noreply.github.com>

* fix(runtime): report an open agent question as blocked to tui-idle waits (#24533)

OpenCode's question tool (and Pi/OMP ask tools and custom modals) put the
hook row in a waiting state but paint no dialog wording the blocked-text
layer knows, so the hook lane read the wait as pending and the tui-idle
wait timed out with no blocked reason. For agents whose hooks are
authoritative, a wait the hook reports with no recognised dialog text now
blocks with the existing agent-interactive-prompt reason, unless input
reached the pane after the row (it may have answered the question before
the next hook arrived).

* fix(runtime): read Codex's 'Implement this plan?' menu as blocked (#24536)

After a Plan-mode turn Codex shows a menu that owns the keyboard, but its Stop
hook has already fired, so a tui-idle wait settled ready and sent text into the
menu. A Codex blocked text anchor now names it an interactive prompt while its
key row ends the tail, written against a new 0.160.0 capture that is replayed
by the readiness census.

* feat: live updates for agent state rules (#24387)

Orca downloads a newer agent-state-rules.json from a fixed GitHub release (stable or next channel), validates it like the bundled rules, and applies it without a restart; a local override wins over the download, which wins over the bundled rules. A hand-started workflow from main is the only publisher; merging publishes nothing.

* fix(runtime): settle Codex tui-idle waits on its hook done, leaving working rows to the rules (#24541)

A headless orca serve has no window to write the Codex ready title, so a
Codex tui-idle wait settled only after three quiet seconds. Rule files gain
profile.hooks: "turn-end": a fresh hook done settles the wait, while a
working or permission row leaves the decision to the rules, so a Codex
whose Esc posts no event (before its Interrupt hook) cannot hang the wait.
Codex moves from identity-only to turn-end.

* fix(release): accept the build identity as a later declarator in the minified telemetry check (#24219)

* fix(windows): link the CLI launcher's C runtime statically so orca.exe runs without the VC++ Redistributable (#24484)

* feat(terminal): point an old terminal's Codex shared-server banner at a new terminal (STA-9051) (#24501)

* feat(terminal): point an old terminal's shared-server banner at a new terminal

A terminal opened before the update that added Orca's codex wrapper is
still served by an older terminal daemon, so a typed codex there joins
Codex's shared server. The banner now says why and offers a new terminal
instead of the global Fix, which changes Codex settings and stops a
server other sessions use.

Detection reads the owning daemon's protocol from the router's in-memory
session map, only after a pane is already found on the shared server.

Refs #24217, STA-9051

* refactor(terminal): simplify the old-terminal banner after review

- Open new terminal now works from Activity, which shows panes from
  worktrees that are not active: it activates the tab's worktree first.
- Inline the legacy-daemon check in the IPC handler over the existing
  getLegacyDaemonAdapters instead of a new routing export.
- One banner frame with the variant chosen inline; the Fix dialog is a
  sibling rather than a children slot.
- Rename CodexSharedServerJoin to CodexSharedServerStatus.

* fix(terminal): open the new terminal in the pane's own workspace, and only promise it where it helps

Open new terminal now always goes through the folder-aware workspace activation
(returning early when that fails) and reveals the floating panel for floating
panes, so folder workspaces and panes viewed from Activity open in the right place.

The old-terminal variant now shows only when the shell Codex was typed into gets
Orca's codex function from this build: zsh, bash and PowerShell from protocol 37,
fish from 39, cmd.exe never. The shell is the parent of the Codex process in the
process table the shared-server check already reads.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(terminal): explain an old terminal's shared server in Learn more

Old-tab banner says Orca now gives each Codex its own server, and gains a
Learn more dialog: why it matters, why this terminal still shares, Open new
terminal, and a quieter way into the existing Turn off / Stop server steps.

* fix(terminal): shorten the old-terminal Learn more copy to one line

* fix(terminal): drop the global fix from the old-terminal dialog

A new terminal already runs Codex on its own server there, so turning off sharing everywhere only changes settings outside Orca and can end other sessions.

* fix(terminal): treat fish without config as a shell Orca does not wrap

* fix(terminal): return focus when a Codex shared-server dialog closes

Both banner dialogs are controlled with no Radix trigger, so Esc or X left
focus on document.body. Capture the active surface when the banner opens a
dialog and restore it on close via useModalReturnFocus; Open new terminal
skips the restore so the new terminal keeps focus.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(terminal): return focus when no new terminal opens, and keep an open banner dialog when Codex ends

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(filesystem): deleting a workspace no longer fails as 'outside allowed directories' in WSL-runtime C:\ projects (#24242)

* fix(filesystem): list a WSL-runtime repo's worktrees through its distro when authorizing paths

* fix(filesystem): record the Git that listed each repo's roots and keep WSL roots under repair

- Registration now takes the distro the caller listed through (create and catalog scan pass theirs)
  instead of re-resolving the runtime, so a stale-routed listing is relisted on the next miss.
- A runtime awaiting repair no longer counts as a routing change, so its last WSL listing stays
  authorized instead of being replaced by a host-Git listing.
- The routing check runs after each awaited refresh, so a runtime switch during an in-flight
  rebuild is corrected in the same request.
- Reuse the shared distro helper for listing; move the drift check into the relist policy and
  refresh the root set once per batch.

* test(worktrees): fail the host-Git scan registration test when nothing registers

* fix: dismiss Codex account prompt and return focus to terminal (#24683)

* chore(worktree): include create timing and spare outcome in the workspace create events (#24483)

* chore(worktree): include create timing and spare outcome in the workspace-created event

The workspace_created and workspace_create_failed events gain optional,
numbers-and-enums-only fields built from what the create already measured:
total and per-phase durations, the prepared-checkout hit/miss and miss
reason, the execution host (local/WSL/SSH), a worktree count bucket, how
many other creates were in flight, whether the repo has a post-checkout
hook (file existence only, probed after the create returns), and for a
failure the phase it died in plus elapsed time. No new git process runs;
consent and opt-out are unchanged.

* fix(worktree): attribute failed_phase by error, label WSL-path repos, skip the hook check with telemetry off

- failed_phase now names the outermost timed step the thrown error (or its cause) left, so a
  caught failure or a concurrent sibling step can no longer be misattributed; the old-relay SSH
  error keeps its cause so it still reads as git_worktree_add.
- execution_host follows the same rule Git routing uses, so a \\wsl.localhost repo reads wsl.
- The post-checkout hook check does not read the repo when telemetry is disabled.
- Privacy page mentions the miss reason code and the failed step.

* test(worktree): pin the old-relay SSH add error to git_worktree_add through its cause

* fix(worktree): name the create event field sets for their role, and type the old-relay test's caught error

* fix(worktree): send create events from runtime creates and record what the spare checkout did

Runtime creates (CLI, agents, phone app, paired clients, orchestration, server
automations) reuse prepared checkouts like the app's own creates, but recorded
no timing and sent no events. Both entry points now start one shared sender
(workspace-create-telemetry.ts), so every create sends exactly one event with
the same fields, plus create_entry_point (app | runtime).

Spare-checkout fields:
- concurrent_preparations: peak prepared-checkout builds and background
  discards running during the create, excluding the one it used; the window
  closes before the create's own re-arm starts.
- prepared_checkout_claim / prepared_checkout_discard phases, so on a miss
  git_worktree_add minus the prepared_checkout_* phases is the plain checkout.
- prepared_checkout_reset (none | base_moved | retargeted) replaces the
  retargeted flag; prepared_checkout_origin (prefetch | rearm) on hits.
- workspace_create_failed carries the spare outcome and its wait.
- repo_index_size_bucket from one stat of .git/index in the existing
  post-create probe (telemetry on, local/WSL only, 2 s cap).

* fix(worktree): add spare build and idle time, the re-arm prefetch origin, and a tracked-file count

- prepared_checkout_build_ms / prepared_checkout_idle_ms on hits: from arming
  the spare to ready, and how long it sat ready before the claim (0 when the
  create waited). readyAt is recorded in the pool's existing ready handler.
- prepared_checkout_origin gains rearm_then_prefetch: an automatic re-arm that
  the dialog prefetch then asked for too, so rearm means the re-arm alone.
- repo_file_count_bucket replaces the index byte size: the entry count from
  the 12-byte index header, which is the same in every index version; left
  out for a split or sparse index.
- The shared sender never lets a failed send change the create's result or
  error; it logs instead and still ends the create's concurrency membership.
- Tests pin the runtime SSH create's timing hand-off and the throwing-send
  cases on both entry points.

* fix(worktree): leave out the file count under any sparse checkout and time spare builds monotonically

- The repo probe also reads .git/config.worktree, where git sparse-checkout
  --sparse-index writes index.sparse, and omits the file count whenever
  sparse checkout or a sparse index is on in either file, with Git's boolean
  spellings. core.hooksPath there is honoured too.
- prepared_checkout_build_ms / _idle_ms use performance.now(), like every
  other duration; the build is timed from its own start (buildStartedAt).
- The origin field comment names all three values.

* fix(worktree): keep the spare's build time on its first build and count worktrees by lock reason

- prepared_checkout_build_ms runs from the first build's start (including any
  wait for the base fetch it is built on) to its first ready; a later tip
  refresh no longer restarts it, though it still counts as new preparation
  work. prepared_checkout_idle_ms runs from the latest ready (build or
  refresh) to the claim.
- The worktree count reads each .git/worktrees entry's locked file and leaves
  out entries whose lock reason names an Orca preparation, the way the
  listing does, instead of subtracting this process's spares. That covers
  spares from other processes, crash leftovers and spares being discarded,
  and cannot run one low while a spare's admin dir does not exist yet. It has
  its own 1.5 s cap inside the probe.

* ci(release): skip the orcad template for tags that predate it (#24872)

A patch cut from a base older than #24155 has no orcad template source, so
the template job could never pass and every desktop build waited on it.

* fix(runtime): make OMP 18.4 workers ready at the composer and keep stale-title clears display-only (#24295)

* fix(runtime): require OMP composer readiness and retain native title evidence

Keep timer-cleared titles in display state so they cannot settle an idle wait.
Require OMP's captured empty composer through the existing screen rules.

Co-authored-by: drakeo338 <paranoyouz@gmail.com>
Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com>
Co-authored-by: Saurav M Hiremath <sauravhiremath@gmail.com>

* fix(runtime): veto OMP setup by screen and keep stale-title clears display-only

OMP readiness no longer keys on the composer's thinking-effort hint, which
OMP retires after a few uses, hides once a conversation exists and renders
differently per glyph preset. Instead OMP's setup wizard (alternate screen or
a "Setup step N of M" heading) vetoes every ready lane, and OMP's own `π >`
idle title counts for the omp identity only once quiet on a screen read clear
of the wizard. Other OMP titles take the general path again, so the
post-turn `π -` title, 17.x `π:` titles and first-party blocked waits behave
as before. `π >` is no longer explicit idle for every identity.

The stale-working-title clear stays out of runtime records, but display
readers (worktree ps, phone status and titles, terminal list, orchestration
pointer, orphan adoption and reveal titles) now project records through one
display accessor, so they show the cleared title and status again. A title
the clear retired can no longer prove an agent is present; the foreground
process must, which also covers the retained Cursor spinner, so the Cursor
presence exclusion is reverted.

* test(runtime): refuse OMP's setup splash, which has no step heading

* test(runtime): a genuine title retires the stale-working display clear

* test(daemon): record the OMP captures' inherited shrink cursor divergence

* fix(runtime): settle OMP's idle title by its age and veto only its alternate screen

OMP 18.4 re-asserts bracketed paste every second once a terminal answers
its capability probe, which Orca's terminals do, so an idle OMP pane never
went quiet and worker-start still timed out. Its idle title now counts once
it has stood a quiescence window on a screen read clear of the setup wizard.
The setup veto now reads only the alternate screen: the wizard always runs
there, and a 'Setup step N of M' line on the normal screen is an answer's
own text.

* test(runtime): let the grid ingest OMP repaints before the title arrives

* fix(runtime): keep the stale-title clear's process checks and record lifetime

The stale-working-title clear is now an explicit branch instead of an early
return. It still skips the title/status evidence readiness and delivery
read, but runs what main's clear did to re-derive process state: the
foreground-agent re-probe (an exited shell-launched agent loses its
identity) and the completion-race exit check behind a fresh done hook. The
clear now lives on the PTY record, so an SSH relay drop and reattach keeps
the cleared display, and a recreated tracker is seeded from it. Presence
judges a retired title by display ordering, so a renderer pane title older
than the clear cannot prove an exited agent is present.

* docs(runtime): OMP's idle title precedes its setup wizard by its startup tail

* fix(runtime): judge agent presence by the displayed title while a clear stands

A title the stale-working timer cleared used to veto presence outright, so a
live, busy agent whose title cleared to its name and whose process Orca
cannot recognize read as no agent, and the answer depended on whether a
renderer pane was mounted. Presence and the terminal status query now read
the display projection instead, which is what they read before the clear
stopped rewriting records: a cwd spinner still clears to a neutral title, so
the foreground process decides for an agent that exited behind it.

* fix(runtime): compare a cleared title with a lifecycle cleared the same way

The stale-working clear no longer writes the prompt lifecycle, so the
blocked-dialog check compared a cleared title with a still-working
lifecycle and ignored a trust or update dialog painted after the clear:
terminal status said idle, the guarded send accepted, and a mounted pane's
interactive wait went missing. The lifecycle is now projected through the
clear wherever a cleared title is compared with it, as main recorded both
together. That comparison can only report a wait on the user, never idle.

* fix(runtime): verify prompts behind a stale-title clear against main's baseline

Prompt-submission verification read the native lifecycle while a
stale-working clear stood, so output after a swallowed Enter behind a stuck
spinner counted as delivered, and the first spinner frame after the clear
was not a new turn. Its starting status now goes through the same lifecycle
projection the blocked-dialog check uses, and the first working status
after a clear starts a new turn, as main recorded it. Readiness, delivery
and the mailbox still read only native evidence. The blocked-dialog comment
now says the projected pair reproduces main's verdict.

---------

Co-authored-by: drakeo338 <paranoyouz@gmail.com>
Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com>
Co-authored-by: Saurav M Hiremath <sauravhiremath@gmail.com>

* fix(files): replace watcher subscriptions after native failure (#24723)

* fix(files): retry failed shallow directory watch replacement (#24724)

* fix(git): share pending watcher reconciliation startup (#24725)

* Skip dependency installation for known headless build inputs (#24716)

* ci: defer headless dependency installation until graph analysis is needed

* docs: align headless CI rollout with platform and cache policy

* test: isolate headless detector output from the parent CI step

* Reuse pnpm verification records in Alpine CI (#24817)

* ci: reuse pnpm verification records in Alpine builders

* ci: qualify consumers of the verification restore action

* ci: match Linux verification cache archive paths

* ci: overlap ARM SSH setup and independent observation waits (#24714)

* fix(tests): run watcher crash harness against current code (#24705)

* fix(tests): resolve watcher crash harness from repository root

* fix(tests): rebuild watcher crash harness from current sources

* fix(tests): register watcher interruption callback as an owner hook

* ci(release): publish after a skipped orcad template (#24882)

* ci(release): publish after a skipped orcad template

#24872 skips orcad-template for tags that predate it, but a skipped ancestor
skips every job that keeps the implicit success(), so publish-release and the
post-release jobs never ran for v1.4.219.

* test: brace-free filter in the orcad downstream contract

* Reduce scheduled CI cache warming to every six hours (#24881)

* Reduce scheduled CI cache warming to every six hours

* Document cache warmer recovery interval and measured tradeoff

* ci: move ARM Mac qualification to macOS 15 (#24760)

* fix(files): keep watcher streams across SSH reconnect races (#24722)

* fix(files): preserve watcher streams across SSH reconnect races

* fix(types): prune the checked SSH watcher from suppression baseline

* fix(files): retain established SSH watches through connection loss

* fix(files): fence initial SSH watcher callbacks across reconnects

* test(files): verify guarded SSH watch errors and cancellation

* Keep large Markdown previews responsive (#24880)

* Keep large Markdown previews responsive

* Fix large preview review navigation and Find budgets

* Initialize preview scroll caches once and check viewport visibility

* Restore large previews after loaded rows are measured

* Refresh loaded Markdown rows after viewport changes

* Keep Markdown revisions visible and reuse bounded search text

* Skip slower Windows root package-store restores in CI (#24885)

* Skip slower Windows root package-store restores in CI

* Update reviewed mobile dependency-store cache expression

* Advance full shutdown deadlines in mocked Codex connection tests (#24893)

* fix(plugins): restore development watchers after folder replacement (#24726)

* fix(files): retry failed shallow directory watch replacement

* fix(plugins): restore development watchers after folder replacement

* fix(plugins): reconcile root bindings without broad macOS watches

* fix(plugins): avoid source-watch restarts on Windows child edits

* fix(plugins): preserve full filesystem identity precision

* fix(plugins): recognize root replacement on inode-less volumes

* fix(plugins): preserve availability without reliable directory identity

* fix(watchers): preserve directory identities when checking replacements (#24888)

* fix(watchers): preserve full directory identity precision

* fix(watchers): recover replaced inode-less directories

* fix(watchers): decline unavailable creation-time identities

* fix(opencode-go): don't fall back to OPENCODE_API_KEY when the credential database is unreadable (#24605)

* fix(opencode-go): stop before OPENCODE_API_KEY when the credential database is unreadable

An unreadable OpenCode credential database read as 'no key', so the Go key
resolver fell through to OPENCODE_API_KEY, which OpenCode shares with its
Zen provider and can show usage for the wrong key. The database read now
reports unreadable separately; with an env key set the resolver stops, and
the usage fetch uses a configured cookie or shows a readable error.

* fix(opencode-go): treat a denied credential-database listing as unreadable, not missing

* fix(codex): skip the hook trust RPC when Codex's session index is unreadable (#24604)

An unreadable state DB was read as 'not busy', so the trust grant ran a
short app-server RPC that can refresh an abandoned backfill lease. A
tri-state pending check lets the trust grant take its fallback, while other
callers keep their existing boolean behaviour.

* refactor(cursor): move the desktop-login read onto the shared foreign SQLite reader worker (#24603)

* refactor(sqlite): rename the OpenCode SQLite worker entry to foreign-sqlite-reader (STA-9122)

The worker thread that reads OpenCode's database off the main thread is about
to read other apps' databases too, so its entry is renamed to what it is:
src/main/foreign-sqlite-readers/foreign-sqlite-reader-entry.ts, built as
out/main/foreign-sqlite-reader-entry.js.

Why now: #24572 fixed the Cursor focus freeze with a second, dedicated
worker. Rather than grow one worker per foreign app, the next commit moves
Cursor onto this entry and deletes that worker. This commit is the rename
only; #24572's cursor-desktop-profile-worker-entry lines stay until then.

It moves out of ai-vault/ into a new foreign-sqlite-readers/ module because
it will no longer be session-scanner code; the module will own the readers,
their dispatch, protocol and main-process client.

The entry still routes only OpenCode kinds in this commit. The OpenCode
dispatch, protocol and process entry stay in ai-vault/ and stay OpenCode-only,
because the SSH/WSL relay reader bundles them (build-relay.mjs).

Every reference is updated: electron.vite.config.ts input key, knip entry,
the plain-node entry guard and its test, the asarUnpack list (the scanner
service still spawns this entry under ELECTRON_RUN_AS_NODE), and the
electron-builder test that reads the filename. The filename and the
beside-or-one-up (Rollup chunks) lookup now live in
foreign-sqlite-reader-entry-path.ts, which the OpenCode spawn reuses, plus an
Electron-main resolver that uses the packaged app.asar path.

* fix(cursor): move the desktop-login read from its dedicated worker onto the foreign SQLite reader (STA-9122)

#24572 fixed the Cursor focus freeze (#24360) with a dedicated worker
(rate-limits/cursor-desktop-profile-worker*.ts). Orca already runs OpenCode's
database reads on a worker, and more foreign-app SQLite reads are coming, so
keeping one worker per app means one entry, build input, asarUnpack line, knip
entry and guard line each. This keeps one pattern instead: Cursor's
state.vscdb read runs on the shared foreign SQLite reader entry, and the
dedicated worker, its entry and its config lines are deleted.

What moves:
- The read itself is a pure cursorProfile reader in
  foreign-sqlite-readers/readers/ (was rate-limits/cursor-desktop-state-db.ts),
  run only on the worker. A separate dispatch owns the new kinds and refuses
  an unknown kind. The entry routes OpenCode kinds to the untouched OpenCode
  dispatch, so the relay's OpenCode reader stays byte-identical.
- ForeignSqliteReaderClient gives each reader its own WorkerThreadRequestQueue
  lane (own lazily started, idle-torn-down thread; one shared factory) with
  in-flight dedupe per database path. Any failure resolves to the reader's
  existing failure value and never falls back to the main thread.

Kept from #24572, so every reader gets them:
- Await worker retirement before respawning. Worker.terminate() cannot
  interrupt a native SQLite call (e.g. a WAL-index rebuild), so the old
  thread lives on until that call returns; respawning at once stacked a new
  thread on the same work for every timed-out read (#24572 measured three
  live workers). This belongs in the shared host, which fire-and-forgot
  terminate(): LazyWorkerThreadHost now takes awaitRetirement and refuses to
  spawn until the terminated worker settles, and the queue fails calls closed
  meanwhile. Opt-in, because pure-JS clients (session scanner abort, port
  scan) respawn right after an abort. A rejected terminate() also ends
  retirement, so it cannot latch the reader off (raised in #24572's review).
- 10 s Cursor timeout, 2 consecutive deaths, a queue cap of 8.
- #24572's worker tests, rewritten against the shared client: responsive
  caller plus coalesced probes, unavailable worker without path leaks,
  stalled-worker recovery, no respawn before retirement, dispose settles.

Tests: reader, dispatch, client (timeout, 10 s default, crash, malformed,
unavailable without a main-thread read, dedupe, queue cap, own thread per
reader), queue retirement (stalled and rejected terminate), import boundary,
and an event-loop test reading a ~50 MB WAL with no -shm on a real worker.

* test(sqlite): walk the reader import boundary with the shared source-tree scan (STA-9122)

* fix(sqlite): key reader dedupe on a caller-supplied key, not the path alone (STA-9122)

* Skip slower root package-store restores in Linux PR jobs (#24896)

* test: advance mocked Claude child exit deadlines (#24897)

* perf(tab-bar): a change to one tab no longer re-renders every tab (#24261)

With many tabs open, a change to any one tab (a retitle, an agent finishing, a tab switch, a git status write, or a browser tab update on SSH and web clients) re-rendered every tab in the strip, so the strip stuttered. Each tab is now a memoized row that re-renders only when its own values change, with stable handlers, a stable drag id list and stable drag sensor options. Editor tabs get their own git status, and mirrored browser tabs keep their page-id list while the ids don't change.

Part of #24241: opening, closing or reordering a tab still re-renders every tab once.

* Reuse the headless detector compiler without installing full dependencies (#24895)

* Reuse the headless detector compiler without full dependency setup

* Keep optional compiler-cache saves from failing cache warming

* Stop mocked renderer imports from qualifying headless CI (#24902)

* Decouple headless running-work tests from the renderer

* Keep the shared running-work probe contract documented

* fix(opencode): read the binder's session store off the main thread; ship the reader worker in orcad (#24638)

* fix(opencode): read the binder's session store on the foreign SQLite reader worker (STA-9122)

Before: the OpenCode session binder listed new sessions from opencode.db with
node:sqlite on the main thread every 60 s (and on SessionStart kicks), so a
large or contended store could stall the app the same way Cursor's did.

After: the read is a pure openCodeBinderSessions reader in
foreign-sqlite-readers/readers/, run only on the worker. The binder's
correlation, pane snapshot and process sweep stay where they were.

- The binder round awaits listSessions and re-checks its generation right
  after, so a stop() during the read discards the round before it touches the
  unbound map or the watermark.
- The client's in-flight dedupe key now includes the cursor, so a stale round
  from before a restart cannot hand its rows to the restarted round.
- Idle teardown is per reader. The binder lane keeps its thread for 120 s,
  longer than its 60 s poll, so the thread is not respawned every round.
- A timeout, crash, malformed reply or unstartable worker resolves to [] (no
  sessions), the value the old read already returned on failure.
- An absent store still reads as [] without a log line, and a permission or
  corrupt-file failure still logs (kept from #24577, now in the reader: it
  stats the path and throws anything but ENOENT/ENOTDIR to the client's log).
- The binder lane inherits #24572's limits from the shared lane: no respawn
  until a timed-out worker has exited, 2 consecutive deaths, a queue cap of
  8. Its timeout stays 60 s, matching its poll.
- dispatch switches on the destructured kind, so a new kind without a case
  still fails to compile.

orcad: the hook server runs there too, so orcad now ships
foreign-sqlite-reader-entry.js beside orcad.js (ORCAD_ARTIFACTS, built as an
orcad child). build-orcad runs a smoke check that starts the built worker
under the build's Node and under the pinned runtime, and does a real binder
read on a fixture DB, a Cursor read of a missing file and an OpenCode history
list. The OpenCode history scanner uses the same entry and was bundled into
orcad without it, so on orcad it always failed closed; it can now run.

Tests: reader (cursor, same-ms ids, OpenCode 2 rows, missing then created,
corrupt, inaccessible directory), retirement gate for the binder lane, dispatch
routing, client lane (rows, failure -> [], dedupe per cursor, own thread, idle
teardown default and override), binder loop with an async listSessions
(failure -> [], stop during the read), orcad path resolution through orcad's
host adapters, artifact list, and the smoke check against good, missing and
non-reading entries.

* test(opencode): cover the binder read deadline with fake timers and name the failure test accurately (STA-9122)

* refactor(ai-vault): delete the unused session-scanner worker thread (#24607)

* refactor(ai-vault): delete the unused session-scanner worker thread

Production always scans through the forked session-scanner service process;
the worker thread was reachable only under NODE_ENV=test or the
undocumented ORCA_AI_VAULT_SERVICE_PROCESS=0 switch, and nothing fell back
to it on service failure. Remove the thread (spawn, client, protocol, entry,
tests), its build entry, knip and plain-node-guard listings, and the
backend switch, so session-scanner-background always routes to the service.

- Move the scan options type to the service protocol as
  AiVaultServiceScanOptions.
- Tests now mock session-scanner-service-spawn, the seam production calls.
- Repoint the hot-path listing reliability gate from the worker-client test
  to the service-client test, which covers the same bounded-queue,
  cancellation, and fault-restart properties for the real executor.

STA-9122

* test(ai-vault): cover the service's Claude-vs-OMP subagent lister choice

Runs the real service entry and subagent reader, replacing only the two
per-agent listers, so a swapped lister choice fails.

STA-9122

* Skip slower root package-store restores in macOS PR jobs (#24908)

* Skip slower root package-store restores in macOS PR jobs

* Update the companion cache-policy contract

* Overlap independent Linux headless runtime builds (#24910)

* test(persistence): wait for real worker readiness before timeout (#24793)

* Add Qoder session history and search (#24614)

* Add Qoder session history and search with real CLI coverage

* Allow the real Qoder marker file to end with a newline

* Keep Qoder tool output out of history previews and search

* Keep Qoder search pages readable by older clients

* Verify persisted Qoder history after a real generated and resumed task

* Negotiate Qoder filters before searching an older execution host

* Combine search client imports for the CI plugin gate

* Keep the relay search oracle aligned with legacy agent filtering

* test(qoder): align search capability contracts and pin old-host fencing

* fix(cursor): show the primary model pool in compact usage (#24830)

* fix(cursor): show the primary usage pool without hiding exhaustion

Use Cursor's reported plan percentage when its base allowance disagrees.
Select Cursor Models for compact display while preserving maximum-pool
warning, overflow, sorting and collapse behavior.

Adopts the plan mapping and primary headline intent from PR23531.

Co-authored-by: DakaAlvarez <149860458+Dacadev97@users.noreply.github.com>

* fix(cursor): describe compact usage summaries

Correct the existing six translations and fallback to describe one summary per provider after the compact Cursor primary-pool adoption. Preserve quota mapping, selectors, alerts, sorting and detailed presentation.

Validated source patch: d2a233e5f90a2cf8ccfb02dfafe99e0e03add48d with normal installed commit hooks, localization catalogs and changed-code quality. Standalone copy uses a private index and preserves the reviewed candidate ancestry.

Co-authored-by: DakaAlvarez <149860458+Dacadev97@users.noreply.github.com>

---------

Co-authored-by: DakaAlvarez <149860458+Dacadev97@users.noreply.github.com>

* fix(native-chat): one ordered journal writer (#24127)

* fix(claude): settle a queued send the CLI withdrew from its own cancelled frame

Claude reports each uuid-stamped command's lifecycle (queued, started,
completed, cancelled). A send it withdraws from its queue gets `cancelled`
before the interrupt or cancel_async_message answer, so a lost or failed
answer no longer leaves that send pending: it settles as withdrawn, with the
same reason and words as the receipt path.

A command the CLI already started also ends `cancelled` when its turn is
interrupted or fails, so `cancelled` after `started` is not a withdrawal;
an echoed send has left the waiter lists and is never reached.

Tests replay real 2.1.280 captures, scrubbed.

* fix(claude): release a doubted send when the CLI reports its session idle

A Claude send whose write ended in doubt is recorded `unknown`, and a live
`unknown` reads as work still owed, so the chat showed Working until the
child exited. Claude sends `session_state_changed idle` only once its whole
queue has drained, so it can no longer be holding that send. The runtime now
routes that report to the host's existing release, the same one Codex's
thread-stopped report uses; it retires `unknown` only, never `pending`.

* fix(claude): keep a command's started mark when a redelivery re-emits queued; fixtures name msg_lifecycle_v1

* fix(claude): settle every terminal lifecycle state of a send the CLI never echoed

A send the CLI started, then cancelled before any echo, stayed pending: it may
already be in the conversation, so it is released as doubt (unknown, recovered),
never withdrawn and never re-sent. A late echo still accepts it.

The 2.1.280 schema has two more terminal states. `discarded` (the CLI ended its
session with the send still queued) settles as not delivered; `refused`
(declined before it queued) settles as not accepted by the provider. After
`started`, either one is doubt, as `cancelled` is.

The late-settlement path gains an `unknown` outcome, which the host records as
released doubt.

* fix(claude): release a send the CLI took but left unanswered when it goes idle

`session_state_changed idle` comes only once the CLI's queue has drained, so a
send it took that is still unanswered there got no echo and never will: a turn
that throws can leave `started` with no terminal state. Idle releases it as
doubt.

What proves the CLI took a send is its lifecycle frame. On a CLI that reports no
lifecycle, it is the send's place on stdin: one whose write finished before an
interrupt went out was read before the interrupt was, so the first idle after
that interrupt releases it too. A send armed ahead of the interrupt but written
after it is left alone, since the CLI may still run it.

* fix(native-chat): keep the idle sweep off a Claude child that holds a send

A Claude retrying a rate-limited request has taken the send but echoes nothing,
so no turn row exists yet and the sweep rested the child after the idle window,
turning the send into doubt. The adapter now reports whether the CLI holds a
send (lifecycle `queued` or `started`, not yet echoed or ended), derived from
the live waiters, and owed work counts it.

Nothing is stored: every held send leaves the live set on its echo, its
terminal lifecycle state, the CLI's idle, or the child's exit, so the hold ends
with the send.

* fix(claude): count only a started send at idle and as a held send

2.1.280's end-of-turn cleanup can report idle before it re-reads its queue, so
a send read in that window goes queued, idle, started. Releasing every taken
send at idle doubted that live send and dropped Working. Only a `started` send
is released at idle or keeps the child from the idle sweep; a `queued` one ends
by starting and echoing, by a terminal lifecycle frame, or with the child.

The stdin-order path for CLIs without lifecycle frames is removed: a doubted
send retired there disables content matching on CLIs that mint their own echo
ids, and no Orca failure called for it. Those CLIs keep the earlier behaviour.

Comments that said only a failed write or child exit ends a waiter, or that
idle comes only once the queue has drained, now say what ends one.

* docs(claude): say only what the CLI's lifecycle frames and idle actually prove

* fix(claude): hold the idle sweep while Claude has a send queued, not only started

The sweep rested a child whose CLI had queued a follow-up behind a turn, dropping
the send it had already taken. The hold now spans the CLI reporting it took the
send until its echo, a terminal lifecycle state, or the child's exit. The idle
release still covers only started sends: 2.1.280 can report idle before it
re-reads its queue.

* refactor(native-chat): give provider-proven late dispatch settlement its own module

* test(claude): pin a steer a Stop interrupts after it started as doubt, not withdrawn

* fix(native-chat): one ordered journal writer

Every journal write, streamed or direct, lands in the chat's one write queue
in the order it is issued, and has landed in the fold when its call returns
(except while an owed import is paid, when it lands in queue order). The event
sink stops being a queue ahead of it; journal-write coalescing, which moved a
replaced write to the tail, is removed; closing a sink no longer loses writes
already handed over. The ten flushStreamedEvents patches go. A streamed text
item takes its place at its first delta, so a direct write inside the
coalescing window never lands above text already streamed. A Stop interrupts
whatever its bookkeeping writes do.

* fix(native-chat): a failed Stop holds the lane until its queue pause lands

A Stop whose note write or stop call failed skipped the wait for its
withdrawal and pause, so the lane freed first; with writes queued behind
owed work, the drain could hand the waiting card to the agent after Stop.

* fix(native-chat): a journal write body is typed synchronous

The queue's ordering rule needs every write body to finish before it returns;
serialize still took a promise-returning body, so an await inside one would
let a later write land first. The body type now refuses a promise.

* fix(native-chat): a stream's first window snapshot always lands

The first-delta write counted as the growth checkpoint, so the window's
snapshot was skipped until 32 more characters arrived: a stream showed only
its first token, and a Codex reasoning row could sit as an empty aside. The
coalescer now marks the row-creating emit and the first snapshot after it,
and both providers' growth throttles write those.

* test(native-chat): streamed text rewritten in place above a Stop note

Covers a stream whose later deltas wait in the window across a Stop, for
Codex and Claude, and a Codex item completed inside the window.

* chore(native-chat): drop comments that still describe coalesced journal writes

* fix(native-chat): type the draft-table write body synchronous too

* fix(native-chat): an empty delta owes no streamed-text emit

The opening snapshot is forced past the growth rule, so an empty second
Codex delta rewrote the row with identical text. The coalescer now marks a
stream dirty only when a delta adds text.

* fix(native-chat): a mutation's open pays an owed import first

With the flushes gone, a Stop naming no turn, a goal set or a /clear could
read the fold while provider rows still waited behind a restore's owed
import: the Stop answered cancelled:false and interrupted nothing. The open
every mutation shares now waits for the import, as a reader's does; a failed
import is reported and never refuses the mutation.

* chore(native-chat): whenImported says mutations await it too

* test(native-chat): a Stop interrupts before its withdrawal or pause settles

Pins the order for a write that is held and then fails, for a Stop naming
its turn and one naming none.

* test(native-chat): a Stop ends a starting child before its withdrawal or pause settles

* test(native-chat): Stop order tests wait for the interrupt, not a 50 ms timer

* test(native-chat): settlement-order test reads rows through the journal row parser

Replaces a Reflect.get field walk, which the low-evidence audit rejects, with
parseJournalRow and the named row types.

* fix(native-chat): an empty delta still owes its emit, just not a forced one

The previous fix stopped an empty delta from marking the stream dirty, which
broke the pinned contract that an empty stream is snapshotted and flushed.
The coalescer now marks a stream dirty on every delta and tracks separately
whether its text changed since the last emit; only a changed snapshot is
the opening one that skips the growth throttle.

* revert(native-chat): drop the first-text row write from the delta coalescer

The immediate first-delta emit (and the opening flag and counters it needed)
had no Orca-observed failure behind it and added a journal commit per
streamed item. The coalescer, the Claude checkpoints and the tests that
pinned the first-text row go back to main's behaviour; the one ordered
writer, the sink hand-off and the Stop rules stay.

* fix(worktrees): list a folder once when git reports it twice (#24357)

When git lists the same folder twice (a leftover worktree registration that points at the main checkout), Orca's runtime listing turned each line into its own worktree with the same id, so `orca worktree current`, `active` and `branch:` failed with selector_ambiguous, and paired clients saw a duplicate row. The runtime scan now keeps git's first row per folder, the rule the desktop sidebar already uses. Separately, for a bare or separate-git-dir repo added through a linked worktree, the scan no longer relabels the main row with that worktree's folder (it relabels only when the folder's git dir is the common git dir), so the worktree keeps its own row and branch in the CLI and the sidebar. No extra git command runs.

Part of #23631: the "Profile state writer command timed out" toast in that issue has a separate cause.

* Bound AI Vault cache loading and keep atomic saves responsive (#24789)

* Bound AI Vault cache loading and cooperative atomic saves

Preserve schema 3 caches across compatible releases while limiting bytes, JSON structure, and newest unique rows. Keep in-process entries authoritative and retain a valid prior snapshot when the newest row cannot fit.

Credits @AmethystLiang for the original PR10708 cache bounds and cooperative persistence intent.

* Use checked cache JSON properties in cooperative serialization

Preserves lazy own-property access and all serializer bounds, yields and errors.

* Resolve explicitly configured command aliases for workers (#24648)

* feat(orchestration): resolve explicitly configured command aliases

* docs(orchestration): explain configured command aliases

* fix(orchestration): validate configured aliases with target shell grammar

* fix(orchestration): use actual shell and refuse assignment-only aliases

* test: preserve typed calls in configured worker target checks

Replace Reflect.apply with the existing typed prototype call pattern so the unchanged regression cases pass the anti-slop lint gate.

* Cancel the journal import test sampler before database teardown (#24767)

* fix(antigravity): bound Windows hook stdin on the owning runtime (#24622)

* fix(antigravity): bound Windows hook stdin before posting status

* fix(antigravity): publish Windows hook companion before core

* test(antigravity): name Windows hook payload cases explicitly

* Add verified native Antigravity Accounts on the owning runtime (#24691)

* Add verified native Antigravity accounts on the owning runtime

* Keep Antigravity usage tied to its observed native account

* Refuse oversized encrypted Antigravity snapshots before writing

* fix(antigravity): localize account heading and search terms

* fix(worktrees): a worktree delete git fails partway stays listed and can be retried (#23952)

* fix(worktrees): delete removed checkouts in git, not in Orca's file pool

Local worktree removal renamed the checkout into a sibling trash root and
deleted it in the background with a recursive fs.rm in the main process.
That queued one request per entry on libuv's shared 4-thread file pool, so
for minutes every other async fs call in the main process (the agent-session
store behind chat sends, file explorer reads) waited behind the delete.

`git worktree remove` now deletes the checkout inline in git's own process
again, so the card stays in its Deleting state for the length of the delete
while Orca's file pool stays free. No timeout applies to the call, so a
large delete is never killed halfway.

If git reports success but the path still exists (Git for Windows leaves
junctions and their parent directories in place), the leftover is deleted
with the existing removeHostTree; WSL checkouts stay with the distro.

Nothing creates trash any more: the scheduling queue, rename/restore
helpers and the trash_rename span are gone. The startup sweep stays to
drain entries older releases left behind, and now removes each emptied
trash root so the obligation ends.

* fix(worktrees): let Git delete Windows checkouts with long paths enabled

Removal now always runs Git's own recursive delete, and worktree creation
checks out with core.longpaths on Windows, so a deep checkout Orca created
could fail to delete with "Filename too long" (#6433). The Windows recovery
then finishes the delete but keeps the branch. Pass the same command-scoped
core.longpaths option to `git worktree remove` so Git can delete what it
created.

Also point the CI shard timing entry at the renamed real-git removal suite.

* fix(worktrees): keep an inherited GIT_ASK_YESNO out of the worktree delete

Git for Windows asks $GIT_ASK_YESNO whether to retry when a file stays
locked during a recursive delete. Orca's git env inherits the user's
environment, so an inherited value would run an arbitrary prompt program
in the middle of a removal. Drop it for the removal call only.

* perf(worktrees): run worktree deletes under their own limit, outside git admission

`git worktree remove` now deletes the whole checkout in Git's own process,
which takes 20-35 s on a large tree. It took a general git admission slot at
status tier for that whole time, and that cap is as small as two slots on a
machine with six or fewer cores, so two deletes blocked every status read.

Deletes now skip general admission and queue under their own limit of two
per host instead: two concurrent deletes already saturate one disk, and more
only slow each other down. Leftover cleanup runs inside the same slot.

* fix(worktrees): delete removed checkouts in the background and mark them removing

Since the checkout is deleted by `git worktree remove` in Git's own process,
a large delete takes 20-35 s. Answering the request only after that made web
and mobile (30 s), paired desktop (60/180 s) and the CLI (60 s) report a
failure for a delete that was still going, and mobile silently re-showed the
row.

The request now does everything that can refuse (lock, cleanliness, archive
hook, watcher/terminal gate, terminal stop, shared-link unlink), records the
removal in an in-memory table on the host and answers `removing: true`. The
delete, branch cleanup and metadata purge run after it in the same order as
before, and the watcher/terminal gate stays held until they finish.

- Listings mark rows in the table `removing` for clients that advertise
  `worktree.background-removal.v1` (the desktop renderer, paired desktop and
  web), and leave them out for everyone else (older clients, mobile, the
  CLI), which already dropped the row when the request answered.
- The outcome (removed, with any preserved branch, or the error) rides the
  existing worktrees-changed event as an optional field, sent after the row
  has left the table.
- A repeat delete while Git runs joins it. A create at the same path or with
  the same branch is refused with "Cleanup is pending; try again shortly";
  create's name search skips the path, so generated names move on.
- Nothing is persisted: after a quit or crash Git still lists the checkout
  and it can be deleted again. WSL checkouts still delete inline.
- `orca worktree rm` says the checkout is still being deleted.

* fix(worktrees): keep the existing Deleting card until the host's Git finishes

The host now answers a local worktree delete on acceptance and deletes in the
background. The renderer keeps the existing delete state set until the host
publishes how it ended:

- The delete that asked waits for the outcome on the worktrees-changed event
  (local IPC or the paired runtime's client event), then runs the same
  teardown, preserved-branch toast and card error an inline delete did. If
  that event is lost to a dropped connection, a listing that shows the row
  gone after it was marked removing finishes the wait, and one that shows it
  back without the marker fails it.
- Any other renderer (a reload, a paired desktop, web) sets the same delete
  state from the host's `removing` marker and clears it when the marker goes.
  A failure the host publishes lands on that card's existing error.
- Web advertises `worktree.background-removal.v1` so the host sends it the
  marker; paired desktop does through the Electron capability list.

No new component, style or state: the card reads the delete state it always
did. A host that predates this answers when done without `removing`, and the
renderer takes that as finished, as before.

* test(worktrees): type the removal harness and projection for the node typecheck

* fix(worktrees): don't fail a delete retry with an earlier attempt's buffered failure

A background removal's outcome that reached this renderer with no waiter (another client's
delete, a host-marked card, or one already settled from listings) was buffered for 60 s and
consumed by the next delete of the same workspace, so retrying a failed delete failed at once
with the old error while the host was deleting. Drop the buffered outcome before sending the
request; only an outcome that arrives after it can belong to it.

* fix(worktrees): let only a gap in host events settle a background delete from listings

Git unlists the checkout before the host deletes the branch, cleans the push target and purges
metadata, and the worktree-directory watcher refetches within 250 ms. The renderer read the
missing row as a finished delete, so the waiter resolved without the preserved branch (no
toast) and a failure in those last steps showed as success; the real outcome was then dropped.
The listing fallback exists only for a lost outcome event, so it now applies only after this
host's event stream had a gap: a new subscription or a replay after reconnect.

* perf(worktrees): let a bulk delete start each same-repo checkout delete once the host accepts the last

A bulk delete ran one worktree at a time per repo (#2259, for packed-refs and ref-lock races in
branch cleanup). With Git now deleting each checkout for 20-35 s before the request settles, N
worktrees in one repo took N times that. The renderer now queues same-repo deletes only until
the host accepts each one; a parent still waits for its nested children to finish. The host
serializes the branch cleanup step per repo itself, which also covers removals started by
different clients.

* test(worktrees): pin the host platform in the mocked removal suites so they pass on Windows

Removal now passes -c core.longpaths=true on Windows, so the exact-argv
assertions and command-keyed mocks never matched there (17 failures on a
Windows host). Pin darwin as the add-worktree suites already do, and drive
the one Windows-specific case through the same spy.

* test(worktrees): type the blocked git remove result instead of a broad object

The anti-slop static-analysis gate rejects `object` parameters.

* test(worktrees): clear the changed-code quality gate in the removal suites

Merge the duplicate node:fs import, build the mock child without a cast, read
worktrees:list rows through one typed helper, and give the remaining casts a SAFETY line.

* fix(worktrees): record each background delete durably and finish it after a quit or crash

A quit mid-delete left git to finish the checkout on its own while the branch
delete and metadata purge never ran; a crash left a normal-looking row. Each
accepted local removal now writes a record beside the profile state before git
starts, clears it on success or failure, and the host runs the same delete
again for any record left at startup, re-deriving what remains from git and
disk. An orderly quit stops the checkout delete without waiting for it.

* test(worktrees): type the interrupted-removal assertions for the node typecheck

* fix(worktrees): finish an interrupted delete that already removed the checkout's .git file

Quit stops git worktree remove mid-delete, and Git deletes the checkout's .git
file wherever it falls in directory order. Git then refuses the checkout
("validation failed ... .git does not exist") on every retry, so the startup
finish failed and the row could never be deleted from Orca. A registered
checkout this record owns that has lost its .git file now finishes like an
unregistered one: leftover files, prune, then the branch.

* fix(worktrees): let Git finish an interrupted delete, and never take a different checkout

A quit or crash that stops `git worktree remove` after it deleted the checkout's
.git file left a registered checkout Git refuses to remove. The previous fix
deleted that leftover inside Orca's process, which is the bulk delete this
change exists to avoid (and on Windows the leftover can be most of the
checkout). The startup finish now rewrites the missing .git file from Git's
own admin entry for that path and lets `git worktree remove --force` delete
it. `git worktree repair` is not used: it also re-points every other
registered path, including a checkout another repository now owns there.
Orca deletes the leftover itself only when no admin entry claims the path.

The startup finish forces, so it now leaves the path alone when the checkout
there is not the one recorded: a registered worktree on a different branch or
head, or a `.git` at a path Git already unregistered. The record is dropped and
the card shows why.

The record write before Git starts is now bounded (2 s, logged when exceeded)
so a stalled disk cannot hold the delete, and the outcome is published before
the record's clear reaches disk.

* test(worktrees): compare worktree paths by value and tear down with Windows lock retries

Git prints forward slashes in `git worktree list` on Windows, so the real-Git
removal suites never found a joined path there: positive checks failed and
negative ones passed without proving anything. They now compare Git's parsed
rows by value. Teardown uses the shared retrying removeTree, since Windows can
hold the deleted checkout busy for a moment after Git exits. Adds a
relative-path worktree case for the .git restore (skipped before Git 2.48).

* fix(worktrees): reply to a worktree delete when it has finished, not on a broadcast event

A current client's delete request now waits for the host's background delete and gets its real
result (removed, a preserved branch, or the error) as the reply, the way it did before the delete
moved off the request. A request that arrives while the delete runs joins it and gets the same
result. Every other view keeps reading the host's `removing` marker: the row leaving means the
delete finished, and the row listed again without the marker shows "The delete did not finish.
Try again." on a card that view had marked Deleting. A request whose reply is lost (a timeout or a
dropped connection) settles the same way from a fresh listing instead of reporting a failure.

Clients without the background-removal capability (mobile, the CLI, older desktops) are still
answered on acceptance and have rows under removal left out of their listings.

This removes the outcome on worktreesChanged and everything it needed: the renderer's outcome
waiters, early-outcome buffer and TTL, per-host event-gap generations, the request pre-registration,
and the accept callback bulk delete used. Bulk delete runs same-repo deletes in parallel only on
this machine, whose host serializes branch cleanup per repo; SSH and paired hosts stay serialized.

* test(worktrees): type the pending-removal host id in the background-removal suite

* fix(worktrees): answer a delete request even when a concurrent removal of the same worktree replaced its record

The desktop app's removal and the runtime removal (CLI, paired clients) coalesce separately, so
both can be accepted for one worktree. The second replaced the first's record, and the first
delete then finished without resolving the request waiting on it, leaving the desktop card on
Deleting indefinitely. Each delete now settles the request it was started for.

* fix(worktrees): run same-repo removal archive hooks and teardown one at a time on the host

Local bulk delete now sends same-repo removals in parallel, so their archive hooks, terminal
teardown and preflight ran at once; a hook that writes refs can race the repo's ref locks
(#2259). The host now serializes each local removal up to acceptance per repo, for every
client; Git's checkout delete still runs in parallel under the delete limit.

* fix(runtime): keep waiting worktree deletes out of a host's foreground call slots

worktree.rm now replies only after Git deletes the checkout (up to minutes), so on paired
desktop and web each waiting delete held one of the host's 8 foreground call slots, and a
bulk delete queued listing refreshes and every other foreground call behind it. Deletes now
run in their own lane with the same bound; the 2-slot background lane stays for status polls.

* fix(worktrees): join a same-worktree delete accepted while a removal waited its repo turn

The desktop app and the runtime (CLI, paired clients, web) check for a running delete before
they queue for the repo's acceptance turn. A delete of the same worktree from the other path,
accepted while this one queued, was missed: this request re-ran the archive hook, stopped the
terminals again and started a second `git worktree remove` on the directory Git was deleting.
The queued acceptance now re-checks and joins the running delete.

* fix(worktrees): fence a resumed delete's checkout from startup, and drop rows a listing read before the delete finished

A delete a quit or crash interrupted took its terminal and file-watcher gate only when the resume
job ran, after the first window was shown; session restore could open a shell or watcher inside the
half-deleted checkout first, and on Windows that handle can fail the resumed git delete. Loading the
records now fences each recorded path, and the resumed job takes the fence over in the same tick it
takes its own gate.

A listing that read git's registration before a delete finished, and replied after the removal
record cleared, returned the row unmarked, so other views briefly showed "The delete did not
finish". Listings now capture the pending removals before reading git and leave out a row whose
delete finished successfully since; a row whose delete failed stays listed as before.

* test(worktrees): keep git's auto-maintenance out of the real-git removal suite

CI's Git 2.55 failed the file-pool test in teardown with ENOTEMPTY on the scratch repo's
objects/pack after the test body passed: the 3,000-file commit's detached auto-maintenance was
still writing a pack. The scratch repo now disables auto-maintenance and auto-gc.

* fix(worktrees): one archive-hook approval covers a same-repo bulk delete again

Local same-repo deletes now start together, so each queued its trust prompt with a state snapshot
taken before the first prompt was answered; approving the first still showed the same prompt once
per remaining worktree. The queued check now reads the store when its turn comes.

* fix(worktrees): a delete Git fails partway stays listed with its error; Delete retries it

`git worktree remove --force` drops the checkout's registration even when it
cannot delete a file (root-owned files, `chflags uchg`, a read-only Windows
directory). Orca lists workspaces from Git, so the row vanished after the error,
leaving the checkout, the branch and Orca's metadata with no way to retry.

- A background delete that fails with the checkout still on disk, unregistered,
  and still the removed checkout's own leftover keeps its durable removal record
  with the error (`failure`) instead of clearing it. Every other failure clears
  it as before.
- Local listings (desktop list/list-all/detected, runtime list/ps/detected)
  add a row for each such record, carrying `removalError`, and for a pending
  removal whose checkout Git no longer lists (shown as removing).
- Delete on that row (desktop IPC and runtime worktree.rm) runs the recorded
  removal again: terminal teardown, then the leftover, prune, branch and
  metadata, under the per-host delete limit.
- The record ends on a successful retry, when the checkout is gone (listing or
  startup)…
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