Skip to content

fix(auxiliary): auth recovery for auto routes and stale fallback candidates - #59837

Merged
teknium1 merged 3 commits into
mainfrom
fix/aux-fallback-auth-refresh
Jul 7, 2026
Merged

fix(auxiliary): auth recovery for auto routes and stale fallback candidates#59837
teknium1 merged 3 commits into
mainfrom
fix/aux-fallback-auth-refresh

Conversation

@teknium1

@teknium1 teknium1 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Auxiliary tasks (compression, title generation, vision) now recover from expired OAuth credentials instead of 401-looping: auto-routed calls refresh the concrete backend's token, and a fallback candidate with a dead credential is refreshed-or-skipped rather than aborting the whole task. Fixes #20832, #23670; closes out the residual failure link from the mattalachia/Luisillo gpt-5.5 compaction debug dump.

Two commits:

1. Auto-route auth refresh (salvage of PR #20837 by @fanyangCS, authorship preserved — surgically reapplied, his branch predated the _retry_same_provider_sync/async extraction). resolved_provider stays "auto" even after the client cache picks a concrete backend, so the auth-refresh retry gate never fired for auto routes. New _auth_refresh_provider_for_route() infers the backend from the client's base URL (Copilot/Codex/Anthropic/Nous), plus a copilot branch in _refresh_provider_credentials (JWT-exchange cache eviction). This is the exact preflight-compression 401 from #23670.

2. Stale fallback-candidate recovery (ours). The fallback call site had no auth handling: a candidate carrying an expired credential (e.g. _try_anthropic() building a client from a dead ANTHROPIC_TOKEN) 401'd and aborted the task with a 60s cooldown. In mattalachia's debug dump this fired 5 times in one session — Codex 120s timeout → Anthropic 401 → no summary ever → session overflowed to 296K → "Cannot compress further". Now: _call_fallback_candidate_sync/async refresh the candidate's credentials and retry once; if unrefreshable, quarantine via _mark_provider_unhealthy and re-walk the chain so the next viable candidate serves. Non-auth errors propagate unchanged.

Changes

  • agent/auxiliary_client.py: _auth_refresh_provider_for_route(), copilot refresh branch, auto-route eviction, _call_fallback_candidate_sync/async wired at both fallback call sites (sync + async)
  • tests/agent/test_auxiliary_client.py: contributor's auto→Copilot tests (sync+async pattern), auto→Codex compression test (Preflight compression surfaces Codex OAuth 401 instead of falling back #23670), TestStaleFallbackCandidateSkip (refresh-retry / quarantine-skip / non-auth-raises)
  • scripts/release.py: AUTHOR_MAP entry

Validation

Before After
auto→Copilot/Codex 401 no refresh, raise refresh + retry succeeds
stale Anthropic fallback during compression abort + 60s cooldown (x5 in live dump) refresh-retry, or skip to next candidate (E2E: openrouter serves)
explicit-provider 401 semantics raise, no silent switch unchanged
tests 331/331 across 3 aux files

E2E replay of the mattalachia chain (codex timeout → expired-anthropic 401 → openrouter serves) via real call_llm against temp HERMES_HOME.

Infographic

infographic

fanyangCS and others added 3 commits July 6, 2026 13:26
Infer the concrete auxiliary auth provider from the selected client base
URL so provider:auto routes can refresh Copilot/Codex/Anthropic/Nous
credentials after auth errors, instead of skipping refresh because
resolved_provider stayed 'auto'. Adds the copilot branch to
_refresh_provider_credentials and evicts the stale auto-route cache
before retrying.

Fixes #20832. Salvaged from PR #20837, reapplied surgically onto current
main (branch predated the _retry_same_provider_sync/async extraction).
…tead of aborting

A fallback candidate can itself carry a stale credential (e.g. an
expired ANTHROPIC_TOKEN picked up by _try_anthropic). Its 401 previously
propagated out of the fallback call site and aborted the auxiliary task
— for compression: a 60s cooldown + context marker while the session
kept growing past the context cap. Live case: mattalachia debug dump
(Jul 2026), Codex timeout → Anthropic 401 x5 → 296K 'Cannot compress
further'.

Now each fallback candidate call is wrapped: on auth error, refresh the
candidate's provider credentials and retry once; if unrefreshable, mark
the provider unhealthy and walk the discovery chain again so the next
viable candidate serves. Sync + async paths. Non-auth errors still
raise unchanged.
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/auth Authentication, OAuth, credential pools provider/anthropic Anthropic native Messages API provider/copilot GitHub Copilot (ACP + Chat) P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 6, 2026
@teknium1
teknium1 merged commit 8f80a98 into main Jul 7, 2026
31 checks passed
@teknium1
teknium1 deleted the fix/aux-fallback-auth-refresh branch July 7, 2026 01:12
dstkwll added a commit to dstkwll/hermes-agent that referenced this pull request Jul 24, 2026
…oken 401

Copilot degrades in two related ways that both abort a turn as non-retryable
and only clear on a gateway restart (a cold process re-runs the token exchange):

1. HTTP 400 model_not_available_for_integrator / model_not_supported — a
   raw/degraded token routes to the restricted copilot-language-server
   integrator whose allowlist omits enterprise-only models (e.g.
   claude-opus-4.8). Because it is a 400 (not 401), the existing 401 refresh
   path never fired. Prevented (retry-with-backoff exchange + on-disk JWT
   persistence + header guard at the client chokepoint) and self-healed at
   runtime (single-shot forced re-exchange + client rebuild + retry before
   fallback).

2. HTTP 401 'IDE token expired: unauthorized: token expired' — the short-TTL
   *exchanged* IDE token expires mid-turn. The clean-401 path DID fire and call
   _try_refresh_copilot_client_credentials(), but that method only re-resolved
   the stable raw ghu_ token and rebuilt the client — it never evicted the
   cached exchanged JWT or forced a fresh exchange, so the retry put the SAME
   expired token back on the wire, 401'd again, and the single-shot guard
   aborted the turn. Fix: force a fresh IDE-token exchange (evict cached JWT via
   evict_cached_exchanged_token + re-mint via get_copilot_api_token) before the
   client rebuild, mirroring the merged auxiliary-path recovery (NousResearch#59837) and the
   400 recovery in this same PR. Graceful fallback to the resolved token if the
   exchange endpoint is unreachable; picks up the enterprise base_url on
   re-exchange.

Brings main-loop clean-401 recovery to parity with the merged auxiliary path
(NousResearch#59837), using the newer on-disk-aware evict helper. Companion context: NousResearch#58743
(this PR, expanded), NousResearch#51313, NousResearch#63204 (which assumed the 401 path already
recovered — it reached the method but the method was too weak).

Tests: exchange retry/persist round-trip, restart-blip disk reuse, stale-cred
400 classifier, 400 recovery, and 3 new 401 cases (fresh exchanged token on the
wire; network-blip fallback to resolved token). 58 copilot tests green on
current main.
teknium1 pushed a commit that referenced this pull request Aug 1, 2026
…oken 401

Copilot degrades in two related ways that both abort a turn as non-retryable
and only clear on a gateway restart (a cold process re-runs the token exchange):

1. HTTP 400 model_not_available_for_integrator / model_not_supported — a
   raw/degraded token routes to the restricted copilot-language-server
   integrator whose allowlist omits enterprise-only models (e.g.
   claude-opus-4.8). Because it is a 400 (not 401), the existing 401 refresh
   path never fired. Prevented (retry-with-backoff exchange + on-disk JWT
   persistence + header guard at the client chokepoint) and self-healed at
   runtime (single-shot forced re-exchange + client rebuild + retry before
   fallback).

2. HTTP 401 'IDE token expired: unauthorized: token expired' — the short-TTL
   *exchanged* IDE token expires mid-turn. The clean-401 path DID fire and call
   _try_refresh_copilot_client_credentials(), but that method only re-resolved
   the stable raw ghu_ token and rebuilt the client — it never evicted the
   cached exchanged JWT or forced a fresh exchange, so the retry put the SAME
   expired token back on the wire, 401'd again, and the single-shot guard
   aborted the turn. Fix: force a fresh IDE-token exchange (evict cached JWT via
   evict_cached_exchanged_token + re-mint via get_copilot_api_token) before the
   client rebuild, mirroring the merged auxiliary-path recovery (#59837) and the
   400 recovery in this same PR. Graceful fallback to the resolved token if the
   exchange endpoint is unreachable; picks up the enterprise base_url on
   re-exchange.

Brings main-loop clean-401 recovery to parity with the merged auxiliary path
(#59837), using the newer on-disk-aware evict helper. Companion context: #58743
(this PR, expanded), #51313, #63204 (which assumed the 401 path already
recovered — it reached the method but the method was too weak).

Tests: exchange retry/persist round-trip, restart-blip disk reuse, stale-cred
400 classifier, 400 recovery, and 3 new 401 cases (fresh exchanged token on the
wire; network-blip fallback to resolved token). 58 copilot tests green on
current main.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…oken 401

Copilot degrades in two related ways that both abort a turn as non-retryable
and only clear on a gateway restart (a cold process re-runs the token exchange):

1. HTTP 400 model_not_available_for_integrator / model_not_supported — a
   raw/degraded token routes to the restricted copilot-language-server
   integrator whose allowlist omits enterprise-only models (e.g.
   claude-opus-4.8). Because it is a 400 (not 401), the existing 401 refresh
   path never fired. Prevented (retry-with-backoff exchange + on-disk JWT
   persistence + header guard at the client chokepoint) and self-healed at
   runtime (single-shot forced re-exchange + client rebuild + retry before
   fallback).

2. HTTP 401 'IDE token expired: unauthorized: token expired' — the short-TTL
   *exchanged* IDE token expires mid-turn. The clean-401 path DID fire and call
   _try_refresh_copilot_client_credentials(), but that method only re-resolved
   the stable raw ghu_ token and rebuilt the client — it never evicted the
   cached exchanged JWT or forced a fresh exchange, so the retry put the SAME
   expired token back on the wire, 401'd again, and the single-shot guard
   aborted the turn. Fix: force a fresh IDE-token exchange (evict cached JWT via
   evict_cached_exchanged_token + re-mint via get_copilot_api_token) before the
   client rebuild, mirroring the merged auxiliary-path recovery (NousResearch#59837) and the
   400 recovery in this same PR. Graceful fallback to the resolved token if the
   exchange endpoint is unreachable; picks up the enterprise base_url on
   re-exchange.

Brings main-loop clean-401 recovery to parity with the merged auxiliary path
(NousResearch#59837), using the newer on-disk-aware evict helper. Companion context: NousResearch#58743
(this PR, expanded), NousResearch#51313, NousResearch#63204 (which assumed the 401 path already
recovered — it reached the method but the method was too weak).

Tests: exchange retry/persist round-trip, restart-blip disk reuse, stale-cred
400 classifier, 400 recovery, and 3 new 401 cases (fresh exchanged token on the
wire; network-blip fallback to resolved token). 58 copilot tests green on
current main.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/anthropic Anthropic native Messages API provider/copilot GitHub Copilot (ACP + Chat) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auxiliary Copilot token refresh is skipped for provider:auto after IDE token expires

3 participants