Sub-agent API 401 (auth expired) is reported to parent agent as "Request interrupted by user for tool use"

Status Open
Reported on v2.1.191
Maintainer reply None cached
Activity 4 comments · opened Jun 25, 2026

Summary

When a sub-agent (launched via the Agent tool with a subagent_type) makes an API call that returns HTTP 401 because the user's auth token has expired, Claude Code reports the failure to the parent agent as [Request interrupted by user for tool use]. The actual authentication_failed / 401 error is hidden, and the parent agent has no way to distinguish a real user cancellation from an upstream auth failure.

Environment

  • Claude Code version: 2.1.191
  • Platform: macOS (Apple Silicon, Homebrew install)
  • Session ID (truncated for privacy): eca7fe60-...

Evidence

The sub-agent's session JSONL transcript contains the real synthetic assistant message (sensitive ids redacted):

{
  "type": "assistant",
  "model": "<synthetic>",
  "stop_reason": "stop_sequence",
  "error": "authentication_failed",
  "isApiErrorMessage": true,
  "apiErrorStatus": 401,
  "content": [{
    "type": "text",
    "text": "Please run /login · API Error: 401 The security token included in the request is expired"
  }]
}

Yet the parent agent's transcript for the same moment shows only:

[Request interrupted by user for tool use]

So the harness saw a synthetic assistant message with isApiErrorMessage: true, apiErrorStatus: 401, and error: "authentication_failed", but rewrote that into a user-interrupt wrapper before handing it to the parent. The parent never sees the auth-error class.

Reproduction

  1. Launch a long-running sub-agent via the Agent tool (e.g. one that runs many Bash/Read/Edit cycles over 5+ minutes — long enough that an auth token can plausibly expire mid-run).
  2. Wait for the user's auth token to expire mid-run (or simulate by revoking it).
  3. The next API call the sub-agent attempts fails with HTTP 401 (authentication_failed).
  4. Observe: the parent agent sees [Request interrupted by user for tool use] as the sub-agent's result and acts on it (typically: asks the user what to do, treats work as user-cancelled).
  5. Expected: the parent should see an explicit auth-expired / API-error class (not a user-cancel wrapper), so it can retry after /login, prompt the user to re-auth, or otherwise pick a correct recovery path.

Why this matters

  • Long-running multi-agent workflows become unreliable. Orchestrator -> specialist sub-agent patterns (e.g. per-story TDD delivery cycles) cannot distinguish an auth blip from a deliberate user stop.
  • Parent agents take the wrong recovery path. "User cancelled" -> ask the user for direction. "Auth expired" -> retry after /login. These are not interchangeable, and conflating them wastes turns and confuses the user.
  • Users are blamed for cancellations they didn't perform. The phrasing "Request interrupted by user" tells the model (and any human reading the transcript) that the user hit Esc. They didn't.
  • In-flight sub-agent work is silently dropped with no diagnostic trail. Committed work survives on disk; in-memory partial reports and uncommitted edits do not, and there is no signal to the parent that the loss happened for an auth reason rather than a deliberate one.
  • Post-hoc diagnosis requires manual forensics on the local jsonl transcripts. This is not a workflow most users will know to perform.

Suggested fix

  1. Surface API errors (401, 429, 5xx) verbatim to the parent agent with HTTP status code and original error message intact. The sub-agent transcript already carries apiErrorStatus, error (class), and isApiErrorMessage: true -- propagate these fields.
  2. Use a distinct error class -- e.g. auth_expired, rate_limited, api_error -- separate from user_interrupted. The current wrapper conflates the two failure modes into one string.
  3. (Stretch) Offer to resume the sub-agent automatically once auth recovers, preserving its in-progress tool-call queue. At minimum, give the parent enough signal to choose between "ask user" and "retry on auth recovery."

Workaround

None. The parent agent has no way to distinguish a real user cancel from an upstream auth failure from the surfaced signal alone. Inspecting the local session jsonl can identify the cause after the fact, but that requires the user to know to look.

