Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
- Open a thread with the Claude provider (
claudeAgent, modelclaude-opus-5-5, runtime modefull-access). - Send a first message. It gets one turn, as expected.
- Send a follow-up message that starts a long turn (many tool calls, e.g. MCP design-tool edits).
- Wait 20–50 seconds while that turn is still running.
I don't have a minimal deterministic repro yet. In the thread below it happened on 6 of 7 user messages (every message after the first).
Expected behavior
One user message → one thread.turn-start-requested → one provider turn.
Actual behavior
About 18–52 s after each user message, the server starts a second provider turn with a new turn id and sends the same user message text into the same Claude session again. Nothing in the UI shows it was sent twice. The first turn is often still running, so two agents run the same prompt at the same time.
Evidence from one thread (91719899-32ae-46d9-aad0-962d9e216e9e):
orchestration_eventshas 7thread.turn-start-requestedevents, butprojection_turnshas 13 turns. The 6 extra turns havepending_message_id = NULL.- The Claude session transcript (
~/.claude/projects/.../<session>.jsonl) has each user prompt written twice, e.g. identical text at14:45:29.478Zand14:46:06.429Z, and at15:05:45.217Zand15:06:03.925Z. Each write has its ownqueue-operation enqueue/dequeuepair. - The provider event log shows a fresh
turn.started+claude/command_lifecycle(queued→started) with a newcommand_uuidfor the duplicate, followed bysession.configured, while the original command has not yet reachedcompleted.
| User message (requested_at) | Original turn | Duplicate turn started | Overlap |
|---|---|---|---|
| 14:26:19 | efeb9433… (done 14:27:09) | 14:27:11 (e23bf5b9…) | no, back-to-back |
| 14:45:29 | b2965c7d… (done 14:46:33) | 14:46:06 (bb43102b…) | yes |
| 14:51:59 | ad7469d9… (done 14:52:42) | 14:52:23 (3edebbd1…) | yes |
| 15:02:29 | 6150906b… (done 15:02:49) | 15:02:54 (5007c960…) | no |
| 15:05:45 | 51f6c9d6… (done 15:07:08) | 15:06:03 (767e66ac…) | yes, ~65 s |
| 15:31:22 | 55fdedfc… (done 15:31:45) | 15:32:18 (0376de9f…) | no |
Consequences:
- Duplicate, often contradictory assistant answers appear interleaved in the thread.
- When the turns overlap, both agents act on the same external state. In my case both edited the same design file through an MCP server and overwrote each other's work. Each agent saw the other as an unknown "other session".
- Tokens and cost are doubled.
It happened again in a second, separate thread the same day: a follow-up message spawned a twin run that rewrote the same files the first run was editing.
Impact
Major degradation or frequent failure
Version or commit
T3 Code (Nightly) 0.0.43-nightly.20260921.2058
Environment
macOS (Darwin 25.4.0), desktop app, service-managed server. Provider: Claude Agent SDK (claudeAgent), model claude-opus-5-5, fast mode off, runtime mode full-access.
Logs or stack traces
# orchestration_events vs projection_turns for the thread
thread.turn-start-requested | 7
projection_turns | 13 (6 with pending_message_id NULL)
# provider log: duplicate turn for the 14:45:29 message
[14:45:29.433Z] CANON turn.started turnId=b2965c7d-...
[14:45:29.445Z] NTIVE claude/command_lifecycle command_uuid=b2965c7d-... state=queued
[14:45:29.449Z] NTIVE claude/command_lifecycle command_uuid=b2965c7d-... state=started
[14:46:06.387Z] CANON turn.started turnId=bb43102b-... <-- no matching turn-start-requested
[14:46:06.392Z] NTIVE claude/command_lifecycle command_uuid=bb43102b-... state=queued
[14:46:06.396Z] NTIVE claude/command_lifecycle command_uuid=bb43102b-... state=started
[14:46:06.444Z] CANON session.configured turnId=bb43102b-...
[14:46:33.056Z] NTIVE claude/command_lifecycle command_uuid=b2965c7d-... state=completedFull provider event logs for the thread (~24 MB) are available if useful.
Workaround
None found. Waiting for the thread to show a single finished response before sending the next message doesn't prevent it, because the duplicate is created server-side 20–50 s after a single send.
Activity
juliusmarminge commentedon Sep 23, 2026
Triage
Confirmed as a server-side Claude turn bug. Still present on current
main. No matching issue. The nightly you are on (v0.0.43-nightly.20260921.2058) andmainhave the same Claude adapter; the onlyProviderCommandReactorchange since that nightly is unrelated worktree-submodule setup.The 6 extra
projection_turnsrows are real provider turns.thread.turn-start-requestedwrites one pending slot per user message. The firstturn.startedfor that thread consumes the slot and storespending_message_id. A laterturn.startedwith no slot left is inserted withpending_message_id = NULL. That matches 7 requests and 13 turns. It is not a projection double-count of the same turn.The duplicate is a second
ClaudeAdapter.sendTurn, not a steer and not a synthetic turn:sendTurnwhile a real turn is open reuses that turn id and does not stamp a new SDK message uuid. Your duplicate has a new turn id, so the adapter had no realturnStatewhen it sent.sendTurnstamps the SDK user messageuuidwith that new turn id, then queues it.command_lifecycle.command_uuidis that client uuid.turn.startedandcommand_lifecycle state=queuedfive milliseconds apart is that queue write landing in the CLI.session.configuredon the duplicate turn is the SDKsystem/initmessage, attributed to the current turn. Init is emitted when a query process starts, not when an already-running query is steered. Combined with each agent seeing an unknown "other session", this is two Claude Code processes running the same prompt, not one process with a queued follow-up.Ruled out:
thread.turn-start-requested. You have one per user message.command:<commandId>, 30 minute TTL) replaying the same event inside one reactor. A second pass of the same command returns beforesendTurn.Continue where you left off.(Claude has no promptless continuation), not the original user text.The first message of the thread being clean also fits the code. That turn only starts a session. Every later turn goes through the existing-session branch in
ensureSessionForThread. ForclaudeAgent, that branch restarts the query when the requested model selection is not structurally equal to the selection remembered in memory (shouldRestartForModelSelectionChange), and also on runtime-mode, cwd, or instance changes. Restart stops the current query,startSessionlogsclaude.session.replacing, and the reactor logsprovider command reactor restarting provider session. The new process then getssendTurn. If the old CLI is still alive, both run the prompt. The 18–56s gap fits a second, cold resume better than a second write on the warm stdin.What is not pinned yet is which signal fired the second
sendTurn18–56s later. The dedupe cache is per process, so a second server process would not see the first process's cache. One service-managed server should not do that.From the server log around one overlapping pair (14:45:29 / 14:46:06 is enough), these lines would settle it:
provider command reactor restarting provider sessionclaude.session.replacingsession.started/session.exitedbetween the twoturn.startedeventsThe full provider log is not required. A slice covering one original
turn.startedthrough the duplicatecommand_lifecycle state=completedis enough, with home-directory paths removed.cestercian commentedon Sep 23, 2026
taking this