Skip to content
12 changes: 12 additions & 0 deletions apps/backend/agents/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,18 @@ async def run_agent_session(

current_tool = None

# Handle rate_limit_event SystemMessage from sdk_patches
elif msg_type == "SystemMessage" and getattr(msg, "subtype", None) == "rate_limit_event":
debug("session", "Rate limit event received — Claude Code is paused, stream remains open")
print("[Rate limit] Claude Code is waiting for rate limit reset...", flush=True)
if task_logger:
task_logger.log(
"Rate limit reached — waiting for reset (stream remains open)",
LogEntryType.INFO,
phase,
print_to_console=False,
)

print("\n" + "-" * 70 + "\n")

# Check if build is complete
Expand Down
1 change: 1 addition & 0 deletions apps/backend/core/error_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def is_rate_limit_error(error: Exception) -> bool:
for p in [
"limit reached",
"rate limit",
"rate_limit", # Catches "Unknown message type: rate_limit_event" from claude_agent_sdk
"too many requests",
"usage limit",
"quota exceeded",
Expand Down
50 changes: 50 additions & 0 deletions apps/backend/core/sdk_patches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Runtime patches for third-party SDK bugs."""

import logging

logger = logging.getLogger(__name__)


def apply_claude_agent_sdk_patches() -> None:
"""Patch claude_agent_sdk to handle rate_limit_event gracefully.

The bundled SDK raises MessageParseError for unknown message types including
rate_limit_event. This patch returns a SystemMessage instead of raising,
allowing the message loop in session.py to handle it and keep the stream open
while Claude Code waits for the rate limit to reset.

Patches both:
- message_parser.parse_message (the module attribute)
- _internal.client.parse_message (the already-bound module-level name that
the client's receive_response() generator actually calls via
`from .message_parser import parse_message` at import time)
"""
try:
from claude_agent_sdk._internal import client as _ic
from claude_agent_sdk._internal import message_parser as _mp
from claude_agent_sdk.types import SystemMessage as _SystemMessage

_orig_parse = _mp.parse_message

def _patched_parse(data: dict) -> object:
try:
return _orig_parse(data)
except Exception:
if isinstance(data, dict) and data.get("type") == "rate_limit_event":
logger.warning(
"Rate limit event received from Claude Code — "
"returning as SystemMessage so the stream stays open: %s",
data,
)
return _SystemMessage(subtype="rate_limit_event", data=data)
raise

_mp.parse_message = _patched_parse
_ic.parse_message = _patched_parse
logger.debug("claude_agent_sdk patched to handle rate_limit_event")
except Exception:
logger.warning(
"Failed to apply claude_agent_sdk rate_limit_event patch — "
"rate limit events may cause unexpected session failures",
exc_info=True,
)
5 changes: 5 additions & 0 deletions apps/backend/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
if "_new_stream" in dir():
del _new_stream

from core.sdk_patches import apply_claude_agent_sdk_patches

apply_claude_agent_sdk_patches()


# Validate platform-specific dependencies BEFORE any imports that might
# trigger graphiti_core -> real_ladybug -> pywintypes import chain (ACS-253)
from core.dependency_validator import validate_platform_dependencies
Expand Down
5 changes: 5 additions & 0 deletions apps/backend/runners/spec_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@
# Add auto-claude to path (parent of runners/)
sys.path.insert(0, str(Path(__file__).parent.parent))


from core.sdk_patches import apply_claude_agent_sdk_patches

apply_claude_agent_sdk_patches()

# Validate platform-specific dependencies BEFORE any imports that might
# trigger graphiti_core -> real_ladybug -> pywintypes import chain (ACS-253)
from core.dependency_validator import validate_platform_dependencies
Expand Down
Loading