fix(server): t3_thread_launch refuses an explicit branch that already exists#31279

open7
main
fix/issue-31263
opened10d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 15h ago

Problem

t3_thread_launch (and t3 thread start --worktree <base> --branch <name>) accepted a new worktree whose explicit branch already existed, returned a threadId and runId, and the thread then failed in the background with git worktree add failed (branch_already_exists). The caller could not correct the name in the same turn, and a failed thread was left behind. Reproduced on main 935818e422 with an isolated server for a branch checked out in another worktree (feature/taken), an idle branch (feature/idle), and the project's own checkout (main).

Change

  • ThreadLaunchService.checkWorktreeBase, the check t3_thread_launch runs before it accepts a launch, now refuses an explicit worktree branch that is already in the project's local branch list (listLocalBranchNames, exact name match) with a new ThreadLaunchBranchExistsError naming the branch. This includes explicit t3/…/t3code/… names; generated names (no branch passed) are not checked. As before, a failure to read the project or git lets the launch through.
  • The MCP handler picks its hint by error: the branch case reads Branch "<name>" already exists in project "<title>". No thread was created. Pass a branch name that does not exist yet, or omit branch to get a generated name. Base-ref refusals keep their wording.
  • GitVcsDriverCore.createWorktree reports its branch-creation step as git branch failed instead of git worktree add failed, for launches that do not go through the pre-check.

Scope and approval

Fixes pingdotgg/t3code#31263

Verification

Focused tests (vp test run on the three suites: 294 passed; vp run check:changed passes):

  • ThreadLaunchService.test.ts › refuses a new worktree whose explicit branch already exists: real temp repo; refuses feature/idle, feature/taken (in another worktree), main, an existing t3code/<uuid>, and a taken branch with a :/text base; accepts feature/fresh and feature/tagged (only a tag named refs/heads/feature/tagged exists). Fails on main: the existing branches are accepted.
  • handlers.test.ts: the branch refusal carries the new hint, base-ref refusals keep theirs, and no launch is started.
  • GitVcsDriverCore.test.ts: two cases pinned the branch step's old git worktree add failed text and now expect git branch failed.

Runtime evidence (server-only fix, no rendered surface, so no captures). Same driver on each side, each against a fresh database created by that side's own dev server, synthetic repo only:

