Worktree handoff during a streaming turn: ClaudeAdapterV2.interruptTurn fails (ProviderAdapterInterruptError) and ProjectionStore.getThread reports thread not found#25146

opened4d agobyCcodetifypr

What happened

An agent running inside a thread called t3_worktree_handoff with a continuationPrompt to move its own thread (2e840833-8286-4529-9b7b-9d89bc0f833b) into a new git worktree (wave-w163-hypermedica-devops). Turn ordinal 11 was still streaming when the handoff started.

From the user's side the handoff succeeded: the continuation started in the new worktree and work continued, and the server stayed up. The only sign of trouble was two failures in the server trace, both under the same trace id face34502590e50a63555c6d48352f89:

  1. ClaudeAdapterV2.interruptTurn → ProviderAdapterInterruptError:

    Failed to interrupt claudeAgent provider turn provider-turn:provider:claudeAgent:native-turn:turn%3A...thread%253A2e840833-8286-4529-9b7b-9d89bc0f833b%253Aordinal%253A11:attempt:1

  2. Shortly after, in the same trace, ProjectionStore.getThread → ProjectionStoreThreadNotFoundError:

    No orchestration projection exists for thread ...

Both lines were read directly from server.trace.ndjson on the affected server (not inferred).

Expected: the handoff interrupts the running turn cleanly, and the projection lookup finds the thread.

Repro steps

  1. Run T3 Code server 0.1.2 (t3 serve, systemd user service) with a claudeAgent thread.
  2. While a turn is actively streaming, have the agent in that thread call t3_worktree_handoff with a continuationPrompt pointing at a new git worktree.
  3. Inspect server.trace.ndjson: a single trace contains ProviderAdapterInterruptError from ClaudeAdapterV2.interruptTurn, followed by ProjectionStoreThreadNotFoundError from ProjectionStore.getThread.

Observed once, on 2026-10-25 ~23:12 UTC. The trace files contain exactly one ProviderAdapterInterruptError, so reliability of repro is unknown.

Diagnosis (hypotheses — not yet mapped to source)

  • Interrupt runs after teardown / after the turn ended. The handoff may replace or close the provider session before interruptTurn runs, or the turn may already have finished. The adapter then finds no live turn and raises ProviderAdapterInterruptError instead of treating "turn already gone" as a no-op.
  • Projection lookup races the move. The getThread in the same trace may run before the projector has written the handed-off thread (or after the old row was replaced).
  • Possible double percent-encoding of the turn id (observation). The id contains %253A (%3A encoded twice). If the id is single-encoded on store but double-encoded on lookup (or vice versa), the interrupt could miss a turn that is still live.

The continuation still ran, so this looks like a cleanup/ordering bug in the handoff path rather than data loss. Worst case: a provider turn left running on the old session after the handoff.

Environment

  • Server version: T3 Code 0.1.2 (t3 serve as systemd user service t3code.service)
  • Triage CLI: 0.1.2
  • Surface: local server on a Linux VM (not website/mobile)
  • OS: Linux x64
  • Provider: claudeAgent (ClaudeAdapterV2)
  • Handoff caller: the agent inside the thread

Evidence

  • Trace id: face34502590e50a63555c6d48352f89
  • Thread 2e840833-8286-4529-9b7b-9d89bc0f833b, turn ordinal 11, attempt 1
  • Target worktree: wave-w163-hypermedica-devops
  • The two error lines quoted above, read from server.trace.ndjson

For maintainers

Useful to pull: the full span tree for this trace id, logs/provider/events.log around 23:12 UTC, and the thread's projection rows before/after the handoff.


Filed via t3 triage (investigation and evidence confirmed from the server trace by Claude).