Repository navigation
[Bug]: Claude provider re-sends each user message as a second, concurrent turn #13275
Description
Activity
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:- A follow-up
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:
- A second click or a second composer submit. That would be another
thread.turn-start-requested. You have one per user message. - The in-memory turn-start dedupe (
command:<commandId>, 30 minute TTL) replaying the same event inside one reactor. A second pass of the same command returns beforesendTurn. - The post-restart continuation. That sends
Continue where you left off.(Claude has no promptless continuation), not the original user text. - A synthetic turn. Those start from an assistant message that arrived with no local turn, and they do not enqueue a user message under the new turn id.
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.replacing- a
session.started/session.exitedbetween the twoturn.startedevents - two server pids both dispatching for that thread
The full provider log is not required. A slice covering one original
turn.startedthrough the duplicatecommand_lifecycle state=completedis enough, with home-directory paths removed.- A follow-up
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.acceptedfeature request acceptedfeature request acceptedvia-triageFiled through npx t3 triageFiled through npx t3 triage
on Sep 23, 2026 taking this
Reacted by Ferran, Bastien Cochet, Léo Mathurin and ClumsyReacted by Clumsy
Before submitting
Area
apps/server
Steps to reproduce
claudeAgent, modelclaude-opus-5-5, runtime modefull-access).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.~/.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.turn.started+claude/command_lifecycle(queued→started) with a newcommand_uuidfor the duplicate, followed bysession.configured, while the original command has not yet reachedcompleted.Consequences:
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), modelclaude-opus-5-5, fast mode off, runtime modefull-access.Logs or stack traces
Full 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.