From 457d9b73f6a5c115eb4b9bb0d77c4455d5ccc3da Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 09:56:53 -0700 Subject: [PATCH] test/docs(nous): trim keepalive tests to the two invariants; document nous.keepalive_interval_seconds --- tests/hermes_cli/test_nous_auth_keepalive.py | 86 -------------------- website/docs/integrations/nous-portal.md | 7 ++ 2 files changed, 7 insertions(+), 86 deletions(-) diff --git a/tests/hermes_cli/test_nous_auth_keepalive.py b/tests/hermes_cli/test_nous_auth_keepalive.py index 97becdbd45..8d07f4da3b 100644 --- a/tests/hermes_cli/test_nous_auth_keepalive.py +++ b/tests/hermes_cli/test_nous_auth_keepalive.py @@ -1,33 +1,9 @@ from hermes_cli import nous_auth_keepalive as keepalive -from hermes_cli.auth import ACCESS_TOKEN_REFRESH_SKEW_SECONDS # Both lifetimes have been observed on real installs. OBSERVED_LIFETIMES_SECONDS = (3594, 899) -def test_resolved_tick_fits_inside_the_token_lifetime(): - """The tick actually used must land before the credential rolls over. - - A tick at or above TTL - skew can miss the refresh window entirely, which - is what made every hour expire into a 401 plus a re-auth round trip. - - This asserts on the derived tick rather than the configured constant, - because the constant is only a ceiling -- the schedule that ships is - whatever the derivation produces. It therefore fails if the derivation - constants regress (a lower TICKS_PER_LIFETIME or a higher - MIN_INTERVAL_SECONDS both break it), which a bare inequality against the - default interval cannot catch. - """ - for lifetime in OBSERVED_LIFETIMES_SECONDS: - tick = keepalive._tick_seconds( - keepalive.NOUS_AUTH_KEEPALIVE_INTERVAL_SECONDS, lifetime - ) - assert tick < lifetime - ACCESS_TOKEN_REFRESH_SKEW_SECONDS, ( - f"tick={tick}s leaves no room to refresh inside a {lifetime}s " - f"lifetime (skew={ACCESS_TOKEN_REFRESH_SKEW_SECONDS}s)" - ) - - def test_refresh_always_fires_before_expiry_for_observed_lifetimes(): """Simulate the tick schedule and assert no credential expires unrefreshed. @@ -60,54 +36,6 @@ def test_refresh_always_fires_before_expiry_for_observed_lifetimes(): ) -def test_tick_derives_from_observed_lifetime(): - default = keepalive.NOUS_AUTH_KEEPALIVE_INTERVAL_SECONDS - - # A short-lived credential pulls the tick down below the configured default. - assert keepalive._tick_seconds(default, 899) == 899 // 4 - - # A long-lived one is still capped by the configured interval. - assert keepalive._tick_seconds(default, 86400) == default - - # A missing or nonsensical lifetime leaves the configured tick alone. - assert keepalive._tick_seconds(default, None) == default - assert keepalive._tick_seconds(default, 0) == default - - # A pathological lifetime cannot spin the thread. - assert ( - keepalive._tick_seconds(default, 4) - == keepalive.NOUS_AUTH_KEEPALIVE_MIN_INTERVAL_SECONDS - ) - - -def test_refresh_horizon_covers_the_gap_between_ticks(): - # A credential that will not survive until the next tick refreshes now. - assert keepalive._refresh_horizon_seconds(900, 120) == 900 + ( - ACCESS_TOKEN_REFRESH_SKEW_SECONDS - ) - # The caller's floor still applies when ticks are very frequent. - assert keepalive._refresh_horizon_seconds(10, 5000) == 5000 - - -def test_observed_lifetime_takes_the_shorter_credential(monkeypatch): - monkeypatch.setattr( - keepalive, - "get_provider_auth_state", - lambda provider: {"expires_in": 3594, "agent_key_expires_in": 899}, - ) - assert keepalive._observed_lifetime_seconds() == 899 - - monkeypatch.setattr(keepalive, "get_provider_auth_state", lambda provider: {}) - assert keepalive._observed_lifetime_seconds() is None - - monkeypatch.setattr( - keepalive, - "get_provider_auth_state", - lambda provider: {"expires_in": "nonsense"}, - ) - assert keepalive._observed_lifetime_seconds() is None - - def test_interval_precedence_and_disable(monkeypatch): def _config(section): monkeypatch.setattr(keepalive, "_nous_config", lambda: section) @@ -137,20 +65,6 @@ def test_interval_precedence_and_disable(monkeypatch): assert keepalive.start_nous_auth_keepalive() is None -def test_interval_survives_an_unreadable_config(monkeypatch): - """A broken config.yaml must not take the keepalive thread down with it.""" - - def _boom(): - raise RuntimeError("config.yaml is unreadable") - - monkeypatch.setattr("hermes_cli.config.load_config", _boom) - assert keepalive._nous_config() == {} - assert ( - keepalive._interval_seconds(None) - == keepalive.NOUS_AUTH_KEEPALIVE_INTERVAL_SECONDS - ) - - def test_keepalive_refreshes_stale_pool_entry(monkeypatch): class _Entry: access_token = "pooled-access-token" diff --git a/website/docs/integrations/nous-portal.md b/website/docs/integrations/nous-portal.md index ffcbe49409..b574213352 100644 --- a/website/docs/integrations/nous-portal.md +++ b/website/docs/integrations/nous-portal.md @@ -237,6 +237,13 @@ The OAuth refresh token is stored separately at `~/.hermes/auth.json` (not in `c Hermes mints a short-lived JWT from your stored Portal refresh token on each inference call rather than reusing a long-lived API key. The token lifecycle is fully automatic — refresh, mint, retry on transient 401 — and you never see it. +Long-running gateway and dashboard processes also run a background keepalive that refreshes the token before it expires, so idle agents don't pay a 401 round-trip on their first request of each credential lifetime. The keepalive derives its tick from the lifetime the Portal actually issued (several ticks per lifetime), bounded above by: + +```yaml +nous: + keepalive_interval_seconds: 900 # upper bound on the tick; 0 disables the keepalive +``` + If the Portal invalidates the refresh token (password change, manual revoke, session expiry), the invalid refresh token is **quarantined locally** so Hermes stops replaying it and you don't see a stream of identical 401s. The next call surfaces a clear "re-authentication required" message. Run `hermes auth add nous` to log in again; the quarantine clears on the next successful login. ## Troubleshooting