Skip to content

[Bug]: Stale branch sync can clear new worktree path before provider start #3653

Description

@patroza

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

  1. Open a project in T3 Code.
  2. Start a new thread.
  3. Select New worktree.
  4. Pick a base branch and send the first prompt.
  5. Watch the lower branch/workspace toolbar while the first turn starts.
  6. It can briefly switch back to Current checkout / main-checkout branch.
  7. 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:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions