Summary
A new thread configured for a fresh worktree can briefly have its worktreePath cleared by stale branch/status synchronization from the main checkout while the first turn is bootstrapping.
If provider startup happens during that window, T3 resolves the provider cwd from the thread read model and falls back to the original project checkout. The provider then legitimately starts in the main checkout, even though the thread later shows the worktree again.
This is a T3 worktree metadata race, not necessarily an OpenCode bug. In later testing OpenCode reported the correct directory once T3 preserved the intended worktree cwd.
Impact
High severity: the user selected an isolated worktree, but the agent can start in and edit the main checkout. The UI may later look correct again, hiding the bad startup cwd.
Environment
- T3 Code around
v0.0.28 / current upstream main after v0.0.28
- Linux
- Providers observed around this flow: OpenCode and Codex
- New thread env mode: new worktree / worktree-isolated first turn
Reproduction
- Open a project in T3 Code.
- Start a new thread.
- Select
New worktree.
- Pick a base branch and send the first prompt.
- Watch the lower branch/workspace toolbar while the first turn starts.
- It can briefly switch back to
Current checkout / main-checkout branch.
- Inspect provider runtime cwd or ask the provider for its cwd.
Expected
Once a thread is assigned a worktree for the first turn, stale UI/source-control metadata must not clear that worktree path.
Provider cwd should resolve to the assigned worktree:
thread.worktree_path == provider runtime cwd == /home/user/.t3/worktrees/<repo>/<worktree>
Actual
A stale metadata update can temporarily clear worktreePath:
thread.worktree_path = null
Provider startup during that window resolves cwd as the project root:
provider runtime cwd = /home/user/pj/project
The projection can later restore the worktree path, so the UI / VS Code opener may look correct after the provider has already started in the wrong checkout.
Evidence From Event Order
In one failing run, the thread event stream showed this sequence:
thread.created
branch = main
worktreePath = null
thread.meta-updated # bootstrap worktree creation
branch = t3code/6ae2566d
worktreePath = /home/user/.t3/worktrees/scanner/scanner-6ae2566d
thread.turn-start-requested
thread.meta-updated # stale branch/status sync from main checkout
branch = codex/dropshipping-selected-carrier
worktreePath = null
thread.meta-updated # another stale sync
branch = codex/dropshipping-selected-carrier
worktreePath = null
thread.meta-updated # later restored
branch = codex/dropshipping-selected-carrier
worktreePath = /home/user/.t3/worktrees/scanner/scanner-6ae2566d
thread.session-set # provider had already started
T3 provider runtime state for that same thread then contained:
{"cwd":"/home/user/pj/project"}
and OpenCode emitted matching metadata:
session.updated.info.directory = /home/user/pj/project
message.info.path.cwd = /home/user/pj/project
So in this failure mode OpenCode was not independently ignoring the worktree; T3 had already supplied/persisted the wrong cwd because the thread read model was temporarily back on the main checkout.
Diagnostic Query
This finds provider sessions whose runtime cwd does not match the projected thread worktree:
SELECT
t.thread_id,
t.title,
t.branch,
t.worktree_path,
json_extract(r.runtime_payload_json, '$.cwd') AS runtime_cwd,
r.provider_name,
r.status,
r.last_seen_at
FROM projection_threads t
JOIN provider_session_runtime r ON r.thread_id = t.thread_id
WHERE t.worktree_path IS NOT NULL
AND json_extract(r.runtime_payload_json, '$.cwd') IS NOT t.worktree_path
ORDER BY r.last_seen_at DESC;
To inspect the event race for one thread:
SELECT
sequence,
event_type,
occurred_at,
command_id,
payload_json
FROM orchestration_events
WHERE stream_id = '<thread-id>'
ORDER BY sequence ASC;
Suspected Cause
The source-control / branch sync path observes git status from the current checkout while a new-worktree first turn is still preparing. It dispatches thread.meta.update with the observed branch and worktreePath: null.
That update races with the bootstrap path that just assigned the real worktree path. The provider command reactor later resolves cwd from the projected thread via resolveThreadWorkspaceCwd(...); if it sees the stale null, it falls back to the project root.
Proposed Fixes
- Do not allow stale branch/status sync to clear an already-assigned
worktreePath on a thread.
- Suspend live branch sync while a new-worktree first turn is preparing.
- Treat “leave this worktree and return to current checkout” as an explicit action, not a side effect of branch/status sync.
- Before provider start, fail closed if a worktree bootstrap was requested but the resolved provider cwd is the project root.
- Keep provider-level cwd validation as a last line of defense: if the provider returns/reports a cwd different from T3's requested cwd, stop and surface an error.
Local Mitigations Tested
A local fork fix that makes assigned worktree paths sticky in the server decider prevents the stale worktreePath: null update from poisoning provider startup.
Additional local safeguards validate OpenCode session directories on create/resume and refuse to persist provider sessions whose returned cwd differs from T3's requested cwd.
Branch under test:
https://github.com/patroza/t3code/tree/patroza/import-external-sessions
Related
The earlier report that framed this as OpenCode ignoring cwd is superseded by this root-cause analysis:
#3656
Related but distinct worktree issues:
Summary
A new thread configured for a fresh worktree can briefly have its
worktreePathcleared by stale branch/status synchronization from the main checkout while the first turn is bootstrapping.If provider startup happens during that window, T3 resolves the provider cwd from the thread read model and falls back to the original project checkout. The provider then legitimately starts in the main checkout, even though the thread later shows the worktree again.
This is a T3 worktree metadata race, not necessarily an OpenCode bug. In later testing OpenCode reported the correct directory once T3 preserved the intended worktree cwd.
Impact
High severity: the user selected an isolated worktree, but the agent can start in and edit the main checkout. The UI may later look correct again, hiding the bad startup cwd.
Environment
v0.0.28/ current upstream main afterv0.0.28Reproduction
New worktree.Current checkout/ main-checkout branch.Expected
Once a thread is assigned a worktree for the first turn, stale UI/source-control metadata must not clear that worktree path.
Provider cwd should resolve to the assigned worktree:
Actual
A stale metadata update can temporarily clear
worktreePath:Provider startup during that window resolves cwd as the project root:
The projection can later restore the worktree path, so the UI / VS Code opener may look correct after the provider has already started in the wrong checkout.
Evidence From Event Order
In one failing run, the thread event stream showed this sequence:
T3 provider runtime state for that same thread then contained:
{"cwd":"/home/user/pj/project"}and OpenCode emitted matching metadata:
So in this failure mode OpenCode was not independently ignoring the worktree; T3 had already supplied/persisted the wrong cwd because the thread read model was temporarily back on the main checkout.
Diagnostic Query
This finds provider sessions whose runtime cwd does not match the projected thread worktree:
To inspect the event race for one thread:
Suspected Cause
The source-control / branch sync path observes git status from the current checkout while a new-worktree first turn is still preparing. It dispatches
thread.meta.updatewith the observed branch andworktreePath: null.That update races with the bootstrap path that just assigned the real worktree path. The provider command reactor later resolves cwd from the projected thread via
resolveThreadWorkspaceCwd(...); if it sees the stalenull, it falls back to the project root.Proposed Fixes
worktreePathon a thread.Local Mitigations Tested
A local fork fix that makes assigned worktree paths sticky in the server decider prevents the stale
worktreePath: nullupdate from poisoning provider startup.Additional local safeguards validate OpenCode session directories on create/resume and refuse to persist provider sessions whose returned cwd differs from T3's requested cwd.
Branch under test:
https://github.com/patroza/t3code/tree/patroza/import-external-sessions
Related
The earlier report that framed this as OpenCode ignoring cwd is superseded by this root-cause analysis:
#3656
Related but distinct worktree issues: