fix(auth): normalise millisecond last_error_reset_at in Codex pool selection
_pool_codex_access_token compared the persisted last_error_reset_at raw, so a millisecond epoch read as far-future and the entry was skipped, while _codex_pool_rate_limit_status normalised the same value to seconds and read it as elapsed. A usable pool credential became invisible to both paths and resolve_codex_runtime_credentials raised "quota exhausted (429); retry after Ns" off the sibling that really was exhausted (#103349). Use the shared _parse_absolute_timestamp normaliser in selection so the two readers can never disagree. Ported from PR #103356. Fixes #103349
This commit is contained in:
@@ -750,11 +750,15 @@ def _pool_codex_access_token() -> str:
|
||||
|
||||
Fallback for ``resolve_codex_runtime_credentials`` when the singleton has no creds.
|
||||
"""
|
||||
from agent.credential_pool import _parse_absolute_timestamp
|
||||
from hermes_cli.auth import _nonempty_str, read_credential_pool
|
||||
try:
|
||||
for entry in _codex_pool_dicts(read_credential_pool("openai-codex")):
|
||||
token, reset_at = entry.get("access_token"), entry.get("last_error_reset_at")
|
||||
in_cooldown = isinstance(reset_at, (int, float)) and reset_at > time.time()
|
||||
token = entry.get("access_token")
|
||||
# Same normaliser as ``_codex_pool_rate_limit_status``: a millisecond epoch compared
|
||||
# raw reads as far-future here and as elapsed there, hiding a usable entry (#103349).
|
||||
reset_at = _parse_absolute_timestamp(entry.get("last_error_reset_at"))
|
||||
in_cooldown = reset_at is not None and reset_at > time.time()
|
||||
if _nonempty_str(token) and not in_cooldown:
|
||||
return token.strip()
|
||||
except Exception:
|
||||
|
||||
@@ -244,6 +244,36 @@ def test_resolver_recovers_when_probe_confirms_reset(tmp_path, monkeypatch):
|
||||
assert entry["last_error_reset_at"] is None
|
||||
|
||||
|
||||
def test_resolver_selects_entry_with_expired_millisecond_reset(tmp_path, monkeypatch):
|
||||
"""#103349: a millisecond ``last_error_reset_at`` that is already in the past must not
|
||||
read as far-future in selection while the rate-limit lookup reads it as elapsed."""
|
||||
now = time.time()
|
||||
store = _pool_only_rate_limited_store(now)
|
||||
main = store["credential_pool"]["openai-codex"][0]
|
||||
main["access_token"] = "tok-main"
|
||||
main["last_error_reset_at"] = (now - 3600) * 1000
|
||||
reserve = dict(main)
|
||||
reserve.update(
|
||||
{
|
||||
"id": "cred-reserve",
|
||||
"access_token": "tok-reserve",
|
||||
"priority": 1,
|
||||
"last_error_reset_at": now + 886,
|
||||
}
|
||||
)
|
||||
store["credential_pool"]["openai-codex"].append(reserve)
|
||||
hermes_home = tmp_path / "hermes"
|
||||
_write_auth_store(hermes_home, store)
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
monkeypatch.setattr(auth_mod, "_probe_codex_quota_restored", lambda token, **kw: False)
|
||||
monkeypatch.setattr(auth_codex, "_probe_codex_quota_restored", lambda token, **kw: False)
|
||||
|
||||
resolved = resolve_codex_runtime_credentials()
|
||||
|
||||
assert resolved["api_key"] == "tok-main"
|
||||
assert resolved["source"] == "credential_pool"
|
||||
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user