From 7c3999d77906f0fc23db8001d7ab366695d2f6ca Mon Sep 17 00:00:00 2001 From: ciregenz Date: Thu, 6 Aug 2026 20:58:42 -0700 Subject: [PATCH] [eric] agents: a self-healing reset-window 401 retries silently instead of flashing the reconnect card --- backend/apps/agents/core/error_classify.py | 13 +++++++++++-- backend/tests/test_capacity_retry.py | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/backend/apps/agents/core/error_classify.py b/backend/apps/agents/core/error_classify.py index 2d2b382c..275a4eca 100644 --- a/backend/apps/agents/core/error_classify.py +++ b/backend/apps/agents/core/error_classify.py @@ -14,6 +14,7 @@ TRANSIENT_CAPACITY_PATTERNS = re.compile( r"|internal\s+server\s+error" r"|rate[_\s-]?limit(?:_error)?" r"|ECONNRESET|ETIMEDOUT|ENETUNREACH|fetch\s+failed" + r"|reset\s+after\s+\d" r"|resource[_\s-]?exhausted" r"|upstream\s+connect\s+error)", re.IGNORECASE, @@ -154,6 +155,10 @@ def is_auth_error(exc: BaseException, extra_text: str = "") -> bool: # A tool-schema translation 400 can carry provider/connection wording that trips the auth regex below; it isn't auth, so don't claim it is. if is_translation_error(exc, extra_text): return False + # A 401 that names its own recovery window ("reset after 1m 57s") is a token mid-refresh; it + # heals itself, so the reconnect card would lie. The transient classifier retries it instead. + if re.search(r"reset\s+after|try\s+again\s+in", combined, re.IGNORECASE): + return False return bool(re.search( r"\b(401|403)\b" r"|invalid\s+authentication\s+credentials" @@ -244,11 +249,15 @@ def p_get_transient_exc_types() -> Tuple[type, ...]: def is_transient_capacity_error(exc: BaseException, extra_text: str = "") -> bool: # The Claude CLI's underlying ProcessError stringifies to a generic "Command failed with exit code 1 / Check stderr output for details"; the real cause (rate_limit_error / No pool capacity available / 429 / overloaded) only surfaces in the subprocess's stderr stream, which we capture via the SDK's `stderr` callback and pass in as extra_text. Classify against both so we catch capacity errors regardless of which channel carried the message. combined = f"{exc!s}\n{extra_text}".strip() - if combined and NON_TRANSIENT_PATTERNS.search(combined): - return False # An overflow can arrive dressed as a 429 ("request too large"); retrying the identical oversized request is guaranteed futile, the valve owns it. if is_context_overflow_error(exc, extra_text): return False + # A failure that names its own recovery window ("reset after 1m 57s") heals itself, even when + # it's dressed as a 401; the reset hint outranks the auth-shaped non-transient veto (caught live). + if combined and re.search(r"reset\s+after\s+\d", combined, re.IGNORECASE): + return True + if combined and NON_TRANSIENT_PATTERNS.search(combined): + return False # Ahead of the empty-string bail on purpose: what the exception IS doesn't depend on whether it bothered to say anything. if isinstance(exc, p_get_transient_exc_types()): return True diff --git a/backend/tests/test_capacity_retry.py b/backend/tests/test_capacity_retry.py index 5ad29db6..1f06a7c3 100644 --- a/backend/tests/test_capacity_retry.py +++ b/backend/tests/test_capacity_retry.py @@ -108,3 +108,19 @@ def test_ordinary_turn_failures_are_not_router_unreachable(): "", ): assert not is_router_unreachable_error(text), text + + +# --- self-healing 401s: mid-refresh tokens must retry, never flash the reconnect card ------------ +from backend.apps.agents.core.error_classify import is_auth_error + + +def test_reset_window_401_is_transient_not_auth(): + # Verbatim live codex body (2026-08-06): healed itself two minutes later, chats were fine. + body = '[codex/gpt-5.2] [401]: Provided authentication token is expired. Please try signing in again. (reset after 1m 57s)' + assert not is_auth_error(Exception(body)), "a self-healing 401 must not show the reconnect card" + assert capacity_retry_wait(Exception(body), 0) == 5, "and the turn silently retries through the window" + + +def test_genuine_auth_death_still_cards(): + assert is_auth_error(Exception("401 unauthorized: invalid authentication credentials")) + assert capacity_retry_wait(Exception("401 unauthorized: invalid api key"), 0) is None