Skip to content

[Bug]: Delegated-task notifications cancel queued Claude tool calls and tell the agent the user refused #15351

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

In T3 Code:

  1. In a Claude thread, start a delegate_task child with mode: "async".
  2. While the child runs, have the parent send a tool batch in which the second call has to wait for the first. Example: a slow Bash command, then a Bash command that writes a file.
  3. Let the child finish while the first call is still running.

The script below reproduces it without T3. It sends the same message ClaudeAdapterV2.steerTurn sends, partway through that kind of tool batch. Run it with now, then with next.

repro.mjs
// node repro.mjs <now|next>   (set CLAUDE_PATH to use a specific claude binary; Ctrl-C to exit)
import { randomUUID } from "node:crypto";
import { query } from "@anthropic-ai/claude-agent-sdk";

const priority = process.argv[2];
const pending = [];
let wake = null;
const offer = (m) => (pending.push(m), wake?.());
async function* input() {
  for (;;) {
    if (pending.length === 0) await new Promise((r) => (wake = r));
    while (pending.length > 0) yield pending.shift();
  }
}
const user = (text, extra = {}) => ({
  type: "user",
  message: { role: "user", content: [{ type: "text", text }] },
  parent_tool_use_id: null,
  session_id: "",
  uuid: randomUUID(),
  ...extra,
});

offer(
  user(
    "Run these two tool calls together in ONE message, in this order: " +
      "(1) Bash: `sleep 12 && echo slept`; (2) Bash: `echo second > second.txt && cat second.txt`. " +
      "Then report what each tool returned.",
  ),
);

const q = query({
  prompt: input(),
  options: {
    model: "claude-opus-5-5",
    permissionMode: "bypassPermissions",
    allowDangerouslySkipPermissions: true,
    settingSources: [],
    ...(process.env.CLAUDE_PATH ? { pathToClaudeCodeExecutable: process.env.CLAUDE_PATH } : {}),
  },
});

let steered = false;
for await (const m of q) {
  for (const b of m.type === "assistant" || m.type === "user" ? m.message.content : []) {
    if (b.type === "tool_result") console.log("tool_result:", JSON.stringify(b.content).slice(0, 200));
    if (b.type === "text") console.log(`${m.type}:`, b.text.slice(0, 300));
    if (b.type === "tool_use" && !steered) {
      steered = true;
      // The text and priority T3 steers in for a delegated-task completion.
      setTimeout(
        () =>
          offer(
            user(
              "Delegated task task-123 reached a terminal state. Use task_status with taskId task-123 to read the result.",
              { priority },
            ),
          ),
        4000,
      );
    }
  }
  if (m.type === "result") console.log("result:", m.terminal_reason);
}

Expected behavior

The notification reaches the agent and every tool call still runs. The orchestrator promises this in the comment above its steer_active routing in apps/server/src/orchestration-v2/Orchestrator.ts: "Never interrupt/restart a turn for a notification."

Actual behavior

The queued call never runs. Its tool result is:

The user doesn't want to take this action right now. STOP what you are doing and wait for the user to tell you how to proceed.

The agent takes that as an instruction from the user. In two real threads it replied "I've paused, as you asked" and waited for a user who never came. For an autonomous thread that's the end of it, because nobody is around to send the next message.

A milder version hits MCP tools that are already running, such as t3_thread_wait. Those fail with "The tool call was interrupted before a result was received". Agents usually retry that one.

Script results on Claude Code 2.1.288:

priority Runs First call Queued call Turn
now 3 ran cancelled with the text above ended with aborted_tools, then a new turn started for the notification
next 2 ran ran one turn; the agent read the notification in the same turn

In two more now runs the second call was read-only, so Claude Code ran it in parallel with the first and nothing was cancelled. Those turns still ended with aborted_tools.

Cause:

  • For a delegated-task completion, the orchestrator picks steer_active when the session reports supportsActiveSteering. The Claude adapter reports true.
  • ClaudeAdapterV2.steerTurn sends every steer with priority: "now". On now, Claude Code ends the current turn and cancels calls that haven't started.
  • The refusal text comes from Claude Code, not from T3. Claude Code 2.1.288 already has a neutral message for this case, "[Tool call skipped: the turn ended to deliver the message that follows before this call ran. Nothing refused it; re-run it if still needed.]" A feature flag that is off by default gates it, so T3 can't count on it.

I think the fix is to send notification steers with priority: "next". Claude Code delivers those at the next tool boundary without ending the turn, which is what the next runs above show. User steers should keep "now", because #12541 explains why users want their own steers to land right away. I can open a PR once this is triaged.

Impact

Major degradation or frequent failure

Version or commit

Observed on the desktop app, 0.0.46-nightly.20261003.2632 on Linux. Code read against main @ 65731f9.

Environment

Linux on WSL2, Claude Code 2.1.288, @anthropic-ai/claude-agent-sdk 0.3.276, claude-opus-5-5, full-access runtime mode.

Logs or stack traces

Timeline from one affected thread, in UTC:

01:00:00.7  Bash call starts (about 7s)
01:00:01.3  Read of a .jpg in the same batch, waiting on the Bash call
01:00:05.0  Delegated-task completion notification steered into the turn
01:00:07.5  Bash finishes normally; Read fails with "The user doesn't want to take this action right now. STOP ..."
01:00:16.9  Agent: "I've paused, as you asked." The thread waits until someone sends a message.

A second thread showed the same sequence: the notification arrived at 02:07:16.9 and the queued Read was cancelled at 02:07:19.2.

Workaround

Tell agents in their prompt that this tool result means the call was cancelled and that they should retry it.

Related: #12541 covers Claude steer timing, and #15173 covers a Claude parent waiting on a delegate_task child.

Investigated with Opus 5.5 in Claude Code, running inside T3 Code.

Activity

  1. juliusmarminge commented on Oct 3, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Triage

    Thanks for the detailed report and the repro script, @UzEE! The code path you describe is still on current main (5e35272fda), and the two files you cited haven't changed since 65731f986b. This looks like a real Claude bug. It's separate from open #15173, where the parent session is idle-released while it waits on a child. It's also separate from #12541, a PR about Claude steering that was closed without merging.

    What happens

    An async delegate_task uses completionWake: "always" (in OrchestratorMcpService). When that child finishes, ProviderContinuationService sends a server-created agent message (createdBy: "agent", creationSource: "server", dispatchMode: "queue_after_active") with the delegatedCompletionWakeDetail text.

    dispatchMessage then upgrades that delivery to steer_active when every task in the batch uses completionWake: "always" and the live session reports supportsActiveSteering, which Claude does. The comment on that block says this delivery should never interrupt or restart a turn. Wait-mode children (settled_only) stay queued, so they don't hit this path.

    ClaudeAdapterV2.steerTurn doesn't check who sent the message, so every steer goes to the SDK as a user message with priority: "now". After a steer, the adapter drops a result whose terminal_reason is aborted_tools or aborted_streaming (isClaudeActiveSteeringAbortResult) and keeps the same T3 turn open for the model's next reply.

    That lines up with the hang you saw. Claude cancels the tool call that hadn't started yet, with "The user doesn't want to take this action right now. STOP…". T3 reads the aborted_tools result as steering, not an interrupt, so the model's "I've paused, as you asked" lands on the turn that's still running. Nothing is queued behind it, so an autonomous thread waits forever. The Claude Code 2.1.286 changelog entry about now messages backgrounding a running call doesn't seem to cover calls that haven't started, which matches what you saw on 2.1.288.

    Likely fix area

    • One option is to send only this server-created delegated-completion delivery with priority: "next", which matches the next column in your script. User steers would keep priority: "now". Runtime-question answers are createdBy: "user", so they'd stay on the current path too.
    • Another option is to stop upgrading async delegated completions to steer_active for Claude, and queue them after the active turn instead.

    Either way, the goal is that tool calls the model has already issued still run, and the model isn't told the user refused them. I didn't re-run the live script. A maintainer will decide on the fix direction.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 3, 2026
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

    bugSomething 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