Skip to content

fix(codex): mark a terminal refresh rejection as needs_relogin - #617

Open
r-uben wants to merge 1 commit into
pleaseai:mainfrom
r-uben:fix/616-codex-needs-relogin
Open

r-uben wants to merge 1 commit into
pleaseai:mainfrom
r-uben:fix/616-codex-needs-relogin

Conversation

@r-uben

@r-uben r-uben commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Summary

A Codex (chatgpt_oauth) pool account whose refresh grant is rejected upstream cooled down for five minutes and then read as Live on the admin dashboard again, forever. The Claude pool already classifies a terminal refresh failure and marks the account needs_relogin; the Codex pool never did.

  • auth::codex::auth gains a typed TerminalRefresh marker (InvalidGrant, NoRefreshToken, WritebackFailed) carried on a new ChatGptAuthError next to the logged detail, mirroring ClaudeResolveError. The token endpoint's status and error body are no longer collapsed into the constant "authentication failed" before the pool sees them.
  • adapters::responses::pool marks needs_relogin from the four paths the Claude pool uses: a terminal failure at resolution or on the 401 → force-refresh (RefreshGrant), a 401 on a token_env credential, and a still-401 retry after a good refresh (ServedRequest). Transient token-endpoint failures keep the plain cooldown.
  • docs/m9-admin-surface.md records the Codex behaviour. Site docs already describe the flag generically for every pool.

Closes #616

Test plan

  • New unit tests: invalid_grant → terminal, 503 → not terminal, no refresh token → terminal.
  • New integration tests in codex_multi_account: an invalid_grant refresh marks account A needs_relogin and serves from B; a 503 only cools A down.
  • cargo fmt --all --check, cargo clippy --all-targets --all-features -- -D warnings.
  • cargo test --all-features --workspace: all pass except three tests in responses_chain_stream that also fail on untouched main on this macOS host (a bound non-listening socket times out instead of refusing).

Summary by cubic

Fixes Codex pool accounts whose refresh grant is terminally rejected (invalid_grant, no refresh token, or lost rotated writeback) being reported as live on the admin dashboard after each five-minute cooldown, forever. These failures now mark the account needs_relogin, matching the Claude pool, while transient token-endpoint failures keep the existing cooldown-only behavior.

  • Marks needs_relogin from the resolution, force-refresh, token_env 401, and still-401-after-refresh paths.
  • Preserves the underlying error detail and OAuth error code in logs instead of collapsing everything to "authentication failed".

Written for commit 63c230b. Summary will update on new commits.

A Codex pool account whose refresh grant is rejected upstream cooled down for
five minutes and then read as Live again, forever. Classify the refresh
failure (invalid_grant, no refresh token, lost rotated writeback) with a typed
marker and mark the account needs_relogin from the resolution, force-refresh,
token_env-401 and still-401-after-refresh paths, as the Claude pool already
does. Transient token-endpoint failures keep the cooldown only.

Closes pleaseai#616

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a structured error-handling mechanism for ChatGPT OAuth authentication by defining a ChatGptAuthError type and a TerminalRefresh enum. This allows the system to distinguish between transient failures and terminal failures, such as invalid refresh tokens. Consequently, the account pool now correctly marks accounts as needs_relogin upon terminal failure, preventing dead accounts from remaining in a continuous cooldown loop. The changes include updates to the account resolution logic, logging, and the addition of comprehensive tests to verify these scenarios. I have no feedback to provide.

@greptile-apps

greptile-apps Bot commented Sep 18, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

This PR should not merge until all failures after refresh-token rotation are classified as terminal when the replacement cannot be persisted.

Fix All in Claude CodeFindings

  1. P1 Rotated Tokens Remain Retryable
  2. P2 Response Bodies Reach Logs
Fix with agent prompt
### Issue 1
src/auth/codex/auth.rs:283-293
Once the provider returns a different refresh token, every failure before that replacement is persisted must be terminal. Here, an access token missing the account-id claim exits through `Err(error)` without writeback, while a blocking write-task failure becomes nonterminal before `rotated` is checked. Either path can leave the consumed refresh token on disk, so the account repeats five-minute cooldowns without being marked `needs_relogin`.

### Issue 2
src/auth/codex/auth.rs:492
The token endpoint's first 200 response characters are copied into `detail` and logged verbatim on refresh failures. A proxy or endpoint response containing newlines, control characters, reflected input, or sensitive diagnostics can therefore corrupt structured logs or disclose response content. Sanitize the body before logging it, or retain only parsed OAuth error fields.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR adds typed terminal-failure classification to Codex OAuth refreshes and propagates those verdicts into account-pool needs_relogin state.

  • Distinguishes rejected or missing refresh grants from transient token-endpoint failures.
  • Marks terminal resolution, forced-refresh, static-token 401, and post-refresh 401 outcomes for operator attention.
  • Preserves detailed server-side refresh errors and adds unit, integration, and administrative-behavior documentation.
  • The rotation handling remains incomplete when credential conversion or the blocking write task fails.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Select Codex account] --> B[Resolve stored credential]
  B --> C{Access token valid?}
  C -->|Yes| D[Send provider request]
  C -->|No| E[Exchange refresh grant]
  E --> F{Refresh outcome}
  F -->|Transient failure| G[Cooldown and rotate]
  F -->|Terminal failure| H[Mark needs_relogin]
  F -->|New token pair| I[Validate credential]
  I --> J[Persist refreshed tokens]
  J --> D
  D --> K{Provider response}
  K -->|401| L[Force refresh and retry]
  K -->|Usable response| M[Mark healthy and relay]
  L -->|Terminal failure| H
  L -->|Retry still 401| H
Loading

Reviews (1) · Last reviewed commit: "fix(codex): mark a terminal refresh reje..."

Comment thread src/auth/codex/auth.rs
Comment on lines 283 to 293
match refreshed.to_credential() {
Ok(credential) => {
tokio::task::spawn_blocking(move || {
write_refreshed_auth(&path, refreshed)
})
.await
.map_err(|error| {
auth_error(format!("ChatGPT auth write task failed: {error}"))
ChatGptAuthError::new(format!(
"ChatGPT auth write task failed: {error}"
))
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Rotated tokens remain retryable

Once the provider returns a different refresh token, every failure before that replacement is persisted must be terminal. Here, an access token missing the account-id claim exits through Err(error) without writeback, while a blocking write-task failure becomes nonterminal before rotated is checked. Either path can leave the consumed refresh token on disk, so the account repeats five-minute cooldowns without being marked needs_relogin.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auth/codex/auth.rs
Line: 283-293

Comment:
**Rotated tokens remain retryable**

Once the provider returns a different refresh token, every failure before that replacement is persisted must be terminal. Here, an access token missing the account-id claim exits through `Err(error)` without writeback, while a blocking write-task failure becomes nonterminal before `rotated` is checked. Either path can leave the consumed refresh token on disk, so the account repeats five-minute cooldowns without being marked `needs_relogin`.

**Knowledge Base Used:**
- [Authentication and account management](https://app.greptile.com/passionfactory/-/custom-context/knowledge-base/pleaseai/shunt/-/docs/authentication.md)
- [Provider OAuth and credential flows](https://app.greptile.com/passionfactory/-/custom-context/knowledge-base/pleaseai/shunt/-/docs/provider-oauth.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

Comment thread src/auth/codex/auth.rs
dashboard, or run `shunt login codex --name <account-name>` again",
));
}
let detail: String = text.chars().take(200).collect();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Response bodies reach logs

The token endpoint's first 200 response characters are copied into detail and logged verbatim on refresh failures. A proxy or endpoint response containing newlines, control characters, reflected input, or sensitive diagnostics can therefore corrupt structured logs or disclose response content. Sanitize the body before logging it, or retain only parsed OAuth error fields.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auth/codex/auth.rs
Line: 492

Comment:
**Response bodies reach logs**

The token endpoint's first 200 response characters are copied into `detail` and logged verbatim on refresh failures. A proxy or endpoint response containing newlines, control characters, reflected input, or sensitive diagnostics can therefore corrupt structured logs or disclose response content. Sanitize the body before logging it, or retain only parsed OAuth error fields.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

@codecov

codecov Bot commented Sep 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.38636% with 31 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/auth/codex/auth.rs 84.02% 23 Missing ⚠️
src/adapters/responses/pool.rs 73.07% 7 Missing ⚠️
src/usage_poll.rs 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will degrade performance by 11.79%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 2 regressed benchmarks
✅ 104 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
parse_body_to_value[50] 756.7 µs 860 µs -12.01%
parse_body_to_value[200] 3 ms 3.4 ms -11.57%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing r-uben:fix/616-codex-needs-relogin (63c230b) with main (bd2cf38)

Open in CodSpeed

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.

pool(codex): a terminal refresh rejection never marks needs_relogin; dead account stays Live

1 participant