Skip to content

Agent steers on Claude threads cancel tools and look like user refusals #21895

Open

Description

@Alb11747

What happened

An agent's progress update sent with t3_thread_send can abort the tool currently running in the receiving Claude thread. Claude then reports "The user doesn't want to take this action right now", although the human refused nothing. Queueing every agent update until the whole turn ends avoids cancellation but delays coordination for long turns.

Diagnosis

The Claude adapter sends active steering as SDKUserMessage with priority: "now". Claude Code treats that priority as an interruption. The activeSteeringInterruptsTools capability already identifies the difference from Codex's steering.

The initial #21158 patch queued agent auto/steer messages whenever an active session declared that capability, including when no tool was currently running. It did not apply this queueing rule to Codex or idle threads. The updated PR proposes replacing that coarse fallback with safe delivery during the active turn.

Steps to reproduce

  1. Start a Claude thread and have it run a command or parallel tool calls.
  2. From another agent, send an automatic or explicit steer to that thread while the calls are running.
  3. Observe the tool cancellation and misleading user-refusal result. With the initial fix(server): Claude follow-ups no longer interrupt tools #21158 patch, observe that the update instead waits until the entire turn ends.

Version and environment

Observed on a Windows desktop personal fork; original failure recorded on 2026-10-18. Exact installed T3 build was not re-queried for this report. Controlled SDK experiments on 2026-10-19 used @anthropic-ai/claude-agent-sdk 0.3.276 and Claude Code 2.1.303. No T3 installation or restart was performed.

Evidence

The original provider log offered the agent update with priority: "now" at 2026-10-18T09:46:43.510Z; cancelled-tool results followed at 09:46:43.964Z. Seven parallel delegation calls were in flight: two succeeded, one returned an interruption, and four returned the user-refusal message.

Fresh bounded SDK experiments found:

  • priority: "next" during a six-second tool let the tool finish normally and folded the update into one native result. Sending it during a no-tool answer instead produced two native results; the first reported queued_turn_count: 0 despite the continuation. Using that priority alone leaves a native continuation that T3 must track and cancel.
  • PostToolBatch.additionalContext delivered once after both tools in one model batch completed, without cancellation, within one native result. The probe's MCP runtime executed those two calls serially; simultaneous execution was not tested. The documented hook contract supplies the completed batch rather than a per-tool boundary.
  • Stop.additionalContext let a no-tool answer consume an update and continue before producing its single native result.
  • Child hook callbacks carried agent_id; filtering those preserved the root update. Clearing the local pending input before interrupt produced no injection or continuation during five seconds of observation.

Options and workaround

The narrow option proposed in #21158 holds agent input in T3 for a root PostToolBatch or Stop hook, up to five seconds, then uses the existing durable next-turn queue if no boundary arrives. Stop revokes pending delivery; task acknowledgement/disposal rechecks ownership before hook handoff. Human steering keeps its current interruption semantics. The committed patch has passed focused tests and independent review; full checks are in progress.

Native next is another option if T3 also implements native queued-input ownership and cancellation across multiple results. A plain stream tool-end callback followed by native now has a race with the next tool starting. A PostToolUse hook also fires before all parallel tools finish; PostToolBatch gives the safer batch boundary.

Explicit mode: "queue" remains the existing workaround when avoiding interruption matters more than prompt delivery. No fix has been installed on this PC.

Related reports

PR #21158 addresses agent-originated messages. #15720 and #15708 concern human steering and Stop around running Bash; #18641 and #15868 propose different human-steering behavior. This report concerns an agent update being mistaken for a user refusal.

Filed by GPT-6.1 Sol at high effort in T3 Code through the Codex harness, using source inspection, retained diagnostic evidence, and fresh controlled SDK probes. This did not run the interactive t3 triage command.

