From 2cbb67d2a9d6036f25cf2fa2f099e860ae8ce5b0 Mon Sep 17 00:00:00 2001 From: Anthony Greco Date: Thu, 6 Aug 2026 19:36:14 -0400 Subject: [PATCH 1/2] Fix Codex weekly window detection --- .../src-tauri/src/commands/bridge.rs | 18 +- .../providers/sections/UsageSection.test.tsx | 19 ++ .../providers/sections/UsageSection.tsx | 23 +- rust/src/cli/usage.rs | 29 +- rust/src/providers/codex/api.rs | 297 ++++++++++++++---- 5 files changed, 304 insertions(+), 82 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index 5dee459180..8299c10759 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -157,7 +157,16 @@ impl ProviderUsageSnapshot { ) -> Self { let usage = &result.usage; - let primary_pace = codexbar::core::UsagePace::weekly(&usage.primary, None, 10080); + // A missing session is represented by an informational primary so the + // weekly lane keeps its canonical role. Use that weekly lane for the + // provider-level pace summary instead of returning no pace at all. + let primary_pace_window = if usage.primary.is_informational { + usage.secondary.as_ref() + } else { + Some(&usage.primary) + }; + let primary_pace = primary_pace_window + .and_then(|window| codexbar::core::UsagePace::weekly(window, None, 10080)); let pace = primary_pace.as_ref().map(|p| PaceSnapshot { stage: pace_stage_str(p.stage), @@ -311,6 +320,13 @@ pub(crate) fn compact_tray_status_label( window: &RateWindowSnapshot, lang: codexbar::settings::Language, ) -> String { + if window.is_informational { + return window + .reset_description + .clone() + .unwrap_or_else(|| "Unavailable".to_string()); + } + let pct = format!("{:.0}%", window.used_percent); if let Some(reset) = compact_reset_description(window, lang) { format!("{pct} • {reset}") diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.test.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.test.tsx index 41b962f378..fd1072f12e 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.test.tsx @@ -80,4 +80,23 @@ describe("UsageSection", () => { expect(await screen.findByText("Additional Budget")).toBeInTheDocument(); expect(screen.getByText("42%")).toBeInTheDocument(); }); + + it("marks an unavailable session without rendering a quota bar", async () => { + const detail = provider(); + detail.session = { + ...rateWindow(0), + isInformational: true, + resetDescription: "No active 5h session", + }; + + render( + + key} /> + , + ); + + const label = await screen.findByText("ProviderSessionLabel"); + expect(label.parentElement).toHaveTextContent("No active 5h session"); + expect(label.parentElement?.querySelector(".provider-usage-bar__track")).toBeNull(); + }); }); diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.tsx index 56f0a4456a..a51c294ed2 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.tsx @@ -93,6 +93,7 @@ function UsageBar({ }) { const usedPct = Number.isFinite(rate.usedPercent) ? Math.max(0, rate.usedPercent) : 0; const pct = Math.min(100, usedPct); + const isInformational = rate.isInformational === true; const formattedReset = useFormattedResetTime( rate.resetsAt, rate.resetDescription, @@ -112,21 +113,25 @@ function UsageBar({ className="provider-usage-bar__pct" data-exhausted={rate.isExhausted || undefined} > - {rate.isExhausted + {isInformational + ? rate.resetDescription?.trim() || formattedReset || "—" + : rate.isExhausted ? usedPct > 100 ? `${usedPct.toFixed(0)}%` : t("DetailWindowExhausted") : `${usedPct.toFixed(0)}%`} -
-
-
- {resetHint && ( + {!isInformational && ( +
+
+
+ )} + {!isInformational && resetHint && ( {resetHint} )}
diff --git a/rust/src/cli/usage.rs b/rust/src/cli/usage.rs index 3d5bc5e254..ba10dd2d92 100755 --- a/rust/src/cli/usage.rs +++ b/rust/src/cli/usage.rs @@ -496,6 +496,12 @@ fn append_usage_window_lines( } fn append_window_line(lines: &mut Vec, label: &str, window: &RateWindow, use_color: bool) { + if window.is_informational { + let description = window.reset_description.as_deref().unwrap_or("unavailable"); + lines.push(format!(" {:<8} {}", format!("{}:", label), description)); + return; + } + let bar = render_progress_bar(window.used_percent, 20, use_color); let reset = window .format_countdown() @@ -547,15 +553,20 @@ fn append_model_specific_line( pub fn render_brief_text(provider: ProviderId, result: &ProviderFetchResult) -> String { let metadata = instantiate_provider(provider).metadata().clone(); let usage = &result.usage; - let reset = usage - .primary - .format_countdown() - .unwrap_or_else(|| "n/a".to_string()); - let mut parts = vec![format!( - "{} {}", - metadata.session_label, - format_percent(usage.primary.used_percent) - )]; + let mut parts = Vec::new(); + let reset = if usage.primary.is_informational { + parts.push(format!("{} unavailable", metadata.session_label)); + usage.secondary.as_ref().unwrap_or(&usage.primary) + } else { + parts.push(format!( + "{} {}", + metadata.session_label, + format_percent(usage.primary.used_percent) + )); + &usage.primary + } + .format_countdown() + .unwrap_or_else(|| "n/a".to_string()); if let Some(secondary) = &usage.secondary { parts.push(format!( "{} {}", diff --git a/rust/src/providers/codex/api.rs b/rust/src/providers/codex/api.rs index d411817aa9..5e85982437 100755 --- a/rust/src/providers/codex/api.rs +++ b/rust/src/providers/codex/api.rs @@ -2,7 +2,10 @@ //! //! Uses OAuth tokens stored by the Codex CLI in ~/.codex/auth.json -use crate::core::{CostSnapshot, NamedRateWindow, ProviderError, RateWindow, UsageSnapshot}; +use crate::core::{ + CostSnapshot, NamedRateWindow, ProviderError, RateWindow, SESSION_WINDOW_MINUTES, + UsageSnapshot, WEEKLY_WINDOW_MINUTES, +}; use chrono::{DateTime, TimeZone, Utc}; use serde::Deserialize; use std::path::PathBuf; @@ -333,34 +336,28 @@ impl CodexApi { if let Some(rate_limit) = json.get("rate_limit") { let primary_opt = rate_limit .get("primary_window") - .map(|w| self.parse_window(w)); + .and_then(|w| self.parse_window_if_present(w)); let secondary_opt = rate_limit .get("secondary_window") - .map(|w| self.parse_window(w)); + .and_then(|w| self.parse_window_if_present(w)); let code_review = rate_limit .get("code_review_window") - .map(|w| self.parse_window(w)); + .and_then(|w| self.parse_window_if_present(w)); - // If primary is missing, promote secondary to primary (weekly-only plans) - let (primary, secondary) = match (primary_opt, secondary_opt) { - (Some(p), s) => (p, s), - (None, Some(s)) => (s, None), - (None, None) => (RateWindow::new(0.0), None), - }; + let (primary, secondary) = normalize_named_windows(primary_opt, secondary_opt); return (primary, secondary, code_review); } // Try rate_limits array - if let Some(rate_limits) = json.get("rate_limits").and_then(|v| v.as_array()) - && let Some(first) = rate_limits.first() - { - let primary = self.parse_window(first); - let secondary = rate_limits.get(1).map(|w| self.parse_window(w)); - let code_review = rate_limits.get(2).map(|w| self.parse_window(w)); - return (primary, secondary, code_review); + if let Some(rate_limits) = json.get("rate_limits").and_then(|v| v.as_array()) { + let windows = rate_limits + .iter() + .filter_map(|window| self.parse_window_if_present(window)) + .collect::>(); + return normalize_array_windows(windows); } // Try direct fields @@ -382,12 +379,12 @@ impl CodexApi { let window_minutes = window .get("limit_window_seconds") - .and_then(|v| v.as_i64()) - .map(|s| (s / 60) as u32); + .and_then(json_i64) + .and_then(|seconds| u32::try_from(seconds / 60).ok()); let reset_at = window .get("reset_at") - .and_then(|v| v.as_i64()) + .and_then(json_i64) .and_then(|ts| Utc.timestamp_opt(ts, 0).single()); RateWindow::with_details( @@ -398,6 +395,10 @@ impl CodexApi { ) } + fn parse_window_if_present(&self, window: &serde_json::Value) -> Option { + (!window.is_null() && !is_placeholder_window(window)).then(|| self.parse_window(window)) + } + fn extract_additional_rate_limits(&self, json: &serde_json::Value) -> Vec { json.get("additional_rate_limits") .and_then(|v| v.as_array()) @@ -493,52 +494,25 @@ impl CodexApi { &self, response: UsageResponse, ) -> Result<(UsageSnapshot, Option), ProviderError> { - // Extract primary rate window - let primary = if let Some(ref rate_limit) = response.rate_limit { - if let Some(ref primary_window) = rate_limit.primary_window { - let reset_at = timestamp_to_datetime(primary_window.reset_at); - RateWindow::with_details( - primary_window.used_percent as f64, - primary_window.limit_window_seconds.map(|s| (s / 60) as u32), - reset_at, - format_reset_countdown(reset_at), - ) - } else { - RateWindow::new(0.0) - } - } else { - RateWindow::new(0.0) - }; - - // Extract secondary rate window - let secondary = response - .rate_limit - .as_ref() - .and_then(|rl| rl.secondary_window.as_ref()) - .map(|window| { - let reset_at = timestamp_to_datetime(window.reset_at); - RateWindow::with_details( - window.used_percent as f64, - window.limit_window_seconds.map(|s| (s / 60) as u32), - reset_at, - format_reset_countdown(reset_at), - ) - }); + let (primary, secondary) = normalize_named_windows( + response + .rate_limit + .as_ref() + .and_then(|rate_limit| rate_limit.primary_window.as_ref()) + .map(rate_window_from_snapshot), + response + .rate_limit + .as_ref() + .and_then(|rate_limit| rate_limit.secondary_window.as_ref()) + .map(rate_window_from_snapshot), + ); // Extract code review rate window let code_review = response .rate_limit .as_ref() - .and_then(|rl| rl.code_review_window.as_ref()) - .map(|window| { - let reset_at = timestamp_to_datetime(window.reset_at); - RateWindow::with_details( - window.used_percent as f64, - window.limit_window_seconds.map(|s| (s / 60) as u32), - reset_at, - format_reset_countdown(reset_at), - ) - }); + .and_then(|rate_limit| rate_limit.code_review_window.as_ref()) + .map(rate_window_from_snapshot); // Build usage snapshot let login_method = response.plan_type.as_ref().map(|pt| match pt.as_str() { @@ -593,6 +567,123 @@ impl CodexApi { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CodexWindowRole { + Session, + Weekly, + Unknown, +} + +fn codex_window_role(window: &RateWindow) -> CodexWindowRole { + match window.window_minutes { + Some(minutes) if minutes == SESSION_WINDOW_MINUTES => CodexWindowRole::Session, + Some(minutes) if minutes >= WEEKLY_WINDOW_MINUTES => CodexWindowRole::Weekly, + _ => CodexWindowRole::Unknown, + } +} + +/// Keep the shared `UsageSnapshot` shape while making an absent session explicit. +/// Informational primaries are omitted from quota math and rendered as unavailable. +fn no_session_primary() -> RateWindow { + let mut window = RateWindow::with_details( + 0.0, + Some(SESSION_WINDOW_MINUTES), + None, + Some("No active 5h session".to_string()), + ); + window.is_informational = true; + window +} + +/// Normalize the named `primary_window`/`secondary_window` fields by duration. +fn normalize_named_windows( + primary: Option, + secondary: Option, +) -> (RateWindow, Option) { + match (primary, secondary) { + (None, None) => (no_session_primary(), None), + (Some(window), None) => { + if codex_window_role(&window) == CodexWindowRole::Weekly { + (no_session_primary(), Some(window)) + } else { + (window, None) + } + } + (None, Some(window)) => { + if codex_window_role(&window) == CodexWindowRole::Weekly { + (no_session_primary(), Some(window)) + } else { + (window, None) + } + } + (Some(primary), Some(secondary)) => { + match (codex_window_role(&primary), codex_window_role(&secondary)) { + (CodexWindowRole::Weekly, CodexWindowRole::Session) => (secondary, Some(primary)), + (CodexWindowRole::Weekly, CodexWindowRole::Unknown) => { + (no_session_primary(), Some(primary)) + } + (CodexWindowRole::Unknown, CodexWindowRole::Session) => (secondary, Some(primary)), + (CodexWindowRole::Session, CodexWindowRole::Weekly) + | (CodexWindowRole::Unknown, CodexWindowRole::Weekly) => (primary, Some(secondary)), + _ => (primary, Some(secondary)), + } + } + } +} + +/// Normalize an array of Codex windows without relying on the API's ordering. +fn normalize_array_windows( + windows: Vec, +) -> (RateWindow, Option, Option) { + if windows.is_empty() { + return (no_session_primary(), None, None); + } + + // Preserve the old positional fallback when the API provides no role + // metadata at all. There is no safe way to infer session vs weekly then. + if !windows + .iter() + .any(|window| codex_window_role(window) != CodexWindowRole::Unknown) + { + let mut windows = windows.into_iter(); + return ( + windows.next().unwrap_or_else(no_session_primary), + windows.next(), + windows.next(), + ); + } + + let mut session = None; + let mut weekly = None; + let mut remaining = Vec::new(); + + for window in windows { + match codex_window_role(&window) { + CodexWindowRole::Session if session.is_none() => session = Some(window), + CodexWindowRole::Weekly if weekly.is_none() => weekly = Some(window), + _ => remaining.push(window), + } + } + + ( + session.unwrap_or_else(no_session_primary), + weekly, + remaining.into_iter().next(), + ) +} + +fn rate_window_from_snapshot(window: &WindowSnapshot) -> RateWindow { + let reset_at = timestamp_to_datetime(window.reset_at); + RateWindow::with_details( + window.used_percent as f64, + window + .limit_window_seconds + .and_then(|seconds| u32::try_from(seconds / 60).ok()), + reset_at, + format_reset_countdown(reset_at), + ) +} + impl Default for CodexApi { fn default() -> Self { Self::new() @@ -760,6 +851,12 @@ fn json_f64(value: &serde_json::Value) -> Option { .or_else(|| value.as_str()?.trim().parse::().ok()) } +fn json_i64(value: &serde_json::Value) -> Option { + value + .as_i64() + .or_else(|| value.as_str()?.trim().parse::().ok()) +} + fn is_placeholder_window(window: &serde_json::Value) -> bool { let has_usage = window .get("used_percent") @@ -768,9 +865,9 @@ fn is_placeholder_window(window: &serde_json::Value) -> bool { .is_some(); let has_duration = window .get("limit_window_seconds") - .and_then(|v| v.as_i64().or_else(|| v.as_str()?.parse::().ok())) + .and_then(json_i64) .is_some(); - let has_reset = window.get("reset_at").is_some(); + let has_reset = window.get("reset_at").and_then(json_i64).is_some(); !has_usage && !has_duration && !has_reset } @@ -1156,6 +1253,80 @@ mod tests { ); } + #[test] + fn keeps_weekly_window_in_secondary_when_session_is_absent() { + let api = CodexApi::new(); + let (usage, _) = api + .build_result_from_json(&json!({ + "rate_limit": { + "secondary_window": { + "used_percent": 25, + "limit_window_seconds": 604800, + "reset_at": 1783036800 + } + } + })) + .expect("codex usage"); + + assert!(usage.primary.is_informational); + assert_eq!(usage.primary.window_minutes, Some(300)); + assert_eq!( + usage.primary.reset_description.as_deref(), + Some("No active 5h session") + ); + + let weekly = usage.secondary.expect("weekly window"); + assert!(!weekly.is_informational); + assert_eq!(weekly.used_percent, 25.0); + assert_eq!(weekly.window_minutes, Some(10080)); + } + + #[test] + fn identifies_rate_limit_array_windows_by_duration() { + let api = CodexApi::new(); + let (usage, _) = api + .build_result_from_json(&json!({ + "rate_limits": [ + { + "used_percent": 25, + "limit_window_seconds": 604800, + "reset_at": 1783036800 + }, + { + "used_percent": 10, + "limit_window_seconds": 18000, + "reset_at": 1783018800 + } + ] + })) + .expect("codex usage"); + + assert!(!usage.primary.is_informational); + assert_eq!(usage.primary.used_percent, 10.0); + assert_eq!(usage.primary.window_minutes, Some(300)); + + let weekly = usage.secondary.expect("weekly window"); + assert_eq!(weekly.used_percent, 25.0); + assert_eq!(weekly.window_minutes, Some(10080)); + } + + #[test] + fn identifies_weekly_only_rate_limit_array_without_a_session() { + let api = CodexApi::new(); + let (usage, _) = api + .build_result_from_json(&json!({ + "rate_limits": [{ + "used_percent": 25, + "limit_window_seconds": 604800, + "reset_at": 1783036800 + }] + })) + .expect("codex usage"); + + assert!(usage.primary.is_informational); + assert_eq!(usage.secondary.expect("weekly window").used_percent, 25.0); + } + #[test] fn maps_codex_spark_additional_rate_limits() { let api = CodexApi::new(); From 55406356a325fe21e6dbf55ab0e750f8d1c133ba Mon Sep 17 00:00:00 2001 From: Finesssee Date: Sat, 8 Aug 2026 13:40:02 +0700 Subject: [PATCH 2/2] refactor(core): share RateWindow::no_active_session across providers Codex no_session_primary() and Claude synthetic_no_session_primary() were duplicate provider-local copies of the same informational 5h session placeholder. Promote the shared shape to a canonical RateWindow::no_active_session() built on RateWindow::informational() plus SESSION_WINDOW_MINUTES, and call it from both providers. --- rust/src/core/rate_window.rs | 28 ++++++++++++++++++++++++++++ rust/src/providers/claude/web_api.rs | 18 ++---------------- rust/src/providers/codex/api.rs | 27 +++++++-------------------- 3 files changed, 37 insertions(+), 36 deletions(-) diff --git a/rust/src/core/rate_window.rs b/rust/src/core/rate_window.rs index 25038e559e..70ad9adde4 100755 --- a/rust/src/core/rate_window.rs +++ b/rust/src/core/rate_window.rs @@ -1,5 +1,6 @@ //! Rate window model - represents a usage limit window (e.g., 5-hour session, 7-day weekly) +use super::session_equivalent_forecast::SESSION_WINDOW_MINUTES; use chrono::{DateTime, Datelike, Utc}; use serde::{Deserialize, Serialize}; @@ -47,6 +48,19 @@ impl RateWindow { } } + /// Informational placeholder for an absent 5-hour session lane. + /// + /// Weekly-only plans (Codex, Claude web) occasionally report no active + /// session window. Informational primaries are omitted from quota math + /// and rendered as unavailable, so one canonical shape keeps providers + /// and downstream surfaces from diverging. + pub fn no_active_session() -> Self { + Self { + window_minutes: Some(SESSION_WINDOW_MINUTES), + ..Self::informational("No active 5h session") + } + } + /// Create a rate window with full details pub fn with_details( used_percent: f64, @@ -172,6 +186,20 @@ mod tests { use super::*; use chrono::TimeZone; + #[test] + fn no_active_session_is_informational_five_hour_placeholder() { + let window = RateWindow::no_active_session(); + + assert!(window.is_informational); + assert_eq!(window.window_minutes, Some(SESSION_WINDOW_MINUTES)); + assert_eq!( + window.reset_description.as_deref(), + Some("No active 5h session") + ); + assert_eq!(window.used_percent, 0.0); + assert_eq!(window.resets_at, None); + } + #[test] fn test_remaining_percent() { let window = RateWindow::new(75.0); diff --git a/rust/src/providers/claude/web_api.rs b/rust/src/providers/claude/web_api.rs index 5de3770525..9998862b7e 100755 --- a/rust/src/providers/claude/web_api.rs +++ b/rust/src/providers/claude/web_api.rs @@ -317,7 +317,7 @@ impl ClaudeWebApiFetcher { .five_hour .as_ref() .map(|w| self.to_rate_window(w, Some(300))) // 5 hours = 300 minutes - .unwrap_or_else(synthetic_no_session_primary); + .unwrap_or_else(RateWindow::no_active_session); // Prefer limits[] weekly_all over legacy seven_day (same as OAuth path). let secondary = super::scoped_weekly::weekly_all_window(&usage.limits).or_else(|| { @@ -646,20 +646,6 @@ impl ClaudeWebApiFetcher { } } -/// Synthetic 5h session placeholder for Claude web when `five_hour` is null. -/// -/// A genuine idle session (object present, 0% used) is NOT marked informational. -fn synthetic_no_session_primary() -> RateWindow { - let mut window = RateWindow::with_details( - 0.0, - Some(300), - None, - Some("No active 5h session".to_string()), - ); - window.is_informational = true; - window -} - impl Default for ClaudeWebApiFetcher { fn default() -> Self { Self::new() @@ -783,7 +769,7 @@ mod tests { #[test] fn null_five_hour_session_is_informational_placeholder() { - let placeholder = super::synthetic_no_session_primary(); + let placeholder = crate::core::RateWindow::no_active_session(); assert!(placeholder.is_informational); assert_eq!(placeholder.window_minutes, Some(300)); assert!((placeholder.used_percent - 0.0).abs() < f64::EPSILON); diff --git a/rust/src/providers/codex/api.rs b/rust/src/providers/codex/api.rs index 5e85982437..20f6f48836 100755 --- a/rust/src/providers/codex/api.rs +++ b/rust/src/providers/codex/api.rs @@ -582,36 +582,23 @@ fn codex_window_role(window: &RateWindow) -> CodexWindowRole { } } -/// Keep the shared `UsageSnapshot` shape while making an absent session explicit. -/// Informational primaries are omitted from quota math and rendered as unavailable. -fn no_session_primary() -> RateWindow { - let mut window = RateWindow::with_details( - 0.0, - Some(SESSION_WINDOW_MINUTES), - None, - Some("No active 5h session".to_string()), - ); - window.is_informational = true; - window -} - /// Normalize the named `primary_window`/`secondary_window` fields by duration. fn normalize_named_windows( primary: Option, secondary: Option, ) -> (RateWindow, Option) { match (primary, secondary) { - (None, None) => (no_session_primary(), None), + (None, None) => (RateWindow::no_active_session(), None), (Some(window), None) => { if codex_window_role(&window) == CodexWindowRole::Weekly { - (no_session_primary(), Some(window)) + (RateWindow::no_active_session(), Some(window)) } else { (window, None) } } (None, Some(window)) => { if codex_window_role(&window) == CodexWindowRole::Weekly { - (no_session_primary(), Some(window)) + (RateWindow::no_active_session(), Some(window)) } else { (window, None) } @@ -620,7 +607,7 @@ fn normalize_named_windows( match (codex_window_role(&primary), codex_window_role(&secondary)) { (CodexWindowRole::Weekly, CodexWindowRole::Session) => (secondary, Some(primary)), (CodexWindowRole::Weekly, CodexWindowRole::Unknown) => { - (no_session_primary(), Some(primary)) + (RateWindow::no_active_session(), Some(primary)) } (CodexWindowRole::Unknown, CodexWindowRole::Session) => (secondary, Some(primary)), (CodexWindowRole::Session, CodexWindowRole::Weekly) @@ -636,7 +623,7 @@ fn normalize_array_windows( windows: Vec, ) -> (RateWindow, Option, Option) { if windows.is_empty() { - return (no_session_primary(), None, None); + return (RateWindow::no_active_session(), None, None); } // Preserve the old positional fallback when the API provides no role @@ -647,7 +634,7 @@ fn normalize_array_windows( { let mut windows = windows.into_iter(); return ( - windows.next().unwrap_or_else(no_session_primary), + windows.next().unwrap_or_else(RateWindow::no_active_session), windows.next(), windows.next(), ); @@ -666,7 +653,7 @@ fn normalize_array_windows( } ( - session.unwrap_or_else(no_session_primary), + session.unwrap_or_else(RateWindow::no_active_session), weekly, remaining.into_iter().next(), )