Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop-tauri/src-tauri/src/capacity_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ impl CapacityEventObserver {
}
match serde_json::to_vec_pretty(self) {
Ok(bytes) => {
if let Err(error) = fs::write(path, bytes) {
if let Err(error) = codexbar::secure_file::atomic_write(path, &bytes) {
tracing::warn!("failed to persist capacity-event history: {error}");
}
}
Expand Down
108 changes: 93 additions & 15 deletions apps/desktop-tauri/src-tauri/src/commands/chart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const CHART_CACHE_TTL: Duration = Duration::from_secs(5 * 60);
// Version 7: Codex priority/fast (2x) pricing for local cost. Entries cached at
// version 6 still hold pre-2x standard dollars for reset windows, so the
// Weekly window card could show ~half of the API-value ring after 1.5.13.
const CHART_CACHE_VERSION: u8 = 7;
const CHART_CACHE_VERSION: u8 = 8;

/// A single (date, value) point for cost or credits history charts.
#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -362,17 +362,27 @@ struct PersistedChartCache {
pub fn get_quota_run_history(
provider_id: String,
account_email: Option<String>,
account_id: Option<String>,
) -> Vec<crate::quota_run_history::QuotaRunSnapshot> {
crate::quota_run_history::list_runs(&provider_id, account_email.as_deref())
crate::quota_run_history::list_runs(
&provider_id,
account_email.as_deref(),
account_id.as_deref(),
)
}

/// Latest quota-run efficiency cards per window (SOU-299).
#[tauri::command]
pub fn get_quota_run_efficiency(
provider_id: String,
account_email: Option<String>,
account_id: Option<String>,
) -> Vec<crate::quota_run_history::QuotaRunEfficiency> {
crate::quota_run_history::efficiency_for_provider(&provider_id, account_email.as_deref())
crate::quota_run_history::efficiency_for_provider(
&provider_id,
account_email.as_deref(),
account_id.as_deref(),
)
}

#[tauri::command]
Expand All @@ -382,6 +392,7 @@ pub async fn get_provider_chart_data(
account_id: Option<String>,
source_label: Option<String>,
usage_windows: Option<Vec<LocalUsageWindowRequest>>,
account_organization: Option<String>,
) -> ProviderChartData {
let usage_windows = usage_windows.unwrap_or_default();
// An account's local logs live under its own config directory. Scanning
Expand All @@ -395,35 +406,49 @@ pub async fn get_provider_chart_data(
let cache_key = chart_cache_key(
&provider_id,
account_email.as_deref(),
account_id.as_deref(),
account_organization.as_deref(),
source_label.as_deref(),
&usage_windows,
);
if let Some(mut cached) = cached_chart_data(&cache_key) {
cached.data.quota_history =
crate::usage_history::provider_history(&provider_id, account_email.as_deref());
cached.data.quota_history = crate::usage_history::provider_history(
&provider_id,
account_email.as_deref(),
account_id.as_deref(),
account_organization.as_deref(),
);
if current_unix_ms().saturating_sub(cached.refreshed_at_ms)
> CHART_CACHE_TTL.as_millis() as i64
{
schedule_chart_cache_refresh(
cache_key,
provider_id,
account_email,
account_id,
account_organization,
scoped_home.clone(),
usage_windows,
);
}
return cached.data;
}

let quota_history =
crate::usage_history::provider_history(&provider_id, account_email.as_deref());
let quota_history = crate::usage_history::provider_history(
&provider_id,
account_email.as_deref(),
account_id.as_deref(),
account_organization.as_deref(),
);
if !quota_history.is_empty() {
let mut immediate = ProviderChartData::empty(provider_id.clone());
immediate.quota_history = quota_history;
schedule_chart_cache_refresh(
cache_key,
provider_id,
account_email,
account_id,
account_organization,
scoped_home.clone(),
usage_windows,
);
Expand All @@ -436,6 +461,8 @@ pub async fn get_provider_chart_data(
build_provider_chart_data_with_cancel(
provider_id,
account_email,
account_id,
account_organization,
scoped_home,
usage_windows,
Some(cancel),
Expand Down Expand Up @@ -483,6 +510,8 @@ fn schedule_chart_cache_refresh(
key: String,
provider_id: String,
account_email: Option<String>,
account_id: Option<String>,
account_organization: Option<String>,
scoped_home: Option<std::path::PathBuf>,
usage_windows: Vec<LocalUsageWindowRequest>,
) {
Expand All @@ -500,6 +529,8 @@ fn schedule_chart_cache_refresh(
build_provider_chart_data_with_cancel(
provider_id,
account_email,
account_id,
account_organization,
scoped_home,
usage_windows,
None,
Expand All @@ -519,12 +550,24 @@ fn schedule_chart_cache_refresh(
fn chart_cache_key(
provider_id: &str,
account_email: Option<&str>,
account_id: Option<&str>,
account_organization: Option<&str>,
source_label: Option<&str>,
usage_windows: &[LocalUsageWindowRequest],
) -> String {
let identity = account_email
let identity = account_id
.map(str::trim)
.filter(|value| !value.is_empty())
.or_else(|| {
account_email
.map(str::trim)
.filter(|value| !value.is_empty())
})
.or_else(|| {
account_organization
.map(str::trim)
.filter(|value| !value.is_empty())
})
.unwrap_or("anonymous")
.to_ascii_lowercase();
let windows = usage_windows
Expand Down Expand Up @@ -585,7 +628,7 @@ fn persist_chart_cache(cache: &PersistedChartCache) {
}
match serde_json::to_vec(cache) {
Ok(bytes) => {
if let Err(error) = fs::write(path, bytes) {
if let Err(error) = codexbar::secure_file::atomic_write(&path, &bytes) {
tracing::warn!("failed to persist chart cache: {error}");
}
}
Expand Down Expand Up @@ -1113,12 +1156,22 @@ pub(crate) fn build_provider_chart_data(
provider_id: String,
account_email: Option<String>,
) -> ProviderChartData {
build_provider_chart_data_with_cancel(provider_id, account_email, None, Vec::new(), None)
build_provider_chart_data_with_cancel(
provider_id,
account_email,
None,
None,
None,
Vec::new(),
None,
)
}

fn build_provider_chart_data_with_cancel(
provider_id: String,
account_email: Option<String>,
account_id: Option<String>,
account_organization: Option<String>,
scoped_home: Option<std::path::PathBuf>,
usage_window_requests: Vec<LocalUsageWindowRequest>,
cancel: Option<Arc<AtomicBool>>,
Expand Down Expand Up @@ -1184,6 +1237,8 @@ fn build_provider_chart_data_with_cancel(
quota_history: crate::usage_history::provider_history(
&provider_id,
account_email.as_deref(),
account_id.as_deref(),
account_organization.as_deref(),
),
provider_id,
cost_history,
Expand Down Expand Up @@ -1793,18 +1848,41 @@ mod tests {
use super::{
CostFetchFailure, LocalEffortCost, LocalModelCost, LocalPlanUsage, LocalProjectCost,
LocalTokenBreakdown, LocalUsageWindowRequest, ProviderLocalUsageSummary, api_value_period,
comparison_period_specs, cost_fetch_failure_allows_early_retry, daily_series_from_report,
effort_breakdown, format_cost_csv, local_midnight_in_tz, local_usage_summary_from_report,
local_yesterday_window_utc, localized_estimate_note, model_breakdown,
parse_api_value_custom_range, period_from_daily_series, pricing_coverage_tokens,
project_breakdown, spend_budget_period_details, token_breakdown, token_cost_cache_is_fresh,
chart_cache_key, comparison_period_specs, cost_fetch_failure_allows_early_retry,
daily_series_from_report, effort_breakdown, format_cost_csv, local_midnight_in_tz,
local_usage_summary_from_report, local_yesterday_window_utc, localized_estimate_note,
model_breakdown, parse_api_value_custom_range, period_from_daily_series,
pricing_coverage_tokens, project_breakdown, spend_budget_period_details, token_breakdown,
token_cost_cache_is_fresh,
};
use crate::commands::is_provider_cache_fresh;
use chrono::{Local, LocalResult, NaiveDate, NaiveTime, TimeZone, Timelike, Utc};
use codexbar::cost_scanner::{CostSummary, CostUsageReport, ModelTokenCounts};
use codexbar::settings::Language;
use std::time::{Duration, Instant};

#[test]
fn chart_cache_separates_managed_accounts_that_share_an_email() {
let personal = chart_cache_key(
"codex",
Some("shared@example.com"),
Some("acct-personal"),
None,
Some("oauth"),
&[],
);
let work = chart_cache_key(
"codex",
Some("shared@example.com"),
Some("acct-work"),
None,
Some("oauth"),
&[],
);

assert_ne!(personal, work);
}

#[test]
fn token_cost_age_does_not_use_provider_quota_age() {
let now = Instant::now();
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop-tauri/src-tauri/src/geometry_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ fn save_file(file: &GeometryFile) -> Result<(), String> {
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let json = serde_json::to_string_pretty(file).map_err(|e| e.to_string())?;
fs::write(&path, json).map_err(|e| e.to_string())
codexbar::secure_file::atomic_write(&path, json.as_bytes()).map_err(|e| e.to_string())
}

/// Look up remembered geometry for a surface mode. Returns `None` when the
Expand Down
43 changes: 29 additions & 14 deletions apps/desktop-tauri/src-tauri/src/quota_run_history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
use crate::capacity_events::{CapacityEventKind, CapacityEventPayload};
use crate::commands::{ProviderUsageSnapshot, RateWindowSnapshot};

const STORE_VERSION: u8 = 2;
const STORE_VERSION: u8 = 3;
/// Keep enough history for run-over-run efficiency (SOU-299) without unbounded growth.
const MAX_RUNS_PER_SCOPE: usize = 40;
const RETENTION_DAYS: i64 = 120;
Expand Down Expand Up @@ -309,14 +309,21 @@ pub fn record_capacity_events(events: &[CapacityEventPayload], snapshot: &Provid
}

/// Completed runs for a provider/account, chronological (oldest first).
pub fn list_runs(provider_id: &str, account_email: Option<&str>) -> Vec<QuotaRunSnapshot> {
pub fn list_runs(
provider_id: &str,
account_email: Option<&str>,
account_id: Option<&str>,
) -> Vec<QuotaRunSnapshot> {
let Ok(guard) = store().lock() else {
return Vec::new();
};
let exact = scope_key_parts(provider_id, account_email, None, None);
let exact = scope_key_parts(provider_id, account_email, account_id, None);
if let Some(runs) = guard.runs.get(&exact) {
return runs.clone();
}
if account_id.is_some() {
return Vec::new();
}
// Fall back to anonymous series for this provider only (never another seat).
if account_email.is_some() {
let anonymous = scope_key_parts(provider_id, None, None, None);
Expand All @@ -331,8 +338,9 @@ pub fn list_runs(provider_id: &str, account_email: Option<&str>) -> Vec<QuotaRun
pub fn efficiency_for_provider(
provider_id: &str,
account_email: Option<&str>,
account_id: Option<&str>,
) -> Vec<QuotaRunEfficiency> {
let runs = list_runs(provider_id, account_email);
let runs = list_runs(provider_id, account_email, account_id);
if runs.is_empty() {
return Vec::new();
}
Expand Down Expand Up @@ -714,12 +722,16 @@ fn scope_key_parts(
account_id: Option<&str>,
organization: Option<&str>,
) -> String {
// Prefer email, then account id, then org — same idea as capacity observation
// scope without requiring source_label so list_runs stays callable from UI.
let identity = account_email
// A Ceiling-managed account id is stable even when two seats share an
// email address. Ambient readings still fall back to provider identity.
let identity = account_id
.map(str::trim)
.filter(|value| !value.is_empty())
.or_else(|| account_id.map(str::trim).filter(|value| !value.is_empty()))
.or_else(|| {
account_email
.map(str::trim)
.filter(|value| !value.is_empty())
})
.or_else(|| {
organization
.map(str::trim)
Expand Down Expand Up @@ -796,7 +808,7 @@ fn persist_store(store: &QuotaRunStore) {
}
match serde_json::to_vec(store) {
Ok(bytes) => {
if let Err(error) = fs::write(path, bytes) {
if let Err(error) = codexbar::secure_file::atomic_write(&path, &bytes) {
tracing::warn!("failed to persist quota run history: {error}");
}
}
Expand Down Expand Up @@ -924,7 +936,7 @@ mod tests {
);
record_capacity_events(&[event], &snap);

let runs = list_runs(provider, Some(email));
let runs = list_runs(provider, Some(email), Some("acct-work"));
assert_eq!(runs.len(), 1);
let run = &runs[0];
assert!(run.complete, "watched from low used through reset");
Expand Down Expand Up @@ -964,7 +976,7 @@ mod tests {
);
record_capacity_events(&[event], &snap);

let runs = list_runs(provider, Some(email));
let runs = list_runs(provider, Some(email), Some("acct-work"));
assert_eq!(runs.len(), 1);
assert!(!runs[0].complete);
assert_eq!(runs[0].reset_kind, QuotaRunResetKind::Surprise);
Expand All @@ -987,7 +999,7 @@ mod tests {
true,
);
record_capacity_events(&[event], &snap);
let runs = list_runs(provider, Some(email));
let runs = list_runs(provider, Some(email), Some("acct-work"));
assert_eq!(runs.len(), 1);
assert!(!runs[0].complete);
assert!(runs[0].while_away);
Expand Down Expand Up @@ -1021,8 +1033,11 @@ mod tests {
work.updated_at = (at + Duration::hours(1)).to_rfc3339();
record_capacity_events(&[event], &work);

assert_eq!(list_runs(provider, Some("me@job.test")).len(), 1);
assert!(list_runs(provider, Some("other@home.test")).is_empty());
assert_eq!(
list_runs(provider, Some("me@job.test"), Some("acct-work")).len(),
1
);
assert!(list_runs(provider, Some("me@job.test"), Some("acct-personal")).is_empty());
}

#[test]
Expand Down
Loading
Loading