From c08c15c3d06232575482d918a8855793b89a9984 Mon Sep 17 00:00:00 2001 From: tsouth89 Date: Fri, 17 Jul 2026 20:17:04 -0400 Subject: [PATCH 1/3] Model explicit limit-window enforcement states (SOU-122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A known limit window can now be tracked, not-currently-enforced, or unavailable, so surfaces never fabricate a percentage for a window that is not actively metering. Previously a window that dropped out of an otherwise-successful response simply vanished, indistinguishable from one the provider deliberately lifted. Model - core: EnforcementState { Tracked, NotEnforced, Unavailable }. Add `state` to InactiveRateWindow (serde default NotEnforced so existing producers and any persisted snapshots keep their meaning) plus an `unavailable` constructor and builder. - bridge + TS: expose `state` on inactiveRateWindows to every surface. Producer - new EnforcementTracker: a process-local, per-scope (provider | source | account identity) cross-snapshot tracker. When a first-class provider's window drops out of a successful response, it appends an `unavailable` row. Additive only — it never edits or removes a real window, and it runs AFTER capacity-event detection so a synthetic row is never mistaken for a lifted window. First read of each scope is a fresh baseline (no false positives after launch); errored snapshots are ignored. Gated to Claude/Codex/Cursor/Copilot/Gemini/Factory. - reuses the capacity-event observer's scope + semantic-window helpers so a window keeps one identity as it moves between states. Rendering - ProviderDetailView labels unavailable windows "Unavailable" (caution tone) vs "Not currently enforced"; PlanStatusCard splits its inactive summary so the two states are never lumped together. CEILING_UI.md documents the state and the never-fabricate rule. Tests cover enum serialization + legacy-JSON default, and the tracker's absence/return/lift/error/scope-isolation/first-class behavior. --- .../src-tauri/src/capacity_events.rs | 13 +- .../src-tauri/src/commands/bridge.rs | 4 + .../src-tauri/src/commands/providers.rs | 12 +- .../src-tauri/src/enforcement.rs | 322 ++++++++++++++++++ apps/desktop-tauri/src-tauri/src/main.rs | 1 + apps/desktop-tauri/src-tauri/src/state.rs | 4 + .../src/components/PlanStatusCard.test.tsx | 29 ++ .../src/components/PlanStatusCard.tsx | 22 +- apps/desktop-tauri/src/styles.css | 14 + .../src/surfaces/ProviderDetailView.test.tsx | 10 + .../src/surfaces/ProviderDetailView.tsx | 21 +- apps/desktop-tauri/src/types/bridge.ts | 6 + docs/CEILING_UI.md | 5 +- rust/src/core/usage_snapshot.rs | 101 +++++- 14 files changed, 542 insertions(+), 22 deletions(-) create mode 100644 apps/desktop-tauri/src-tauri/src/enforcement.rs diff --git a/apps/desktop-tauri/src-tauri/src/capacity_events.rs b/apps/desktop-tauri/src-tauri/src/capacity_events.rs index c8c1f0e2..acb04f69 100644 --- a/apps/desktop-tauri/src-tauri/src/capacity_events.rs +++ b/apps/desktop-tauri/src-tauri/src/capacity_events.rs @@ -525,7 +525,11 @@ fn inactive_windows(snapshot: &ProviderUsageSnapshot) -> HashMap .collect() } -fn ignored_capacity_window(snapshot: &ProviderUsageSnapshot, id: &str, title: &str) -> bool { +pub(crate) fn ignored_capacity_window( + snapshot: &ProviderUsageSnapshot, + id: &str, + title: &str, +) -> bool { if snapshot.provider_id != "cursor" { return false; } @@ -535,7 +539,7 @@ fn ignored_capacity_window(snapshot: &ProviderUsageSnapshot, id: &str, title: &s || identity.contains("ondemand") } -fn semantic_inactive_window_id(provider_id: &str, id: &str, title: &str) -> String { +pub(crate) fn semantic_inactive_window_id(provider_id: &str, id: &str, title: &str) -> String { let title_id = normalize_window_id(title); if let Some(core_id) = core_window_id(&title_id) { return core_id.to_string(); @@ -592,7 +596,7 @@ fn to_observed_window(label: &str, window: &RateWindowSnapshot) -> Option) -> String { +pub(crate) fn semantic_window_id(label: &str, window_minutes: Option) -> String { let normalized = normalize_window_id(label); if matches!( normalized.as_str(), @@ -624,7 +628,7 @@ fn normalize_window_id(value: &str) -> String { .to_string() } -fn observation_scope(snapshot: &ProviderUsageSnapshot) -> String { +pub(crate) fn observation_scope(snapshot: &ProviderUsageSnapshot) -> String { let identity = snapshot .account_email .as_deref() @@ -729,6 +733,7 @@ mod tests { id: id.into(), title: title.into(), description: "Not currently limited".into(), + state: "notEnforced".into(), }); snapshot } diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index 63ba86a7..4fd637b4 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -75,6 +75,9 @@ pub struct InactiveRateWindowSnapshot { pub id: String, pub title: String, pub description: String, + /// "notEnforced" (provider reported no active limit) or "unavailable" (the + /// window dropped out of an otherwise-successful response). + pub state: String, } #[derive(Debug, Clone, Serialize)] @@ -275,6 +278,7 @@ impl ProviderUsageSnapshot { id: window.id.clone(), title: window.title.clone(), description: window.description.clone(), + state: window.state.as_str().to_string(), }) .collect(), promo_signals: usage diff --git a/apps/desktop-tauri/src-tauri/src/commands/providers.rs b/apps/desktop-tauri/src-tauri/src/commands/providers.rs index 5fab9bc0..6cd2e966 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/providers.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/providers.rs @@ -286,8 +286,18 @@ async fn refresh_provider(app: tauri::AppHandle, id: ProviderId, ctx: FetchConte let state = app.state::>(); let (snapshot, capacity_events, notifications_armed) = if let Ok(mut guard) = state.lock() { - let snapshot = preserve_last_good_transient_failure(&mut guard, id, snapshot); + let mut snapshot = preserve_last_good_transient_failure(&mut guard, id, snapshot); + // Detect resets/lifts against the real response first, so the synthetic + // `unavailable` rows added next are never mistaken for a lifted window. let capacity_events = guard.capacity_event_observer.observe(&snapshot); + let unavailable = guard.enforcement_tracker.annotate(&mut snapshot); + if !unavailable.is_empty() { + tracing::debug!( + provider = %snapshot.provider_id, + windows = ?unavailable, + "windows dropped out of a successful response; marked unavailable" + ); + } let notifications_armed = guard.notification_manager.notifications_are_armed(); upsert_provider_cache(&mut guard.provider_cache, snapshot.clone()); (snapshot, capacity_events, notifications_armed) diff --git a/apps/desktop-tauri/src-tauri/src/enforcement.rs b/apps/desktop-tauri/src-tauri/src/enforcement.rs new file mode 100644 index 00000000..9309a994 --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/enforcement.rs @@ -0,0 +1,322 @@ +//! Cross-snapshot enforcement tracking. +//! +//! A window that vanishes from an otherwise-successful provider response is not +//! the same as a window the provider explicitly lifted. The provider adapters +//! already emit the explicit "not currently enforced" case as an inactive +//! window; this tracker adds the missing third state, `unavailable`, by +//! remembering which windows a provider was reporting and flagging any that +//! quietly drop out. Surfaces can then show honest uncertainty instead of +//! silently losing a limit or fabricating a percentage for it. +//! +//! Scope is `provider | data source | account identity` (shared with the +//! capacity-event observer), so accounts, sources, and providers never bleed +//! into each other. State is process-local: the first read of each scope after +//! launch is a fresh baseline that never emits `unavailable`, mirroring the +//! observer's re-baseline-on-launch behaviour so changes that happened while +//! Ceiling was closed are not replayed as surprises. + +use std::collections::HashMap; + +use crate::capacity_events::{ + ignored_capacity_window, observation_scope, semantic_inactive_window_id, semantic_window_id, +}; +use crate::commands::{InactiveRateWindowSnapshot, ProviderUsageSnapshot}; + +/// Providers Ceiling treats as first-class subscription meters. Only these are +/// tracked: minor or experimental providers have noisier payloads where an +/// omitted window is routine, and flagging those would be more noise than +/// signal. Keep this list small and deliberate. +const FIRST_CLASS_PROVIDERS: &[&str] = + &["claude", "codex", "cursor", "copilot", "gemini", "factory"]; + +const UNAVAILABLE_DESCRIPTION: &str = "Not reported in the latest update"; + +#[derive(Debug, Default)] +pub struct EnforcementTracker { + /// scope -> (semantic window id -> last-seen title) + seen_windows: HashMap>, +} + +impl EnforcementTracker { + pub fn new() -> Self { + Self::default() + } + + /// Append `unavailable` inactive rows for tracked windows that dropped out + /// of this successful snapshot. Additive only — it never edits or removes a + /// real window, so it cannot regress metered display or event detection. + /// Returns the titles of any windows newly flagged unavailable (for logging). + pub fn annotate(&mut self, snapshot: &mut ProviderUsageSnapshot) -> Vec { + // An errored snapshot is not a successful response; do not treat a + // window's absence as meaningful, and do not disturb the baseline. + if snapshot.error.is_some() { + return Vec::new(); + } + if !FIRST_CLASS_PROVIDERS.contains(&snapshot.provider_id.as_str()) { + return Vec::new(); + } + + let scope = observation_scope(snapshot); + let present = present_window_identities(snapshot); + let mut newly_unavailable = Vec::new(); + + if let Some(previous) = self.seen_windows.get(&scope) { + let mut missing: Vec<(&String, &String)> = previous + .iter() + .filter(|(id, _)| !present.contains_key(id.as_str())) + .collect(); + // Deterministic ordering keeps output stable across refreshes. + missing.sort_by(|a, b| a.0.cmp(b.0)); + for (id, title) in missing { + if snapshot.inactive_rate_windows.iter().any(|w| &w.id == id) { + continue; + } + snapshot.inactive_rate_windows.push(InactiveRateWindowSnapshot { + id: id.clone(), + title: title.clone(), + description: UNAVAILABLE_DESCRIPTION.to_string(), + state: "unavailable".to_string(), + }); + newly_unavailable.push(title.clone()); + } + } + + // Baseline is the genuinely-present set only; the synthetic unavailable + // rows are deliberately excluded so a window that stays missing keeps + // being flagged, and a window that returns simply stops being flagged. + self.seen_windows.insert(scope, present); + newly_unavailable + } +} + +/// The semantic id -> title of every window the provider is currently +/// reporting, whether metered (primary/secondary) or explicitly inactive. +/// Mirrors the capacity-event observer's identity scheme so a window keeps the +/// same id as it moves between tracked and not-enforced. +fn present_window_identities(snapshot: &ProviderUsageSnapshot) -> HashMap { + let mut out = HashMap::new(); + + let primary_label = snapshot.primary_label.as_deref().unwrap_or("Plan"); + out.insert( + semantic_window_id(primary_label, snapshot.primary.window_minutes), + primary_label.to_string(), + ); + + if let Some(secondary) = snapshot.secondary.as_ref() { + let label = snapshot.secondary_label.as_deref().unwrap_or("Secondary"); + out.insert( + semantic_window_id(label, secondary.window_minutes), + label.to_string(), + ); + } + + for extra in &snapshot.extra_rate_windows { + if ignored_capacity_window(snapshot, &extra.id, &extra.title) { + continue; + } + out.insert( + semantic_inactive_window_id(&snapshot.provider_id, &extra.id, &extra.title), + extra.title.clone(), + ); + } + + for inactive in &snapshot.inactive_rate_windows { + if ignored_capacity_window(snapshot, &inactive.id, &inactive.title) { + continue; + } + out.insert( + semantic_inactive_window_id(&snapshot.provider_id, &inactive.id, &inactive.title), + inactive.title.clone(), + ); + } + + out +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::commands::{NamedRateWindowSnapshot, RateWindowSnapshot}; + + fn window(minutes: Option) -> RateWindowSnapshot { + RateWindowSnapshot { + used_percent: 40.0, + remaining_percent: 60.0, + window_minutes: minutes, + resets_at: None, + reset_description: None, + is_exhausted: false, + reserve_percent: None, + reserve_description: None, + reserve_will_last_to_reset: false, + reserve_eta_seconds: None, + } + } + + fn codex_snapshot() -> ProviderUsageSnapshot { + ProviderUsageSnapshot { + provider_id: "codex".into(), + display_name: "Codex".into(), + primary: window(Some(300)), + primary_label: Some("5-hour".into()), + secondary: Some(window(Some(10_080))), + secondary_label: Some("Weekly".into()), + model_specific: None, + tertiary: None, + extra_rate_windows: Vec::new(), + inactive_rate_windows: Vec::new(), + promo_signals: Vec::new(), + reset_credits_available: None, + cost: None, + plan_name: None, + account_email: Some("person@example.com".into()), + source_label: "oauth".into(), + updated_at: "2026-07-17T00:00:00Z".into(), + error: None, + pace: None, + account_organization: None, + tray_status_label: None, + fetch_duration_ms: None, + wayfinder_usage: None, + } + } + + fn unavailable_titles(snapshot: &ProviderUsageSnapshot) -> Vec { + snapshot + .inactive_rate_windows + .iter() + .filter(|w| w.state == "unavailable") + .map(|w| w.title.clone()) + .collect() + } + + #[test] + fn first_read_is_a_baseline_and_never_flags_absence() { + let mut tracker = EnforcementTracker::new(); + let mut only_five_hour = codex_snapshot(); + only_five_hour.secondary = None; + only_five_hour.secondary_label = None; + + assert!(tracker.annotate(&mut only_five_hour).is_empty()); + assert!(only_five_hour.inactive_rate_windows.is_empty()); + } + + #[test] + fn a_window_that_drops_out_becomes_unavailable() { + let mut tracker = EnforcementTracker::new(); + // Baseline: both 5-hour and weekly present. + tracker.annotate(&mut codex_snapshot()); + + // Next read omits the weekly window entirely. + let mut dropped = codex_snapshot(); + dropped.secondary = None; + dropped.secondary_label = None; + + let flagged = tracker.annotate(&mut dropped); + assert_eq!(flagged, vec!["Weekly".to_string()]); + assert_eq!(unavailable_titles(&dropped), vec!["Weekly".to_string()]); + assert_eq!(dropped.inactive_rate_windows[0].state, "unavailable"); + } + + #[test] + fn an_explicitly_lifted_window_is_not_unavailable() { + let mut tracker = EnforcementTracker::new(); + tracker.annotate(&mut codex_snapshot()); + + // Weekly is reported as inactive (provider lifted it), not absent. + let mut lifted = codex_snapshot(); + lifted.secondary = None; + lifted.secondary_label = None; + lifted.inactive_rate_windows.push(InactiveRateWindowSnapshot { + id: "codex-weekly".into(), + title: "Weekly".into(), + description: "Not currently enforced".into(), + state: "notEnforced".into(), + }); + + assert!(tracker.annotate(&mut lifted).is_empty()); + assert!(unavailable_titles(&lifted).is_empty()); + } + + #[test] + fn a_returning_window_stops_being_flagged() { + let mut tracker = EnforcementTracker::new(); + tracker.annotate(&mut codex_snapshot()); + + let mut dropped = codex_snapshot(); + dropped.secondary = None; + dropped.secondary_label = None; + assert_eq!(tracker.annotate(&mut dropped).len(), 1); + + // Weekly comes back on the following read. + let mut restored = codex_snapshot(); + assert!(tracker.annotate(&mut restored).is_empty()); + assert!(unavailable_titles(&restored).is_empty()); + } + + #[test] + fn errored_snapshot_is_ignored_and_does_not_disturb_the_baseline() { + let mut tracker = EnforcementTracker::new(); + tracker.annotate(&mut codex_snapshot()); + + let mut errored = codex_snapshot(); + errored.secondary = None; + errored.error = Some("network".into()); + assert!(tracker.annotate(&mut errored).is_empty()); + assert!(errored.inactive_rate_windows.is_empty()); + + // The failed read must not have consumed the baseline: a later + // successful read that still omits weekly is flagged. + let mut dropped = codex_snapshot(); + dropped.secondary = None; + dropped.secondary_label = None; + assert_eq!(tracker.annotate(&mut dropped), vec!["Weekly".to_string()]); + } + + #[test] + fn non_first_class_providers_are_not_tracked() { + let mut tracker = EnforcementTracker::new(); + let mut first = codex_snapshot(); + first.provider_id = "venice".into(); + tracker.annotate(&mut first); + + let mut dropped = codex_snapshot(); + dropped.provider_id = "venice".into(); + dropped.secondary = None; + dropped.secondary_label = None; + assert!(tracker.annotate(&mut dropped).is_empty()); + assert!(dropped.inactive_rate_windows.is_empty()); + } + + #[test] + fn different_accounts_do_not_flag_each_other() { + let mut tracker = EnforcementTracker::new(); + tracker.annotate(&mut codex_snapshot()); + + // A different account's first read is its own baseline, even though it + // omits weekly relative to the first account. + let mut other = codex_snapshot(); + other.account_email = Some("other@example.com".into()); + other.secondary = None; + other.secondary_label = None; + assert!(tracker.annotate(&mut other).is_empty()); + assert!(other.inactive_rate_windows.is_empty()); + } + + #[test] + fn extra_named_windows_are_tracked_too() { + let mut tracker = EnforcementTracker::new(); + let mut with_spark = codex_snapshot(); + with_spark.extra_rate_windows.push(NamedRateWindowSnapshot { + id: "codex-spark-weekly".into(), + title: "Codex Spark Weekly".into(), + window: window(Some(10_080)), + }); + tracker.annotate(&mut with_spark); + + // Spark disappears on the next successful read. + let mut dropped = codex_snapshot(); + let flagged = tracker.annotate(&mut dropped); + assert_eq!(flagged, vec!["Codex Spark Weekly".to_string()]); + } +} diff --git a/apps/desktop-tauri/src-tauri/src/main.rs b/apps/desktop-tauri/src-tauri/src/main.rs index 3b2ca402..2fa9f031 100644 --- a/apps/desktop-tauri/src-tauri/src/main.rs +++ b/apps/desktop-tauri/src-tauri/src/main.rs @@ -5,6 +5,7 @@ use std::time::Duration; mod auto_refresh; mod capacity_events; mod commands; +mod enforcement; mod events; mod floatbar; mod geometry_store; diff --git a/apps/desktop-tauri/src-tauri/src/state.rs b/apps/desktop-tauri/src-tauri/src/state.rs index 1d5f0c0e..c64a82ef 100644 --- a/apps/desktop-tauri/src-tauri/src/state.rs +++ b/apps/desktop-tauri/src-tauri/src/state.rs @@ -137,6 +137,9 @@ pub struct AppState { pub notification_manager: codexbar::notifications::NotificationManager, /// Provider-authoritative reset observer persisted separately from settings. pub capacity_event_observer: crate::capacity_events::CapacityEventObserver, + /// Tracks which windows each provider scope is reporting so a window that + /// drops out of a successful response can be surfaced as `unavailable`. + pub enforcement_tracker: crate::enforcement::EnforcementTracker, /// Instant when the tray panel was last shown — used to suppress /// spurious blur-dismiss during the show animation on Windows. pub last_shown_at: Option, @@ -197,6 +200,7 @@ impl AppState { proof_config: None, notification_manager: codexbar::notifications::NotificationManager::new(), capacity_event_observer: crate::capacity_events::CapacityEventObserver::load_default(), + enforcement_tracker: crate::enforcement::EnforcementTracker::new(), last_shown_at: None, startup_tray_blur_grace_until: None, suppress_geometry_capture_until: None, diff --git a/apps/desktop-tauri/src/components/PlanStatusCard.test.tsx b/apps/desktop-tauri/src/components/PlanStatusCard.test.tsx index 76f7307d..d319c784 100644 --- a/apps/desktop-tauri/src/components/PlanStatusCard.test.tsx +++ b/apps/desktop-tauri/src/components/PlanStatusCard.test.tsx @@ -197,4 +197,33 @@ describe("PlanStatusCard", () => { expect(screen.getByText("not currently enforced")).toBeTruthy(); expect(screen.queryByText("lifted")).toBeNull(); }); + + it("separates unavailable windows from not-enforced ones", () => { + render( + , + ); + + expect(screen.getByText("not currently enforced")).toBeTruthy(); + expect(screen.getByText("unavailable")).toBeTruthy(); + // The unavailable window must not be lumped under "not currently enforced". + expect(screen.getByText("Weekly")).toBeTruthy(); + }); }); diff --git a/apps/desktop-tauri/src/components/PlanStatusCard.tsx b/apps/desktop-tauri/src/components/PlanStatusCard.tsx index 8bb23856..78c946d2 100644 --- a/apps/desktop-tauri/src/components/PlanStatusCard.tsx +++ b/apps/desktop-tauri/src/components/PlanStatusCard.tsx @@ -40,9 +40,13 @@ function pressureLabel(level: string): string | null { return null; } -function inactiveWindowSummary(provider: ProviderUsageSnapshot): string | null { +function inactiveWindowSummary( + provider: ProviderUsageSnapshot, + state: "notEnforced" | "unavailable", +): string | null { const labels = [...new Set( (provider.inactiveRateWindows ?? []) + .filter((window) => (window.state ?? "notEnforced") === state) .map((window) => window.title.trim()) .filter(Boolean), )]; @@ -165,7 +169,8 @@ export default function PlanStatusCard({ const meters = glanceMeters(provider); const freshness = capacityFreshness(provider); const planName = displayPlanName(provider.planName, provider.displayName); - const inactiveSummary = inactiveWindowSummary(provider); + const notEnforcedSummary = inactiveWindowSummary(provider, "notEnforced"); + const unavailableSummary = inactiveWindowSummary(provider, "unavailable"); const resetCredits = codexResetCredits(provider); const className = [ @@ -237,15 +242,24 @@ export default function PlanStatusCard({ hero={false} /> ))} - {inactiveSummary && ( + {notEnforcedSummary && (
- {inactiveSummary} + {notEnforcedSummary} not currently enforced
)} + {unavailableSummary && ( +
+ + + {unavailableSummary} + + unavailable +
+ )} )} diff --git a/apps/desktop-tauri/src/styles.css b/apps/desktop-tauri/src/styles.css index d5054f06..e410d7bb 100644 --- a/apps/desktop-tauri/src/styles.css +++ b/apps/desktop-tauri/src/styles.css @@ -4419,6 +4419,16 @@ html:has(.menu-surface--tray) { border-top: 1px dashed color-mix(in srgb, var(--state-lifted) 70%, transparent); } +/* Unavailable reads as caution (a tracked window we could not read), distinct + from the calm "not currently enforced" state. */ +.plan-status-card__inactive--unavailable .plan-status-card__inactive-mark { + border-top-style: solid; + border-top-color: color-mix(in srgb, var(--usage-bar-high) 80%, transparent); +} +.plan-status-card__inactive--unavailable > span:last-child { + color: var(--usage-bar-high); +} + .plan-status-card__inactive-name { min-width: 0; overflow: hidden; @@ -6805,6 +6815,10 @@ body:has(.dashboard-shell) #root { color: var(--text-secondary); font-size: 11px; } +.provider-focus__inactive--unavailable > span:last-child { + color: var(--usage-bar-high); + font-weight: 600; +} .provider-focus__info { display: grid; place-items: center; diff --git a/apps/desktop-tauri/src/surfaces/ProviderDetailView.test.tsx b/apps/desktop-tauri/src/surfaces/ProviderDetailView.test.tsx index 89645de9..c8ac2e65 100644 --- a/apps/desktop-tauri/src/surfaces/ProviderDetailView.test.tsx +++ b/apps/desktop-tauri/src/surfaces/ProviderDetailView.test.tsx @@ -43,6 +43,13 @@ function codex(): ProviderUsageSnapshot { id: "session", title: "5-hour session", description: "Not currently enforced by OpenAI", + state: "notEnforced", + }, + { + id: "weekly", + title: "Weekly", + description: "Not reported in the latest update", + state: "unavailable", }, ], cost: null, @@ -114,6 +121,9 @@ describe("ProviderDetailView", () => { expect(screen.getByText("Codex Spark")).toBeInTheDocument(); expect(screen.getByText("5-hour session")).toBeInTheDocument(); expect(screen.getByText("Not currently enforced")).toBeInTheDocument(); + // A window that dropped out of a successful response reads as Unavailable, + // never as "not currently enforced" or a fabricated percentage. + expect(screen.getByText("Unavailable")).toBeInTheDocument(); expect(screen.queryByText("Promotional")).toBeNull(); expect(screen.getAllByText(/Weekly pace/)).toHaveLength(2); expect(screen.getAllByText(/Far ahead of budget/)).toHaveLength(2); diff --git a/apps/desktop-tauri/src/surfaces/ProviderDetailView.tsx b/apps/desktop-tauri/src/surfaces/ProviderDetailView.tsx index 53082c94..a82a4e3b 100644 --- a/apps/desktop-tauri/src/surfaces/ProviderDetailView.tsx +++ b/apps/desktop-tauri/src/surfaces/ProviderDetailView.tsx @@ -301,13 +301,20 @@ export default function ProviderDetailView({ showAsUsed={showAsUsed} /> ))} - {(provider.inactiveRateWindows ?? []).map((metric) => ( -
- {metric.title} - i - Not currently enforced -
- ))} + {(provider.inactiveRateWindows ?? []).map((metric) => { + const unavailable = metric.state === "unavailable"; + return ( +
+ {metric.title} + i + {unavailable ? "Unavailable" : "Not currently enforced"} +
+ ); + })} )} diff --git a/apps/desktop-tauri/src/types/bridge.ts b/apps/desktop-tauri/src/types/bridge.ts index 9f84d0d3..209f004b 100644 --- a/apps/desktop-tauri/src/types/bridge.ts +++ b/apps/desktop-tauri/src/types/bridge.ts @@ -415,6 +415,12 @@ export interface ProviderUsageSnapshot { id: string; title: string; description: string; + /** + * "notEnforced": the provider reported no active limit for this window. + * "unavailable": a tracked window dropped out of an otherwise-successful + * response. Optional for back-compat; treat a missing value as notEnforced. + */ + state?: "notEnforced" | "unavailable"; }>; promoSignals?: Array<{ id: string; diff --git a/docs/CEILING_UI.md b/docs/CEILING_UI.md index 1b5bae75..68704f5e 100644 --- a/docs/CEILING_UI.md +++ b/docs/CEILING_UI.md @@ -85,9 +85,10 @@ Detail lists **every** measured window plus inactive rows. Do not truncate to tw | Live | Normal pill, no chip | Quiet freshness text | | Cached / stale | Muted pill + `stale` chip | Dim metrics + stale label | | Error | Crit outline + `error` chip | Error line, no fake bars | -| Not enforced | No chip from inactive windows; the active window headlines and normal/stale/error freshness still controls the pill | Quiet named text row via `inactiveRateWindows` | +| Not enforced | No chip from inactive windows; the active window headlines and normal/stale/error freshness still controls the pill | Quiet named text row via `inactiveRateWindows` (`state: "notEnforced"`) | +| Unavailable | Same quiet treatment, in a caution (amber) tone; the window was tracked but dropped out of a successful response | Named row via `inactiveRateWindows` (`state: "unavailable"`), labeled "Unavailable" | -Never invent `0%` or `100%` for an inactive window. +`inactiveRateWindows[].state` carries the explicit enforcement state: `notEnforced` (provider reported no active limit) vs `unavailable` (a first-class provider's window vanished from an otherwise-successful response, detected by the cross-snapshot enforcement tracker). Never invent `0%` or `100%` for either — surface the named state instead. ## Promo signals diff --git a/rust/src/core/usage_snapshot.rs b/rust/src/core/usage_snapshot.rs index 5a7c102c..99c53afa 100755 --- a/rust/src/core/usage_snapshot.rs +++ b/rust/src/core/usage_snapshot.rs @@ -46,17 +46,55 @@ pub struct NamedRateWindow { pub window: RateWindow, } -/// A known provider limit window that was deliberately not reported in an -/// otherwise-successful snapshot. This is distinct from a 0% usage window: -/// the provider has not supplied a limit to meter right now. +/// Explicit enforcement state for a known limit window, so surfaces never have +/// to fabricate a percentage for a window that is not actively metering. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum EnforcementState { + /// The provider is actively metering this window; it carries a real + /// percentage and appears as a normal `RateWindow`, not here. + Tracked, + /// The provider responded successfully and explicitly reported no active + /// limit for this window (e.g. a lifted five-hour cap). + NotEnforced, + /// A window we were tracking is absent from an otherwise-successful + /// response. We do not know whether it was lifted or merely omitted, so we + /// surface honest uncertainty instead of a made-up value. + Unavailable, +} + +impl EnforcementState { + /// The wire/UI token for this state (matches the camelCase serde form). + pub fn as_str(self) -> &'static str { + match self { + EnforcementState::Tracked => "tracked", + EnforcementState::NotEnforced => "notEnforced", + EnforcementState::Unavailable => "unavailable", + } + } +} + +/// A known provider limit window that was not reported as an active meter in an +/// otherwise-successful snapshot. This is distinct from a 0% usage window: the +/// provider has not supplied a limit to meter right now (`NotEnforced`), or the +/// window we track dropped out of the response entirely (`Unavailable`). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InactiveRateWindow { pub id: String, pub title: String, pub description: String, + /// Why this window is not actively metering. Defaults to `NotEnforced` so + /// existing producers and any persisted snapshots keep their meaning. + #[serde(default = "default_inactive_state")] + pub state: EnforcementState, +} + +fn default_inactive_state() -> EnforcementState { + EnforcementState::NotEnforced } impl InactiveRateWindow { + /// A window the provider explicitly reports as not currently enforced. pub fn new( id: impl Into, title: impl Into, @@ -66,6 +104,21 @@ impl InactiveRateWindow { id: id.into(), title: title.into(), description: description.into(), + state: EnforcementState::NotEnforced, + } + } + + /// A tracked window that dropped out of an otherwise-successful response. + pub fn unavailable( + id: impl Into, + title: impl Into, + description: impl Into, + ) -> Self { + Self { + id: id.into(), + title: title.into(), + description: description.into(), + state: EnforcementState::Unavailable, } } } @@ -239,7 +292,8 @@ impl UsageSnapshot { self } - /// Builder pattern: append a named window with no active provider meter. + /// Builder pattern: append a named window the provider explicitly reports + /// as not currently enforced. pub fn with_inactive_rate_window( mut self, id: impl Into, @@ -251,6 +305,19 @@ impl UsageSnapshot { self } + /// Builder pattern: append a tracked window that dropped out of an + /// otherwise-successful response (state `Unavailable`). + pub fn with_unavailable_rate_window( + mut self, + id: impl Into, + title: impl Into, + description: impl Into, + ) -> Self { + self.inactive_rate_windows + .push(InactiveRateWindow::unavailable(id, title, description)); + self + } + /// Builder pattern: append a promotional signal. pub fn with_promo_signal(mut self, signal: PromoSignal) -> Self { self.promo_signals.push(signal); @@ -468,4 +535,30 @@ mod tests { assert_eq!(cost.used_percent(), None); assert_eq!(cost.format_used(), "$0.00"); } + + #[test] + fn enforcement_state_serializes_as_camel_case() { + assert_eq!( + serde_json::to_string(&EnforcementState::NotEnforced).unwrap(), + "\"notEnforced\"" + ); + assert_eq!( + serde_json::to_string(&EnforcementState::Unavailable).unwrap(), + "\"unavailable\"" + ); + assert_eq!(EnforcementState::Unavailable.as_str(), "unavailable"); + } + + #[test] + fn inactive_rate_window_state_defaults_to_not_enforced_for_legacy_json() { + // Snapshots persisted before the state field must still deserialize. + let legacy = r#"{"id":"codex-weekly","title":"Weekly","description":"x"}"#; + let window: InactiveRateWindow = serde_json::from_str(legacy).unwrap(); + assert_eq!(window.state, EnforcementState::NotEnforced); + + let unavailable = InactiveRateWindow::unavailable("weekly", "Weekly", "gone"); + let round_tripped: InactiveRateWindow = + serde_json::from_str(&serde_json::to_string(&unavailable).unwrap()).unwrap(); + assert_eq!(round_tripped.state, EnforcementState::Unavailable); + } } From 5aa2278e57d2b6074108c86078b9fb6bddfd1a96 Mon Sep 17 00:00:00 2001 From: tsouth89 Date: Fri, 17 Jul 2026 20:28:27 -0400 Subject: [PATCH 2/3] Harden EnforcementTracker: sticky unavailable + core-window-only (review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two correctness fixes from adversarial self-review of the tracker: - Sticky, not one-shot. The baseline was overwritten with only the present windows each refresh, so a window that stayed missing was flagged on the first post-drop refresh and then dropped from the baseline — the "unavailable" row would flash for one cycle and vanish, reintroducing the silent loss the feature prevents. The baseline is now an accumulating "expected" set: a missing window keeps being flagged every refresh until it reappears (bounded to the process session). - Track core subscription windows only. Previously every window, including conditional bonus pools (Codex Spark, Copilot additional budget, Factory premium, promos), was tracked, so a promo simply ending would be reported as "unavailable". Now only core windows (session/weekly/monthly/plan/auto/api/total) are tracked; conditional pools coming and going are never flagged. This also makes the first-class provider list safe regardless of their bonus-pool payloads. - Removed a dead dedup guard that compared a real row's raw id against a semantic id (never matched); present-set membership already prevents duplicating a lifted window. Adds regression tests: persistently-missing window flagged every refresh, conditional bonus windows never flagged, core window still flagged. --- .../src-tauri/src/enforcement.rs | 151 +++++++++++++----- 1 file changed, 107 insertions(+), 44 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/enforcement.rs b/apps/desktop-tauri/src-tauri/src/enforcement.rs index 9309a994..64ca1af8 100644 --- a/apps/desktop-tauri/src-tauri/src/enforcement.rs +++ b/apps/desktop-tauri/src-tauri/src/enforcement.rs @@ -29,12 +29,27 @@ use crate::commands::{InactiveRateWindowSnapshot, ProviderUsageSnapshot}; const FIRST_CLASS_PROVIDERS: &[&str] = &["claude", "codex", "cursor", "copilot", "gemini", "factory"]; +/// Only core subscription windows are tracked. Conditional bonus pools (promos, +/// Codex Spark, Copilot additional budget, on-demand credits) legitimately come +/// and go, so treating their disappearance as `unavailable` would be a false +/// alarm. These are exactly the ids `core_window_id` / the primary-label match +/// in `capacity_events` resolve to. +const CORE_WINDOW_IDS: &[&str] = + &["session", "weekly", "monthly", "plan", "auto", "api", "total"]; + const UNAVAILABLE_DESCRIPTION: &str = "Not reported in the latest update"; +fn is_core_window(id: &str) -> bool { + CORE_WINDOW_IDS.contains(&id) +} + #[derive(Debug, Default)] pub struct EnforcementTracker { - /// scope -> (semantic window id -> last-seen title) - seen_windows: HashMap>, + /// scope -> the set of core windows we expect to see, as (id -> last-seen + /// title). Once a scope has seen a core window it stays expected until it + /// reappears, so a window that stays missing keeps being flagged every + /// refresh (not just the first one after it drops out). + expected_windows: HashMap>, } impl EnforcementTracker { @@ -42,10 +57,10 @@ impl EnforcementTracker { Self::default() } - /// Append `unavailable` inactive rows for tracked windows that dropped out - /// of this successful snapshot. Additive only — it never edits or removes a - /// real window, so it cannot regress metered display or event detection. - /// Returns the titles of any windows newly flagged unavailable (for logging). + /// Append `unavailable` inactive rows for expected core windows that dropped + /// out of this successful snapshot. Additive only — it never edits or + /// removes a real window, so it cannot regress metered display or event + /// detection. Returns the titles flagged unavailable this refresh (logging). pub fn annotate(&mut self, snapshot: &mut ProviderUsageSnapshot) -> Vec { // An errored snapshot is not a successful response; do not treat a // window's absence as meaningful, and do not disturb the baseline. @@ -58,33 +73,41 @@ impl EnforcementTracker { let scope = observation_scope(snapshot); let present = present_window_identities(snapshot); - let mut newly_unavailable = Vec::new(); - if let Some(previous) = self.seen_windows.get(&scope) { - let mut missing: Vec<(&String, &String)> = previous - .iter() - .filter(|(id, _)| !present.contains_key(id.as_str())) - .collect(); - // Deterministic ordering keeps output stable across refreshes. - missing.sort_by(|a, b| a.0.cmp(b.0)); - for (id, title) in missing { - if snapshot.inactive_rate_windows.iter().any(|w| &w.id == id) { - continue; - } - snapshot.inactive_rate_windows.push(InactiveRateWindowSnapshot { - id: id.clone(), - title: title.clone(), - description: UNAVAILABLE_DESCRIPTION.to_string(), - state: "unavailable".to_string(), - }); - newly_unavailable.push(title.clone()); - } + // The first read of a scope this process is a fresh baseline: record + // what is present and never flag, so changes that happened while Ceiling + // was closed are not replayed as surprises. + let Some(expected) = self.expected_windows.get_mut(&scope) else { + self.expected_windows.insert(scope, present); + return Vec::new(); + }; + + // Anything we still expect but is absent now is unavailable. Missing + // windows are intentionally retained in `expected` so they keep being + // flagged until they reappear. + let mut missing: Vec<(String, String)> = expected + .iter() + .filter(|(id, _)| !present.contains_key(id.as_str())) + .map(|(id, title)| (id.clone(), title.clone())) + .collect(); + missing.sort_by(|a, b| a.0.cmp(&b.0)); + + let mut newly_unavailable = Vec::new(); + for (id, title) in missing { + snapshot.inactive_rate_windows.push(InactiveRateWindowSnapshot { + id: id.clone(), + title: title.clone(), + description: UNAVAILABLE_DESCRIPTION.to_string(), + state: "unavailable".to_string(), + }); + newly_unavailable.push(title); } - // Baseline is the genuinely-present set only; the synthetic unavailable - // rows are deliberately excluded so a window that stays missing keeps - // being flagged, and a window that returns simply stops being flagged. - self.seen_windows.insert(scope, present); + // Merge the present set in: refresh titles and add newly-seen windows, + // while keeping still-missing windows expected. + for (id, title) in present { + expected.insert(id, title); + } newly_unavailable } } @@ -95,28 +118,30 @@ impl EnforcementTracker { /// same id as it moves between tracked and not-enforced. fn present_window_identities(snapshot: &ProviderUsageSnapshot) -> HashMap { let mut out = HashMap::new(); + let mut record = |id: String, title: &str| { + if is_core_window(&id) { + out.insert(id, title.to_string()); + } + }; let primary_label = snapshot.primary_label.as_deref().unwrap_or("Plan"); - out.insert( + record( semantic_window_id(primary_label, snapshot.primary.window_minutes), - primary_label.to_string(), + primary_label, ); if let Some(secondary) = snapshot.secondary.as_ref() { let label = snapshot.secondary_label.as_deref().unwrap_or("Secondary"); - out.insert( - semantic_window_id(label, secondary.window_minutes), - label.to_string(), - ); + record(semantic_window_id(label, secondary.window_minutes), label); } for extra in &snapshot.extra_rate_windows { if ignored_capacity_window(snapshot, &extra.id, &extra.title) { continue; } - out.insert( + record( semantic_inactive_window_id(&snapshot.provider_id, &extra.id, &extra.title), - extra.title.clone(), + &extra.title, ); } @@ -124,9 +149,9 @@ fn present_window_identities(snapshot: &ProviderUsageSnapshot) -> HashMap Date: Fri, 17 Jul 2026 21:24:22 -0400 Subject: [PATCH 3/3] Scope observations by account org as well as email (CodeRabbit #44) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit observation_scope preferred account_email and ignored account_organization when both were present, so the same email across a personal vs business workspace (distinct limits) shared one baseline — the enforcement tracker could then falsely mark org-specific windows unavailable, and the reset observer could cross-contaminate. Fold both identifiers into the scope. Also hardens the capacity-event observer against the same collision. Adds a same-email/different-organization regression test. --- .../src-tauri/src/capacity_events.rs | 14 +++++++------- apps/desktop-tauri/src-tauri/src/enforcement.rs | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/capacity_events.rs b/apps/desktop-tauri/src-tauri/src/capacity_events.rs index acb04f69..00d8677f 100644 --- a/apps/desktop-tauri/src-tauri/src/capacity_events.rs +++ b/apps/desktop-tauri/src-tauri/src/capacity_events.rs @@ -629,14 +629,14 @@ fn normalize_window_id(value: &str) -> String { } pub(crate) fn observation_scope(snapshot: &ProviderUsageSnapshot) -> String { - let identity = snapshot - .account_email - .as_deref() - .or(snapshot.account_organization.as_deref()) - .unwrap_or("anonymous"); + // Scope by BOTH account identifiers, not either/or: the same email can + // belong to different organizations (e.g. a personal vs a business + // workspace) with distinct limits, and those must never share a baseline. + let email = snapshot.account_email.as_deref().unwrap_or(""); + let organization = snapshot.account_organization.as_deref().unwrap_or(""); let raw = format!( - "{}|{}|{}", - snapshot.provider_id, snapshot.source_label, identity + "{}|{}|{}|{}", + snapshot.provider_id, snapshot.source_label, email, organization ); format!("{}:{:016x}", snapshot.provider_id, fnv1a64(raw.as_bytes())) } diff --git a/apps/desktop-tauri/src-tauri/src/enforcement.rs b/apps/desktop-tauri/src-tauri/src/enforcement.rs index 64ca1af8..fb6edabe 100644 --- a/apps/desktop-tauri/src-tauri/src/enforcement.rs +++ b/apps/desktop-tauri/src-tauri/src/enforcement.rs @@ -345,6 +345,21 @@ mod tests { assert!(other.inactive_rate_windows.is_empty()); } + #[test] + fn different_organizations_under_the_same_email_do_not_flag_each_other() { + // The same email can span a personal and a business workspace with + // different limits; switching between them must not reuse a baseline. + let mut tracker = EnforcementTracker::new(); + tracker.annotate(&mut codex_snapshot()); + + let mut other_org = codex_snapshot(); + other_org.account_organization = Some("org_business".into()); + other_org.secondary = None; + other_org.secondary_label = None; + assert!(tracker.annotate(&mut other_org).is_empty()); + assert!(other_org.inactive_rate_windows.is_empty()); + } + #[test] fn conditional_bonus_windows_are_never_flagged_unavailable() { // Bonus pools (Spark, promos, additional budget) legitimately end. Their