fix(server): t3_thread_launch refuses an explicit branch that already exists#31279
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 checkt3_thread_launchruns 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 newThreadLaunchBranchExistsErrornaming the branch. This includes explicitt3/…/t3code/…names; generated names (nobranchpassed) 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.createWorktreereports its branch-creation step asgit branch failedinstead ofgit 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; refusesfeature/idle,feature/taken(in another worktree),main, an existingt3code/<uuid>, and a taken branch with a:/textbase; acceptsfeature/freshandfeature/tagged(only a tag namedrefs/heads/feature/taggedexists). 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 oldgit worktree add failedtext and now expectgit 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:
--branch | Before (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/idle | exit 0, thread created, then the same failure | exit 1, no thread |
main (project checkout) | exit 0, thread created, then the same failure | exit 1, no thread |
feature/fresh (control) | completed | completed |
Before transcript (main 935818e422)
| 1 | # head 935818e422 |
| 2 | Added 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"} |
| 5 | exit=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"} |
| 8 | exit=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"} |
| 11 | exit=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"} |
| 14 | exit=0 |
| 15 | ### threads |
| 16 | mcp:8d81be93-b1ef-435a-861d-e1ed5a3b56df completed |
| 17 | mcp:bc47fed1-2685-4e6d-9056-84bc3a0680bb failed |
| 18 | mcp:ddf1cc09-9767-45bb-8b4f-52dec3a61ab8 failed |
| 19 | mcp:0aca4327-fe91-4752-9052-7f38758d3170 failed |
| 20 | ### git worktree list |
| 21 | repo 9f861e9 [main] |
| 22 | <t3-home>/worktrees/repo/feature-fresh 9f861e9 [feature/fresh] |
| 23 | wt 9f861e9 [feature/taken] |
| 24 | + feature/fresh |
| 25 | feature/idle |
| 26 | + feature/taken |
| 27 | * main |
| 28 | ### recorded thread failures |
| 29 | mcp: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) |
| 30 | mcp: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) |
| 31 | mcp: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 |
| 2 | Added project 28906850-595e-47d9-a41b-91b48c554eaf (repo) at repo. |
| 3 | ### attempt branch=feature/taken |
| 4 | Branch "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. |
| 5 | exit=1 |
| 6 | ### attempt branch=feature/idle |
| 7 | Branch "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. |
| 8 | exit=1 |
| 9 | ### attempt branch=main |
| 10 | Branch "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. |
| 11 | exit=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"} |
| 14 | exit=0 |
| 15 | ### threads |
| 16 | mcp:fa628281-8d3c-438a-92db-c4d7886d07f4 completed |
| 17 | ### git worktree list |
| 18 | repo f7e5271 [main] |
| 19 | <t3-home>/worktrees/repo/feature-fresh f7e5271 [feature/fresh] |
| 20 | wt 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)
| 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> |
| 5 | set -u |
| 6 | CHECKOUT=$1; HOME_DIR=$2; R=$3 |
| 7 | T3="node $CHECKOUT/apps/server/src/bin.ts" |
| 8 | mkdir -p "$R" && cd "$R" && git init -q -b main repo && cd repo |
| 9 | git -c user.email=a@b -c user.name=t commit -q --allow-empty -m init |
| 10 | git worktree add -q -b feature/taken ../wt # taken: checked out in another worktree |
| 11 | git branch feature/idle # idle: exists, not checked out |
| 12 | echo "# head $(git -C "$CHECKOUT" rev-parse --short HEAD)" |
| 13 | $T3 project add . --base-dir "$HOME_DIR" |
| 14 | for 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=$?" |
| 19 | done |
| 20 | sleep 8 |
| 21 | echo "### 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)]' |
| 23 | echo "### git worktree list"; git worktree list; git branch |
| 24 | echo "### recorded thread failures" |
| 25 | sqlite3 -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
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
Approvability
Verdict: Approved at 358c74a
Macroscope's review found this PR approvable — Explicit worktree branches that already exist now receive an actionable error before a thread is created, rather than failing during provisioning. The added check is read-only and leaves generated branch names and accepted retries unchanged.
You can add or adjust custom eligibility rules. Learn more.
Thread transfer impact
✅ Thread transfer remains within every enforced ceiling.
| Provider | Metric | Main baseline | This PR | Impact | PR ceiling | |
|---|---|---|---|---|---|---|
| Codex | Total thread wire | 5.0 KiB | 5.0 KiB | 0 B (0.0%) | 6.8 KiB | ✅ |
| Codex | Thread snapshot wire | 3.8 KiB | 3.8 KiB | 0 B (0.0%) | 4.9 KiB | ✅ |
| Codex | Live turn WebSocket wire | 1.2 KiB | 1.2 KiB | 0 B (0.0%) | 2.0 KiB | ✅ |
| Codex | Live turn WebSocket decoded | 21.5 KiB | 21.5 KiB | 0 B (0.0%) | 24.4 KiB | ✅ |
| Codex | Live turn messages | 1 | 1 | 0 (0.0%) | 8 | ✅ |
| Claude | Total thread wire | 5.0 KiB | 5.0 KiB | 0 B (0.0%) | 6.8 KiB | ✅ |
| Claude | Thread snapshot wire | 3.8 KiB | 3.8 KiB | 0 B (0.0%) | 4.9 KiB | ✅ |
| Claude | Live turn WebSocket wire | 1.2 KiB | 1.2 KiB | 0 B (0.0%) | 2.0 KiB | ✅ |
| Claude | Live turn WebSocket decoded | 21.9 KiB | 21.9 KiB | 0 B (0.0%) | 24.4 KiB | ✅ |
| Claude | Live turn messages | 1 | 1 | 0 (0.0%) | 8 | ✅ |
Baseline: 5a90658 · PR result: 358c74a · 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.
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.tsThe launch service checks requested branch names against local branches. It still checks base refs when applicable, and its 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.tsThe launch tool returns separate guidance for existing branches and invalid base refs. Tests verify both refusals and confirm that neither creates a thread. Branch-creation failure diagnostics
apps/server/src/vcs/GitVcsDriverCore.ts,apps/server/src/vcs/GitVcsDriverCore.test.tsBranch-creation failures report git branch failed. Worktree-add failures retaingit worktree add failed.Estimated code review effort: 3 (Moderate) | ~20 minutes
Change: Bug fix · Severity of issue fixed: Medium
Sequence Diagram(s)
mermaid1 sequenceDiagram 2 participant Caller 3 participant t3_thread_launch 4 participant ThreadLaunchService 5 participant GitVcsDriver 6 Caller->>t3_thread_launch: Request worktree launch 7 t3_thread_launch->>ThreadLaunchService: checkWorktreeBase 8 ThreadLaunchService->>GitVcsDriver: listLocalBranchNames 9 GitVcsDriver-->>ThreadLaunchService: Local branch names 10 ThreadLaunchService-->>t3_thread_launch: ThreadLaunchBranchExistsError 11 t3_thread_launch-->>Caller: invalid_request Possibly related issues
pingdotgg/t3code#29297— The change rejects exact existing branch names, but does not cover Git ref-namespace conflicts.pingdotgg/t3code#29515— The pre-launch check prevents the existing-branch failure path described by this issue.pingdotgg/t3code#26577— The pre-launch check rejects an already checked-out branch before the delayed failure described by this issue.
Suggested reviewers:
tris203
Merge Risk: External image Minimal · up to 358c7
-
Explicit existing branch names are refused before launch. No merge-blocking risk was established.
Pre-merge checks: Passed 4
-
Check name Status Explanation Title check Passed The title clearly and concisely describes the main change: refusing explicit worktree branches that already exist. Description check Passed The description includes all required sections, explains the problem and fix, links the issue for scope, and provides detailed focused-test and runtime verification results, including an identified ma… Linked Issues check Passed Directly linked issue #31263requires that t3_thread_launch refuse an explicit worktree branch that already exists, before it returns a threadId. It also requires that the error name the branch, and t…Out of Scope Changes check Passed All changed areas connect to issue #31263:
- ThreadLaunchService.ts: the new branch pre-check, the new ThreadLaunchBranchExistsError class, and the widened checkWorktreeBase error type. The base-ref b…Full details: Description check
-
Explanation
The description includes all required sections, explains the problem and fix, links the issue for scope, and provides detailed focused-test and runtime verification results, including an identified macOS limitation.
Full details: Linked Issues check
-
Explanation
Directly linked issue
#31263requires that t3_thread_launch refuse an explicit worktree branch that already exists, before it returns a threadId. It also requires that the error name the branch, and that the message name the correct failing step.Implementation at the reviewed head:
- ThreadLaunchService.checkWorktreeBase (ThreadLaunchService.ts) now calls git.listLocalBranchNames and refuses an explicit branch with an exact name match. It returns the new ThreadLaunchBranchExistsError, whose message names the branch and the project. This covers idle branches, branches checked out in another worktree, and the project checkout branch. Generated names (no branch passed) are not checked.
- Because the refusal runs before creation, no thread is created. This removes the failed-thread outcome described in the issue.
- A failure to read the project or git lets the launch through, as before.
- GitVcsDriverCore.createWorktree now reports the branch step as git branch failed, which fixes the misleading message for launches that bypass the pre-check.
- The MCP handler (handlers.ts) returns branch-specific guidance. Base-ref refusals keep their wording.
Automated tests: ThreadLaunchService.test.ts covers existing idle, checked-out, main, and taken-with-ref-base branches (refused), and fresh and tag-only names (accepted). handlers.test.ts and GitVcsDriverCore.test.ts cover the new hint and the updated step text.
I did not run the tests. The result rests on the source diff of ThreadLaunchService.ts and the PR description. I did not read the diffs of handlers.ts or GitVcsDriverCore.ts.
The PR does not handle macOS case-insensitive variants such as Feature/Idle. The issue reproduces only exact names, so this gap does not fail the issue's stated requirements. The PR states this limitation.
Full details: Out of Scope Changes check
-
Explanation
All changed areas connect to issue
#31263:- ThreadLaunchService.ts: the new branch pre-check, the new ThreadLaunchBranchExistsError class, and the widened checkWorktreeBase error type. The base-ref block was restructured so that the branch check can run for :/ search refs. I compared the old and new base-ref logic. For a non-resolving base ref, the old code returned an error only when HEAD resolved. The new code returns an error only when HEAD resolves. The base-ref semantics are preserved.
- handlers.ts and handlers.test.ts: the branch-specific hint and its test. This matches the issue's request for an actionable error.
- GitVcsDriverCore.ts and GitVcsDriverCore.test.ts: the step text change from git worktree add failed to git branch failed. The issue names this wrong-step message as a defect.
- ThreadLaunchService.test.ts: the new integration test for branch refusal, plus a test fixture that provides local branch names.
No unrelated changes were found in the reviewed diff. The handler and driver changes rely on the PR summary, not a full diff read.
-
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
-
- Source mode: Auto
- Repository: pingdotgg/t3code
- Branch: fix/issue-31263
- File: .coderabbit.config.ts
- Commit: 358c74a
- Layers: Repository TS config
- Review profile: CHILL
- Plan: Team
- Run ID:
8c30f4f9-6140-482f-8d3b-6061643f978f
External image Commits
-
Reviewing files that changed from the base of the PR and between 5a906583fed3fa41dc806f2a19bd9d095537ed3e and 358c74a7ecd7c428b15c3604a7ed07e0a3fffd95.
External image Files selected for processing: 6
-
apps/server/src/mcp/toolkits/project/handlers.test.tsapps/server/src/mcp/toolkits/project/handlers.tsapps/server/src/orchestration-v2/ThreadLaunchService.test.tsapps/server/src/orchestration-v2/ThreadLaunchService.tsapps/server/src/vcs/GitVcsDriverCore.test.tsapps/server/src/vcs/GitVcsDriverCore.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
External image Recent review info
-
External image Run configuration
-
- Source mode: Auto
- Repository: pingdotgg/t3code
- Branch: fix/issue-31263
- File: .coderabbit.config.ts
- Commit: 358c74a
- Layers: Repository TS config
- Review profile: CHILL
- Plan: Team
- Run ID:
8c30f4f9-6140-482f-8d3b-6061643f978f
External image Commits
-
Reviewing files that changed from the base of the PR and between 5a906583fed3fa41dc806f2a19bd9d095537ed3e and 358c74a7ecd7c428b15c3604a7ed07e0a3fffd95.
External image Files selected for processing: 6
-
apps/server/src/mcp/toolkits/project/handlers.test.tsapps/server/src/mcp/toolkits/project/handlers.tsapps/server/src/orchestration-v2/ThreadLaunchService.test.tsapps/server/src/orchestration-v2/ThreadLaunchService.tsapps/server/src/vcs/GitVcsDriverCore.test.tsapps/server/src/vcs/GitVcsDriverCore.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
-
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.