Skip to content

[Bug]: Claude wake run stays running forever when a queued user message answers its native turn first #22510

Closed

Description

@UzEE

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

Deterministic with the adapter's wake test harness. The test below fails on main at ca140c435f:

  1. A Claude user turn ends with its result. No T3 turn is active.
  2. Claude opens a native turn on its own (a wake) and emits a root system/init. bufferWakeMessage buffers the init and offers a continuation, so T3 queues a continuation run.
  3. A user message that was queued earlier (here, a PR watch notification) starts its run before the continuation does and sends its prompt.
  4. Claude folds that prompt into the native turn it already opened. One native turn and one result serve both. The user run takes the result and completes.
  5. The continuation run starts. It replays the stale buffered init, marks the wake as started, and waits for a result that already went to the user run.
Failing test (drop into the "ClaudeAdapterV2 background wake turns" describe in ClaudeAdapterV2.test.ts)
it.effect("settles a wake run whose native turn a user turn already answered", () =>
  Effect.scoped(
    Effect.gen(function* () {
      const harness = yield* makeWakeHarness;
      const now = yield* DateTime.now;
      const startTurn = (ordinal: number, text: string, wake = false) =>
        harness.runtime.startTurn(
          makeClaudeTestTurnInput({
            threadId: harness.threadId,
            providerThread: harness.providerThread,
            now,
            attemptId: RunAttemptId.make(`attempt-claude-wake-answered-${ordinal}`),
            text,
            attachments: [],
            providerTurnOrdinal: ordinal,
            ...(wake
              ? { messageCreatedBy: "agent" as const, messageCreationSource: "provider" as const }
              : {}),
          }),
        );
      const lifecycle = (promptIndex: number, state: string, uuid: string) =>
        claudeSdkFrame({
          type: "command_lifecycle",
          command_uuid: harness.promptUuid(promptIndex),
          state,
          uuid,
          session_id: WAKE_NATIVE_SESSION,
        });

      yield* startTurn(1, "Open the PR.");
      yield* harness.offerAndWait(
        claudeSdkFrame({
          ...makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000a01", result: "Opened." }),
          user_message_uuid: harness.promptUuid(0),
        }),
      );
      assert.equal((yield* Queue.take(harness.terminalReceipts)).status, "completed");

      // Claude opens a native wake turn on its own while T3 is idle.
      yield* harness.offerAndWait(wakeTurnInit);
      yield* Queue.take(harness.continuationReceipts);

      // A queued user message runs before the continuation. Claude folds it
      // into the wake turn it already opened and ends both with one result.
      yield* startTurn(2, "A check failed on the PR.");
      for (const frame of [
        lifecycle(1, "queued", "00000000-0000-4000-8000-000000000a02"),
        lifecycle(1, "started", "00000000-0000-4000-8000-000000000a03"),
        makeAssistantTextFrame({
          uuid: "00000000-0000-4000-8000-000000000a04",
          text: "Fixed the check.",
        }),
        lifecycle(1, "completed", "00000000-0000-4000-8000-000000000a05"),
        claudeSdkFrame({
          ...makeResultFrame({
            uuid: "00000000-0000-4000-8000-000000000a06",
            result: "Fixed the check.",
          }),
          user_message_uuid: harness.promptUuid(1),
        }),
      ]) {
        yield* harness.offerAndWait(frame);
      }
      assert.equal((yield* Queue.take(harness.terminalReceipts)).status, "completed");

      // The continuation has nothing left to answer, and Claude is idle.
      yield* startTurn(3, "Background task completed.", true);
      yield* TestClock.adjust("2 minutes");
      assert.lengthOf(harness.terminalEvents(), 3);
      assert.equal((yield* Queue.take(harness.terminalReceipts)).status, "completed");
      assert.lengthOf(harness.offeredMessages, 2);
    }).pipe(
      Effect.provide(Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer)),
    ),
  ),
);

It fails with expected [ …(2) ] to have a length of 3 but got 2: the continuation never reaches a terminal state, even after the 2-minute deadline.

Expected behavior

The continuation run settles as completed. The user run already answered the native turn it was opened for, and Claude is idle.

Actual behavior

The continuation run stays running forever, and every run queued behind it waits. In my case that was 5 hours: a review result, two delegated-task completions and two PR watch updates sat queued while the thread showed Working and the Claude CLI sat idle. It only moved when another thread sent a message, which T3 delivered into the stuck run.

The cause is in ClaudeAdapterV2.ts. The user-turn path in startTurn leaves the buffered root init and the requestedContinuations entry in place. When the continuation drains the buffer, replaying that init through handleSdkMessage sets context.nativeWakeStarted = true. No buffered result follows, so nothing finalizes. The #19196 deadline only fires while nativeWakeStarted is false, so it never fires here.

Pi, Muse and OpenCode hit the same race and handle it. When a user turn starts while a wake is held, the user turn takes the wake over and the later continuation finishes at once (OpenCode takeRunningWake, Pi's adoptedWake, Muse's if (continuation && !held) return finish(...)). I checked the other adapters: Codex, Grok, ACP-registry and Antigravity can't hit it, and Cursor doesn't request continuations.

Related, but different: #22033 has the same root weakness (a user turn leaves an old buffered start in the wake buffer), but there the symptom is a subagent stuck Running. #20430 is a continuation run that never starts because it's held in the queue. Here the run starts and never ends. #22049 and #20318 don't fix this; the test above still fails with each applied.

Impact

Blocks work completely

Version or commit

0.1.1-nightly.20261020.3411; still fails on main @ ca140c4

Environment

Linux (WSL2), T3 server as a background service, Claude Code CLI 2.1.304, claude-opus-5-5, full access

Logs or stack traces

# provider event log, one Claude CLI process (UTC)
13:56:02.563 incoming result success                         # run 9 ends
13:56:02.616 incoming command_lifecycle started 0b35bb54     # no T3 prompt: Claude starts a wake
13:56:02.636 incoming system/init                            # adapter buffers it, logs claude-wake-turn-detected, run 11 queued
13:56:03.671 outgoing prompt.offer                           # run 10 (queued earlier) sends its prompt
13:56:03.673 incoming command_lifecycle queued 511fd19c
13:56:07.090 incoming command_lifecycle started 511fd19c
14:00:36.918 incoming command_lifecycle completed 511fd19c
14:00:36.921 incoming result success                         # one result for both; run 10 completes
14:00:36.929 incoming command_lifecycle completed 0b35bb54
             (no events for 5 hours; run 11 started 14:00:37.707 and stays running)

Workaround

Send any message to the stuck thread. It's delivered into the stuck run, Claude starts a turn, and its result finally ends the run. I didn't try Stop.

Activity

  1. juliusmarminge commented on Oct 20, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Thanks for the failing test, it maps cleanly onto current main. When the continuation run starts, it replays every buffered frame except result through handleSdkMessage (apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts:9520-9545). A replayed root init sets nativeWakeStarted (lines 8559-8566). The two-minute fallback that settles a wake Claude never opens only runs while nativeWakeStarted is false (lines 9546-9575). So when a queued user run has already taken the result for the turn Claude opened, the stale init makes the continuation look started, and nothing is left to end it.

    This isn't covered by the existing reports. #20430 needs a held queued run blocking the continuation gate, #17099 is a wake continuing on another provider, and #17809 is about subagent rows after a completion wake. #21239 drops obsolete pull request watch notifications before they start, which would avoid this particular trigger only when the watch is stale, not the race itself.

    Accepting this as a bug.

  2. added
    bugSomething is broken or behaving incorrectly.
    acceptedfeature request accepted
    via-triageFiled through npx t3 triage
    on Oct 20, 2026
  3. added a commit that references this issue on Oct 20, 2026
    b85e28d
  4. added a commit that references this issue on Oct 21, 2026
    1391510
  5. kkolodziej7 commented on Oct 23, 2026

    @kkolodziej7

    Confirming this on the released desktop builds 0.1.1 and 0.1.2 (macOS, claudeAgent, Claude Code CLI 2.1.308 and 2.1.309, claude-opus-5-5). It hit one long-lived thread three times in about 13 hours on Oct 22–23. Each time the sequence matched this report:

    1. A background shell command finishes while the thread is busy. T3 queues a provider continuation for the wake (createdBy=agent, creationSource=provider).
    2. A queued user message (queue_after_active) starts first. The native turn ends with one successful result, and the user run takes it and completes. In the case we inspected, that result had task-notification origin, num_turns=7 and queued_turn_count=0.
    3. The continuation run starts 0.5–1.5 s later and never gets another native frame. It stays running, and everything queued behind it waits.
    # Build result taken by the user run (UTC) Continuation started Stuck for
    1 0.1.1, CLI .308 Oct 22 19:39:24.520 19:39:25.008 21m26s (moved when a new prompt was offered)
    2 0.1.1, CLI .308 Oct 22 22:08:40.771 22:08:41.292 10h10m overnight (interrupted)
    3 0.1.2, CLI .309 Oct 23 08:50:00.888 08:50:02.418 41m32s (interrupted)

    During the waits nothing else was pending: no runtime request, open tool, hook or compaction, and the CLI had no open TCP connection. The two-minute wake deadline did not fire in any of the three cases, which fits the replayed init setting nativeWakeStarted. To recover, the stuck run had to be interrupted and the 7 messages queued behind it cancelled by hand, then the thread restarted.

    0.1.2 still has this. Could #22513 make the next release?

  6. added a commit that references this issue on Oct 25, 2026
    048c855
  7. BWBama85 commented on Oct 29, 2026

    @BWBama85

    Possible related occurrence on T3 Code desktop 0.1.5 (6d450a4284852af137b2f3407fddd89eb76e7644), macOS / Apple Silicon, Claude Code 2.1.312, claudeAgent, claude-opus-5-5.

    The user asked Claude to pause. Claude acknowledged the pause, its Stop hooks finished successfully, and the CLI emitted result/success, but the thread continued showing Thinking with an increasing working timer. This is a possible match for this issue, not an independently reproduced confirmation of the exact race.

    Timeline on 2026-10-29, all times UTC:

    Time Observed event
    17:26:33.147 T3 run ordinal 6 requested
    17:26:33.487 Outgoing prompt.offer for the pause message
    17:26:33.496 command_lifecycle: queued, command fddef959-f78b-4d8c-919c-719dbe14af5b
    17:26:37.434 Same command reports started
    17:26:59.665 Final assistant pause acknowledgment
    17:26:59.672 Response stream reports stop_reason: end_turn
    17:29:58.727 Stop hooks finished; quality gates took 179042 ms, passed, with no hook errors or prevented continuation
    17:29:58.729 Same command reports completed
    17:29:58.734 CLI emits the successful result below
    17:29:58.780 T3 run ordinal 5 recorded as completed
    17:30:54 Screenshot still shows Thinking and an increasing working timer

    Redacted/selected fields from the final native result:

    {
      "type": "result",
      "subtype": "success",
      "is_error": false,
      "num_turns": 3,
      "stop_reason": "end_turn",
      "terminal_reason": "completed",
      "user_message_uuid": "fddef959-f78b-4d8c-919c-719dbe14af5b",
      "user_message_uuids": ["fddef959-f78b-4d8c-919c-719dbe14af5b"],
      "origin": {
        "kind": "task-notification",
        "producer": "session-task"
      },
      "queued_turn_count": 0
    }

    A successful read-only database query during the investigation found:

    Run ordinal requested_at status completed_at
    5 2026-10-29T17:23:22.528Z completed 2026-10-29T17:29:58.780Z
    6 2026-10-29T17:26:33.147Z running NULL

    There were no later native provider events at inspection time. The approximately three-minute delay after the assistant reply is explained by Stop quality gates; the stale running state after successful completion is the problem.

    The installed ClaudeAdapterV2.ts still replays non-result wake frames through handleSdkMessage, and only schedules the wake fallback while nativeWakeStarted is false. The root-frame handler can set that flag during replay, consistent with the mechanism described here.

    Limit: We did not establish ordinal 6's creation reason from its payload or reproduce the sequence in the adapter test harness. The mixed task-notification origin and matching user-prompt UUID make this a candidate for this issue, but do not prove identical causality. No claim is made that Stop or restart was tested as a recovery.

    Cross-linking #15197 for the completed-provider/stuck-running symptom. Does the proposed fix in #22513 also cover this case, where a task-notification-origin result explicitly acknowledges the user's pause prompt?

  8. achat-z commented on Oct 31, 2026

    @achat-z

    Confirming on stable 0.1.6 (desktop, Windows 10 x64, claudeAgent, claude-opus-5-5), with a trigger variant: the wake comes from a background native subagent (Agent with run_in_background, relaunched with SendMessage), not a background shell command. One long-lived, busy thread (many queued messages) hit it twice in one evening. Each time the T3 run stayed Working with no items and no output, everything queued behind it waited, and t3_thread_interrupt cleared it.

    Incident 1 (2026-10-31, UTC, from the provider event log)

    Time Event
    17:14:56 Background subagent relaunched via SendMessage (task_started)
    17:15:13.829 task_notification status=completed, background_tasks_changed → []
    17:15:21.744 Root result success / end_turn (ends the user turn)
    17:15:21.774 Root system/init: Claude opens a wake turn by itself
    17:15:24.218 Queued user message: outgoing prompt.offer, command_lifecycle queued
    17:15:29.717 Same command started: Claude folds it into the wake turn it just opened
    17:17:33.756 Command completed + one root result, taken by the user run
    17:17:36 → 17:18:13 Two more queued user runs complete normally
    17:18:13 → 20:35:10 No native frames at all. The continuation run sits at Working for about 3 h 15
    20:35:10 query.interrupt / query.close (thread interrupt), then the queue drains normally

    This is the exact sequence in the issue body: root init buffered → queued user prompt absorbed into that native turn → its single result goes to the user run → the continuation replays the stale init and waits forever.

    Incident 2 (same thread, UTC)

    Time Event
    20:41:06–09 Background subagent started and backgrounded; root result (tool_use), root init, root result end_turn at 20:41:31
    20:41:33.966 Next queued user run starts
    20:41:46.669 task_notification completed for the background subagent while that user turn is open; background_tasks_changed → []
    20:42:35.473 Single root result for that native turn, taken by the user run
    20:42:37 → 20:43:18 Two more queued user runs complete normally
    20:43:18 → 21:05:59 No native frames. Stuck Working for about 23 min
    21:05:59 query.interrupt / query.close, then normal

    Here the notification landed inside a user turn rather than right before one. The end state is the same: a provider continuation with nothing left to answer it. I did not pin the exact start time of the stuck run from the server trace.

    In neither case did the two-minute claude-wake-turn-never-opened fallback fire. Checked against the v0.1.6 source: ClaudeAdapterV2.ts still sets nativeWakeStarted from a replayed root frame (around line 9481), and the fallback is gated on !context.nativeWakeStarted (around lines 10650–10662). Both are unchanged from the analysis above. Neither #22513 nor #28010 is in v0.1.6 or v0.1.7-nightly.20261031.3991, so this still reproduces on current releases.

    Workaround in use: interrupt the thread; avoid background native subagents on busy threads.

    Triage by Claude Opus 5.5 (Claude Code, inside T3 Code), from the user's local provider event log.

  9. TylerJNewman commented on Nov 2, 2026

    @TylerJNewman

    Note

    Written by Claude Opus 5.5 in Claude Code on behalf of Tyler.

    Three more occurrences on T3 Code 0.1.5, all claudeAgent, on 2026-11-02: two macOS machines and one Linux server. The continuation runs were stuck for 31 minutes, 65 minutes and 15.9 hours, each with queued messages behind it, until someone pressed Stop. Each matches this report: a background task's task_notification and the wake init arrive while no run is active, the next queued T3 prompt is folded into that native turn (transcript queue-operation remove with reason: "absorbed_mid_turn"), its one result settles the user run, and the continuation that starts within a second never settles. The Claude transcript has no record after that until Stop.

    We checked both open fixes against our sequence on current main (71646e031a), using an adapter test in the wake harness: a background task, its notification and the wake init while idle, then a user turn answered by an assistant frame and a result that carry the prompt's uuid in user_message_uuids, then the continuation. On main the continuation never settles (the case times out at 120 s). With either #22513 or #27416 applied on main, it settles as completed, and the full ClaudeAdapterV2.test.ts passes (291/291 for each).

  10. juliusmarminge commented on Nov 9, 2026

    @juliusmarminge
    Member

    Thanks for the detailed report and regression test. #28010, merged in 692dd9ce6f, settles a wake absorbed by a user turn while preserving the buffered frames.

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