Compare commits

...

1 Commits

Author SHA1 Message Date
Ben
f90afa03cc fix: proactive Codex CLI sync before refresh + retry on failure
OpenAI OAuth refresh tokens are single-use and rotate on every refresh.
When the Codex CLI (or another Hermes profile) refreshes its token,
the pool entry's refresh_token becomes stale. Previously, the sync from
~/.codex/auth.json only ran for EXHAUSTED entries in _available_entries().

Now:
1. _refresh_entry() proactively syncs from ~/.codex/auth.json BEFORE
   attempting the OAuth refresh, picking up tokens refreshed by the
   Codex CLI or VS Code extension.
2. On refresh failure, re-syncs and retries once (mirrors the existing
   Anthropic retry pattern), handling the race where the CLI refreshes
   between the proactive sync and the actual refresh call.
3. If the synced entry has a valid (non-expired) token, uses it
   directly without an unnecessary refresh round-trip.
2026-04-10 08:40:20 +10:00

View File

@@ -513,6 +513,13 @@ class CredentialPool:
except Exception as wexc: except Exception as wexc:
logger.debug("Failed to write refreshed token to credentials file: %s", wexc) logger.debug("Failed to write refreshed token to credentials file: %s", wexc)
elif self.provider == "openai-codex": elif self.provider == "openai-codex":
# Proactively sync from ~/.codex/auth.json before refresh.
# The Codex CLI (or another Hermes profile) may have already
# consumed our refresh_token. Syncing first avoids a
# "refresh_token_reused" error when the CLI has a newer pair.
synced = self._sync_codex_entry_from_cli(entry)
if synced is not entry:
entry = synced
refreshed = auth_mod.refresh_codex_oauth_pure( refreshed = auth_mod.refresh_codex_oauth_pure(
entry.access_token, entry.access_token,
entry.refresh_token, entry.refresh_token,
@@ -598,6 +605,35 @@ class CredentialPool:
# Credentials file had a valid (non-expired) token — use it directly # Credentials file had a valid (non-expired) token — use it directly
logger.debug("Credentials file has valid token, using without refresh") logger.debug("Credentials file has valid token, using without refresh")
return synced return synced
# For openai-codex: the refresh_token may have been consumed by
# the Codex CLI between our proactive sync and the refresh call.
# Re-sync and retry once.
if self.provider == "openai-codex":
synced = self._sync_codex_entry_from_cli(entry)
if synced.refresh_token != entry.refresh_token:
logger.debug("Retrying Codex refresh with synced token from ~/.codex/auth.json")
try:
refreshed = auth_mod.refresh_codex_oauth_pure(
synced.access_token,
synced.refresh_token,
)
updated = replace(
synced,
access_token=refreshed["access_token"],
refresh_token=refreshed["refresh_token"],
last_refresh=refreshed.get("last_refresh"),
last_status=STATUS_OK,
last_status_at=None,
last_error_code=None,
)
self._replace_entry(synced, updated)
self._persist()
return updated
except Exception as retry_exc:
logger.debug("Codex retry refresh also failed: %s", retry_exc)
elif not self._entry_needs_refresh(synced):
logger.debug("Codex CLI has valid token, using without refresh")
return synced
self._mark_exhausted(entry, None) self._mark_exhausted(entry, None)
return None return None