Skip to content

[Bug]: Bulk thread delete aborts on "git worktree remove failed" when the worktree is already gone, and the error hides git's stderr #4513

Description

@Gablas

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server (git driver) + apps/web (bulk delete loop)

Summary

Deleting several threads at once fails with:

Failed to delete threads
Git command failed in GitVcsDriver.removeWorktree (/Users/me/repos/my-repo): git worktree remove failed

Thread deleted, but worktree removal failed
Could not remove t3code-9f6ab5f1. Git command failed in GitVcsDriver.removeWorktree (...): git worktree remove failed

Three separate defects stack up here:

  1. git worktree remove is invoked for worktrees git no longer tracks, so it exits 128 and the whole delete is reported as failed even though the thread was deleted and the worktree is already gone.
  2. The error text drops git's stderr, so the real reason (fatal: '<path>' is not a working tree) never reaches the user or the logs.
  3. The bulk delete loop returns on the first failure, so one stale worktree aborts the rest of the selection.

Steps to reproduce

The reliable repro is a worktree that is recorded on a thread but no longer registered with git. Two ways to get there:

A. Two threads sharing one worktree (what happened here)

  1. Create a worktree thread on a repo.
  2. Use "New thread on <branch>" / pick the existing worktree so a second thread records the same worktree_path.
  3. Select both threads in the sidebar and delete them in one action.
  4. The first delete removes the worktree, the second one fails.

B. Worktree removed outside the app

  1. Create a worktree thread.
  2. git worktree remove it (or git worktree prune after deleting the directory) from a terminal.
  3. Delete the thread in t3code.

Expected behavior

  • A worktree that is already absent is a no-op, not an error. Deleting the thread succeeds silently.
  • If several threads reference the same worktree, only the last one removes it.
  • A worktree failure in a bulk delete does not abort the remaining threads, and the error surfaces git's actual stderr.

Actual behavior

  • vcs.removeWorktree fails with the generic git worktree remove failed, with no stderr anywhere (UI, server-child.log, or server.trace.ndjson).
  • The batch stops at the first failing thread, so repeated manual retries are needed to clear a selection.
  • The thread is deleted anyway, so the state is inconsistent with the error the user sees.

Evidence

All 7 vcs.removeWorktree RPCs in my trace logs failed, each in 20-33 ms:

07-25 14:23:04.099 Failure 32.3
07-25 14:24:41.255 Failure 21.2
07-25 14:24:50.044 Failure 27.6
07-25 14:24:56.879 Failure 33.2
07-25 14:26:23.481 Failure 28.1
07-25 14:26:28.138 Failure 66.5
07-25 14:26:32.120 Failure 22.8

20-33 ms is far too fast for git to have walked a real worktree. On the same repo, a successful git worktree remove --force of a nearly empty worktree takes ~400 ms, while the already-removed case matches the observed timing exactly:

$ time git worktree remove --force /Users/me/.t3/worktrees/my-repo/zz-probe2
fatal: '/Users/me/.t3/worktrees/my-repo/zz-probe2' is not a working tree
git worktree remove --force  0.01s user 0.02s system 91% cpu 0.031 total   # exit 128

state.sqlite shows the shared-worktree case directly. Three threads recorded the same worktree_path, two of them deleted 1.1 s apart in the same bulk action:

select thread_id, branch, worktree_path, created_at, deleted_at
from projection_threads
where worktree_path is not null
group by worktree_path having count(*) > 1;
9ec6b178...  t3code/notetaker-local-fixes    .../t3code-17e025a6  2026-04-15T08:01:17Z  2026-07-25T12:24:40.617Z
bf57272b...  t3code/notetaker-local-fixes    .../t3code-17e025a6  2026-04-15T09:16:40Z  (alive)
26de6b89...  t3code/notetaker-permission-ux  .../t3code-17e025a6  2026-04-15T12:33:22Z  2026-07-25T12:24:39.531Z

Those deletion timestamps line up with the 14:24 local failures above.

The drift is widespread, not a one-off. Comparing projection_threads.worktree_path against git worktree list --porcelain for this repo: 4 paths registered, 22 alive threads pointing at worktree paths that no longer exist on disk. Every one of those will hit this bug when deleted.

Where the code goes wrong

From app.asar of the nightly (apps/server/dist/bin.mjs):

const removeWorktree = Effect.fn("removeWorktree")(function* (input) {
  const args = ["worktree", "remove"];
  if (input.force) args.push("--force");
  args.push(input.path);
  yield* executeGit("GitVcsDriver.removeWorktree", input.cwd, args, {
    timeoutMs: 15e3,
    fallbackErrorDetail: "git worktree remove failed"
  });
});

executeGit builds GitCommandError from fallbackErrorDetail plus stderrLength, never stderr itself, so the message stops at "git worktree remove failed". Other call sites go through executeGitWithStableDiagnostics, and those errors do carry the git text (for example GitVcsDriver.statusDetailsRemote.branch: git rev-parse --abbrev-ref HEAD (...) - fatal: ambiguous argument 'HEAD' ... shows up fully in server-child.log). removeWorktree should do the same.

Client side, the bulk delete handler bails out of the loop on the first failure:

for (const { threadRef } of entries) {
  const r = await deleteThreadWithWorktree(threadRef, { deletedThreadKeys });
  if (r._tag === "Failure") {
    // toast "Failed to delete threads"
    return;   // <-- remaining selected threads are never deleted
  }
}

Suggested fix

  1. Make worktree removal idempotent: resolve the path against git worktree list --porcelain first (or tolerate exit 128 with is not a working tree / a missing directory) and treat "already gone" as success.
  2. Skip removal when another non-deleted thread still references the same worktree_path.
  3. In the bulk loop, collect failures and continue, then report once at the end.
  4. Route removeWorktree through the stable-diagnostics path so git's stderr reaches the toast and the logs.

Impact

Major degradation or frequent failure. Bulk delete is effectively unusable on a workspace with any worktree drift, and the error gives no way to diagnose it.

Version or commit

0.0.29-nightly.20260725.899 (nightly channel)

Environment

macOS (Darwin 25.5.0), Apple git 2.50.1, large pnpm monorepo with ~70 registered worktrees.

Workaround

git worktree prune in the repo does not help, because the thread rows keep pointing at paths git has already forgotten. Deleting threads one at a time still errors, but at least each one does get deleted; the batch just has to be retried until the selection is empty.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions