Skip to content

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

Open

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
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