Skip to content

[Bug]: Claude provider re-sends each user message as a second, concurrent turn #13275

Description

@ferran9908

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

  1. Open a thread with the Claude provider (claudeAgent, model claude-opus-5-5, runtime mode full-access).
  2. Send a first message. It gets one turn, as expected.
  3. Send a follow-up message that starts a long turn (many tool calls, e.g. MCP design-tool edits).
  4. 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_events has 7 thread.turn-start-requested events, but projection_turns has 13 turns. The 6 extra turns have pending_message_id = NULL.
  • The Claude session transcript (~/.claude/projects/.../<session>.jsonl) has each user prompt written twice, e.g. identical text at 14:45:29.478Z and 14:46:06.429Z, and at 15:05:45.217Z and 15:06:03.925Z. Each write has its own queue-operation enqueue/dequeue pair.
  • The provider event log shows a fresh turn.started + claude/command_lifecycle (queued → started) with a new command_uuid for the duplicate, followed by session.configured, while the original command has not yet reached completed.
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=completed

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.

Activity

  1. juliusmarminge commented on Sep 23, 2026

    @juliusmarminge
    Member

    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) and main have the same Claude adapter; the only ProviderCommandReactor change since that nightly is unrelated worktree-submodule setup.

    The 6 extra projection_turns rows are real provider turns. thread.turn-start-requested writes one pending slot per user message. The first turn.started for that thread consumes the slot and stores pending_message_id. A later turn.started with no slot left is inserted with pending_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 sendTurn while 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 real turnState when it sent.
    • sendTurn stamps the SDK user message uuid with that new turn id, then queues it. command_lifecycle.command_uuid is that client uuid. turn.started and command_lifecycle state=queued five milliseconds apart is that queue write landing in the CLI.
    • session.configured on the duplicate turn is the SDK system/init message, 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 before sendTurn.
    • 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. For claudeAgent, 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, startSession logs claude.session.replacing, and the reactor logs provider command reactor restarting provider session. The new process then gets sendTurn. 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 sendTurn 18–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 session
    • claude.session.replacing
    • a session.started / session.exited between the two turn.started events
    • two server pids both dispatching for that thread

    The full provider log is not required. A slice covering one original turn.started through the duplicate command_lifecycle state=completed is enough, with home-directory paths removed.

  2. added
    bugSomething is broken or behaving incorrectly.
    acceptedfeature request accepted
    via-triageFiled through npx t3 triage
    on Sep 23, 2026
  3. cestercian commented on Sep 23, 2026

    @cestercian
    Contributor

    taking this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions