From f5c57acabf2c38db5ee293dc4b4a68e396f2b5c6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 23 Aug 2026 22:26:41 +0000 Subject: [PATCH] Include directory account_id in observation_scope Capacity-event and enforcement baselines keyed only on provider|source|email|org, so Codex personal and Team seats that share a login email collapsed. usage_history, thresholds, and predictive pace already isolate on snapshot.account_id (SBS-1079). Co-authored-by: Tyler --- .../src-tauri/src/capacity_events.rs | 92 ++++++++++++++++++- .../src-tauri/src/enforcement.rs | 36 ++++++-- 2 files changed, 119 insertions(+), 9 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/capacity_events.rs b/apps/desktop-tauri/src-tauri/src/capacity_events.rs index e5520c35..a4c85d0a 100644 --- a/apps/desktop-tauri/src-tauri/src/capacity_events.rs +++ b/apps/desktop-tauri/src-tauri/src/capacity_events.rs @@ -920,14 +920,22 @@ fn normalize_window_id(value: &str) -> String { } pub(crate) fn observation_scope(snapshot: &ProviderUsageSnapshot) -> String { - // Scope by BOTH account identifiers, not either/or: the same email can + // Scope by every account identifier, 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. + // Directory seats that share a login email (Codex personal + Team) also + // stamp distinct `account_id`s; omitting that id collapsed them (SBS-1079). let email = snapshot.account_email.as_deref().unwrap_or(""); let organization = snapshot.account_organization.as_deref().unwrap_or(""); + let account_id = snapshot + .account_id + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or(""); let raw = format!( - "{}|{}|{}|{}", - snapshot.provider_id, snapshot.source_label, email, organization + "{}|{}|{}|{}|{}", + snapshot.provider_id, snapshot.source_label, email, organization, account_id ); format!("{}:{:016x}", snapshot.provider_id, fnv1a64(raw.as_bytes())) } @@ -1554,6 +1562,84 @@ mod tests { assert!(observer.observe(&other).is_empty()); } + /// Two Codex directory seats can share a login email (personal + Team). + /// After #379 they stamp distinct `account_id`s; usage_history, threshold + /// toasts, and predictive pace already key on those. Capacity-event + /// observation used to hash provider|source|email|org only, so both seats + /// shared a baseline (SBS-1079). + fn shared_email_directory_seats( + at: DateTime, + used: f64, + reset: DateTime, + account_id: &str, + ) -> ProviderUsageSnapshot { + let mut seat = snapshot(at, used, reset); + seat.account_email = Some("Same@Example.com".into()); + seat.account_organization = Some("Team".into()); + seat.account_id = Some(account_id.into()); + seat + } + + #[test] + fn observation_scope_separates_directory_seats_that_share_an_email() { + let now = Utc::now(); + let reset = now + Duration::hours(4); + let personal = shared_email_directory_seats(now, 90.0, reset, "acct-personal"); + let work = shared_email_directory_seats(now, 10.0, reset, "acct-work"); + + assert_ne!(observation_scope(&personal), observation_scope(&work)); + assert_eq!( + observation_scope(&personal), + observation_scope(&shared_email_directory_seats( + now + Duration::minutes(1), + 91.0, + reset, + "acct-personal", + )) + ); + } + + #[test] + fn shared_email_directory_seats_do_not_confirm_each_others_resets() { + // Without account_id in the scope, the work seat's drop from the + // personal baseline looks like a confirmed surprise reset. + let start = Utc::now(); + let old_reset = start + Duration::hours(4); + let new_reset = start + Duration::hours(9); + let mut observer = CapacityEventObserver::default(); + + assert!( + observer + .observe(&shared_email_directory_seats( + start, + 85.0, + old_reset, + "acct-personal", + )) + .is_empty() + ); + assert!( + observer + .observe(&shared_email_directory_seats( + start + Duration::minutes(5), + 10.0, + new_reset, + "acct-work", + )) + .is_empty() + ); + let events = observer.observe(&shared_email_directory_seats( + start + Duration::minutes(10), + 12.0, + new_reset + Duration::minutes(2), + "acct-work", + )); + assert!( + events.is_empty(), + "shared-email seats must not share a capacity baseline: {events:?}" + ); + } + #[test] fn reset_time_shift_requires_confirmation() { let start = Utc::now(); diff --git a/apps/desktop-tauri/src-tauri/src/enforcement.rs b/apps/desktop-tauri/src-tauri/src/enforcement.rs index 58af9d55..34c61f19 100644 --- a/apps/desktop-tauri/src-tauri/src/enforcement.rs +++ b/apps/desktop-tauri/src-tauri/src/enforcement.rs @@ -8,12 +8,13 @@ //! 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. +//! Scope is `provider | data source | account identity` including directory +//! `account_id` (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; @@ -370,6 +371,29 @@ mod tests { assert!(other_org.inactive_rate_windows.is_empty()); } + #[test] + fn shared_email_directory_seats_do_not_flag_each_other() { + // Codex personal + Team seats share a login email and can share an + // organization label; directory `account_id` is what keeps their + // enforcement baselines apart (SBS-1079). Without it, the work seat's + // first read would inherit the personal seat's expected windows. + let mut tracker = EnforcementTracker::new(); + let mut personal = codex_snapshot(); + personal.account_email = Some("Same@Example.com".into()); + personal.account_organization = Some("Team".into()); + personal.account_id = Some("acct-personal".into()); + tracker.annotate(&mut personal); + + let mut work = codex_snapshot(); + work.account_email = Some("Same@Example.com".into()); + work.account_organization = Some("Team".into()); + work.account_id = Some("acct-work".into()); + work.secondary = None; + work.secondary_label = None; + assert!(tracker.annotate(&mut work).is_empty()); + assert!(work.inactive_rate_windows.is_empty()); + } + #[test] fn conditional_bonus_windows_are_never_flagged_unavailable() { // Bonus pools (Spark, promos, additional budget) legitimately end. Their