Skip to content

fix(auth): keep Claude OAuth named pools and stop provider hops - #1304

Open
eddieparc wants to merge 4 commits into
code-yeongyu:mainfrom
eddieparc:fix/claude-sdk-oauth-named-pool
Open

fix(auth): keep Claude OAuth named pools and stop provider hops#1304
eddieparc wants to merge 4 commits into
code-yeongyu:mainfrom
eddieparc:fix/claude-sdk-oauth-named-pool

Conversation

@eddieparc

@eddieparc eddieparc commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Keep a provider-owned Claude SDK OAuth account pool on login instead of appending a fake login-N slot copied from the managed sentinel.
  • Treat a rotation-selected concrete OAuth slot as configured so a second login does not fail with Provider is not configured: claude-sdk-oauth.
  • Do not classify that auth miss as a hard-error model fallback, so the turn stays off other providers such as opengateway.

Root causes

  1. appendLoginSlot always treated the login result as a flat credential. Claude SDK OAuth already returns the full named pool, so the second account became login-2 with sentinel tokens.
  2. With two or more slots, credential rotation projects one slot (no accounts array). oauth.check only counted accounts.length, so every request looked unconfigured.
  3. AgentSession treated that ModelsError("auth") as a hard-error and walked the fallback chain onto another provider that happened to have a key.

Test plan

  • packages/ai credential-pool-resolve-slot.test.ts — named pool preserved; unnamed flat still becomes login-N
  • packages/coding-agent claude-sdk-oauth-login.test.ts — projected non-sentinel slot passes check; sentinel does not
  • packages/coding-agent retry-fallback-hard-error.test.tsProvider is not configured: does not switch models

This is a focused replay of the pool/readiness fixes from #1196 (that branch is far behind main) plus the hard-error hop guard. No internal tracker ids.


Summary by cubic

Fixes Claude SDK OAuth login, retry, session continuity, and interactive login behavior so account selection stays stable and transient failures do not trigger unrelated provider fallbacks.

Bug Fixes

  • Preserves provider-owned named OAuth pools instead of appending a fake login-N slot copied from the managed sentinel.
  • Treats a rotation-selected concrete OAuth slot as configured while excluding the synthetic sentinel.
  • Keeps auth misses, session lock contention, stream-start timeouts, and bare invalid_request on the same model instead of hopping providers.
  • Forks persisted Claude SDK bindings at the last assistant UUID when account, model, prompt, or toolset options drift, avoiding a full transcript flatten.
  • Remounts the single login Input widget so a browser callback no longer displays two live > prompts.

Written for commit 501c106. Summary will update on new commits.

Review in cubic

Preserve provider-owned account pools on login so a second Claude SDK
OAuth account keeps its prompted name and tokens instead of a sentinel
login-N slot. Count a rotation-selected concrete OAuth slot as
configured, and do not treat Provider is not configured as a hard-error
model fallback onto another provider.