Activity

  1. Alb11747 commented on Oct 19, 2026

    @Alb11747
    ContributorAuthor

    The safe tool-boundary implementation is now in #21158. It uses root PostToolBatch/Stop hook context, with a five-second durable queue fallback and cancellation/ownership checks. Focused SDK probes and regression tests are recorded there; final independent GPT-6.1 Sol xhigh review has no remaining findings. Fork checks are running. No build has been installed and T3 has not been restarted.

  2. juliusmarminge commented on Oct 21, 2026

    @juliusmarminge
    Member

    Triaged against main at 70e5e384da. Scanned open issue titles for agent steering, tool cancellation, and user-refusal wording; no matching duplicate found. Runtime reproduction was not performed. The proposed fix changes agent-originated t3_thread_send steer/queue delivery semantics, which this automated triage flow reserves for maintainer review. Leaving open for Julius; no implementation changes.

  3. juliusmarminge commented on Oct 22, 2026

    @juliusmarminge
    Member

    Reproduced on Linux against main 8acbefefc5, using the isolated web dev server and Claude Opus 5.5. An agent-originated explicit steer interrupted all three parallel 45-second t3_thread_wait calls immediately; a separate running Bash command was also aborted. The MCP calls returned interruption errors rather than the older user-refusal wording. Runtime transcript. Existing #21158 remains open and unchanged.

  4. immakermatty commented on Oct 24, 2026

    @immakermatty
    Contributor

    One more failure mode with the same cause, in case it helps the maintainer takeover of #21158: an agent message can also cancel a pending question or approval. The person never sees the question.

    What we observed (a fork based on v0.1.1, 2026-10-24, Claude Opus 5.5 in Claude Code):

    • A coordinating Claude thread asked the person through AskUserQuestion.
    • While the question was pending, another thread's message arrived via t3_thread_send with the default mode: auto.
    • The tool call returned Tool permission request aborted, and the question disappeared from the UI before the person answered it.
    • It happened five times in one thread in one day. A question asked while no message arrived went through normally.

    The same path on main (909e4c9):

    • ThreadManagementService.sendToThread turns auto into steer_active whenever a steerable run exists.
    • The steer aborts the SDK callback, and the pending request resolves without an answer. awaitClaudeUserInputAnswers resumes with empty answers, and awaitClaudeApprovalDecision resumes with cancel.

    As far as I can read it, the takeover's guard (agent sends to a session with activeSteeringInterruptsTools go to the durable queue) covers this case for Claude, so no separate fix is needed.

    Our interim fix: until it lands, our fork queues an auto send while the thread has a pending runtime request, with a focused test (commit). We will drop it once #21158 is in a release. I have not checked whether steering a Codex turn affects a pending request_user_input, so I am not proposing that broader guard here.

    Prepared through T3 Code with Claude Opus 5.5.

  5. guryanov-a commented on Nov 2, 2026

    @guryanov-a

    Human steers from the composer hit this too. As taken over, #21158 leaves human steering unchanged, so this case remains once it lands.

    Seen on T3 Code 0.1.6, Claude Opus 5.5, Claude Code 2.1.316 (from the thread's activity log, not a controlled reproduction). A Bash call and a ToolSearch were issued together. I sent a steer 19 s into the Bash call. Bash finished normally, and at that same moment the waiting ToolSearch returned "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 followed my steer, but abandoned the work that call belonged to and told me "you stopped me".

    Claude Code 2.1.316 also ships neutral texts for this situation, such as "[Tool call did not complete: the turn was ended to deliver the message that follows. Nothing refused it; re-run it if still needed.]", but this steer got the refusal text. Delivering human steers at the next tool boundary (#15287) would avoid the cancellation altogether.

  6. jamesdchen commented on Nov 3, 2026

    @jamesdchen

    Same bug on T3 Code 0.1.6 (Windows x64, Claude Opus 5.5), from an orchestration run with about 15 Claude delegates. A coordinator thread steers delegates with t3_thread_send (mode: auto), and the delegates read the cancellation as a human decision. No human saw or refused anything.

    Counts from one evening's provider logs: 14 tool_results with is_error: true and this text, across 5 threads, within about 30 minutes: 13 Bash, 1 t3_thread_send. Each came 1-25 s after the call was issued.

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

    Why it costs more than one retried call:

    • Agents obey it. The text says a human refused, so a well-behaved agent stops and doesn't retry. One delegate stopped twice and asked its coordinator for permission to proceed, and work held up behind it waited (116 lines of a queued batch, the second time).
    • It reads as a policy decision. The agent took it as "the user objects to this kind of action", and reasoned about what the user might have meant. A coordinator can't tell it apart from a real refusal. I asked the human directly whether they'd clicked anything, and they hadn't. Nothing in the thread's UI or the message says it was another agent's steer.
    • Compound commands get cut mid-way. One cancelled Bash had written local files and was piping them to a remote host, so the agent had to work out what had actually happened before it could continue.

    What would help beyond the queueing in #21158:

    • When the cancel comes from a steer, say so in the tool result: for example, "Cancelled because a message arrived. Nobody refused this. Read the message, then re-run if it's still needed." That removes the false attribution even where cancellation can't be avoided.
    • Document it in the t3_thread_send description: steer and auto can kill the receiver's running tool. Agents currently have no way to know, and queue looks like merely a slower option.

    Caveat: I matched these to steers by timing and by the coordinator's send log. I don't have a per-call record tying each one to a specific priority: "now" frame. I can share redacted log lines if useful.

  7. Meorir commented on Nov 3, 2026

    @Meorir

    Frequency data and controlled runs for this bug, from a setup where many Claude threads coordinate through t3_thread_send. The main additions: queue caused no interruptions in our data, the outcome depends on the tool's state, and one outcome is an empty error that earlier comments don't mention.

    Environment: T3 Code desktop on Windows 11, now 0.1.6 with Claude Code 2.1.317. The 14-day window also includes earlier T3 builds and Claude Code back to 2.1.304. steer_active for auto and priority: "now" are still on main today.

    How often (14 days to 2026-11-03)

    • About 460 tool calls cancelled in about 285 interruptions, in 62 of 1,109 local transcripts (subagent transcripts count separately).
    • 2 to 23 interruptions a day until 1 Nov, then 101 on 2 Nov, a day of heavy cross-thread coordination, and 45 so far on 3 Nov.
    • 433 of 466 cancelled results came within 1 s of a message entering the receiver's queue (queue-operation in the transcript; this count comes from a first pass and includes a few Esc interrupts).

    Interruptions by the mode of the incoming message, matched on exact message text:

    Mode Sends Interruptions
    auto (explicit or default) 5,188 234
    steer 40 4
    queue 2,680 0
    No matching send n/a 47

    "No matching send" is probably human composer steers, wake-ups and senders outside these transcripts. We did not separate them. The send counts include idle and non-Claude recipients, so they don't give a per-busy-thread rate.

    What the agent receives

    Call state when the steer arrives Result text Count Effect
    Waiting behind another tool call The user doesn't want to take this action right now. STOP what you are doing... 209 Did not run in our controlled runs
    No permission decision recorded yet Error calling tool (Bash): with nothing after the colon 146 Probably never started; not reproduced in a controlled run
    MCP call already past permission The tool call was interrupted before a result was received. It may or may not have completed... 106 Unknown, by its own wording
    Foreground PowerShell running Command running in background with ID: ... 21 Keeps running
    Foreground Bash running Normal result, once the command ends n/a The steer waits for the command

    The neutral wording mentioned above ("Nothing refused it") appeared 0 times. All 146 empty errors and all 209 STOP results have no permissionDecision in the transcript, while 106 of the 108 remaining interrupted MCP calls have one. The 146 and the 209 span Bash, Edit, Write, PowerShell and MCP. That is why we read the empty error as "interrupted before the permission decision", but this is an inference.

    The empty error is the hardest case for an agent: it has no reason text, so the agent can't tell what happened or whether the command ran.

    Controlled runs

    Each run used a Claude Haiku 5.5 child thread with a 1 s tick script and a Write queued behind it. The parent sent the message, on 0.1.6 with Claude Code 2.1.317.

    Run Sent Script Write Message delivered
    PowerShell + Write steer, about 12 s in Moved to background, kept ticking to its normal end 84 s later STOP, file not created At once
    Bash + Write steer, about 12 s in Finished normally STOP, file not created After the script ended, 53 s later
    PowerShell + Write queue Finished normally Succeeded 5 s after the tools finished, as a new turn

    So in the PowerShell case the agent reads a refusal while its first command is still running. A retry of that command would run it twice. Afterwards, the child in the first run described its write as having been rejected.

    Three more steers during 16 sequential short PowerShell calls: one moved a just-started command to the background, and two landed between calls and cancelled nothing.

    Cost

    About one interruption in eight (35) was followed by more than 60 s before the agent's next tool call, and 16 by more than 5 minutes. That is an upper bound, because some of those gaps are ordinary ends of turns. In one time-boxed rollout, an agent waiting on what it read as a human refusal contributed to a 48-minute delay.

    What would help

    1. A status on fix(server): Claude follow-ups no longer interrupt tools #21158. Our queue numbers support its direction, but it currently has merge conflicts and was last updated on 25 Oct.
    2. A warning in the t3_thread_send description that auto and steer can cancel running tools. Agents can't learn this today, and it's what we have just added to our own instructions as a workaround (mode: "queue" by default). We have no results from that yet.
    3. +1 to the earlier suggestion of saying in the tool result that a message caused the cancellation, including for the empty error.

    These are aggregates from local transcripts. The transcripts contain business data, so we can't attach them, but we can run other counts on request.

    Prepared through T3 Code with Claude Opus 5.5.

  8. bootherr commented on Nov 4, 2026

    @bootherr

    As #15351 noted, Claude Code's neutral text for a call that a "now" steer skips is behind a feature flag that's off by default. 2.1.317 added an env switch that turns it on. The result is picked here (2.1.317, minified):

    function hqe(e){let n=$i(e.reason);if(n==="permission-stop")return kje;if(n==="interrupt"&&(i.CLAUDE_CODE_FIZZY_GROVE??v("tengu_fizzy_grove",!1)))return vje;return hC}

    hC is the "The user doesn't want to take this action right now. STOP…" text. vje is "[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.]".

    I ran the real 2.1.317 CLI offline (stream-json, --permission-prompt-tool stdio, mock API, two parallel calls to a slow MCP tool) and sent a "now" message while the first call was running:

    • default, CLAUDE_CODE_FIZZY_GROVE=0 or empty: the waiting call gets the STOP text
    • CLAUDE_CODE_FIZZY_GROVE=1 or true: it gets the "Nothing refused it" text
    • the running call gets "interrupted… may or may not have completed" either way
    • a real Stop (the interrupt control request) still reads as a stop with the switch on, because it aborts with remote-cancel, not interrupt

    Defaulting CLAUDE_CODE_FIZZY_GROVE=1 in mergeClaudeProcessEnvironment would fix the wording for agent and composer steers alike. Other interrupt aborts can also select vje for skipped calls, and the switch is undocumented. I haven't tested how a real model reacts to the new text, or Meorir's empty-error case.

    I have the change drafted with tests and can open a PR if you want it, and I'm happy to share the repro scripts. Until then, setting the variable in a Claude instance's environment works on 2.1.317.

    Prepared through T3 Code with Claude Opus 5.5

  9. OscarSwanros commented on Nov 4, 2026

    @OscarSwanros

    Another case for the tool-boundary delivery: a bridge that relays a person's chat
    messages (WhatsApp, a phone app) into a T3 thread with t3 thread send. Those
    messages are often that person correcting the task the thread is running.

    Today t3 thread send hard-codes mode: "queue" (apps/server/src/cli/thread.ts),
    so a correction waits for the whole turn. Passing auto instead would hit this
    issue and cancel the running tools. And with #21158 as it stands, sends through
    MCP are createdBy: "agent", so they would queue anyway.

    What would serve this case: the PostToolBatch/Stop delivery described above, for
    sends like these, and a --mode option on t3 thread send that passes through
    to t3_thread_send's existing modes. Would a focused PR for that option be
    welcome once delivery is safe?

  10. bompus commented on Nov 5, 2026

    @bompus
    Contributor

    Measured on 2026-11-05 on a Linux host (local T3 build; version not re-queried), Claude thread running Sonnet 5.5, one foreground Bash call date +%T.%N; sleep 75; date +%T.%N.

    1. A message sent with t3_thread_send in queue mode 8 s into the call: the call ran the full 75.0 s with no interruption, and the message started a new run after the turn ended.
    2. The same call with a message sent in steer mode: delivered at 17:46:40.6Z; the call failed 5.0 s later (17:46:45.7Z) with Error calling tool (Bash): interrupt.

    So steering a Claude thread interrupts a running foreground tool call after a fixed ~5 s, which matches the priority: "now" diagnosis above, while queue is safe. In one long-lived thread transcript, 16 of 18 interrupted Bash calls were exactly 5.0 s after a queued message entry. Settings > General > Follow-up behavior (Queue or Steer) decides which path a message typed into a running thread takes.

  11. mitschabaude commented on Nov 9, 2026

    @mitschabaude

    @juliusmarminge to me this issue effectively means that steering claude in t3 code doesn't work. in my experience, steering always interrupts tool calls and gives claude the impression that the I was unhappy about what it did, which confuses it and causes it to end up in unproductive circles

  12. mcarkade commented on Nov 9, 2026

    @mcarkade

    Also seeing this on T3 Code 0.1.8-nightly.20261108.4353 (Windows 11) with Claude Code 2.1.319. Follow-up behavior is set to Steer.

    • If I send a follow-up while a tool call is running, the call is cancelled and Claude says I denied it. I didn't.
    • The same happens when another thread messages this one through t3_thread_send in the default auto mode.

    On main at 4f0b74e, the Claude steer still goes out with priority: "now" (ClaudeAdapterV2.ts:11672). CLAUDE_CODE_FIZZY_GROVE isn't set anywhere, so the cancelled call still gets the refusal text. #21158 covers only agent-sent messages, so steers typed in the composer would still hit this after it lands.

    Switching to Queue avoids it, but then Steer can't be used on Claude threads.

    Posted on my behalf by Claude Opus 5.5 in T3 Code.

  13. 1kuna commented on Nov 9, 2026

    @1kuna
    Contributor

    Title: Additional cancellation counts: even a peer acknowledgement can look like human refusal

    Problem: An agent-originated t3_thread_send in default auto mode can steer a running Claude turn and cancel pending tool calls. The receiver then interprets the cancellation as a human refusal and stops, despite the trigger being an ordinary peer acknowledgement.

    Reproduction or observation: A retained local-transcript audit counted 19 instances in one orchestrator transcript since October 24, and 28 across the inspected transcripts. These populations overlap; they should not be added. In one case, three outbound sends were cancelled 3 milliseconds after a peer's one-line acknowledgement was enqueued. This is timing evidence from retained records, not a controlled reproduction of every instance or a fresh test on current main.

    The cancelled results had toolDenialKind: "cancelled" and Claude Code's text: "The user doesn't want to take this action right now. STOP what you are doing and wait for the user…" A genuine denial in the inspected records used different wording: "doesn't want to proceed with this tool use". Receivers stopped because they read the cancellation as the human declining the action.

    Proposed behavior: Queue agent-originated messages by default unless the sender explicitly requests steering, or deliver them at a safe boundary as proposed in #21158. Where steering still cancels a call, provide trusted turn context explaining that an arriving message caused the cancellation, so receivers can distinguish it from an actual human denial. Preserve deliberate human refusal and Stop semantics.

    The quoted refusal wording is generated by Claude Code, not T3; changing that upstream wording itself is outside T3's scope. T3 can control delivery timing and expose the cause/provenance of its own steering. #21158 remains open and unmerged at this check.

    Why native: T3 knows the authenticated message origin and chosen delivery mode. A harmless acknowledgement should not acquire the meaning of a human veto solely because it arrived during a tool batch.

  14. 1kuna commented on Nov 9, 2026

    @1kuna
    Contributor

    Correction to my earlier report: the interruption lever is directly under T3’s control. In the installed T3 bundle, ClaudeAdapterV2.steerTurn hard-codes priority: "now" in its outgoing Claude user message. That priority causes the turn interruption; arriving stream-json messages need not all behave this way.

    Fresh headless measurements on Claude Code 2.1.321, routed Haiku 5.5, isolated config/disposable working directories, three serial eight-second MCP tool calls emitted in one assistant reply. A second stream-json user message was sent about 0.8 seconds after the tool batch was emitted:

    • priority: "now", flag unset: one in-flight call was interrupted; the two pending calls each returned exactly: 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.
    • priority: "now", CLAUDE_CODE_FIZZY_GROVE=1: the same two pending calls each returned exactly: [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.] The flag fixes the wording; it does not prevent interruption.
    • priority: "next", flag unset: all three calls completed with SLOW_OK; none was cancelled, and the second message was acknowledged.

    In both now cases, the in-flight call returned exactly: The tool call was interrupted before a result was received. It may or may not have completed on the server — verify before assuming it succeeded, and retry if needed. This is distinct from the pending-call wording above.

    Proposal: use native next for agent-originated messages, preserving now for human input. The measured pending-call cancellation wording can also be made accurate today with CLAUDE_CODE_FIZZY_GROVE=1, but this is an undocumented, experimental Claude Code flag and should be re-checked after upgrades. These probes exercise the CLI protocol; they are not an end-to-end test of T3 queue ownership/cancellation or the proposed delivery change.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions