fix(host-service): remove worktree with a native rm before git unregisters (#6887)
Fixes #6887
What & why
Deleting a worktree lets git own the recursive delete — git worktree remove --force --force calls git's remove_dir_recursively, a single-threaded walk that cannot finish a node_modules-heavy tree (161k+ entries, pnpm hardlinks) inside the task's 120 s budget. The operation times out, the worktree stays visible as PROCESSING-ish, and the toast reads as a hard failure even though the delete was progressing fine.
This PR stops delegating the delete to git. gitWorktreeRemoveTask now:
- Removes the directory with the native Node recursive
rm(force, so a missing or concurrently-cleaned path is a no-op) — measurably several times faster than git's serial walk on the same tree. - Then calls
git worktree remove --force --forcepurely to unregister the now-empty worktree — near-instant. - Then checks
git worktree list --porcelain, exactly as today.
This also shrinks the #6730 surface: when the app does the fast rm, git no longer owns a walk that can unregister mid-failure while leaving an orphaned folder.
How I tested it
- New regression tests (
packages/host-service/src/workers/tasks/git-remove.test.ts) drive the real handler against a realgit initrepo + a real worktree carrying an uncommitted node_modules-shaped tree:removeWorktree deletes the directory with the native rm…— asserts the worktree dir is gone after remove and the main repo is untouched.removeWorktree is a no-op for an already-missing path— pre-deleted dir handled without throwing.
bun test src/workers/tasks/git-remove.test.ts src/workers/host-worker-pool.test.ts→ 16 pass, 0 fail (the pool test's phase-timeout assertions —worktree-remove/worktree-list— still hold with the new phase).bun x tsc --noEmit -p tsconfig.json→ clean.biome checkon the two changed files → clean.
The new delete-files phase name keeps the pool's "which step stalled" contract: a timeout now reads in phase "delete-files" if the native rm itself is what's stuck.
Checklist
- PR title follows conventional commits (
type(scope): subject) -
bun run lintandbun run typecheckpass - "Allow edits from maintainers" is checked on fork PRs
Summary by cubic
Deletes worktree directories with a native recursive rm before letting git unregister them, so node_modules-heavy worktrees no longer time out and get stuck processing.
Bug Fixes
- The worker only native-deletes a path git lists as a worktree of the repo, so a stale or corrupt
worktreePathrow never deletes a folder git didn't own. - Native deletion only runs when the caller confirms the path is inside the project's managed worktrees root and a forced removal was requested, keeping git's dirty-worktree refusal when
forceis off. - A failing native rm records
removeErrorand still lets git unregister; if git also fails, both errors are joined inremoveErrorinstead of the task failing. - The task now returns
wasRegistered; the caller warns instead of running the recursive fallback for paths git never registered, and keeps it for registered worktrees whose folder survived (#6730). - Reports a new
delete-filesphase so pool timeouts identify whether native deletion stalls, with the delete finishing before git's unregister begins. - Adds real-repository regression tests covering heavy uncommitted trees, missing paths, force-off, unregistered paths, and the git-only fallback.
Summary by CodeRabbit
- Bug Fixes
- Worktree cleanup now removes associated directories and deeply nested uncommitted files before unregistering the worktree.
- If direct directory removal fails, Git removal is still attempted and the error is reported.
- Direct removal is restricted to managed worktree locations; paths outside them are left untouched.
- Cleanup handles already-missing paths and reports removal status based on the final filesystem state, including unreadable paths and dangling links.