mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-02 14:28:59 +02:00
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01En8dRGsJPLrJCQBEkTH4Mp
72 lines
3.5 KiB
Python
72 lines
3.5 KiB
Python
"""One transparent retry when a subscription token expires mid-session (ENG-294).
|
|
|
|
The router surfaces an upstream 401 as assistant TEXT, and the old handling was a banner telling
|
|
the user to open Settings, find Models, click Reconnect, wait, and re-send: six actions to recover
|
|
from a token doing the one thing tokens always do. The router's own dispatcher usually refreshes
|
|
the credential within moments; what stays stale is OUR side, a pooled CLI still carrying the old
|
|
env. So the first expiry in an ask now rebuilds the session (fresh CLI, fresh router token) and
|
|
queues one hidden continuation to redo the failed step. A second expiry in the same ask means the
|
|
credential is genuinely dead, and the honest banner still fires; swallowing every 401 forever is
|
|
the failure mode this deliberately refuses.
|
|
"""
|
|
from typeguard import typechecked
|
|
|
|
from backend.apps.agents.core.models import AgentSession
|
|
|
|
AUTH_RETRY_PROMPT = (
|
|
"The model provider returned an expired-credential error on your last step; the connection "
|
|
"has been rebuilt with a refreshed token. Redo that one step, then carry on where you left off."
|
|
)
|
|
|
|
|
|
@typechecked
|
|
def try_auth_self_heal(session: AgentSession, delay_s: int = 0) -> bool:
|
|
"""Queue the one hidden retry on a new CLI process that resumes the same transcript (the
|
|
process is what holds the stale token; the conversation is fine). False = budget spent or a
|
|
continuation is already pending, and the caller should show the honest banner instead.
|
|
|
|
delay_s: codex tokens ROTATE on a 1-2 minute cadence; an instant retry lands inside the same
|
|
rotation window, burns the one-shot budget, and the user then gets a banner for a condition
|
|
that would have healed itself (field screenshot, 2026-08-19). Callers pass ~75s for
|
|
rotation-shaped failures so the retry fires after the window closes."""
|
|
if session.auth_retry_used or session.pending_continuation:
|
|
return False
|
|
session.auth_retry_used = True
|
|
session.needs_respawn = True
|
|
session.pending_continuation = True
|
|
session.pending_continuation_prompt = AUTH_RETRY_PROMPT
|
|
session.pending_continuation_delay_s = max(0, delay_s)
|
|
return True
|
|
|
|
|
|
TRANSIENT_RETRY_PROMPT = (
|
|
"The model provider returned a temporary error instead of an answer on your last step, and it "
|
|
"has now cleared. Redo that one step, then carry on where you left off."
|
|
)
|
|
|
|
# Two is the whole budget: looping past it trades a visible stop for an invisible one, which is worse.
|
|
TRANSIENT_RETRY_MAX = 2
|
|
|
|
|
|
@typechecked
|
|
def try_transient_self_heal(session: AgentSession, delay_s: int = 0) -> bool:
|
|
"""Queue a hidden retry for a provider error that waiting can actually fix.
|
|
|
|
Separate budget from the auth one-shot on purpose: these arrive by the same door (assistant
|
|
TEXT, no exception) but for opposite reasons, and sharing a counter would let a rate limit
|
|
consume the retry an expired token needs moments later.
|
|
|
|
No fresh session here, unlike the auth path. A rate limit is the provider's verdict on the
|
|
ACCOUNT, so rebuilding the CLI costs a respawn and changes nothing (there is a standing test
|
|
that a 429 must not respawn the CLI); the connection case is handled by simply waiting.
|
|
"""
|
|
if session.pending_continuation:
|
|
return False
|
|
if session.transient_retry_count >= TRANSIENT_RETRY_MAX:
|
|
return False
|
|
session.transient_retry_count += 1
|
|
session.pending_continuation = True
|
|
session.pending_continuation_prompt = TRANSIENT_RETRY_PROMPT
|
|
session.pending_continuation_delay_s = max(0, delay_s)
|
|
return True
|