Ultraworked with [omo](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 11 files

You’re at about 99% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/coding-agent/src/core/agent-session.ts">

<violation number="1" location="packages/coding-agent/src/core/agent-session.ts:7381">
P3: This guard couples fallback behavior to the exact English wording of an error message that is generated in two other files (`Provider is not configured: ${model.provider}` at packages/ai/src/models.ts:680 and packages/coding-agent/src/core/model-runtime.ts:711), with no shared constant. If either generator is reworded or localized, the startsWith check silently stops matching and the auth-miss hard-error hop this fix is meant to prevent quietly returns. Export a shared prefix constant (e.g. in packages/ai where ModelsError is defined) and reference it in both throw sites and this guard, as is done with TURN_RETRY_SUPPRESSION_PREFIX.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

private _isHardErrorFallbackEligible(message: AssistantMessage): boolean {
return (
!message.errorMessage?.startsWith(TURN_RETRY_SUPPRESSION_PREFIX) &&
!message.errorMessage?.startsWith("Provider is not configured:") &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This guard couples fallback behavior to the exact English wording of an error message that is generated in two other files (Provider is not configured: ${model.provider} at packages/ai/src/models.ts:680 and packages/coding-agent/src/core/model-runtime.ts:711), with no shared constant. If either generator is reworded or localized, the startsWith check silently stops matching and the auth-miss hard-error hop this fix is meant to prevent quietly returns. Export a shared prefix constant (e.g. in packages/ai where ModelsError is defined) and reference it in both throw sites and this guard, as is done with TURN_RETRY_SUPPRESSION_PREFIX.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/coding-agent/src/core/agent-session.ts, line 7381:

<comment>This guard couples fallback behavior to the exact English wording of an error message that is generated in two other files (`Provider is not configured: ${model.provider}` at packages/ai/src/models.ts:680 and packages/coding-agent/src/core/model-runtime.ts:711), with no shared constant. If either generator is reworded or localized, the startsWith check silently stops matching and the auth-miss hard-error hop this fix is meant to prevent quietly returns. Export a shared prefix constant (e.g. in packages/ai where ModelsError is defined) and reference it in both throw sites and this guard, as is done with TURN_RETRY_SUPPRESSION_PREFIX.</comment>

<file context>
@@ -7378,6 +7378,7 @@ export class AgentSession {
 	private _isHardErrorFallbackEligible(message: AssistantMessage): boolean {
 		return (
 			!message.errorMessage?.startsWith(TURN_RETRY_SUPPRESSION_PREFIX) &&
+			!message.errorMessage?.startsWith("Provider is not configured:") &&
 			message.stopReason === "error" &&
 			!isContextOverflow(message, this.model?.contextWindow ?? 0) &&
</file context>

Remount the single Input widget when the account-name prompt follows an
unsubmitted paste-code field, so a browser-callback login no longer shows
two stacked > rows.

Ultraworked with [omo](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files (changes from recent commits).

You’re at about 99% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/coding-agent/src/modes/interactive/components/login-dialog.ts">

<violation number="1" location="packages/coding-agent/src/modes/interactive/components/login-dialog.ts:85">
P3: The new liveHint dedup only covers showManualInput and showPrompt. showWaiting, showDetails, and showInfo(showCloseHint) still append their own (to cancel)/(to close) hint rows without removing a previously remounted hint, so an interleaved wait/info step leaves a stale hint beside the live one. Route those hint additions through the same remount/tracking so only one hint row is ever live.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}

/** The Input widget is a single instance; mounting it twice paints two live `>` rows. */
private remountInput(hint: Text): void {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new liveHint dedup only covers showManualInput and showPrompt. showWaiting, showDetails, and showInfo(showCloseHint) still append their own (to cancel)/(to close) hint rows without removing a previously remounted hint, so an interleaved wait/info step leaves a stale hint beside the live one. Route those hint additions through the same remount/tracking so only one hint row is ever live.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/coding-agent/src/modes/interactive/components/login-dialog.ts, line 85:

<comment>The new liveHint dedup only covers showManualInput and showPrompt. showWaiting, showDetails, and showInfo(showCloseHint) still append their own (to cancel)/(to close) hint rows without removing a previously remounted hint, so an interleaved wait/info step leaves a stale hint beside the live one. Route those hint additions through the same remount/tracking so only one hint row is ever live.</comment>

<file context>
@@ -80,6 +81,16 @@ export class LoginDialogComponent extends Container implements Focusable {
 	}
 
+	/** The Input widget is a single instance; mounting it twice paints two live `>` rows. */
+	private remountInput(hint: Text): void {
+		this.contentContainer.children = this.contentContainer.children.filter(
+			(child) => child !== this.input && child !== this.liveHint,
</file context>

eddieparc and others added 2 commits September 3, 2026 07:11
Classify "Lock file is already being held" as a transient retry and remint
the same model instead of hard-error-falling back onto another provider.

Ultraworked with [omo](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
After a stream-start timeout the live registry entry is gone, so prompt/toolset
drift flattened megabytes of transcript, then invalid_request hard-hopped onto
an unauthenticated OpenGateway route. Fork the persisted binding instead, and
remint stream stalls plus bare invalid_request on the same model.

Ultraworked with [omo](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
code-yeongyu added a commit that referenced this pull request Sep 3, 2026
A provider whose own login returns the complete pooled credential was double-pooled: appendLoginSlot read that result's top-level fields as a flat credential and appended them as a second slot. For claude-sdk-oauth those fields are the managed sentinel, so a second account produced a login-2 slot holding placeholder tokens.

Adopts the slots.ts hunk from #1304 and #1196 by @eddieparc.

Ultraworked with [omo](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
code-yeongyu added a commit that referenced this pull request Sep 3, 2026
…d account

Shared credential rotation projects one named slot onto the flat credential shape and strips accounts, so the availability predicate counted zero accounts and reported "Provider is not configured: claude-sdk-oauth". A projected sentinel still counts as zero, leaving the ambient opt-in path unchanged.

Adopts the oauth-login.ts hunk from #1304 and #1196 by @eddieparc.

Ultraworked with [omo](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
@code-yeongyu

Copy link
Copy Markdown
Owner

#1309 (merged as 10f58c0) adopted the appendLoginSlot provider-owned-pool guard and the configuredFor projected-slot count from this PR (and #1196), credited to @eddieparc in the PR body, and closes #1279. The remaining hunks here (agent-session hard-error classification, retry.ts, session-continuity, login-dialog) were deliberately left out of that PR because they change fallback policy rather than login storage - they can be rebased onto main as a focused follow-up if you still want them; happy to review that separately.

@code-yeongyu

Copy link
Copy Markdown
Owner

Thanks @eddieparc — the Claude SDK OAuth named-pool / projected-slot fixes from this PR were adopted on main via #1309 (e12ae5b08 fix(ai): keep a provider-owned credential pool intact on login, d3dbe39be fix(claude-sdk-oauth): treat a rotation-projected slot as a configured account; merge 10f58c073). Those hunks are credited in the #1309 changes.md entries.

Please do not merge this branch onto current main (6dd662c4b). GitHub reports CONFLICTING/DIRTY, the pool/readiness files are now duplicates, and the leftover AgentSession / continuity edits change fallback policy in ways that collide with #1312 and #1318.

Remaining work if this PR stays open:

  1. Rebase onto main (6dd662c4b) and drop slots.ts, oauth-login.ts, the duplicate login/pool tests, and the fix(auth): keep provider-owned Claude OAuth pools intact on login #1309 changelog/changes.md entries.
  2. Drop the AgentSession Provider is not configured: hard-error exclusion. The rotation false-positive is gone after fix(auth): keep provider-owned Claude OAuth pools intact on login #1309; the guard now blocks legitimate fallback for every unconfigured provider (models.ts:680, model-runtime.ts:711).
  3. Drop the bare invalid_request same-model remint. fix(claude-sdk-oauth): surface SDK error text and classify is_error results #1312 classifies invalid_request as non-retryable (errors.ts:17) and landed is_error → failover / model fallback on purpose (Claude SDK ambient 429 result with success subtype bypasses model fallback #1169).
  4. Drop the stream-stall same-model remint. Stalls are already retryable; _handleRetryableError documents hop-after-budget (agent-session.ts:7617-7621). _isClaudeSdkSameModelRemintError is also not provider-scoped.
  5. Keep, isolate: login-dialog.ts remount so a browser callback cannot paint two live > prompts (still broken at login-dialog.ts:140 and :160). Test: packages/coding-agent/test/suite/regressions/5433-extension-oauth-prompt-input.test.ts. Optional follow-up: route showWaiting / showInfo hints through the same remount.
  6. Keep, isolate, justify: classify Lock file is already being held as retryable in packages/ai/src/utils/retry.ts and, if you still want never-hop, same-model remint only for that lock string in AgentSession. Do not bundle stall/invalid_request. Tests: packages/ai/test/retry.test.ts, lock cases in retry-fallback-hard-error.test.ts.
  7. Keep only after a fix(claude-sdk-oauth): never resume an SDK session id that init never confirmed #1318-aware rebase: fork a confirmed persisted binding on options/model drift instead of flatten (decideFromBinding at session-continuity.ts:157), leaving unconfirmed ids on flatten(session_unconfirmed) via withoutUnconfirmedResume (:143-151, :235). Do not fork account_changed without the failover lane's cross-account check. Rewrite claude-sdk-oauth-restored-security.test.ts and claude-sdk-oauth-continuity-retry-checkpoint.test.ts; run claude-sdk-oauth-unconfirmed-binding.test.ts so #7562 does not regress.
  8. Refresh changes.md + Unreleased CHANGELOG only for hunks that remain. The 2026-09-02 pool/auth-miss write-ups are already on main.

Until that split/rebase, this stays open and unmerged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants