Skip to content

fix(credential-pool): re-select in acquire_lease after a deferred single-use-token refresh - #78299

Merged
kshitijk4poor merged 1 commit into
NousResearch:mainfrom
kshitijk4poor:fix-lease-refresh-reselect
Aug 4, 2026
Merged

fix(credential-pool): re-select in acquire_lease after a deferred single-use-token refresh#78299
kshitijk4poor merged 1 commit into
NousResearch:mainfrom
kshitijk4poor:fix-lease-refresh-reselect

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Follow-up to the #71775 salvage (#77714), found by a post-merge gate sweep of that campaign's merged PRs.

Context — what this fixes, for whom

Anyone using a credential pool whose entries are single-use OAuth tokens (openai-codex, xai-oauth). If every entry in the pool needs a token refresh at the moment a lease is requested — the normal state after an idle period, and the universal state for a single-entry pool — acquire_lease() performs the refresh and then still returns None. The caller sees "no credentials available" and fails a request that was fully answerable: the refresh had just succeeded and the credential was sitting there healthy.

select() does not have this bug. When #71775 moved deferred refreshes outside the pool lock, it gave select() a re-select pass:

# select(), credential_pool.py
if pending_refresh:
    entry, _ = self._select_under_lock()   # retry now that refresh is done

acquire_lease() got the same refactor but not the retry:

chosen_id, pending_refresh = self._acquire_lease_under_lock(credential_id)
if pending_refresh:
    self._refresh_pending_entries(pending_refresh)
return chosen_id          # <- still the pre-refresh answer (None)

Because _acquire_lease_under_lock returns early (if not available: return None, pending_refresh) precisely when a refresh is pending, the None is guaranteed in exactly the case the refresh was meant to resolve.

The fix

Mirror select(): retry the lease once, and only when the first pass came back empty AND a refresh actually ran. No retry for a healthy pool (no wasted selection pass), and no loop when the refresh doesn't help.

Verification

  • New tests/agent/test_credential_pool_lease_refresh_reselect.py: 3 tests — the bug case, a no-double-select guard for the healthy path, and a still-returns-None guard for a failed refresh.
  • Mutation check: with agent/credential_pool.py reverted to main, the bug test FAILS and the two guard tests pass; restored, all 3 pass. The regression test genuinely pins the fix.
  • Full surface: test_credential_pool.py + test_credential_pool_deferred_refresh.py + test_reset_aware_primary_restore.py + the new file → 79 passed.
  • ruff clean.

Provenance

This is a gap-closing pass over the salvage campaign's merged PRs (#77714 / #77631 / #77740 all touched credential_pool.py). Three related checks came back clean and are worth recording: every _available_entries() caller correctly unpacks the tuple (no second dead gate like #77740 fixed), the next_available_at lock probe correctly tests ownership from a helper thread rather than same-thread — which would be vacuous under an RLock — and the RLock downgrade introduced no re-entrancy path that mutates state a caller then reads stale.

…resh

select() re-selects once deferred single-use-token refreshes complete;
acquire_lease() performed the refresh but returned its pre-refresh
answer. Since _acquire_lease_under_lock returns early exactly when a
refresh is pending (if not available: return None, pending_refresh),
a pool whose entries all needed a refresh always returned None — the
caller failed an answerable request right after the refresh succeeded.

Retry once, only when the first pass was empty and a refresh ran.

Post-merge gate-sweep finding on the NousResearch#71775 salvage (NousResearch#77714).
@kshitijk4poor
kshitijk4poor enabled auto-merge (rebase) August 4, 2026 07:39
@kshitijk4poor
kshitijk4poor merged commit db0bd42 into NousResearch:main Aug 4, 2026
35 checks passed
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/auth Authentication, OAuth, credential pools provider/openai OpenAI / Codex Responses API provider/xai xAI (Grok) sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Aug 4, 2026
@kshitijk4poor
kshitijk4poor deleted the fix-lease-refresh-reselect branch August 5, 2026 07:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/openai OpenAI / Codex Responses API provider/xai xAI (Grok) sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants