From 486b85bbfc331a6de8a8a2abb9c6c27649cef156 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sun, 31 May 2026 22:38:54 +0800 Subject: [PATCH] {"schema":"decodex/commit/1","summary":"Weight Codex account pool usage by capacity","authority":"manual"} --- README.md | 9 ++- .../Sources/DecodexApp/AccountPanelView.swift | 28 +++---- .../Sources/DecodexApp/Models.swift | 27 +++++-- apps/decodex/src/accounts.rs | 75 ++++++++++++++++--- .../src/orchestrator/operator_dashboard.html | 36 +++++++-- .../tests/operator/status/dashboard.rs | 14 ++-- docs/reference/operator-control-plane.md | 2 +- docs/reference/workspace-layout.md | 2 + docs/spec/runtime.md | 2 +- 9 files changed, 145 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 150c7aa6..8817b6c4 100644 --- a/README.md +++ b/README.md @@ -144,10 +144,11 @@ privacy-preserving names. Client-only presentation preferences such as theme, so and whether identities are hidden remain local to each UI. Usage probes keep bounded seven-day account usage estimates in `~/.codex/decodex/account-usage-history.jsonl`; the file stores daily percentage -snapshots for local display and no token material. To switch the account used by the -Codex CLI itself, run `decodex account use ` or use the Decodex App row -action; this overwrites `$CODEX_HOME/auth.json` or `~/.codex/auth.json` from the -matching `accounts.jsonl` entry. +snapshots plus non-secret capacity weights for local display and no token material. +To switch the account used by the Codex CLI itself, run +`decodex account use ` or use the Decodex App row action; this overwrites +`$CODEX_HOME/auth.json` or `~/.codex/auth.json` from the matching `accounts.jsonl` +entry. `decodex diagnose --json` writes the local agent evidence index under `~/.codex/decodex/agent-evidence//` and prints the same handoff index for diff --git a/apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift b/apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift index 11685649..6f73454a 100644 --- a/apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift +++ b/apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift @@ -743,18 +743,16 @@ struct AccountRowView: View { .truncationMode(.middle) .layoutPriority(1) - if let planLabel = account.planLabel { - Text("·") - .font(PanelFont.accountDetail) - .foregroundStyle(PanelPalette.secondaryText(colorScheme).opacity(0.62)) - .fixedSize(horizontal: true, vertical: false) + Text("·") + .font(PanelFont.accountDetail) + .foregroundStyle(PanelPalette.secondaryText(colorScheme).opacity(0.62)) + .fixedSize(horizontal: true, vertical: false) - Text(planLabel) - .font(PanelFont.accountDetail) - .foregroundStyle(PanelPalette.secondaryText(colorScheme)) - .lineLimit(1) - .fixedSize(horizontal: true, vertical: false) - } + Text(account.capacityLabel) + .font(PanelFont.accountDetail) + .foregroundStyle(PanelPalette.secondaryText(colorScheme)) + .lineLimit(1) + .fixedSize(horizontal: true, vertical: false) if let healthLabel = account.compactHealthLabel { Text("·") @@ -1251,13 +1249,15 @@ struct AccountPoolUsageEstimateView: View { } let previousRecords = measuredAccounts.compactMap { account in - usageRecord(for: account, on: previousDate) + usageRecord(for: account, on: previousDate).map { (account, $0) } } guard previousRecords.count == measuredAccounts.count else { return estimate.averageDailyPoolPercent } - let previousUsedPercent = previousRecords.reduce(0) { total, record in - total + record.usedPercent + let previousUsedPercent = previousRecords.reduce(0) { total, pair in + let (account, record) = pair + + return total + record.usedPercent * (record.capacityMultiplier ?? account.capacityWeight) } let previousPoolPercent = (Double(previousUsedPercent) / Double(estimate.totalCapacityPercent)) * 100 diff --git a/apps/decodex-app/Sources/DecodexApp/Models.swift b/apps/decodex-app/Sources/DecodexApp/Models.swift index d4137963..f4517f56 100644 --- a/apps/decodex-app/Sources/DecodexApp/Models.swift +++ b/apps/decodex-app/Sources/DecodexApp/Models.swift @@ -93,6 +93,7 @@ struct AccountUsageEstimate: Decodable, Equatable { struct AccountUsageRecord: Decodable, Identifiable, Equatable { let date: String let usedPercent: Int + let capacityMultiplier: Int? let checkedAtUnixEpoch: Int var id: String { @@ -102,6 +103,7 @@ struct AccountUsageRecord: Decodable, Identifiable, Equatable { enum CodingKeys: String, CodingKey { case date case usedPercent = "used_percent" + case capacityMultiplier = "capacity_multiplier" case checkedAtUnixEpoch = "checked_at_unix_epoch" } } @@ -123,6 +125,7 @@ struct CodexAccount: Decodable, Identifiable, Equatable { let cooldownUntilUnixEpoch: Int? let note: String? let planType: String? + let capacityMultiplier: Int? let refreshStatus: String? let checkedAtUnixEpoch: Int? let primaryWindowSeconds: Int? @@ -211,12 +214,12 @@ struct CodexAccount: Decodable, Identifiable, Equatable { } } - var planLabel: String? { - guard let planType, !planType.isEmpty else { - return nil - } + var capacityWeight: Int { + max(1, capacityMultiplier ?? Self.capacityMultiplier(for: planType)) + } - return planType.replacingOccurrences(of: "_", with: " ").capitalized + var capacityLabel: String { + "\(capacityWeight)x" } var hasUsageWindowData: Bool { @@ -272,6 +275,7 @@ struct CodexAccount: Decodable, Identifiable, Equatable { cooldownUntilUnixEpoch: cooldownUntilUnixEpoch, note: note, planType: planType, + capacityMultiplier: capacityMultiplier, refreshStatus: refreshStatus, checkedAtUnixEpoch: checkedAtUnixEpoch, primaryWindowSeconds: primaryWindowSeconds, @@ -307,6 +311,7 @@ struct CodexAccount: Decodable, Identifiable, Equatable { case cooldownUntilUnixEpoch = "cooldown_until_unix_epoch" case note case planType = "plan_type" + case capacityMultiplier = "capacity_multiplier" case refreshStatus = "refresh_status" case checkedAtUnixEpoch = "checked_at_unix_epoch" case primaryWindowSeconds = "primary_window_seconds" @@ -323,6 +328,18 @@ struct CodexAccount: Decodable, Identifiable, Equatable { case sevenDayDailyAveragePercent = "seven_day_daily_average_percent" case usageRecords = "usage_records" } + + private static func capacityMultiplier(for planType: String?) -> Int { + guard let planType, !planType.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + return 1 + } + + if planType.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "pro" { + return 20 + } + + return 1 + } } enum AccountTone { diff --git a/apps/decodex/src/accounts.rs b/apps/decodex/src/accounts.rs index 7b98a69b..bc5494a9 100644 --- a/apps/decodex/src/accounts.rs +++ b/apps/decodex/src/accounts.rs @@ -22,6 +22,8 @@ use crate::{ const USAGE_ESTIMATE_WINDOW_DAYS: i64 = 7; const USAGE_ESTIMATE_WINDOW_SECONDS: i64 = USAGE_ESTIMATE_WINDOW_DAYS * 24 * 60 * 60; +const DEFAULT_ACCOUNT_CAPACITY_MULTIPLIER: i64 = 1; +const PRO_ACCOUNT_CAPACITY_MULTIPLIER: i64 = 20; const ACCOUNT_RANDOM_NAMES: &[&str] = &[ "Alex", "Avery", "Bailey", "Blake", "Casey", "Charlie", "Clara", "Dana", "Drew", "Eden", "Elliot", "Emery", "Evan", "Finley", "Harper", "Hayden", "Iris", "Jamie", "Jordan", "Kai", @@ -515,12 +517,15 @@ impl AccountListResponse { let account_count = self.accounts.len(); let account_estimate_count = self.accounts.iter().filter(|account| account.seven_day_used_percent.is_some()).count(); + let total_capacity_percent = + self.accounts.iter().map(AccountSummary::capacity_percent).sum::(); let total_used_percent = - self.accounts.iter().filter_map(|account| account.seven_day_used_percent).sum::(); + self.accounts.iter().map(AccountSummary::used_capacity_percent).sum::(); self.usage_estimate = AccountUsageEstimateSummary::new( account_count, account_estimate_count, + total_capacity_percent, total_used_percent, ); } @@ -560,14 +565,13 @@ impl AccountUsageEstimateSummary { fn new( account_count: usize, account_estimate_count: usize, + total_capacity_percent: i64, total_used_percent: i64, ) -> Option { if account_count == 0 || account_estimate_count == 0 { return None; } - let total_capacity_percent = - i64::try_from(account_count).unwrap_or(i64::MAX / 100).saturating_mul(100); let total_used_of_capacity_percent = percent_ratio(total_used_percent, total_capacity_percent); @@ -590,6 +594,7 @@ impl AccountUsageEstimateSummary { pub(crate) struct AccountUsageDailySummary { pub(crate) date: String, pub(crate) used_percent: i64, + pub(crate) capacity_multiplier: i64, pub(crate) checked_at_unix_epoch: i64, } @@ -611,6 +616,7 @@ pub(crate) struct AccountSummary { pub(crate) cooldown_until_unix_epoch: Option, pub(crate) note: Option, pub(crate) plan_type: Option, + pub(crate) capacity_multiplier: i64, pub(crate) refresh_status: Option, pub(crate) checked_at_unix_epoch: Option, pub(crate) primary_window_seconds: Option, @@ -631,6 +637,7 @@ impl AccountSummary { fn apply_usage_summary(&mut self, summary: &CodexAccountActivitySummary) { self.status = summary.status.clone(); self.plan_type = summary.plan_type.clone(); + self.capacity_multiplier = account_capacity_multiplier(self.plan_type.as_deref()); self.refresh_status = Some(summary.refresh_status.clone()); self.checked_at_unix_epoch = summary.checked_at_unix_epoch; self.primary_window_seconds = summary.primary_window_seconds; @@ -675,11 +682,22 @@ impl AccountSummary { account_fingerprint: self.account_fingerprint.clone(), email: self.email.clone(), used_percent: basis.used_percent, + capacity_multiplier: self.capacity_multiplier, window_seconds: basis.window_seconds, checked_at_unix_epoch, resets_at_unix_epoch: basis.resets_at_unix_epoch, }) } + + fn capacity_percent(&self) -> i64 { + normalized_account_capacity_multiplier(self.capacity_multiplier).saturating_mul(100) + } + + fn used_capacity_percent(&self) -> i64 { + self.seven_day_used_percent + .unwrap_or_default() + .saturating_mul(normalized_account_capacity_multiplier(self.capacity_multiplier)) + } } #[derive(Clone, Copy)] @@ -730,6 +748,8 @@ struct AccountUsageHistoryRecord { #[serde(skip_serializing_if = "Option::is_none")] email: Option, used_percent: i64, + #[serde(default = "default_account_capacity_multiplier")] + capacity_multiplier: i64, #[serde(skip_serializing_if = "Option::is_none")] window_seconds: Option, checked_at_unix_epoch: i64, @@ -741,6 +761,7 @@ impl AccountUsageHistoryRecord { AccountUsageDailySummary { date: self.date.clone(), used_percent: self.used_percent, + capacity_multiplier: normalized_account_capacity_multiplier(self.capacity_multiplier), checked_at_unix_epoch: self.checked_at_unix_epoch, } } @@ -845,6 +866,8 @@ impl AccountUsageHistory { matching_records.iter().max_by_key(|record| record.checked_at_unix_epoch) { account.seven_day_used_percent = Some(latest.used_percent); + account.capacity_multiplier = + normalized_account_capacity_multiplier(latest.capacity_multiplier); account.seven_day_daily_average_percent = Some(latest.used_percent as f64 / USAGE_ESTIMATE_WINDOW_DAYS as f64); } @@ -1066,6 +1089,7 @@ impl AccountPoolRecord { cooldown_until_unix_epoch: self.cooldown_until_unix_epoch, note: Some(String::from("local account pool")), plan_type: None, + capacity_multiplier: DEFAULT_ACCOUNT_CAPACITY_MULTIPLIER, refresh_status: None, checked_at_unix_epoch: None, primary_window_seconds: None, @@ -1659,6 +1683,21 @@ fn accepts_secondary_usage_window(window_seconds: Option) -> bool { window_seconds.is_none_or(is_seven_day_usage_window) } +const fn default_account_capacity_multiplier() -> i64 { + DEFAULT_ACCOUNT_CAPACITY_MULTIPLIER +} + +fn account_capacity_multiplier(plan_type: Option<&str>) -> i64 { + match plan_type.map(str::trim).filter(|value| !value.is_empty()) { + Some(plan_type) if plan_type.eq_ignore_ascii_case("pro") => PRO_ACCOUNT_CAPACITY_MULTIPLIER, + _ => DEFAULT_ACCOUNT_CAPACITY_MULTIPLIER, + } +} + +fn normalized_account_capacity_multiplier(value: i64) -> i64 { + value.max(DEFAULT_ACCOUNT_CAPACITY_MULTIPLIER) +} + fn is_seven_day_usage_window(window_seconds: i64) -> bool { window_seconds .checked_sub(USAGE_ESTIMATE_WINDOW_SECONDS) @@ -2044,6 +2083,7 @@ mod tests { assert_eq!(response.accounts[0].secondary_remaining_percent, Some(91)); assert_eq!(response.accounts[0].credits_balance.as_deref(), Some("9.99")); assert_eq!(response.accounts[0].seven_day_used_percent, Some(9)); + assert_eq!(response.accounts[0].capacity_multiplier, 20); assert_close(response.accounts[0].seven_day_daily_average_percent, 9.0 / 7.0); } @@ -2074,8 +2114,8 @@ mod tests { .expect("records should save"); let summaries = [ - usage_summary("copy@example.com", "...123456", 40), - usage_summary("other@example.com", "...654321", 70), + usage_summary("copy@example.com", "...123456", "pro", 40), + usage_summary("other@example.com", "...654321", "plus", 70), ]; let mut response = store.list().expect("account list should load"); @@ -2092,19 +2132,31 @@ mod tests { assert_eq!(estimate.window_days, 7); assert_eq!(estimate.account_count, 2); assert_eq!(estimate.account_estimate_count, 2); - assert_eq!(estimate.total_capacity_percent, 200); - assert_eq!(estimate.total_used_percent, 90); + assert_eq!(estimate.total_capacity_percent, 2_100); + assert_eq!(estimate.total_used_percent, 1_230); - assert_close(Some(estimate.total_used_of_capacity_percent), 45.0); - assert_close(Some(estimate.average_daily_used_percent), 90.0 / 7.0); - assert_close(Some(estimate.average_daily_pool_percent), 45.0 / 7.0); + assert_close(Some(estimate.total_used_of_capacity_percent), 58.571); + assert_close(Some(estimate.average_daily_used_percent), 1_230.0 / 7.0); + assert_close(Some(estimate.average_daily_pool_percent), 58.571 / 7.0); assert_eq!(response.accounts[0].usage_records.len(), 1); assert_eq!(response.accounts[0].usage_records[0].date, record_date); assert_eq!(response.accounts[0].usage_records[0].used_percent, 60); + assert_eq!(response.accounts[0].usage_records[0].capacity_multiplier, 20); + assert_eq!(response.accounts[1].usage_records[0].capacity_multiplier, 1); assert_eq!(history.lines().count(), 2); assert!(history.contains(r#""used_percent":60"#)); + assert!(history.contains(r#""capacity_multiplier":20"#)); assert!(history.contains(r#""used_percent":30"#)); + assert!(history.contains(r#""capacity_multiplier":1"#)); + } + + #[test] + fn capacity_multiplier_counts_only_pro_above_plus_weight() { + assert_eq!(super::account_capacity_multiplier(Some("pro")), 20); + assert_eq!(super::account_capacity_multiplier(Some("plus")), 1); + assert_eq!(super::account_capacity_multiplier(Some("team")), 1); + assert_eq!(super::account_capacity_multiplier(None), 1); } #[test] @@ -2181,12 +2233,13 @@ mod tests { fn usage_summary( email: &str, account_fingerprint: &str, + plan_type: &str, secondary_remaining_percent: i64, ) -> CodexAccountActivitySummary { CodexAccountActivitySummary { account_fingerprint: String::from(account_fingerprint), email: Some(String::from(email)), - plan_type: Some(String::from("pro")), + plan_type: Some(String::from(plan_type)), status: String::from("available"), refresh_status: String::from("not_needed"), checked_at_unix_epoch: Some(1_800_000_000), diff --git a/apps/decodex/src/orchestrator/operator_dashboard.html b/apps/decodex/src/orchestrator/operator_dashboard.html index 569ca8c8..a2d1f05a 100644 --- a/apps/decodex/src/orchestrator/operator_dashboard.html +++ b/apps/decodex/src/orchestrator/operator_dashboard.html @@ -3489,7 +3489,7 @@

Run History

const PROJECT_SORT_STORAGE_KEY = "decodex.operator.projectSort"; const ACCOUNT_POOL_SORT_COLUMNS = [ ["account", "Account"], - ["plan", "Plan"], + ["plan", "Weight"], ["primary", "5h"], ["secondary", "7d"], ["credits", "Credits"], @@ -6229,8 +6229,26 @@

Run History

: compactAccountIdentity(selector); } - function codexAccountPlanLabel(account) { - return account?.plan_type ? humanizeToken(account.plan_type) : "-"; + function codexAccountCapacityMultiplier(account) { + const explicit = codexAccountNumber(account?.capacity_multiplier); + if (explicit != null && explicit > 0) { + return explicit; + } + + const planType = String(account?.plan_type || "").trim().toLowerCase(); + return planType === "pro" ? 20 : 1; + } + + function codexAccountCapacityLabel(account) { + return `${codexAccountCapacityMultiplier(account)}x`; + } + + function codexAccountUsageRecordCapacityMultiplier(account, record) { + const explicit = codexAccountNumber(record?.capacity_multiplier); + + return explicit != null && explicit > 0 + ? explicit + : codexAccountCapacityMultiplier(account); } function codexAccountTokenLabel(refreshStatus) { @@ -6424,7 +6442,11 @@

Run History

const previousUsedPercent = measuredAccounts.reduce((total, account) => { const record = usageRecordForDate(account, previousDate); - return total + (codexAccountNumber(record?.used_percent) || 0); + const used = codexAccountNumber(record?.used_percent) || 0; + + return ( + total + used * codexAccountUsageRecordCapacityMultiplier(account, record) + ); }, 0); const previousPoolPercent = (previousUsedPercent / totalCapacity) * 100; @@ -6775,7 +6797,7 @@

Run History

} function renderCodexAccountPoolRow(account, snapshot) { - const plan = codexAccountPlanLabel(account); + const weight = codexAccountCapacityLabel(account); const statusTone = codexAccountStatusTone(account); const toneClass = statusTone ? ` is-${statusTone}` : ""; const fixed = codexAccountMatchesConfiguredSelector(account, snapshot); @@ -6798,7 +6820,7 @@

Run History

${renderCodexAccountNameControl(account, snapshot)} ${renderCodexAccountRandomNameButton(account)} - + ${renderCodexAccountPoolWindow(account, "primary")} ${renderCodexAccountPoolWindow(account, "secondary")}