Related

  • #12447 -- OAuth token expiration disrupts autonomous workflows (root condition: auth expires mid-run; this issue is about how that failure is reported to a parent agent, which #12447 doesn't cover).
  • #64177 -- Workflow counts API-errored subagents as "completed" (adjacent mis-accounting of sub-agent API errors; different surface and different misclassification, but same shape: parent's view of sub-agent outcome doesn't match what actually happened).
  • #62311 -- Distinguish an explicit user interrupt (Esc) from a message that merely arrived mid-turn (related but inverse: that issue is about distinguishing user-interrupt from queued-message; this issue is about distinguishing user-interrupt from auth-failure).
  • #25818 -- Orchestrator has no diagnostic context when subagent fails (same family of "parent receives degraded failure info"; closed as stale).

View original on GitHub ↗

4 Comments

kcarriedo · 2 months ago

This is a serious ergonomics problem in multi-agent workflows -- the parent agent losing diagnostic context on a 401 is worse than just a confusing message. It picks the wrong recovery strategy (asks the user "did you cancel?" when the real answer is "your token expired, run /login").

The error conflation is the most impactful part: auth_expired, rate_limited (429), and user_cancel are three completely different situations that all land as [Request interrupted by user for tool use] right now. Each needs a different recovery path.

Surfacing the apiErrorStatus to the parent would let orchestrators implement proper retry logic -- e.g. pause the fan-out, bubble a /login prompt to the top-level session, then resume. Without it, any multi-agent workflow that runs longer than a token TTL is fragile by design.

arcavenai · 2 months ago

Update: same misclassification pattern observed in a different upstream cause — connection drops mid-response, not auth expiry.

In a long-running multi-agent session this week, the parent agent received [Request interrupted by user for tool use] on a sub-agent dispatch. The session JSONL transcript shows the actual upstream signal was NOT a 401:

{
  "type": "assistant",
  "model": "<synthetic>",
  "error": "server_error",
  "apiErrorStatus": null,
  "isApiErrorMessage": true,
  "content": [{
    "type": "text",
    "text": "API Error: Connection closed mid-response. The response above may be incomplete."
  }]
}

Two independent instances in one session (~6 hours apart). The 401 case (which this issue filed) was also present once in the same session — three <synthetic> events total: one authentication_failed/401 plus two server_error/null.

Both variants exhibit the same harness behaviour: a <synthetic> model message with isApiErrorMessage: true, bubbled to the parent agent as [Request interrupted by user for tool use] — so a fix targeting just the 401 case would leave the connection-closed case unaddressed.

Suggested scope expansion: any <synthetic> isApiErrorMessage event (regardless of error class — authentication_failed, server_error, future variants) should propagate to the parent with the original error class and status preserved, not collapsed into the user-interrupt class.

kristoffersingleton-yale · 1 month ago

---
TL;DR — additive independent repro; please do NOT auto-close as duplicate. This
confirms and extends #70958; it is not a restatement of it, and it is distinct from
every near-neighbor:

  • Not #69856 — that report's cause is pkill bun (an external process kill), and

it's misfiled under a model-behavior title. Ours involves no pkill; the trigger
is an upstream 401/429 returned to a subagent mid-tool-use.

  • Not the #29238 → #21358 chain — that cluster was auto-deduped into "bash

