Skip to content

fix(oauth): preserve non-rotating providers' refresh_token on unrecoverable refresh (#3679) - #3766

Merged
diegosouzapw merged 1 commit into
release/v3.8.24from
fix/3679-preserve-refresh-token
Jun 13, 2026
Merged

diegosouzapw merged 1 commit into
release/v3.8.24from
fix/3679-preserve-refresh-token

Conversation

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Closes #3679

Problem

Google-family OAuth connections (gemini-cli / antigravity / gemini) died after ~1h with refresh_token = NULL and "No valid refresh token available". Reporter's evidence: token present (279 chars) at import → after the first proactive health-check refresh at the 60-min mark, refresh_token became NULL and the connection was unrecoverable.

Root cause

src/lib/tokenHealthCheck.ts — the isUnrecoverableRefreshError deactivation branch wrote refreshToken: null. That null was designed for rotating one-time-use tokens (Codex/OpenAI, whose tokens are genuinely consumed), but it fired for all providers, including non-rotating Google ones. ROTATING_REFRESH_PROVIDERS (already defined in the same function) does NOT include gemini-cli/antigravity/gemini — yet they still got nulled. Nulling the token removes the user's only recovery artifact and produces the "No valid refresh token available" cascade.

Fix

Gate the refreshToken: null write on isRotatingProvider (one-line conditional spread). Non-rotating providers keep their stored refresh_token; rotating providers are unchanged. Also makes the log/lastError message accurate ("rejected" vs "consumed") for non-rotating providers.

Validation (Hard Rule #18 — TDD)

tests/unit/token-health-check.test.ts — new test "preserves refresh_token for non-rotating providers on unrecoverable error (#3679)":

  • RED on pre-fix tree: refresh_token becomes null (assertion fails).
  • GREEN after fix: refresh_token preserved as rt-preserve-3679.
  • Full file: 8/8 pass. eslint 0 errors, typecheck:core clean.

Note: the underlying why of the upstream invalid_grant (Google-side token rotation/limit for imported gemini-cli creds) is environment-specific; this fix removes the destructive NULL + recovery-blocking cascade regardless of that trigger.

…erable refresh (#3679)

The proactive health-check refresh deactivation branch nulled the stored
refresh_token on any unrecoverable error (e.g. invalid_grant). That was
only meant for rotating one-time-use tokens (Codex/OpenAI); for
non-rotating Google-family providers (gemini-cli/antigravity/gemini) it
destroyed the user's only recovery artifact, leaving the connection
permanently showing 'No valid refresh token available'. Gate the null on
isRotatingProvider so non-rotating tokens are preserved.

Closes #3679
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@diegosouzapw
diegosouzapw merged commit d1088f0 into release/v3.8.24 Jun 13, 2026
2 checks passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request fixes an issue where the stored refresh_token of non-rotating OAuth providers (such as Google-family providers) was incorrectly nulled out during a failed proactive health-check refresh. The fix gates the clearing of the refresh token on isRotatingProvider, preserving it for non-rotating providers to allow recovery. A corresponding unit test has been added to prevent regressions. There are no review comments to address, so I have no further feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

testStatus: "expired",
lastError: `Refresh token consumed (${result.error}). Please re-authenticate this account.`,
lastError: isRotatingProvider
? `Refresh token consumed (${result.error}). Please re-authenticate this account.`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Deactivation message hides the actionable OAuth code

For Google/Qwen invalid_grant responses, the refresh functions normalize to { error: "unrecoverable_refresh_error", code: "invalid_grant" }, but this message interpolates only result.error. The new "rejected" wording is more accurate than "consumed", but it still reports the sentinel instead of the underlying OAuth code operators need to diagnose. Use result.code ?? result.error in both ternary arms.

Suggested change
? `Refresh token consumed (${result.error}). Please re-authenticate this account.`
? `Refresh token consumed (${result.code ?? result.error}). Please re-authenticate this account.`

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

// the connection unrecoverable ("No valid refresh token available"). The null was
// only meant for rotating one-time-use tokens (Codex/OpenAI). Non-rotating providers
// must keep the stored refresh_token as the recovery artifact.
test("checkConnection preserves refresh_token for non-rotating providers on unrecoverable error (#3679)", async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Add paired rotating-provider regression coverage

This test verifies the new non-rotating preservation path, but it does not assert the existing rotating-provider behavior still clears refreshToken on unrecoverable refresh. A paired test using a provider in ROTATING_REFRESH_PROVIDERS would guard against accidentally preserving rotating tokens and reintroducing repeated stale-token loops.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

diegosouzapw added a commit that referenced this pull request Jun 13, 2026
… v3.8.24 merges

These 5 files exceeded the frozen baseline solely because of already-merged,
already-reviewed v3.8.24 PRs that landed after this branch froze its baseline:
  - src/lib/providers/validation.ts        4209→4302  (#3767 qwen-web fix)
  - src/shared/components/RequestLoggerV2.tsx 1232→1276 (#3760 logs light-mode)
  - open-sse/executors/antigravity.ts      1553→1572  (#3766 antigravity refresh)
  - open-sse/executors/base.ts             1199→1205  (#3756 xhigh effort)
  - open-sse/translator/response/openai-responses.ts 873→878 (#3762 tool-call integrity)

None are touched by this branch; the bump reflects current release reality.
@kilo-code-bot

kilo-code-bot Bot commented Jun 13, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

CRITICAL

File Line Issue

WARNING

File Line Issue
src/lib/tokenHealthCheck.ts 547 Deactivation message hides the actionable OAuth code by interpolating result.error instead of result.code ?? result.error.

SUGGESTION

File Line Issue
tests/unit/token-health-check.test.ts 478 Add paired rotating-provider regression coverage to ensure rotating providers still clear refreshToken on unrecoverable refresh.
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
src/lib/tokenHealthCheck.ts 388 ROTATING_REFRESH_PROVIDERS omits qwen, which is treated as rotating elsewhere and can return a new refreshToken; this may allow interval refreshes to burn single-use Qwen tokens.
Files Reviewed (3 files)
  • CHANGELOG.md - 0 issues
  • src/lib/tokenHealthCheck.ts - 1 inline issue, 1 other observation
  • tests/unit/token-health-check.test.ts - 1 issue

Fix these issues in Kilo Cloud


Reviewed by nex-n2-pro:free · 1,389,779 tokens

@diegosouzapw
diegosouzapw deleted the fix/3679-preserve-refresh-token branch June 13, 2026 14:18
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
…erable refresh (cheaper-inference#3679) (cheaper-inference#3766)

The proactive health-check refresh deactivation branch nulled the stored
refresh_token on any unrecoverable error (e.g. invalid_grant). That was
only meant for rotating one-time-use tokens (Codex/OpenAI); for
non-rotating Google-family providers (gemini-cli/antigravity/gemini) it
destroyed the user's only recovery artifact, leaving the connection
permanently showing 'No valid refresh token available'. Gate the null on
isRotatingProvider so non-rotating tokens are preserved.

Closes cheaper-inference#3679
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.

1 participant