diff --git a/agent/credential_pool.py b/agent/credential_pool.py index 7f27873a7fb88..12463809834b6 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -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]: @@ -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 diff --git a/scripts/release.py b/scripts/release.py index c388116cff6fa..685673d4255ee 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -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", diff --git a/tests/agent/test_credential_pool.py b/tests/agent/test_credential_pool.py index 299567a9a6ff9..c0c7a4b0db6d0 100644 --- a/tests/agent/test_credential_pool.py +++ b/tests/agent/test_credential_pool.py @@ -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