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:
fangliquanflq
2026-09-18 23:31:42 -07:00
committed by Teknium
parent 604d8803a1
commit 07135710db
2 changed files with 36 additions and 2 deletions

View File

@@ -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:

View File

@@ -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"
# ---------------------------------------------------------------------------