Skip to content

fix(desktop): give worktree git the budget the runtime already allows - #164

Merged
TsekaLuk merged 3 commits into
mainfrom
fix/worktree-create-gets-the-runtimes-budget
Aug 21, 2026
Merged

fix(desktop): give worktree git the budget the runtime already allows#164
TsekaLuk merged 3 commits into
mainfrom
fix/worktree-create-gets-the-runtimes-budget

Conversation

@TsekaLuk

Copy link
Copy Markdown
Contributor

无法创建平行宇宙 — os error 35

Creating a worktree failed intermittently with:

failed to read resource write response: Resource temporarily unavailable (os error 35)

That is not a network hiccup. It is the desktop client's own read deadline, surfacing as EAGAIN from set_read_timeout (native/rust-host/src/runtime_status_probe.rs:867).

The mismatch

Runtime's budget for git worktree add 2 minutes (gitWorktreeCommandLimit, manager.go:28)
What the client allowed 1500ms — the default meant for status reads
Measured checkout of this repo (7245 files) 0.84–1.25s

The deadline was a coin flip, which is why it failed sometimes rather than always.

It corrupts, not just reports

When the deadline fires the client drops the socket, the runtime's request context is cancelled, and exec.CommandContext kills git part-way through. Measured: git died at 301ms with 1673 of 7246 files written.

What survives:

  • a worktree git has registered, marked locked with reason initializing
  • a branch that exists
  • no record of any of it in the runtime

Neither git worktree prune nor git worktree remove --force clears it — the app's own removal passes a single --force. The next attempt then fails with a branch named 'x' already exists, a stranger error than the first.

Why not a retry

The transport already refuses to replay non-GET requests, correctly: writes may have landed. And a replay would inherit the same 1500ms. The bug is the deadline.

Removal gets the same budget — timing out there abandons a half-removed worktree for the same reason.

The sibling calls in this file already do this: clone passes ten minutes, create-on-host sixty seconds. Worktree create and delete were the two that ran git without saying so.

How this was found, and what I got wrong first

I searched apps/, packages/ and runtime/ for the error string, concluded it was not in the repo, and guessed at a non-blocking-socket EAGAIN that needed a retry.

Both halves were wrong. The string is in native/ — a directory I never searched — and the cause is a deadline, not a missing retry. An adversarial review of that conclusion is what turned it up.

Verification

  • New test asserts the git-running worktree calls carry at least the runtime's budget. Confirmed it fails when the constant is reverted to 1500 and passes when restored.
  • pnpm --filter @pebble/desktop typecheck && vitest run — the command the release build runs
  • oxlint — clean

🤖 Generated with Claude Code

TsekaLuk and others added 3 commits August 21, 2026 10:39
The verify job spends 643s of 1062s compiling the Tauri shell — 61% — on
every pull request, including ones that only touch TypeScript.

Only native sources, their manifests, or the build inputs they embed can
change that output, so the compile now runs when those change and is
skipped when they do not. The decision fails open: an unreadable base, a
missing diff, or a path nobody thought of all resolve to compiling. A
gate that skips work it should have done is the failure this repo already
paid for once, when a typecheck that did not cover apps/desktop let a
broken release cut.

Splitting the renderer bundle out is what makes the skip safe. Tauri's
beforeBuildCommand bundles the renderer, so skipping the shell would have
skipped that too — and a bundling failure, a bad dynamic import or a
missing asset, is invisible to both typecheck and vitest. Measured, the
bundle is 26s against the shell's 643s, so it runs every time and only
the expensive half is conditional.

A TypeScript-only pull request now pays 26s where it paid 643s. A native
one pays the bundle twice, 4% more, which is the price of the coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first version of this gate was both dead and dangerous, and an
adversarial read of it found three things I had not.

Dead: actions/checkout defaults to fetch-depth 1, so neither end of the
PR range is in the object store. `git diff base head` exits 128, the
fail-open branch fires, and the shell is compiled every time. The
optimisation saved nothing while looking like it worked.

Dangerous: `Smoke packaged CLI` is the one step that consumes
target/release, and it was not gated. The moment the skip started working
it would have run against a binary that was never produced.

Incomplete: the path rule missed inputs that provably reach the build —
updater-signature-verifier, a [[bin]] Cargo compiles from outside
src-tauri; resources/, whose icons and entitlements generate_context!
embeds; scripts/, which beforeBuildCommand runs; .npmrc and
pnpm-workspace.yaml, which change module resolution. It also missed
git's own quoting: core.quotePath puts a quote before a non-ASCII path,
which defeated the anchor.

config/scripts/check-shell-build-gate.sh holds the cases, each with the
reason that path is a build input, so the next edit to the rule has to
answer for them. One known limit is recorded rather than fixed: a rename
whose destination lands outside every pattern.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Creating a worktree failed intermittently with "failed to read resource
write response: Resource temporarily unavailable (os error 35)".

That is the desktop client's own read deadline. `git worktree add` runs
synchronously inside the request and the runtime allows it two minutes
(gitWorktreeCommandLimit, manager.go:28), but the call passed no
timeoutMs and inherited the 1500ms default meant for status reads.
Checkout of this repository measures 0.84-1.25s, so the deadline was a
coin flip — which is why it failed sometimes rather than always.

The mismatch did more than report badly. When the client's deadline fires
it drops the socket, the runtime's request context is cancelled, and
exec.CommandContext kills git part-way through: measured, git died at
301ms with 1673 of 7246 files written. What is left is a worktree git has
registered and marked locked with reason "initializing", a branch that
exists, and no record of any of it in the runtime. Neither `git worktree
prune` nor `git worktree remove --force` clears it, because the app's own
removal only ever passes a single --force. The next attempt then fails
with "a branch named 'x' already exists", which is a stranger error than
the first one.

Retrying is not the fix and the transport is right to refuse it: writes
may already have landed, so only GETs replay. Removal gets the same
budget, since timing out there abandons a half-removed worktree for the
same reason.

The sibling calls in this file already do this — clone passes ten
minutes, create-on-host sixty seconds. Worktree create and delete were
the ones that ran git without saying so.

Found by an adversarial review of my own earlier conclusion. I had
searched apps/, packages/ and runtime/ for the error string, decided it
was not in the repo, and guessed at a non-blocking-socket EAGAIN needing
a retry. The string is at native/rust-host/src/runtime_status_probe.rs:867
— a directory I never searched — and the cause is a deadline, not a
missing retry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@TsekaLuk
TsekaLuk merged commit e23e1aa into main Aug 21, 2026
7 of 10 checks passed
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