Skip to content

Worktree submodule init is killed by the 30 s default git timeout and leaves broken submodules silently #26390

Closed as duplicate

Description

@netherguy4

Version: T3 Code 0.1.3-nightly.20261024.3630 (Linux)

What happens

For a repository with 17 submodules, new worktrees (threads, delegate_task
with isolation: "worktree") usually start with broken submodules. Most submodules have an index but
no working-tree files, so git status shows every file as a staged deletion. Others are not initialized
at all. The agent is not told about this and builds or tests against empty trees.

On this machine, 86 of the worktree creations logged
WARN worktree submodule checkout failed; submodule paths are empty with
operation: 'GitVcsDriver.createWorktree.updateSubmodules' and detail: 'Git command timed out.'. Most
of the existing worktrees under .t3-worktrees/<project>/ are in this state.

Cause (from the bundled source)

  • createWorktree runs git submodule update --init --recursive through runGit(...) without a
    timeoutMs, so the generic DEFAULT_TIMEOUT_MS = 3e4 (30 s) applies. git worktree add has its own
    WORKTREE_ADD_TIMEOUT_MS = 3e5.
  • Timeouts of 30 s or less also go through the gitProcesses semaphore, withPermits(1) out of 8.
  • When the timeout fires, git is interrupted mid-checkout. The submodule index has already been written,
    but files have not been checked out. The failure is only logged: onSubmodulesFinished({ok:false})
    updates progress, the thread starts anyway, and nothing is cleaned up or retried.

Expected

  • Submodule init uses a timeout that fits large repositories (like WORKTREE_ADD_TIMEOUT_MS), or none
    (it reports progress).
  • On failure, either retry, or leave a consistent state (git submodule deinit / remove the half-written
    module) and tell the agent and user clearly, for example in the thread's first message or as a blocking
    setup error.
  • Optional: git submodule update --init --jobs N to parallelize.

Reproduce

  1. Create a repository with about 15+ submodules of moderate size (each a separate GitHub clone).
  2. Start a thread in a new worktree.
  3. After start: git -C <worktree>/<sub> status --short | head shows D/D for every file.
  4. The server log shows the WARN above.

Corrected workaround

Keep t3.json with "worktreeSubmodules": "none" for new bases, and configure the effective project
settings
(not only project.scripts) as below. For older worktree bases without t3.json, also set the
project override worktreeSubmodules: "none". No T3 files/settings were modified by this investigation.

The repair script must use git submodule update --init --force -- <path> for interrupted modules,
which checks out the superproject index's recorded gitlink, rather than git reset --hard HEAD.
Only staged deletions qualify for repair, including when HEAD differs from the gitlink (+ in
git submodule status). Preserve and warn for other dirt or a clean moved HEAD. After repairing or
initializing, require both the recorded HEAD and clean status, otherwise exit nonzero.

Why the delegated worktree setup did not run

All physical file:line references below use this bundle:
~/.t3/runtime/versions/0.1.3-nightly.20261024.3630/t3.

  • Delegation does run setup for a new isolation: "worktree" request: lines 973752–973770 resolve
    the parent's local HEAD and build a worktree strategy. Lines 973789–973793 call
    threadLaunch.prepareRun when the returned child has a run.created event with
    workspacePreparation. prepareRun at 937106–937114 only proceeds for a preparing run with that
    preparation payload and a newly reserved command. Retrying an already accepted delegation does not
    construct a new strategy (973752); shared isolation does not request a fresh worktree/setup.
  • scheduleRunPreparation (937127–937150) uses the child thread's project ID and the saved strategy.
    prepareInBackground resolves that ID (936683), creates the worktree from project.workspaceRoot
    (936772), and calls setupScripts.runForThread for a non-null worktree path at 936858–936869.
    It supplies the project ID and the project object, including its scripts. A selected script with
    async: false is awaited and a nonzero exit blocks agent release (936881–936909).
  • The decisive bug is script resolution, at 734825–734832: first use
    settings.projectSettingsOverrides[project.id]?.defaultProjectScripts; if absent and
    projectSettingsFolded is true, immediately return settings.defaultProjectScripts, ignoring
    project.scripts. Only an unfolded configuration reaches the legacy/project script fallback.
    setupProjectScript takes the first runOnWorktreeCreate script (734842–734844).
    runForThread returns {status: "no-script"} without opening a terminal if none is selected
    (915397–915400); no informational boot-service log is emitted for this branch.
  • Read-only evidence: project 40cc4f7f-d27b-4e1a-8319-9797d306ad9d has one saved
    Prepare worktree script, updated 2026-10-28T09:39:30.800Z. Settings have
    projectSettingsFolded: true, no defaultProjectScripts override for that ID, and no global
    defaultProjectScripts key. The missing global key decodes to [] (608345).
    The one-time fold explicitly returns early once folded (696242); updating project.scripts
    writes the project metadata (731441–731453), not that effective override.
    Executing the exact extracted resolver with these selected fields returns []; adding the
    proposed override returns the saved synchronous setup script. Evidence:
    worktree-setup-fix-settings-resolution.json.
  • Thus the skipped script is explained by the folded-settings precedence, not by delegated worktrees
    being exempt. The recorded t3-bb836156 child has the correct project ID and worktree preparation;
    requested 2026-10-28T09:39:45.413Z, started 09:40:20.271Z, completed. This is consistent with
    the no-script branch; we did not launch another child to retest.

Workspace path lookup

