Summary
When Claude Code hits the max_turns limit (20 by default for tasks), the resulting error is misclassified as an authentication failure. This causes scheduled tasks to fail with misleading "Authentication failure: Subscription token may be expired" errors, even though authentication is working correctly. This is affecting fleet-wide scheduled task execution.
Component
Agent Runtime / claude_code.py
Priority
P1 - Major feature broken (scheduled tasks failing fleet-wide with incorrect error diagnosis)
Error
[Headless Task] Auth failure (fallback detection): Subscription token may be expired or revoked. Generate a new one with 'claude setup-token'.
Actual Claude Code output when max_turns is hit:
{
"type": "result",
"subtype": "error_max_turns",
"is_error": true,
"terminal_reason": "max_turns",
"errors": ["Reached maximum number of turns (20)"]
}
Note: The result field is empty/missing when max_turns is reached.
Location
- File:
docker/base-image/agent_server/services/claude_code.py
- Function:
process_stream_json_result() and error handling flow
- Related:
_diagnose_exit_failure(), _is_auth_failure_message()
Root Cause
The error handling flow has a circular self-triggering pattern:
- Claude Code hits max_turns limit and returns
is_error=true with empty result field
- Code extracts
result_text = msg.get("result", "") → empty string
- Empty
error_preview triggers the fallback: error_preview = _diagnose_exit_failure(return_code, metadata)
_diagnose_exit_failure() returns: "Subscription token may be expired or revoked. Generate a new one with 'claude setup-token'."
_is_auth_failure_message(error_preview) pattern-matches "setup-token" in that message
- Code raises HTTP 503 with "Authentication failure" error
The fallback diagnosis message itself triggers the auth failure detection.
Reproduction Steps
- Configure an agent with
max_turns_task: 20 (default)
- Run a complex task that requires >20 tool calls (e.g., a heartbeat task with multiple web searches, file reads, and writes)
- Observe task runs for 5-15 minutes, then fails with "Authentication failure"
- Verify actual auth works by running a simple one-turn task
Suggested Fix
Check for terminal_reason == "max_turns" or subtype == "error_max_turns" BEFORE falling through to generic error handling:
# In process_stream_json_result(), after extracting msg.get("result"):
if msg_type == "result":
# ... existing code ...
# NEW: Check for max_turns termination before generic error handling
terminal_reason = msg.get("terminal_reason")
subtype = msg.get("subtype")
if terminal_reason == "max_turns" or subtype == "error_max_turns":
turns_used = msg.get("num_turns", "unknown")
errors = msg.get("errors", [])
error_msg = errors[0] if errors else f"Execution stopped after {turns_used} turns"
metadata.error_type = "max_turns"
metadata.error_message = error_msg
logger.warning(f"[Headless Task] Max turns reached: {error_msg}")
raise HTTPException(
status_code=422, # Unprocessable Entity - task couldn't complete
detail=f"Task exceeded turn limit: {error_msg}. Consider breaking into smaller subtasks."
)
Also consider preventing the circular detection by checking error_type before auth pattern matching:
# In error handling section:
if metadata.error_type not in ("max_turns", "rate_limit", "billing_error"):
if _is_auth_failure_message(error_preview):
# ... auth failure handling ...
Environment
- Claude Code version: 2.1.110
- Guardrails:
max_turns_task: 20 (default)
- Affected: All agents running complex multi-turn scheduled tasks
Related
docker/base-image/agent_server/services/claude_code.py - Main error handling logic
docker/base-image/hooks/guardrails-baseline.json - Default max_turns config
- Guardrails feature:
cf186fe (GUARD-001)
Workaround
Until fixed, increase max_turns_task in guardrails config to accommodate complex tasks:
{
"max_turns_task": 50,
"max_turns_chat": 100
}
Set via AGENT_GUARDRAILS env var or update guardrails-baseline.json.
Summary
When Claude Code hits the
max_turnslimit (20 by default for tasks), the resulting error is misclassified as an authentication failure. This causes scheduled tasks to fail with misleading "Authentication failure: Subscription token may be expired" errors, even though authentication is working correctly. This is affecting fleet-wide scheduled task execution.Component
Agent Runtime / claude_code.py
Priority
P1 - Major feature broken (scheduled tasks failing fleet-wide with incorrect error diagnosis)
Error
Actual Claude Code output when max_turns is hit:
{ "type": "result", "subtype": "error_max_turns", "is_error": true, "terminal_reason": "max_turns", "errors": ["Reached maximum number of turns (20)"] }Note: The
resultfield is empty/missing when max_turns is reached.Location
docker/base-image/agent_server/services/claude_code.pyprocess_stream_json_result()and error handling flow_diagnose_exit_failure(),_is_auth_failure_message()Root Cause
The error handling flow has a circular self-triggering pattern:
is_error=truewith emptyresultfieldresult_text = msg.get("result", "")→ empty stringerror_previewtriggers the fallback:error_preview = _diagnose_exit_failure(return_code, metadata)_diagnose_exit_failure()returns: "Subscription token may be expired or revoked. Generate a new one with 'claude setup-token'."_is_auth_failure_message(error_preview)pattern-matches "setup-token" in that messageThe fallback diagnosis message itself triggers the auth failure detection.
Reproduction Steps
max_turns_task: 20(default)Suggested Fix
Check for
terminal_reason == "max_turns"orsubtype == "error_max_turns"BEFORE falling through to generic error handling:Also consider preventing the circular detection by checking error_type before auth pattern matching:
Environment
max_turns_task: 20(default)Related
docker/base-image/agent_server/services/claude_code.py- Main error handling logicdocker/base-image/hooks/guardrails-baseline.json- Default max_turns configcf186fe(GUARD-001)Workaround
Until fixed, increase
max_turns_taskin guardrails config to accommodate complex tasks:{ "max_turns_task": 50, "max_turns_chat": 100 }Set via
AGENT_GUARDRAILSenv var or updateguardrails-baseline.json.