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/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 d411817aa9..20f6f48836 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,110 @@ 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,
+ }
+}
+
+/// 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) => (RateWindow::no_active_session(), None),
+ (Some(window), None) => {
+ if codex_window_role(&window) == CodexWindowRole::Weekly {
+ (RateWindow::no_active_session(), Some(window))
+ } else {
+ (window, None)
+ }
+ }
+ (None, Some(window)) => {
+ if codex_window_role(&window) == CodexWindowRole::Weekly {
+ (RateWindow::no_active_session(), 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) => {
+ (RateWindow::no_active_session(), 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 (RateWindow::no_active_session(), 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(RateWindow::no_active_session),
+ 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(RateWindow::no_active_session),
+ 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 +838,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 +852,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 +1240,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();