runForThread prefers supplied project, then project ID, and only then
getByWorkspaceRoot(projectCwd) (915381–915392). Delegated preparation supplies the project and ID,
so the differing paths do not cause this failure. The fallback lookup normalizes using
path.resolve(expandHomePath(...)), not realpath (730572–730593), and compares workspaceRoot
(731408–731415); it does not match repositoryIdentity.rootPath.
The registered workspace is ~/Projects/<project>;
repository identity's root is /mnt/projects/<project>.
For a caller relying only on projectCwd, use the registered workspace spelling or pass the project ID.
T3CODE_PROJECT_ROOT also uses the registered workspaceRoot, while T3CODE_WORKTREE_PATH uses the
actual new worktree (915403–915409; 734833–734837).

Why built-in init still timed out after the workaround was added to main

The child's recorded base, which has
no t3.json (git show <base>:t3.json fails). Delegation starts from the parent's local commit,
not origin/main. The Git driver reads t3.json from the newly created worktree, unless a project/
environment submodule setting overrides it (722605–722614). Therefore the new main's file could not
suppress recursive init on that old base. The log records the timeout at
boot-service.log:2330446 on 11:40:18.408 local time. Both independent settings matter.

Proposed effective project setting (do not apply without coordinator authorization)

Set/merge this value at
projectSettingsOverrides["40cc4f7f-d27b-4e1a-8319-9797d306ad9d"], preserving any other keys:

{
  "worktreeSubmodules": "none",
  "defaultProjectScripts": [
    {
      "id": "prepare-worktree",
      "name": "Prepare worktree",
      "command": "git -C \"$T3CODE_WORKTREE_PATH\" show origin/main:scripts/worktree_setup.py | python3 -I - \"$T3CODE_WORKTREE_PATH\"",
      "icon": "configure",
      "runOnWorktreeCreate": true,
      "async": false
    }
  ]
}

The command itself is unchanged. After the fix PR merges, its origin/main ref must contain the
corrected script. Updating only legacy project.scripts is insufficient with folded settings.
No fresh delegated-worktree setup was run because this task forbids creating children or changing T3.

Confirmed workaround (2026-10-28)

Verified on a fresh delegated worktree, where all 17 submodules were at their recorded commits before the agent
started:

  • t3.json at the repository root with "worktreeSubmodules": "none".
  • A project script in the project settings UI, stored as projectSettingsOverrides[<project>].defaultProjectScripts,
    with runOnWorktreeCreate: true and async: false. It runs an idempotent script: for each submodule, run
    git submodule update --init --force when the submodule is uninitialized or has only staged deletions; leave
    moved or dirty submodules alone. It takes about 16 s on a broken worktree and 0.5 s when the worktree is ready.

Activity

  1. bramley-jetcharge commented on Oct 30, 2026

    @bramley-jetcharge

    Confirming this on macOS, with only 2 submodules, plus a worse variant of the broken state.

    Environment: T3 Code (Beta) desktop app, local server 0.1.3, macOS 15 (Darwin 24.6.0) arm64, git 2.50.1 (Apple Git-155). The repo has 2 top-level submodules, cloned from GitHub over SSH. worktreeSubmodules is at its default.

    Timeline (new-worktree thread, local time):

    Time Event
    11:52:27 git worktree add finishes and git submodule update --init starts
    11:52:39 Submodule A is cloned. Its reflog has only clone: from github.com:…, with HEAD on the remote default branch main. The checkout step never ran.
    11:52:58 The worktree's modules/ dir is modified (~30 s), most likely the killed clone of submodule B being cleaned up. B stays uninitialized (- in git submodule status).
    11:52:59 The thread's first prompt goes to the agent

    It's intermittent: my other 14 worktrees of the same repo initialized fine. So a single slow GitHub clone is enough, and repos don't need many submodules to hit it.

    Worse variant: an unintended gitlink bump, not just empty files. The half-initialized submodule was on the remote's main (820beb5), while the superproject records an older commit (5900c64). So from the first git status the superproject showed M <submodule> as a gitlink change, not only the staged deletions inside the submodule. A commit that staged everything recorded the bump 5900c64 → 820beb5 in an unrelated change, and it was nearly pushed in a PR. The agent caught it, traced it to the submodule's clone-only reflog, amended it out, and ran git submodule update.

    When the recorded commit matches the remote default branch, this case looks like the empty-tree one already described here. When the default branch is ahead, it shows up as a real diff that's easy to commit by accident.

    On the fix: #26409 raises the timeout to 5 minutes, which should cover this case. But failure handling is unchanged, so any failure that remains (network, auth, longer than 5 min) can still leave a moved gitlink behind. On failure, I'd suggest running git submodule update --init again or deinitializing the half-cloned modules, so the superproject doesn't show a changed gitlink. Telling the agent the setup failed would also help. Reusing the source checkout's submodule objects (#9853) would make the timeout much less likely in the first place.

    Diagnosed with T3 Code triage (t3 triage) using Claude Opus 5.5 in Claude Code.

  2. theashleygirl commented on Nov 12, 2026

    @theashleygirl

    Note

    Comment written by Claude Opus 5.5 in T3 Code, on behalf of Jaiden

    The 30 s timeout described here looks fixed by #28482 (merged 2026-11-01, 8195530 on main). It used a link rather than a closing keyword, so the issue stayed open.

    How I got there: on main, createWorktree's git submodule update --init [--recursive] call now passes timeoutMs: null (apps/server/src/vcs/GitVcsDriverCore.ts, around line 4574). That matches the cancellable git worktree add path, so slow submodule checkouts are no longer killed at DEFAULT_TIMEOUT_MS. The PR adds tests that fail with the old deadline.

    Not changed: a submodule failure is still best-effort and logged, now worded as "potentially incomplete". The agent is still not told. If that part matters on its own, it may be worth tracking separately.

  3. juliusmarminge commented on Nov 12, 2026

    @juliusmarminge
    Member

    This is the same problem as #28482, so I'm closing it in favour of that one. If it is different, say how and it will be reopened.

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