--branchBefore (main 935818e422)After (358c74a7ec)
feature/taken (checked out in another worktree)exit 0, thread created, then fails git worktree add failed (branch_already_exists)exit 1, Branch "feature/taken" already exists…, no thread
feature/idleexit 0, thread created, then the same failureexit 1, no thread
main (project checkout)exit 0, thread created, then the same failureexit 1, no thread
feature/fresh (control)completedcompleted
Before transcript (main 935818e422)
1# head 935818e422
2Added project 780ed0c1-b9ae-455c-a5e4-d2f4997f592a (repo) at repo.
3### attempt branch=feature/taken
4{"threadId":"mcp:0aca4327-fe91-4752-9052-7f38758d3170","projectId":"780ed0c1-b9ae-455c-a5e4-d2f4997f592a","runId":"run:thread:mcp%3A0aca4327-fe91-4752-9052-7f38758d3170:ordinal:1"}
5exit=0
6### attempt branch=feature/idle
7{"threadId":"mcp:ddf1cc09-9767-45bb-8b4f-52dec3a61ab8","projectId":"780ed0c1-b9ae-455c-a5e4-d2f4997f592a","runId":"run:thread:mcp%3Addf1cc09-9767-45bb-8b4f-52dec3a61ab8:ordinal:1"}
8exit=0
9### attempt branch=main
10{"threadId":"mcp:bc47fed1-2685-4e6d-9056-84bc3a0680bb","projectId":"780ed0c1-b9ae-455c-a5e4-d2f4997f592a","runId":"run:thread:mcp%3Abc47fed1-2685-4e6d-9056-84bc3a0680bb:ordinal:1"}
11exit=0
12### attempt branch=feature/fresh
13{"threadId":"mcp:8d81be93-b1ef-435a-861d-e1ed5a3b56df","projectId":"780ed0c1-b9ae-455c-a5e4-d2f4997f592a","runId":"run:thread:mcp%3A8d81be93-b1ef-435a-861d-e1ed5a3b56df:ordinal:1"}
14exit=0
15### threads
16mcp:8d81be93-b1ef-435a-861d-e1ed5a3b56df completed
17mcp:bc47fed1-2685-4e6d-9056-84bc3a0680bb failed
18mcp:ddf1cc09-9767-45bb-8b4f-52dec3a61ab8 failed
19mcp:0aca4327-fe91-4752-9052-7f38758d3170 failed
20### git worktree list
21repo 9f861e9 [main]
22<t3-home>/worktrees/repo/feature-fresh 9f861e9 [feature/fresh]
23wt 9f861e9 [feature/taken]
24+ feature/fresh
25 feature/idle
26+ feature/taken
27* main
28### recorded thread failures
29mcp:0aca4327-fe91-4752-9052-7f38758d3170|Workspace preparation failed during provision worktree: Git command failed in GitVcsDriver.createWorktree.createBranch (repo): git worktree add failed (branch_already_exists)
30mcp:ddf1cc09-9767-45bb-8b4f-52dec3a61ab8|Workspace preparation failed during provision worktree: Git command failed in GitVcsDriver.createWorktree.createBranch (repo): git worktree add failed (branch_already_exists)
31mcp:bc47fed1-2685-4e6d-9056-84bc3a0680bb|Workspace preparation failed during provision worktree: Git command failed in GitVcsDriver.createWorktree.createBranch (repo): git worktree add failed (branch_already_exists)
After transcript (358c74a7ec)
1# head 358c74a7ec
2Added project 28906850-595e-47d9-a41b-91b48c554eaf (repo) at repo.
3### attempt branch=feature/taken
4Branch "feature/taken" already exists in project "repo". No thread was created. Pass a branch name that does not exist yet, or omit branch to get a generated name.
5exit=1
6### attempt branch=feature/idle
7Branch "feature/idle" already exists in project "repo". No thread was created. Pass a branch name that does not exist yet, or omit branch to get a generated name.
8exit=1
9### attempt branch=main
10Branch "main" already exists in project "repo". No thread was created. Pass a branch name that does not exist yet, or omit branch to get a generated name.
11exit=1
12### attempt branch=feature/fresh
13{"threadId":"mcp:fa628281-8d3c-438a-92db-c4d7886d07f4","projectId":"28906850-595e-47d9-a41b-91b48c554eaf","runId":"run:thread:mcp%3Afa628281-8d3c-438a-92db-c4d7886d07f4:ordinal:1"}
14exit=0
15### threads
16mcp:fa628281-8d3c-438a-92db-c4d7886d07f4 completed
17### git worktree list
18repo f7e5271 [main]
19<t3-home>/worktrees/repo/feature-fresh f7e5271 [feature/fresh]
20wt f7e5271 [feature/taken]
21+ feature/fresh
22 feature/idle
23+ feature/taken
24* main
25### recorded thread failures
Driver (run with PROVIDER and MODEL set, against a dev server started with T3CODE_DEV_SEED=0)
sh
1#!/usr/bin/env bash
2# Driver for pingdotgg/t3code#31263. Synthetic repo only.
3# Usage: repro-31263.sh <checkout> <t3-home-of-running-dev-server> <scratch-dir>
4# Start the server first: T3CODE_DEV_SEED=0 vp run dev:server --home-dir <t3-home>
5set -u
6CHECKOUT=$1; HOME_DIR=$2; R=$3
7T3="node $CHECKOUT/apps/server/src/bin.ts"
8mkdir -p "$R" && cd "$R" && git init -q -b main repo && cd repo
9git -c user.email=a@b -c user.name=t commit -q --allow-empty -m init
10git worktree add -q -b feature/taken ../wt # taken: checked out in another worktree
11git branch feature/idle # idle: exists, not checked out
12echo "# head $(git -C "$CHECKOUT" rev-parse --short HEAD)"
13$T3 project add . --base-dir "$HOME_DIR"
14for br in feature/taken feature/idle main feature/fresh; do
15 echo "### attempt branch=$br"
16 $T3 thread start --base-dir "$HOME_DIR" --json --worktree main --branch "$br" \
17 --provider "${PROVIDER:?set PROVIDER}" --model "${MODEL:?set MODEL}" . "reply ok"
18 echo "exit=$?"
19done
20sleep 8
21echo "### threads"
22$T3 thread list --base-dir "$HOME_DIR" --json | python3 -c 'import json,sys; [print(t["threadId"], t["status"]) for t in json.load(sys.stdin)]'
23echo "### git worktree list"; git worktree list; git branch
24echo "### recorded thread failures"
25sqlite3 -readonly "$HOME_DIR/userdata/statev2.sqlite" \
26 "select json_extract(payload_json,'\$.threadId'), json_extract(payload_json,'\$.failure.message') from orchestration_v2_projection_turn_items where json_extract(payload_json,'\$.type')='error'"

