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

open5
main
fix/issue-31263
opened5d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 5h 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 · 11 skipped
All checks passed
358c74a

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 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.

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: 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.

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 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.ts
The 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.ts
Branch-creation failures report git branch failed. Worktree-add failures retain git worktree add failed.

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

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

mermaid
1sequenceDiagram
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 nameStatusExplanation
Title checkPassedThe title clearly and concisely describes the main change: refusing explicit worktree branches that already exist.
Description checkPassedThe 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 checkPassedDirectly linked issue #31263 requires 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 checkPassedAll 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 #31263 requires 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

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.ts
  • apps/server/src/mcp/toolkits/project/handlers.ts
  • apps/server/src/orchestration-v2/ThreadLaunchService.test.ts
  • apps/server/src/orchestration-v2/ThreadLaunchService.ts
  • apps/server/src/vcs/GitVcsDriverCore.test.ts
  • 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; 9 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.