From 5d7042e6559e2ba6f581ac9f5bdf80610849c2fc Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 30 Sep 2026 20:37:32 +0200 Subject: [PATCH] hive-agent: warn on usage responses with no recognisable windows addresses argus's review on #4842 (c88740): fetch's success path was silent when the response parsed but windows() found nothing to record. should_warn_empty tracks the same once-per-streak shape as the existing token-skip logging, reset the moment a response has windows again, and never logs the body. Also notes in the observability doc that a past resets_at means the paired percent gauge is stale. --- docs/scheduler/observability.md | 3 ++- hive-agent/src/claude_usage_watch.rs | 39 +++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/docs/scheduler/observability.md b/docs/scheduler/observability.md index 3766afea..648c0f64 100644 --- a/docs/scheduler/observability.md +++ b/docs/scheduler/observability.md @@ -378,7 +378,8 @@ with the OAuth access token from the agent's own `~/.claude/.credentials.json`. The harness only reads that token and never refreshes it — claude owns refresh-token rotation. An agent with no OAuth session (API-key backends, not yet logged in) or an expired token skips the -poll, so a gauge keeps its last value until the next successful poll. +poll, so a gauge keeps its last value until the next successful poll — a +`resets_at` in the past means the paired `percent` is stale. Resource attributes (`service.name`, `agent`, `hive`, `swarm`) come from the same container-wide `OTEL_RESOURCE_ATTRIBUTES` as everything else in this diff --git a/hive-agent/src/claude_usage_watch.rs b/hive-agent/src/claude_usage_watch.rs index f806222f..66ba5b69 100644 --- a/hive-agent/src/claude_usage_watch.rs +++ b/hive-agent/src/claude_usage_watch.rs @@ -83,6 +83,11 @@ pub async fn run() { }; let creds = crate::paths::claude_dir().join(CREDENTIALS_FILE); let mut skip_logged = false; + // Separate streak from `skip_logged`: a token can be usable while the + // response still carries no recognisable window (endpoint change, + // unexpected account state), and that deserves its own once-per-streak + // warning rather than silence. + let mut empty_logged = false; let mut tick = tokio::time::interval(POLL_INTERVAL); tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); loop { @@ -104,7 +109,13 @@ pub async fn run() { skip_logged = false; match fetch(&client, &token).await { Ok(body) => { - for w in windows(&body) { + let windows = windows(&body); + if should_warn_empty(&mut empty_logged, windows.len()) { + tracing::warn!( + "claude usage watch: usage response had no recognisable windows" + ); + } + for w in windows { crate::otel_turn_metrics::record_claude_usage(&w.name, w.percent, w.resets_at); } } @@ -159,6 +170,21 @@ async fn fetch(client: &reqwest::Client, token: &AccessToken) -> Result bool { + if window_count > 0 { + *empty_logged = false; + return false; + } + if *empty_logged { + return false; + } + *empty_logged = true; + true +} + /// Window entries are the top-level objects carrying both `utilization` and /// `resets_at`; other top-level keys (`extra_usage`, `limits`, …) are not /// windows. A window whose `utilization` is `null` is not reported. @@ -242,6 +268,17 @@ mod tests { assert!(matches!(token_from(f, 1000), Token::Expired)); } + /// Empty responses warn once per streak; a non-empty response in + /// between resets it, so a second empty streak warns again. + #[test] + fn empty_windows_warn_once_per_streak() { + let mut empty_logged = false; + assert!(should_warn_empty(&mut empty_logged, 0)); + assert!(!should_warn_empty(&mut empty_logged, 0)); + assert!(!should_warn_empty(&mut empty_logged, 2)); + assert!(should_warn_empty(&mut empty_logged, 0)); + } + #[test] fn missing_or_malformed_credentials_are_absent() { assert!(matches!(token_from(b"{}", 0), Token::Absent));