Skip to content

fix(codex-limits): harden deduped usage refresh#71

Closed
ndycode wants to merge 1 commit into
mainfrom
supersede/pr70-codex-limits
Closed

fix(codex-limits): harden deduped usage refresh#71
ndycode wants to merge 1 commit into
mainfrom
supersede/pr70-codex-limits

Conversation

@ndycode
Copy link
Copy Markdown
Owner

@ndycode ndycode commented Mar 7, 2026

Summary

  • harden codex-limits usage fetching with a timeout
  • preserve active-account labeling across deduplicated refresh-token views
  • keep duplicate stored credentials in sync when refresh-token rotation occurs
  • add regression tests for deduplication, missing refresh tokens, and rotated token propagation

Context

This PR supersedes #70.

I accidentally pushed the fix from #70 to main, reverted that accidental push in 6e18b35, and reapplied the intended change on top of current main in supersede/pr70-codex-limits so it can be reviewed normally here.

Validation

  • npm run lint
  • npm run typecheck
  • npm test

Summary by CodeRabbit

  • Bug Fixes

    • Improved timeout handling for account operations, now throws clearer timeout errors when operations take too long.
    • Fixed account deduplication to properly track active accounts during token rotation.
    • Enhanced token refresh propagation across duplicate accounts for consistent state.
  • Refactor

    • Optimized storage synchronization to reduce redundant data reloading.

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

this PR hardens the codex-limits tool with three targeted fixes: a configurable fetch timeout via AbortController, correct active-account labeling across deduplicated refresh-token views, and fan-out propagation of rotated credentials to all duplicate stored accounts. the changes are well-motivated and the new regression tests cover the main scenarios.

key changes:

  • fetchUsage now wraps every network call in an AbortController timeout sourced from getFetchTimeoutMs(loadPluginConfig())
  • deduplication now coerces refreshToken to a string and treats falsy/empty tokens as non-deduplicable (each missing-token account appears separately)
  • activeRefreshToken is captured before the loop so that the [active] marker follows the credential identity, not just the array index
  • on token rotation, applyRefreshedCredentials fans out to every account sharing previousRefreshToken in storage
  • inline cache-invalidation logic replaced with invalidateAccountManagerCache()

issues found:

  • logic gap: the fan-out in applyRefreshedCredentials only fires when refreshResult.refresh !== previousRefreshToken (rotation). when a non-rotating refresh occurs (new accessToken, same refreshToken), duplicate accounts still get their stale accessToken/expiresAt written back to disk via saveAccounts, which can cause failures in code paths that access those duplicates directly
  • missing test coverage: no vitest test exercises the AbortController abort path — a regression in signal wiring won't be caught, especially on windows where timer behavior under load differs
  • windows concurrency: saveAccountsinvalidateAccountManagerCache is not atomic; antivirus file locks or concurrent execute() calls on windows can read stale tokens between the two operations; no in-code comment documents the redaction strategy for refreshResult.refresh/refreshResult.access in error paths

Confidence Score: 3/5

  • safe to merge after resolving the non-rotated refresh propagation gap, which leaves stale tokens persisted to disk for duplicate accounts
  • the core timeout and deduplication logic is correct, and the rotation fan-out is a real improvement. however the else branch in applyRefreshedCredentials leaves duplicate accounts with stale access tokens on disk after a non-rotating refresh — a real (if limited-blast-radius) logic bug. the missing timeout test and lack of windows concurrency commentary (per repo policy) also hold this back from a higher score.
  • index.ts lines 4390–4398 (non-rotation refresh propagation branch)

Important Files Changed

Filename Overview
index.ts adds fetch timeout via AbortController, deduplication hardening for empty/null refresh tokens, active-account labeling across deduplicated views, and fan-out credential propagation on token rotation; logic gap: non-rotated refreshes don't propagate updated accessToken/expiresAt to duplicate accounts, leaving stale credentials persisted to disk
test/index.test.ts adds three solid regression tests for deduplication, missing-token pass-through, and rotated-token fan-out; missing coverage for the AbortController timeout path introduced in this PR

Sequence Diagram

sequenceDiagram
    participant E as execute()
    participant S as storage.accounts
    participant Q as queuedRefresh
    participant F as fetch (CODEX_BASE_URL)
    participant D as disk (saveAccounts)

    E->>S: loadAccounts()
    E->>E: deduplicate by refreshToken (skip empty tokens)
    E->>E: capture activeRefreshToken

    loop for each uniqueIndex i
        E->>S: read account[i]
        alt accessToken missing or expired
            E->>Q: queuedRefresh(account.refreshToken)
            Q-->>E: {type:"success", access, refresh, expires}
            alt refresh rotated (new != old)
                E->>S: fan-out applyRefreshedCredentials<br/>to ALL accounts with old refreshToken
            else no rotation
                E->>S: applyRefreshedCredentials(account[i] only)
                Note over S: ⚠️ duplicate accounts<br/>retain stale accessToken
            end
        end
        E->>F: GET /wham/usage (with AbortController timeout)
        alt timeout fires
            F-->>E: AbortError → "timed out after Xms"
        else response ok
            F-->>E: UsagePayload JSON
        end
        E->>E: format output lines
    end

    alt storageChanged
        E->>D: saveAccounts(storage)
        E->>E: invalidateAccountManagerCache()
        Note over D,E: ⚠️ not atomic on Windows
    end
Loading

Fix All in Codex

Last reviewed commit: bb446e5

Greptile also left 1 inline comment on this PR.

Context used:

  • Rule used - What: Every code change must explain how it defend... (source)

Co-authored-by: Codex <noreply@openai.com>
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 7, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

The changes enhance account credential management by implementing per-request timeout handling for API calls, tracking active accounts via refresh tokens, propagating rotated credentials across duplicate accounts, and optimizing storage synchronization through cache invalidation rather than in-place reloading. Tests are expanded to validate deduplication logic, active marker preservation, and token rotation propagation.

Changes

Cohort / File(s) Summary
Timeout & Active State Management
index.ts
Adds AbortController-based timeout handling for Codex usage fetch with configurable timeout duration and AbortError wrapping. Introduces activeRefreshToken tracking to determine active accounts, replacing simple activeIndex comparison with token-driven logic. Optimizes storage synchronization via cache invalidation (invalidateAccountManagerCache) instead of in-place reloading after credential refresh.
Credential Propagation
index.ts
Introduces helper mechanism to apply refreshed credentials to either the current account or all accounts sharing a previous refresh token when token rotation occurs. Ensures consistent credential updates across duplicate accounts in the system.
Test Coverage Expansion
test/index.test.ts
Replaces basic deduplication test with comprehensive suite: validates deduplication with active marker preservation, tests handling of accounts missing refresh tokens, and verifies token propagation across duplicate stored accounts during refresh rotation scenarios.

Sequence Diagram

sequenceDiagram
    participant Client
    participant AccountMgr as Account Manager
    participant API as Codex API
    participant Storage

    Client->>AccountMgr: Fetch account limits
    AccountMgr->>AccountMgr: Setup AbortController with timeout
    AccountMgr->>API: Request usage data
    alt Timeout occurs
        API--xAccountMgr: Request aborted
        AccountMgr->>AccountMgr: Clear timeout, throw timeout error
    else Success
        API-->>AccountMgr: Usage response
        AccountMgr->>AccountMgr: Clear timeout
    end

    Client->>AccountMgr: Handle token rotation
    AccountMgr->>Storage: Get duplicate accounts with old token
    AccountMgr->>AccountMgr: Identify all accounts sharing refreshToken
    AccountMgr->>AccountMgr: Update credentials for matching accounts
    AccountMgr->>Storage: Save updated accounts
    AccountMgr->>AccountMgr: Invalidate account manager cache
    AccountMgr->>Client: Return refreshed state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 With timeouts armed and tokens tracked,
Our accounts refresh and react,
When rotations twirl through duplicate lanes,
Each credential flows through matching veins,
The cache awaits its lazy refresh—
No reload bloat, just data fresh! 🚀

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: hardening the deduped usage refresh with specific improvements (timeout handling, active account tracking, token rotation sync).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch supersede/pr70-codex-limits

Comment @coderabbitai help to get the list of available commands and usage tips.

@ndycode ndycode closed this Mar 7, 2026
@ndycode ndycode deleted the supersede/pr70-codex-limits branch March 7, 2026 23:34
Comment thread index.ts
Comment on lines +4390 to +4398
if (previousRefreshToken && refreshResult.refresh !== previousRefreshToken) {
for (const storedAccount of storage.accounts) {
if (storedAccount?.refreshToken === previousRefreshToken) {
applyRefreshedCredentials(storedAccount);
}
}
} else {
applyRefreshedCredentials(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.

Stale credentials persist for non-rotated refreshes

when the server issues a fresh accessToken but keeps the same refreshToken (no rotation), the else branch only updates the single account object in the current iteration. duplicate entries in storage.accounts that share previousRefreshToken are skipped by deduplication and never receive the new accessToken / expiresAt. those stale values are then written back to disk via saveAccounts, so any code path outside this tool that reads those duplicate account objects will still see an expired token.

the fan-out propagation already implemented in the rotation branch should apply unconditionally whenever previousRefreshToken is set, regardless of whether the token was rotated:

if (previousRefreshToken) {
    for (const storedAccount of storage.accounts) {
        if (storedAccount?.refreshToken === previousRefreshToken) {
            applyRefreshedCredentials(storedAccount);
        }
    }
} else {
    applyRefreshedCredentials(account);
}

this also closes a windows token-leakage window: stale tokens sitting on disk can be picked up by an antivirus scanner or a concurrent read before saveAccounts replaces them.

Prompt To Fix With AI
This is a comment left during a code review.
Path: index.ts
Line: 4390-4398

Comment:
**Stale credentials persist for non-rotated refreshes**

when the server issues a fresh `accessToken` but keeps the same `refreshToken` (no rotation), the `else` branch only updates the single `account` object in the current iteration. duplicate entries in `storage.accounts` that share `previousRefreshToken` are skipped by deduplication and never receive the new `accessToken` / `expiresAt`. those stale values are then written back to disk via `saveAccounts`, so any code path outside this tool that reads those duplicate account objects will still see an expired token.

the fan-out propagation already implemented in the rotation branch should apply unconditionally whenever `previousRefreshToken` is set, regardless of whether the token was rotated:

```
if (previousRefreshToken) {
    for (const storedAccount of storage.accounts) {
        if (storedAccount?.refreshToken === previousRefreshToken) {
            applyRefreshedCredentials(storedAccount);
        }
    }
} else {
    applyRefreshedCredentials(account);
}
```

this also closes a windows token-leakage window: stale tokens sitting on disk can be picked up by an antivirus scanner or a concurrent read before `saveAccounts` replaces them.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Comment thread index.ts
Comment on lines +4390 to +4398
if (previousRefreshToken && refreshResult.refresh !== previousRefreshToken) {
for (const storedAccount of storage.accounts) {
if (storedAccount?.refreshToken === previousRefreshToken) {
applyRefreshedCredentials(storedAccount);
}
}
} else {
applyRefreshedCredentials(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.

When refresh tokens do NOT rotate, duplicate accounts are not updated with the new accessToken and expiresAt.

If the refresh succeeds but returns the same refresh token (line 4390 condition is false), only the current account object is updated (line 4397). However, other accounts in storage that share the same refresh token will retain their old expired accessToken and expiresAt values.

Fix by always updating all matching accounts:

if (previousRefreshToken && refreshResult.refresh !== previousRefreshToken) {
    // Token rotated - update all accounts with old token
    for (const storedAccount of storage.accounts) {
        if (storedAccount?.refreshToken === previousRefreshToken) {
            applyRefreshedCredentials(storedAccount);
        }
    }
} else {
    // Token didn't rotate - still need to update all accounts with same token
    const currentToken = refreshResult.refresh;
    for (const storedAccount of storage.accounts) {
        if (storedAccount?.refreshToken === currentToken) {
            applyRefreshedCredentials(storedAccount);
        }
    }
}
Suggested change
if (previousRefreshToken && refreshResult.refresh !== previousRefreshToken) {
for (const storedAccount of storage.accounts) {
if (storedAccount?.refreshToken === previousRefreshToken) {
applyRefreshedCredentials(storedAccount);
}
}
} else {
applyRefreshedCredentials(account);
}
if (previousRefreshToken && refreshResult.refresh !== previousRefreshToken) {
// Token rotated - update all accounts with old token
for (const storedAccount of storage.accounts) {
if (storedAccount?.refreshToken === previousRefreshToken) {
applyRefreshedCredentials(storedAccount);
}
}
} else {
// Token didn't rotate - still need to update all accounts with same token
const currentToken = refreshResult.refresh;
for (const storedAccount of storage.accounts) {
if (storedAccount?.refreshToken === currentToken) {
applyRefreshedCredentials(storedAccount);
}
}
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

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