Skip to content

fix: scope session-equivalent forecast history by account - #276

Open
zaszlo wants to merge 1 commit into
nesszer:mainfrom
zaszlo:fix/forecast-history-account-scope
Open

fix: scope session-equivalent forecast history by account#276
zaszlo wants to merge 1 commit into
nesszer:mainfrom
zaszlo:fix/forecast-history-account-scope

Conversation

@zaszlo

@zaszlo zaszlo commented Aug 10, 2026

Copy link
Copy Markdown

Problem

Session-equivalent forecast history is keyed by provider_id alone, so it is shared across every account on that provider.

SessionEquivalentHistoryStore and LastFullSessionEstimateStore are both HashMap<String, _> keyed by provider, and bridge.rs passes id.cli_name() — the literal string "codex" or "claude" — with no account discriminator:

codexbar::core::record_provider_windows(provider_id, session, Some(weekly), now);

Fetch itself is single-account, so nothing mixes concurrently. The problem is switching: switch_active_account() changes which account is fetched, while the ring buffer — sized sample_limit * 8, minimum 24 entries — still holds the previous account's observations.

After a switch the burn median is computed from a mixture of two accounts' samples until the old entries age out. Moving between plans of different sizes produces a silently wrong "estimated windows to exhaust" for the following several windows, with nothing in the UI signalling the number is unreliable.

The existing source comment already anticipates the correct key:

ponytail: history is process-local and lost on restart; upgrade path = disk cache keyed by provider+account once forecast quality justifies persistence.

This is the same class of fix as #203, which scoped quota notifications by account identity. That change never reached forecast history.

Change

Introduce ForecastScope { provider_id, account_key } and key both stores by it.

  • SessionEquivalentHistoryStore.by_providerby_scope
  • LastFullSessionEstimateStore.by_providerby_scope
  • record_provider_windows(), forecast_for_provider(), remember_full_session_estimate(), record(), histories() take the account discriminator

Sourcing the discriminator

Claude publishes account_email, but Codex publishes neither email nor organization — under the ADR 0003 ambient/managed lane split its only stable discriminator is the managed token-account id. Keying off the email alone would have left this bug live for Codex, which is the provider with first-class multi-account support.

So the identity is resolved where it actually lives and threaded down:

spawn_provider_refreshes (has inputs.token_accounts) → refresh_providerfetch_provider_snapshotfrom_fetch_result

A new forecast_account_key() deliberately mirrors quota_notification_account_identity precedence — token-account:{uuid} → email → org:{org}. It is a separate function only because that one consumes an already-built ProviderUsageSnapshot while the forecast needs the key while the snapshot is being built.

Behaviour for single-account users is unchanged: one account means one scope.

Notes

  • The account key is in-memory only, and the email is already resident in UsageSnapshot. A comment marks that it should be hashed rather than written in the clear if this history is later persisted to disk.
  • An account with no discoverable identity gets account_key: None, which is its own bucket rather than a catch-all shared with identified accounts.
  • No change to the forecast math, the snapshot shape, or the TS bridge contract.

Tests

  • history_is_isolated_per_account — two accounts on one provider; asserts neither account's samples appear in the other's history and that the None bucket stays empty.
  • forecast_account_key_matches_notification_identity — pins forecast_account_key to quota_notification_account_identity across all 8 combinations of email/org/token, so the two subsystems can never see one account as two identities. If they diverged, an account's burn history would split and silently halve the sample count behind every forecast.
  • history_ring_retains_latest_samples updated for the new signature.

Verification

cargo fmt --all --check                                              # clean
cargo clippy -p codexbar --all-targets -- -D warnings                # exit 0
cargo clippy -p codexbar-desktop-tauri --all-targets -- -D warnings  # exit 0
cargo test -p codexbar --lib                                         # 1207 passed, 0 failed
cargo test -p codexbar-desktop-tauri                                 # 342 passed, 0 failed

Note on cli::tty_runner::tests::test_run_sends_script_through_pty: it failed on one
full-suite run here and passed on others. I checked it against untouched main
(116af24b), where it also fails under full-suite parallel load, so it is pre-existing
flakiness in this environment and unrelated to this change.

No CUA Driver proof included: this changes no UI, tray, or Settings surface. The forecast line in MenuCardDetails renders exactly as before for a single-account user.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants