Skip to content

bug: Max-turns termination misclassified as authentication failure #361

Description

@vybe

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:

  1. Claude Code hits max_turns limit and returns is_error=true with empty result field
  2. Code extracts result_text = msg.get("result", "") → empty string
  3. Empty error_preview triggers the fallback: error_preview = _diagnose_exit_failure(return_code, metadata)
  4. _diagnose_exit_failure() returns: "Subscription token may be expired or revoked. Generate a new one with 'claude setup-token'."
  5. _is_auth_failure_message(error_preview) pattern-matches "setup-token" in that message
  6. Code raises HTTP 503 with "Authentication failure" error

The fallback diagnosis message itself triggers the auth failure detection.

Reproduction Steps

  1. Configure an agent with max_turns_task: 20 (default)
  2. Run a complex task that requires >20 tool calls (e.g., a heartbeat task with multiple web searches, file reads, and writes)
  3. Observe task runs for 5-15 minutes, then fails with "Authentication failure"
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions