fix(host-service): remove worktree with a native rm before git unregisters (#6887)

AN
7d ago
+388-13

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:

  1. 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.
  2. Then calls git worktree remove --force --force purely to unregister the now-empty worktree — near-instant.
  3. 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 real git init repo + 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 check on 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 lint and bun run typecheck pass
  • "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 worktreePath row 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 force is off.
  • A failing native rm records removeError and still lets git unregister; if git also fails, both errors are joined in removeError instead 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-files phase 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.