Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions agent/credential_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ def from_dict(cls, provider: str, payload: Dict[str, Any]) -> "PooledCredential"
data.setdefault("priority", 0)
data.setdefault("source", SOURCE_MANUAL)
data.setdefault("access_token", "")
for ts_key in ("last_status_at", "last_error_reset_at"):
if ts_key in data and data[ts_key] is not None:
data[ts_key] = _parse_absolute_timestamp(data[ts_key])
return cls(provider=provider, **data)

def to_dict(self) -> Dict[str, Any]:
Expand Down Expand Up @@ -277,8 +280,10 @@ def _exhausted_until(entry: PooledCredential) -> Optional[float]:
reset_at = _parse_absolute_timestamp(getattr(entry, "last_error_reset_at", None))
if reset_at is not None:
return reset_at
if entry.last_status_at:
return entry.last_status_at + _exhausted_ttl(entry.last_error_code)
if entry.last_status_at is not None:
ts = _parse_absolute_timestamp(entry.last_status_at)
if ts is not None:
return ts + _exhausted_ttl(entry.last_error_code)
return None


Expand Down
1 change: 1 addition & 0 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@
"juan.ovalle@mistral.ai": "jjovalle99",
"julien.talbot@ergonomia.re": "Julientalbot",
"kagura.chen28@gmail.com": "kagura-agent",
"kagura.agent.ai@gmail.com": "kagura-agent",
"1342088860@qq.com": "youngDoo",
"kamil@gwozdz.me": "kamil-gwozdz",
"skmishra1991@gmail.com": "bugkill3r",
Expand Down
51 changes: 51 additions & 0 deletions tests/agent/test_credential_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,57 @@ def test_exhausted_401_entry_resets_after_five_minutes(tmp_path, monkeypatch):
assert entry.last_status == "ok"


def test_iso_string_last_status_at_does_not_crash_exhausted_until(tmp_path, monkeypatch):
"""Regression: ISO-8601 last_status_at from disk should not cause TypeError.

See https://github.com/NousResearch/hermes-agent/issues/25516
"""
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
_write_auth_store(
tmp_path,
{
"version": 1,
"credential_pool": {
"openai": [
{
"id": "cred-1",
"label": "primary",
"auth_type": "api_key",
"priority": 0,
"source": "manual",
"access_token": "***",
"last_status": "exhausted",
"last_status_at": "2026-05-11T08:23:20.891066+00:00",
"last_error_code": 429,
},
{
"id": "cred-2",
"label": "secondary",
"auth_type": "api_key",
"priority": 1,
"source": "manual",
"access_token": "***",
"last_status": "ok",
"last_status_at": None,
"last_error_code": None,
},
]
},
},
)

from agent.credential_pool import load_pool

pool = load_pool("openai")
# Should not raise TypeError: can't add str and int
entry = pool.select()

assert entry is not None
# ISO timestamp from May 2026 is long expired; cred-1 should be usable again
assert entry.id == "cred-1"
assert entry.last_status == "ok"


def test_explicit_reset_timestamp_overrides_default_429_ttl(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
# Prevent auto-seeding from Codex CLI tokens on the host
Expand Down
Loading