commands timeout on linux," an unrelated bug, then locked. This is subagent API
failure → fake user-interrupt, with transcript evidence.

  • Not the "task stuck in /workflows" reports (e.g. #65925) — those are UI/restart

state reconciliation. This is a different layer: the run genuinely dies because
agents never resolve and no terminal event is ever written.

New vs #70958 itself: (1) also fires on 429, not just 401; (2) hits
Workflow-tool subagents, not only Agent-tool ones; (3) the downstream effect
silently kills the entire workflow (journal frozen at started:N/result:M, task
stuck "running", nothing surfaced).
---

Independent repro of this on Workflow-tool subagents, with two additions: the
trigger isn't only 401, and there's a downstream consequence that silently kills
whole workflows.

Environment: Claude Code (macOS, Apple Silicon). Observed across two unrelated
projects ("Definitely-Not-Prod" and "Held-Together-With-Duct-Tape").

Same mechanism, wider trigger. Subagents spawned inside a Workflow-tool run hit
upstream API failures mid-tool-use — both 401 (auth expired) and 429 (rate
limit)
— and the failure is reported to the parent as
[Request interrupted by user for tool use] / "The user doesn't want to proceed
with this tool use." No user acted. Raw signature counts in the affected agent
transcripts: run A = 27×401 / 6×429; run B = 49×401 / 67×429. Each dead agent's
final entry is a tool_use with stop_reason: tool_use, immediately followed by the
bogus user-interrupt marker. So the fix shouldn't be scoped to auth (401) alone — any
upstream subagent failure (429 included) is being laundered into a fake user
cancellation.

Downstream consequence (new): the whole workflow silently dies. A workflow run's
journal.jsonl is an append-only heartbeat — one line per agent start, one per agent
result. When subagents are killed by this mislabeled interrupt, they never emit a
result, so the journal is left at started:N / result:M (M<N) with no terminal
aborted/error event
. Nothing surfaces this: /workflows shows nothing, no
notification fires, and the task stays "running" indefinitely. Of 14 terminal runs
sampled, 2 died exactly this way (~14%) — intermittent, and correlated with periods
of upstream 401/429.

How to detect it from disk (no special tooling — the journal is enough). Match
agents on their key field (a hash of prompt+opts, stable across resumes; raw
started/result counts are not, since the journal is cumulative across resumes). A key
with a started and no result, on a run whose journal has stopped being written,
is a dead agent:

import json, os, time

def classify(run_dir, stall=300):
journal = os.path.join(run_dir, "journal.jsonl") # .../subagents/workflows/wf_*/
started, resolved = {}, set()
for line in open(journal):
line = line.strip()
if not line:
continue
try:
ev = json.loads(line)
except json.JSONDecodeError:
continue
key = ev.get("key") # prompt+opts hash; resume-safe
if ev.get("type") == "started" and key:
started[key] = ev.get("agentId")
elif ev.get("type") == "result" and key:
resolved.add(key)

unresolved = [a for k, a in started.items() if k not in resolved]
idle = time.time() - os.path.getmtime(journal) # age of newest heartbeat
if idle < stall: # still writing → alive (maybe mid-think)
return "ALIVE", unresolved
return ("SILENT_DEATH" if unresolved else "COMPLETE"), unresolved

Pointed at an affected run dir, this returns SILENT_DEATH plus the agentIds that
started but never resolved — the same ones whose transcripts carry the false
"user rejected" marker.

Asks:

  1. Don't launder upstream subagent failures (401 and 429, and presumably other

5xx/API errors) into [Request interrupted by user for tool use] — surface the
real status to the parent/workflow.

  1. On any subagent/runtime teardown, write a terminal aborted/error event to

journal.jsonl instead of leaving started:N / result:M.

  1. Surface a dead workflow to the main loop and /workflows rather than leaving the

task "running".

Related: #6836 (meta tool_use/tool_result mismatch), #69856 (same workflow-journal
silent-death signature, misfiled under a model-behavior title).

arcaven · 1 month ago

Thanks @kristoffersingleton-yale for the careful differentiation from the neighboring reports; that precision is what keeps this thread from being folded into the wrong cluster. Your trigger matches what we observe on our fleet: an upstream auth failure returned to a subagent mid-tool-use, no external process kill, surfaced to the parent as a user interrupt. Your 429 flavor landing in the same wrapper strengthens the case that this message is a catch-all for subagent API failures rather than anything user-initiated.