fix(codex): adopt refresh_token from auth.json even without access_token (#70097)
Two defects in the openai-codex credential pool recovery path: Defect 1 — adoption path silently no-ops when store_access is empty _sync_codex_entry_from_auth_store() skipped adoption when the auth store had no access_token (only last_refresh). When another process rotated the token pair, the stale profile's entry kept the consumed refresh_token and replayed it, getting refresh_token_reused and going terminally DEAD. Fix: also adopt when store_refresh differs from entry_refresh, even when store_access is empty. Keep the entry's existing access_token in that case (store_access or entry.access_token). Defect 2 — false 'auth refreshed' success log _try_refresh_codex_client_credentials() returned True whenever resolve_codex_runtime_credentials() returned any non-empty credentials, including the same stale token when the underlying refresh failed. The conversation loop then logged 'auth refreshed after 401' right before the retry failed with the identical token_expired. Fix: compare the access token before/after the refresh. If unchanged, return False so the 401-retry path logs the truth. Fixes #70097
This commit is contained in:
@@ -811,19 +811,43 @@ class CredentialPool:
|
||||
# Adopt auth.json tokens when either side differs. Codex refresh
|
||||
# tokens are single-use too, so a fresh refresh_token from
|
||||
# another process means our entry's pair is consumed/stale.
|
||||
#
|
||||
# Also adopt when the store has a refresh_token but no
|
||||
# access_token — another process may have rotated the pair
|
||||
# and the store entry's access_token was already consumed;
|
||||
# the important signal is the refresh_token difference.
|
||||
entry_access = entry.access_token or ""
|
||||
entry_refresh = entry.refresh_token or ""
|
||||
should_adopt = False
|
||||
if store_access and (
|
||||
store_access != entry_access
|
||||
or (store_refresh and store_refresh != entry_refresh)
|
||||
):
|
||||
should_adopt = True
|
||||
elif (
|
||||
store_refresh
|
||||
and store_refresh != entry_refresh
|
||||
and not store_access
|
||||
):
|
||||
# Store has only a refresh_token (no access_token) —
|
||||
# another process rotated the pair. Adopt the
|
||||
# refresh_token so we don't replay the consumed one.
|
||||
logger.info(
|
||||
"Pool entry %s: auth.json has newer refresh_token "
|
||||
"but no access_token; adopting refresh_token to "
|
||||
"avoid replaying consumed token",
|
||||
entry.id,
|
||||
)
|
||||
should_adopt = True
|
||||
|
||||
if should_adopt:
|
||||
logger.debug(
|
||||
"Pool entry %s: syncing Codex tokens from auth.json "
|
||||
"(refreshed by another process)",
|
||||
entry.id,
|
||||
)
|
||||
field_updates: Dict[str, Any] = {
|
||||
"access_token": store_access,
|
||||
"access_token": store_access or entry.access_token,
|
||||
"refresh_token": store_refresh or entry.refresh_token,
|
||||
"last_status": None,
|
||||
"last_status_at": None,
|
||||
|
||||
Reference in New Issue
Block a user