Skip to content

[Bug]: Scheduled tasks that launch a fresh thread always branch from main and fail every run in repos without it #16183

Description

@coygeek

Summary

A scheduled task created through the Orchestrator MCP schedule_task tool that launches a fresh thread per run (bindToCurrentThread: false, or a projectId other than the caller's, which defaults to unbound) always provisions that thread's worktree from a hard-coded baseRef: "main" with startFromOrigin: true. In a Git project whose default branch is not main (for example master or trunk) and which has no main branch locally or on origin, every run creates a new top-level thread whose only run fails at workspace preparation with git worktree add failed. The scheduler records each of these runs as lastRunStatus: "succeeded", so the task stays enabled and keeps producing failed threads without any signal in the task list or to the scheduling agent.

Steps to reproduce

Known case, not executed end-to-end against a running T3 server:

  1. Open a Git project whose only branch is master, with an origin remote that also has only master.
  2. From an agent thread in that project, call schedule_task with {"title":"Hourly check","prompt":"Report repository status.","schedule":{"type":"interval","everyMs":3600000},"bindToCurrentThread":false}.
  3. Call run_scheduled_task_now with the returned task id, or wait for the next interval.
  4. Open the thread created by that run, then call list_scheduled_tasks.

The git commands the launch path issues for step 3 were executed in a disposable repository with the same shape (bare origin containing only master, plus a clone):

$ git remote get-url origin                                   # exit 0
$ git fetch --quiet origin '+refs/heads/main:refs/remotes/origin/main'
fatal: couldn't find remote ref refs/heads/main               # exit 128; code then falls back to `git fetch origin` (exit 0)
$ git show-ref --verify --quiet refs/remotes/origin/main      # exit 1, so the local baseRef "main" is used
$ git -c checkout.workers=0 worktree add -b t3code/abc123 ../wt/t3code-abc123 main
fatal: invalid reference: main                                # exit 128
$ git symbolic-ref refs/remotes/origin/HEAD
refs/remotes/origin/master
$ git -c checkout.workers=0 worktree add -b t3code/ok ../wt/t3code-ok master
Preparing worktree (new branch 't3code/ok')                   # control, exit 0

A repository with only trunk and no remote fails the same worktree add ... main with fatal: invalid reference: main.

Expected behavior

The schedule_task tool description promises that an unbound task launches a working fresh thread per run: "use false only when the user wants a fresh top-level thread per run. Elsewhere each run launches a fresh thread." The tool exposes no base-branch parameter and says nothing about a main branch being required, so a fresh-thread run in a valid Git project with a different default branch should start its agent in a usable workspace.

Actual behavior

By static trace of the supported entry point: schedule_task stores { type: "worktree", baseRef: "main", startFromOrigin: true } for every unbound task, and update_scheduled_task re-applies the same value whenever bindToCurrentThread is passed. Each run calls ThreadLaunchService.launch, which returns after dispatching thread creation and forking workspace preparation into the background. Preparation finds no origin/main, keeps the literal local main, and runs git worktree add -b t3code/<id> <path> main, which fails as shown above. The thread receives a "Workspace preparation failed" item ending in git worktree add failed (stderr is dropped; see #4380). Because launch already returned, the scheduler's Effect.exit is a success, and it records lastRunStatus: "succeeded" with lastRunError: null, re-arms nextRunAt, and leaves the task enabled. The Settings badge and the list_scheduled_tasks summary both show success. Runtime reproduction through a running T3 server was not executed.

Evidence

Runtime reproduction through T3 was not run. The static proof covers the reachable path from the MCP tool: the default at OrchestratorMcpService.ts:1322-1324 makes any other-project projectId unbound; ScheduledTaskService.upsert stores the strategy without validation; GitVcsDriverCore.fetchRemote falls back to a plain fetch on a missing remote ref; remoteBranchExists is false; and createWorktree passes the unresolved main to git worktree add. The git commands were executed directly and produced the failures shown. The Settings UI scheduling path also defaults its base to main (apps/web/src/components/settings/ScheduledTasksSettings.tsx:595) but lets the user pick another branch, so it is not a clean control. GitVcsDriverCore.resolveDefaultBranchName already exists in the server.

Restoration check

Using the reproduction above (a master-only project with a master-only origin), an unbound scheduled run currently yields a thread whose run failed at workspace preparation with git worktree add failed, while the task reports lastRunStatus: "succeeded". After the fix, the same run starts its agent in a prepared worktree (in this fixture, from master). Control: a project that does have main must still prepare its fresh-thread runs from main, and bound tasks (bindToCurrentThread: true) must keep posting into the calling thread without a worktree.

Additional context

The scheduler records whether a run was dispatched, not whether its thread was prepared, for both bound and unbound runs; no contract text defines lastRunStatus, so this report treats the misleading "succeeded" status only as a consequence that hides the failure, not as a separate defect. Workarounds available today: use bindToCurrentThread: true (runs share the calling thread's context and checkout and queue behind its activity), or edit the task in Settings, Scheduled tasks, and pick a real base branch (a later update_scheduled_task that passes bindToCurrentThread resets the base to main). A non-Git project with an unbound schedule also fails every run at the same preparation step, with "Failed to resolve the VCS driver for this Git command."; that may share the restoration but was not traced further. Related: #4380 (git stderr discarded), discussion #6866 (configurable default base branch for new worktree threads).

Area

apps/server

Impact

Major degradation or frequent failure. Every fresh-thread run of an MCP-created schedule fails in Git projects without a main branch, and the scheduler records each run as succeeded, so failed threads accumulate unnoticed.

Version or commit

main at 6f9cea00ae967f38fa3cdc9c07f92031806f4264.

Environment

Source inspection of pingdotgg/t3code main at 6f9cea0. Git behavior checked with git 2.56.0 on macOS with global and system config disabled. The T3 server itself was not run.

Workaround

Editing the task's base branch in Settings, or binding runs to the calling thread, avoids the failure today, at the cost of manual per-task setup or giving up per-run fresh threads.

Before submitting

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

Activity

  1. Ayushlm10 commented on Oct 7, 2026

    @Ayushlm10

    The same hard-coded strategy also breaks the "No project" (scratch) project. That project isn't a git repository, so creating a worktree fails no matter which base branch is used.

    Repro on T3 Code Nightly 0.0.46-nightly.20261007.2761, macOS 26.4.1, with a Claude Code agent:

    1. From an agent thread, call schedule_task with projectId set to the No project (scratch) project, bindToCurrentThread: false, and an interval schedule.
    2. Call run_scheduled_task_now.

    The run's thread fails before the agent starts:

    Workspace preparation failed during provision worktree: Git command failed in GitWorkflowService.remoteExists (~/.t3/scratch): Failed to resolve the VCS driver for this Git command.
    

    run_scheduled_task_now and list_scheduled_tasks still report lastRunStatus: "succeeded", as this issue describes.

    The cause is scheduledTaskWorkspaceStrategy in apps/server/src/mcp/OrchestratorMcpService.ts. It returns { type: "worktree", baseRef: "main", startFromOrigin: true } for every unbound task, whatever the project is.

    Setting the task's Workspace to Project checkout in Settings → Scheduled tasks works around it. The next run got its own scratch folder and completed.

    Resolving the workspace from the project would fix both cases. For a project that isn't a git repository, that means running in the project folder.

  2. jlipworth commented on Oct 9, 2026

    @jlipworth

    Another case: a repo whose only remote isn't named origin. In mine the remote is gitlab (gitlab/HEAD -> gitlab/<default>), there's no main anywhere, and an unbound weekly task failed with the same git worktree add failed.

    With #16215 as it stands, I think this repo works, but only by luck:

    • resolveDefaultBranchName(workspaceRoot, "origin") finds nothing, so the PR falls back to the branch currently checked out in the root. That happens to be the default branch here. If the root were sitting on a feature branch, every scheduled run would branch from that feature branch.
    • ThreadLaunchService.ts:336 turns startFromOrigin off when there's no origin remote, so these runs never fetch. They start from the local branch even though the task asked to start from the remote.

    Possible fix: when origin doesn't exist, use the branch's upstream remote, or the only remote if there's just one, both for finding the default branch and for the fetch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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