fix(coding-agent): trust structured auth classification; explicit model switch clears stale auth - #2046
Open
snimu wants to merge 8 commits into
Open
Conversation
snimu
force-pushed
the
sebastian/retry-auth-heuristics-2026-09-04
branch
2 times, most recently
from
September 4, 2026 12:12
f4077f2 to
d45b0de
Compare
snimu
force-pushed
the
sebastian/retry-auth-heuristics-2026-09-04
branch
4 times, most recently
from
September 4, 2026 14:17
6a6236e to
b220ab5
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit eee8f74. Configure here.
snimu
force-pushed
the
sebastian/retry-auth-heuristics-2026-09-04
branch
from
September 4, 2026 16:55
2c4c36e to
1513dd3
Compare
…cit model selection clear stale auth - Delete the 401/403 message-text regexes in _isConcreteProviderAuthFailure: every provider now records provider_stream_failure diagnostics, so the session trusts only structured kind "auth". The regexes classified non-auth 403s (region blocks, org policy, model-access denials) as concrete auth failures and locked out the whole provider. - classifyStreamFailure no longer maps a bare 403 status to "auth"; an explicit authentication/permission error type or 401 is required. - Explicit model selection (session.setModel and the in-process/daemon set_model lookups) clears the provider's stale-auth marking so the request runs again; a structured auth failure on that request re-marks the provider. This unwedges sessions stuck on "Model not found" after a transient auth-shaped incident.
…unused details delegate
…lockouts only on successful explicit selection Fresh-eyes review fixes: - New "permission" failure kind: 403s and permission/forbidden/access-denied error types (Anthropic permission_error, SDK PermissionDeniedError, AWS AccessDeniedException) are entitlement or policy denials, not bad credentials. They are permanent (no retry) but never mark auth stale, so a model/org/region-scoped 403 cannot lock out the whole provider. - An auth verdict now needs structured evidence (401 status or an explicit authentication error type); free-form message text alone no longer launders into a stale-marking "structured" auth diagnostic. - AgentSession.setModel is the single owner of the stale-auth clear and commits it only when staleness is the sole blocker of an explicit selection; the in-process and daemon set_model lookups consult the full catalog for stale-auth providers instead of mutating stale state before validation, so a mistyped model id or failed refresh no longer unlocks a provider that was proven bad.
…e-auth lockout Macroscope follow-up: setModel cleared the stale markers before canUseModel, so a failed private-model (team availability) validation still unlocked the provider. canUseModel gains an assumeAuthConfigured option that evaluates availability under a hypothetical clear: for private prime-inference models it answers the team-authorization question directly when stale auth excludes the provider from the available list. setModel validates first and commits the clear only after every check passes, making the "failed selections never unlock a provider" claim true for all paths.
… verdicts Review follow-up: the message-text demotion only fired when both type and status were missing, so a 500 whose text mentioned authentication still produced a provider-locking auth kind. Without a structured error type, auth/permission now come solely from the status (401/403).
…le-auth validation Review follow-up: the hypothetical-clear validation called refreshAvailableModels, whose keyless run (stale prime-inference auth) cleared authorizedPrivatePrimeInferenceModelIds - the recovery path destroyed the cached entitlement it was validating against, so a previously authorized private model failed with a team-availability error and the lockout stayed. - canUseModel with assumeAuthConfigured is now side-effect-free: it answers from the current catalog and entitlement caches, no refresh. - The keyless entitlement refresh preserves the previously fetched ids while prime-inference auth is merely stale (a stale marking is not a logout; the auth filter still hides the models until recovery), and still clears them when auth is actually gone.
…omments Reduction pass, no behavior change: shared structured-failure and lockout helpers, merged the two private-model selection pins into one sequential pin, table-driven text-verdict rows, and one-line invariant comments (34 -> 15 added comment lines, 193 -> 154 added test lines vs the stack base).
…t preserve Review follow-ups on the stale-preserve mechanism: - The keyless refresh restored entitlements even after the user selected a different prime-inference team, so wrong-team private models validated and then failed at the provider. The preserve now requires the current team selection to match the snapshot's team; a switch clears instead. - Concurrent refreshAvailableModels calls could snapshot the entitlement set between another call's clear (inside refresh()) and its restore, then restore that empty snapshot - losing the cached authorization permanently despite the preserve rule. Refresh sequences that snapshot and restore the sets now run serialized through one promise chain (getExecutableModels' standalone entitlement refresh included).
snimu
force-pushed
the
sebastian/retry-auth-heuristics-2026-09-04
branch
from
September 4, 2026 17:07
1513dd3 to
567dba4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Deletes the session-layer 401/403 message-text auth sniffing and gives users an explicit recovery path from stale-auth lockouts (Linear: RES-1271; discussion evidence: #1501, #1777, #1611-diagnosability).
Stacked on #2045: it relies on every provider recording structured
provider_stream_failurediagnostics (added there for openai-completions and codex).Why
_isConcreteProviderAuthFailurematched any error containing401/403plus keywords (status code,forbidden,token,credential, ...). Regional service-availability errors, org-policy blocks, and model-access denials are commonly 403s, so one non-auth 403 on one model marked the WHOLE provider auth-stale ([Bug] Cannot switch to another OpenCode Go model after a regional availability error #1501)./modelthen failed with the misleadingModel not foundbecause stale providers' models are excluded from the available list, while a fresh process worked fine ([Bug] Could not restore model anthropic/claude-opus-5. Using prime-inference/z-ai/glm-5.2 #1777).classifyStreamFailuremapped barestatus === 403toauth, so even the structured path over-classified.What
Deletion:
_isConcreteProviderAuthFailure(and the now-unused_getProviderStreamFailureAuthStatus); auth-stale marking now requires structured kindauth.status === 403removed from theauthclassification; a 403 is auth only with an explicit authentication/permission error type. Bare 403 becomes a normal request error (bounded visible retries, no provider lockout).Recovery:
modelRegistry.clearProviderAuthStale(provider)clears both the request-config and auth-storage stale maps.AgentSession.setModelplus the in-process and daemonset_modellookups (the lookups must clear beforerefreshAvailableModels, otherwise the stale provider's models stay excluded and the switch fails withModel not found). A structured auth failure on the next request re-marks the provider.Not changed: the interactive-mode
/modelflow additionally offers a login prompt when it sees an unconfigured/stale provider; that path already recovers via re-login and is untouched.Size
Total src: +102/−65 (net +37); tests: +190/−43 (net +147).
Net src LOC: +32/−37 (net negative; the additions are the clear method and three one-line call sites). Tests: 2 new pins (registry clear restores availability; explicit setModel clears a lockout), 1 new negative pin (unstructured 401 text no longer marks stale), 2 reworked (bare-401 pins converted to structured), 2 classify-table rows.
Validation
Linear: RES-1271
Note
Medium Risk
Changes how auth failures are detected and when providers are marked stale, which affects retries, credential UX, and model availability; mistakes could leave bad keys unflagged or skip legitimate recovery.
Overview
Provider stream failures now distinguish bad credentials from access denials: bare HTTP 403 and permission-shaped errors map to a new
permissionkind (permanent, no retry) instead ofauth, andextractStreamFailureInfono longer infersauth/permissionfrom error message text alone—only structured provider error types or status codes (401/403) decide.Coding-agent stale-auth behavior stops treating regex-matched
401/403text or structuredpermissionfailures as reason to mark a whole provider auth-stale; only structured diagnostics with kindauthtrigger lockout. Explicit model selection can recover: when the only blocker is staleness,setModelvalidates withassumeAuthConfigured, thenclearProviderAuthStalecommits; failed model lookups or entitlement checks leave the lockout intact. Daemon/in-processset_modelcan resolve models hidden from the “available” list while stale. Prime private-model entitlements are preserved across stale-auth refreshes (same team, serialized refresh) so re-selection can validate against cached authorization.Reviewed by Cursor Bugbot for commit 567dba4. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Trust structured auth classification and clear stale auth on explicit model switch
classifyStreamFailurenow requires structured evidence (status 401 or auth/unauthorized type) to classify a failure asauth; 403 and permission-type errors classify aspermission.authorpermissionverdicts; only status codes do when no structured provider error type exists.permissionas a non-retryable stream failure kind inisPermanentProviderFailureKind._isConcreteProviderAuthFailurereturns true only for structuredauthfailures, removing status-code and message heuristics from stale-auth decisions.clearProviderAuthStale; failed lookups leave the provider locked out.runSerializedEntitlementRefresh, and same-team Prime inference entitlements are preserved across stale-auth refreshes.classifyStreamFailureor_isConcreteProviderAuthFailuretreating 403 as auth must update.Macroscope summarized 567dba4.