Not checked: macOS case-insensitive branch names (e.g. Feature/Idle when feature/idle exists) still pass the exact-name check and fail in provisioning, as on main.

AI-assisted; verified with the tests and tools listed above.

🤖 Generated with Claude Code

Checks

22 successful · 12 skipped
All checks passed
9d2c911

Added 5 commits

  • Merge main into fix/issue-31263358c74a
  • fix(server): refuse an existing explicit branch even when it looks temporary86ebcd6
  • fix(server): t3_thread_launch refuses an explicit branch that already exists92e15ef
  • fix(server): match an existing launch branch by exact namea744fc3
  • test(server): reproduce worktree launches onto an existing branch (#31263)fd9a40e
Synced from GitHub

Approvability

Verdict: Approved at 9d2c911

Macroscope's review found this PR approvable — New MCP worktree launches reject explicitly requested branch names that already exist before creating a thread, with actionable guidance. The check only reads local branches; generated-name behavior and accepted-launch replays remain unchanged.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire5.0 KiB5.0 KiB0 B (0.0%)6.8 KiB✅
CodexThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiB✅
CodexLive turn WebSocket wire1.2 KiB1.2 KiB0 B (0.0%)2.0 KiB✅
CodexLive turn WebSocket decoded21.5 KiB21.5 KiB0 B (0.0%)24.4 KiB✅
CodexLive turn messages110 (0.0%)8✅
ClaudeTotal thread wire5.0 KiB5.0 KiB0 B (0.0%)6.8 KiB✅
ClaudeThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiB✅
ClaudeLive turn WebSocket wire1.2 KiB1.2 KiB0 B (0.0%)2.0 KiB✅
ClaudeLive turn WebSocket decoded21.9 KiB21.9 KiB0 B (0.0%)24.4 KiB✅
ClaudeLive turn messages110 (0.0%)8✅

Baseline: 1a2a4a5 · PR result: 9d2c911 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Synced from GitHub

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

Worktree launch validation now rejects requested branch names that already exist, including when the base ref uses a search prefix. Tool responses distinguish branch conflicts from invalid base refs. Branch-creation failures now report git branch failed.

Changes: Worktree Launch Validation

Layer / File(s)Summary
Pre-launch branch and base-ref checks
apps/server/src/orchestration-v2/ThreadLaunchService.ts, apps/server/src/orchestration-v2/ThreadLaunchService.test.ts
The launch service checks requested branch names against local branches. It checks base refs when applicable. Tests cover existing branches, fresh names, tags, and search-style base refs.
Caller-facing launch errors
apps/server/src/mcp/toolkits/project/handlers.ts, apps/server/src/mcp/toolkits/project/handlers.test.ts
The launch tool returns separate guidance for existing branches and invalid base refs. Tests verify both refusals and that neither creates a thread.
Branch-creation failure diagnostics
apps/server/src/vcs/GitVcsDriverCore.ts, apps/server/src/vcs/GitVcsDriverCore.test.ts
Branch-creation failures report git branch failed. Tests update the expected detail for bare or unborn repositories and a missing ref.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Possibly related issues

  • Issue 29297 — The change rejects exact existing branch names, but does not address Git ref-namespace conflicts.
  • Issue 29515 — The change rejects an existing explicit branch before launch, but does not remove branches left by failed checkouts.
  • Issue 26577 — The change rejects an existing local branch before launch, but does not address the reported delay generally.

Possibly related PRs

  • pingdotgg/t3code#29328 — Both PRs add pre-launch rejection for conflicting explicit worktree branch names. This PR checks exact-name matches; #29328 checks ref-namespace prefix conflicts.

Suggested reviewers: tris203

Merge Risk: External image Minimal · up to 9d2c9

Ready to merge after normal checks. No change-introduced issue requiring a fix was established.

Pre-merge checks: Passed 4

Check nameStatusExplanation
Title checkPassedThe title clearly describes the main change: rejecting an explicit branch that already exists during worktree launch.
Description checkPassedThe description covers the required Problem, Change, Scope and approval, and Verification sections. It explains the issue and fix, links the issue, reports focused tests and runtime results, and notes…
Linked Issues checkPassedOpen issue #31263 requires an explicit existing branch to be rejected before launch, with the branch named in the error and no failed thread created. The reviewed implementation checks explicit branch…
Out of Scope Changes checkPassedThe whole-PR changes support issue #31263. The current regression test narrows its assertions but retains coverage for the requested branch conflict behavior. The handler message and `git branch faile…
Full details: Description check

Explanation

The description covers the required Problem, Change, Scope and approval, and Verification sections. It explains the issue and fix, links the issue, reports focused tests and runtime results, and notes an untested case.

Full details: Linked Issues check

Explanation

Open issue #31263 requires an explicit existing branch to be rejected before launch, with the branch named in the error and no failed thread created. The reviewed implementation checks explicit branch names against local branches and returns ThreadLaunchBranchExistsError; the handler provides branch-specific guidance. The current regression test still covers existing idle and checked-out branches, a generated-name branch, a fresh branch, and a tag-only name. The incremental test edits do not change the branch-check implementation. The case-insensitive limitation does not contradict the issue's exact-name requirement.

Full details: Out of Scope Changes check

Explanation

The whole-PR changes support issue #31263. The current regression test narrows its assertions but retains coverage for the requested branch conflict behavior. The handler message and git branch failed error detail address the issue's incorrect or incomplete error reporting. No unrelated whole-PR change is established by the available summary and focused diff.

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
External image Recent review info

External image Run configuration

External image Commits

Reviewing files that changed from the base of the PR and between 358c74a7ecd7c428b15c3604a7ed07e0a3fffd95 and 9d2c911b27d85bba9d5002e17deead519837d517.

External image Files selected for processing: 4

  • apps/server/src/mcp/toolkits/project/handlers.ts
  • apps/server/src/orchestration-v2/ThreadLaunchService.test.ts
  • apps/server/src/vcs/GitVcsDriverCore.test.ts
  • apps/server/src/vcs/GitVcsDriverCore.ts
External image Files skipped from review as they are similar to previous changes: 1

  • apps/server/src/vcs/GitVcsDriverCore.ts

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

Synced from GitHub

Wouldn't it make more sense to just make this work? a new worktree on an existing branch should work, as long as the branch is not checked out in another worktree.

Added 2 commits

  • Merge remote-tracking branch 'origin/main' into fix/issue-3126334b5f57
  • test(server): trim the existing-branch launch check to its regressions9d2c911

Evidence

On both commits, start a fresh isolated development server, seed an initial Git commit on main, create an occupied feature/taken worktree and an idle feature/idle branch, then register the repository through the CLI. Run t3 thread start --json --worktree main --branch <branch> --provider <configured-provider> --model <configured-model> . "reply ok" for feature/taken, feature/idle, main, and unused feature/unused. List threads before and after each launch and inspect accepted threads.

Environment on both sides: Linux, real development server and CLI, fresh database, equivalent synthetic Git seed, development replay provider, 1440×900 dark terminal. Videos replay timestamped command output with JSON fields projected for readability; full redacted transcripts and original terminal recordings are linked below.

BeforeAfter
1a2a4a5e969d2c911b27
Before recordingAfter recording
All three existing branches return exit 0 and thread/run IDs, then create failed threads.All three return exit 1 immediately with the branch name and actionable guidance; thread lists remain empty.
Unused branch provisions a worktree and reaches running status.Unused branch provisions a worktree and reaches running status.
Full transcript, terminal recording, persisted provisioning failuresFull transcript, terminal recording, empty provisioning-failure export

Scripts: shared seed and CLI driver, recording renderer, read-only evidence collector. Uploaded using ghdrop.

AI-assisted; verified with the tests and tools listed above.

Not shown:

  • Client rendering was not exercised; this proof directly captures the real server behavior through the CLI and persisted provisioning events.
  • The unused-branch control verified successful launch, worktree provisioning, and running status; natural-language turn completion was not verified.
  • Automatic branch naming and project/Git lookup-failure fallback were not exercised; the reproduction covers explicit existing branches and an explicit unused branch.
Synced from GitHub

+1, I hit this too, and a stable explicit branch name makes it worse.

When an earlier worktree for a branch was removed but its local branch was left behind, t3_thread_launch with workspaceStrategy: {type: "worktree", baseRef: <branch>, branch: <branch>, startFromOrigin: true} is accepted. The thread then fails during provisioning with branch_already_exists, and Retry replays the same name, so it fails every time until the local branch is deleted by hand.

Refusing at launch, as this PR does, lets the caller handle it instead of being left with a failed thread.

Synced from GitHub

This fixes the failed-thread half of #31263. One launch it now refuses is one that wants the existing branch: baseRef and branch both name a local branch that is not checked out anywhere, as when opening a thread on an open PR's branch (repro).

createWorktree already checks out an existing branch when newRefName is omitted, so checkWorktreeBase could let that case through and provisioning could pass refName: branch with no newRefName. A branch checked out in another worktree, or one pointing at a different commit than baseRef, would still be refused as here. Fine as a follow-up if you would rather keep this PR to the refusal.