Repository navigation
Description
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)
createWorktreerunsgit submodule update --init --recursivethroughrunGit(...)without a
timeoutMs, so the genericDEFAULT_TIMEOUT_MS = 3e4(30 s) applies.git worktree addhas its own
WORKTREE_ADD_TIMEOUT_MS = 3e5.- Timeouts of 30 s or less also go through the
gitProcessessemaphore,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 Nto parallelize.
Reproduce
- Create a repository with about 15+ submodules of moderate size (each a separate GitHub clone).
- Start a thread in a new worktree.
- After start:
git -C <worktree>/<sub> status --short | headshowsD/Dfor every file. - 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.prepareRunwhen the returned child has arun.createdevent with
workspacePreparation.prepareRunat 937106–937114 only proceeds for apreparingrun 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.
prepareInBackgroundresolves that ID (936683), creates the worktree fromproject.workspaceRoot
(936772), and callssetupScripts.runForThreadfor 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: falseis 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
projectSettingsFoldedis true, immediately returnsettings.defaultProjectScripts, ignoring
project.scripts. Only an unfolded configuration reaches the legacy/project script fallback.
setupProjectScripttakes the firstrunOnWorktreeCreatescript (734842–734844).
runForThreadreturns{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-9797d306ad9dhas one saved
Prepare worktreescript, updated2026-10-28T09:39:30.800Z. Settings have
projectSettingsFolded: true, nodefaultProjectScriptsoverride for that ID, and no global
defaultProjectScriptskey. The missing global key decodes to[](608345).
The one-time fold explicitly returns early once folded (696242); updatingproject.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 recordedt3-bb836156child has the correct project ID and worktree preparation;
requested2026-10-28T09:39:45.413Z, started09: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.jsonat the repository root with"worktreeSubmodules": "none".- A project script in the project settings UI, stored as
projectSettingsOverrides[<project>].defaultProjectScripts,
withrunOnWorktreeCreate: trueandasync: false. It runs an idempotent script: for each submodule, run
git submodule update --init --forcewhen 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
bramley-jetcharge commentedon Oct 30, 2026
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.
worktreeSubmodulesis at its default.Timeline (new-worktree thread, local time):
git worktree addfinishes andgit submodule update --initstartsclone: from github.com:…, with HEAD on the remote default branchmain. The checkout step never ran.modules/dir is modified (~30 s), most likely the killed clone of submodule B being cleaned up. B stays uninitialized (-ingit submodule status).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 firstgit statusthe superproject showedM <submodule>as a gitlink change, not only the staged deletions inside the submodule. A commit that staged everything recorded the bump5900c64 → 820beb5in 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 rangit 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 --initagain 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.theashleygirl commentedon Nov 12, 2026
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'sgit submodule update --init [--recursive]call now passestimeoutMs: null(apps/server/src/vcs/GitVcsDriverCore.ts, around line 4574). That matches the cancellablegit worktree addpath, so slow submodule checkouts are no longer killed atDEFAULT_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.
juliusmarminge commentedon Nov 12, 2026
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.