Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3d16cd6
Port 0.56.4 growing Codex rollouts
Finesssee Sep 6, 2026
33d6a21
Port 0.56.4 Antigravity step timestamps
Finesssee Sep 6, 2026
3c34223
Port 0.56.4 managed workspace authority
Finesssee Sep 6, 2026
b56a73f
Port 0.56.4 Windows UI and Bedrock guidance
Finesssee Sep 6, 2026
4dc8af8
Port 0.56.4 Claude Cloudflare recovery
Finesssee Sep 6, 2026
3d901b1
Fix Bedrock monitoring guidance
Finesssee Sep 6, 2026
e2abce9
Fix 0.56.4 native Rust compile regressions
Finesssee Sep 7, 2026
ce41298
Finish 0.56.4 native Rust compile fixes
Finesssee Sep 7, 2026
c6e263d
Finish 0.56.4 native Tauri compile fixes
Finesssee Sep 7, 2026
f2e388c
Fix 0.56.4 shared timestamp regression test
Finesssee Sep 7, 2026
ff0ec22
Make 0.56.4 Rust port Clippy-clean
Finesssee Sep 7, 2026
bf2448c
refactor(claude): move recovery policy into provider
Finesssee Sep 8, 2026
77ade63
style: apply workspace rustfmt
Finesssee Sep 8, 2026
4815d32
fix: attach async_trait to provider trait
Finesssee Sep 8, 2026
b924b14
fix(claude): attach async_trait to provider impl
Finesssee Sep 8, 2026
fe5b7a5
Merge repaired 0.56.3 base
Finesssee Sep 8, 2026
80ef86d
Merge formatted repaired base for #438
Finesssee Sep 8, 2026
181e7aa
Merge repaired 0.56.3 base
Finesssee Sep 8, 2026
0a7824f
Merge final repaired base for #438
Finesssee Sep 8, 2026
a970b6f
Merge final format fix for #438
Finesssee Sep 8, 2026
e142f3f
test: complete Claude fetch result fixtures
Finesssee Sep 8, 2026
7bdf9d5
Merge stabilized 0.56.3 base
Finesssee Sep 8, 2026
896c2c4
Merge remote-tracking branch 'origin/main' into HEAD
Finesssee Sep 8, 2026
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
108 changes: 100 additions & 8 deletions apps/desktop-tauri/src-tauri/src/commands/codex_accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,19 @@ pub(crate) async fn refresh_codex_account_lanes(
let api = CodexAccountApi::new();
let home_path = account.codex_home_path.clone();
let email_hint = account.email_hint.clone();
let workspace_account_id = account.effective_workspace_account_id();
match tokio::time::timeout(
std::time::Duration::from_secs(DEFAULT_FETCH_TIMEOUT_SECONDS),
api.fetch_snapshot(&home_path, email_hint.as_deref(), true),
api.fetch_snapshot_for_workspace(
&home_path,
email_hint.as_deref(),
workspace_account_id.as_deref(),
true,
),
)
.await
{
Ok(Ok(snapshot)) => Some((account.id, snapshot)),
Ok(Ok(snapshot)) => Some((account, snapshot)),
Ok(Err(e)) => {
tracing::debug!(
"codex account lane {} failed: {}",
Expand All @@ -128,10 +134,28 @@ pub(crate) async fn refresh_codex_account_lanes(
}));
}

let current_accounts = load_codex_accounts().unwrap_or_default();
let current_by_id: HashMap<Uuid, CodexAccount> = current_accounts
.iter()
.cloned()
.map(|account| (account.id, account))
.collect();
let mut snapshots = SnapshotStore::new().load().unwrap_or_default();
snapshots.retain(|id, snapshot| {
current_by_id
.get(id)
.is_some_and(|account| account_snapshot_belongs_to(account, snapshot))
});
for handle in handles {
if let Ok(Some((id, snapshot))) = handle.await {
snapshots.insert(id, snapshot);
if let Ok(Some((fetched_account, snapshot))) = handle.await
&& current_by_id
.get(&fetched_account.id)
.is_some_and(|current| {
account_lane_is_current(&fetched_account, current)
&& account_snapshot_belongs_to(current, &snapshot)
})
{
snapshots.insert(fetched_account.id, snapshot);
}
}
if let Err(e) = SnapshotStore::new().save(&snapshots) {
Expand Down Expand Up @@ -232,16 +256,30 @@ pub async fn codex_account_fetch(
let api = CodexAccountApi::new();
let home_path = target.codex_home_path.clone();
let email_hint = target.email_hint.clone();
let workspace_account_id = target.effective_workspace_account_id();
let snapshot = tokio::time::timeout(
std::time::Duration::from_secs(DEFAULT_FETCH_TIMEOUT_SECONDS),
api.fetch_snapshot(&home_path, email_hint.as_deref(), true),
api.fetch_snapshot_for_workspace(
&home_path,
email_hint.as_deref(),
workspace_account_id.as_deref(),
true,
),
)
.await
.map_err(|_| "Timed out waiting for the Codex usage API.".to_string())?
.map_err(into_api_message)?;

// Persist snapshot to the snapshot store, keyed by account id.
if let Ok(mut snapshots) = SnapshotStore::new().load() {
if let Ok(mut snapshots) = SnapshotStore::new().load()
&& load_codex_accounts().ok().is_some_and(|accounts| {
accounts.iter().any(|account| {
account.id == target.id
&& account_lane_is_current(&target, account)
&& account_snapshot_belongs_to(account, &snapshot)
})
})
{
snapshots.insert(target.id, snapshot.clone());
let _ = SnapshotStore::new().save(&snapshots);
}
Expand Down Expand Up @@ -306,6 +344,58 @@ fn into_api_message(error: CodexApiError) -> String {
}
}

fn account_home_key(account: &CodexAccount) -> String {
std::path::absolute(&account.codex_home_path)
.unwrap_or_else(|_| account.codex_home_path.clone())
.to_string_lossy()
.to_lowercase()
}

/// In-flight results are only authoritative for the selected workspace and
/// managed home that started the request.
fn account_lane_is_current(started: &CodexAccount, current: &CodexAccount) -> bool {
started.id == current.id
&& account_home_key(started) == account_home_key(current)
&& started.effective_workspace_account_id() == current.effective_workspace_account_id()
}

fn account_snapshot_belongs_to(
account: &CodexAccount,
snapshot: &codexbar::codex_accounts::AccountUsageSnapshot,
) -> bool {
let snapshot_workspace = snapshot
.provider_account_id
.as_deref()
.map(str::trim)
.filter(|id| !id.is_empty())
.map(str::to_lowercase);
match (account.effective_workspace_account_id(), snapshot_workspace) {
(Some(account_workspace), Some(snapshot_workspace)) => {
account_workspace == snapshot_workspace
}
(None, None) => true,
_ => false,
}
}

fn snapshots_for_accounts(
accounts: &[CodexAccount],
snapshots: HashMap<Uuid, codexbar::codex_accounts::AccountUsageSnapshot>,
) -> HashMap<Uuid, codexbar::codex_accounts::AccountUsageSnapshot> {
let accounts_by_id: HashMap<Uuid, &CodexAccount> = accounts
.iter()
.map(|account| (account.id, account))
.collect();
snapshots
.into_iter()
.filter(|(id, snapshot)| {
accounts_by_id
.get(id)
.is_some_and(|account| account_snapshot_belongs_to(account, snapshot))
})
.collect()
}

#[derive(Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodexAccountsStateBridge {
Expand All @@ -320,10 +410,12 @@ pub fn get_codex_accounts_state(
) -> Result<CodexAccountsStateBridge, String> {
let _guard = state.lock().map_err(|e| e.to_string())?;
let accounts = load_codex_accounts()?;
let display_names = display_names_by_id(&accounts);
let snapshots = snapshots_for_accounts(&accounts, codex_account_snapshots()?);
Ok(CodexAccountsStateBridge {
display_names: display_names_by_id(&accounts),
accounts,
snapshots: codex_account_snapshots()?,
display_names,
snapshots,
})
}

Expand Down
155 changes: 58 additions & 97 deletions apps/desktop-tauri/src-tauri/src/commands/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub(crate) fn build_fetch_context(
api_keys: &ApiKeys,
token_accounts: &HashMap<ProviderId, ProviderAccountData>,
) -> FetchContext {
let provider = instantiate_provider(id);
let cookie_source = settings.cookie_source(id);
let stored_cookie = cookies.get(id.cli_name()).map(|s| s.to_string());
let stored_api_key = api_keys.get(id.cli_name()).map(|s| s.to_string());
Expand All @@ -26,6 +27,9 @@ pub(crate) fn build_fetch_context(
let active_token_cookie = token_override
.as_ref()
.and_then(|override_data| override_data.cookie_header.clone());
let defer_provider_browser_cookie_lookup = provider.owns_browser_cookie_resolution()
&& active_token_cookie.is_none()
&& stored_cookie.is_none();
let active_token_env = token_override
.as_ref()
.and_then(|override_data| override_data.env_override.as_ref());
Expand Down Expand Up @@ -78,14 +82,18 @@ pub(crate) fn build_fetch_context(
}
// `browser` is accepted as a legacy alias from older settings.
"auto" | "browser" | "web" => {
// Try browser cookie extraction as fallback when no manual cookie is set.
// On non-Windows this is a harmless no-op that returns an error.
// Claude resolves its cached cookie and browser fallback inside
// the provider; other providers retain the shell fallback.
let cookie_header = active_token_cookie.or(stored_cookie).or_else(|| {
provider_cookie_domain(id, settings).and_then(|domain| {
codexbar::browser::cookies::get_cookie_header(domain)
.ok()
.filter(|h| !h.is_empty())
})
if defer_provider_browser_cookie_lookup {
None
} else {
provider_cookie_domain(id, settings).and_then(|domain| {
codexbar::browser::cookies::get_cookie_header(domain)
.ok()
.filter(|h| !h.is_empty())
})
}
});
(usage_source, cookie_header)
}
Expand All @@ -97,10 +105,7 @@ pub(crate) fn build_fetch_context(
// historically mapped "manual + no cookie" to Cli, which surfaces as
// "Source mode 'Cli' not supported". Remap to Web and try browser cookies
// unless the user explicitly disabled cookies ("off").
if source_mode == SourceMode::Cli
&& cookie_source != "off"
&& !instantiate_provider(id).supports_cli()
{
if source_mode == SourceMode::Cli && cookie_source != "off" && !provider.supports_cli() {
if cookie_header
.as_deref()
.map(str::trim)
Expand Down Expand Up @@ -505,28 +510,13 @@ pub(super) fn preserve_last_good_transient_failure(
id: ProviderId,
snapshot: ProviderUsageSnapshot,
) -> ProviderUsageSnapshot {
if snapshot.error.is_none() {
guard.transient_provider_failure_counts.remove(&id);
return snapshot;
}

if id != ProviderId::Claude {
let Some(error) = snapshot.error.as_deref() else {
guard.transient_provider_failure_counts.remove(&id);
return snapshot;
}

let error = snapshot.error.as_deref();
// Hard auth loss / subscription-unavailable answers should not keep stale bars.
if is_hard_claude_auth_loss(error) {
guard.transient_provider_failure_counts.remove(&id);
return snapshot;
}
};

let preservable = is_transient_claude_auth_error(error)
|| is_claude_cli_usage_parse_failure(error)
|| is_claude_cli_rate_limit_failure(error)
|| is_claude_timeout_failure(error);
if !preservable {
let policy = instantiate_provider(id).last_good_failure_policy(error);
if policy == codexbar::core::LastGoodFailurePolicy::Replace {
guard.transient_provider_failure_counts.remove(&id);
return snapshot;
}
Expand All @@ -540,82 +530,53 @@ pub(super) fn preserve_last_good_transient_failure(
return snapshot;
};

// Parse / rate-limit / timeout: keep last-good every time (upstream #2247).
// Transient auth (unauthorized-ish) still only preserves once so real logout surfaces.
let parse_or_rate = is_claude_cli_usage_parse_failure(error)
|| is_claude_cli_rate_limit_failure(error)
|| is_claude_timeout_failure(error);

let count = guard
.transient_provider_failure_counts
.entry(id)
.or_insert(0);
if parse_or_rate || *count == 0 {
if !parse_or_rate {
match policy {
codexbar::core::LastGoodFailurePolicy::Preserve => {
tracing::warn!(
provider = id.cli_name(),
error,
"preserving last good provider snapshot after transient failure"
);
previous
}
codexbar::core::LastGoodFailurePolicy::PreserveOnce if *count == 0 => {
*count = 1;
tracing::warn!(
provider = id.cli_name(),
error,
"preserving last good provider snapshot after transient failure"
);
previous
}
tracing::warn!(
provider = id.cli_name(),
error = error.unwrap_or(""),
"preserving last good Claude snapshot after transient failure"
);
previous
} else {
*count = count.saturating_add(1);
snapshot
codexbar::core::LastGoodFailurePolicy::PreserveOnce => {
*count = count.saturating_add(1);
snapshot
}
codexbar::core::LastGoodFailurePolicy::PreserveOnceThenSurface if *count == 0 => {
*count = 1;
tracing::warn!(
provider = id.cli_name(),
error,
"preserving last good provider snapshot after transient failure"
);
previous
}
codexbar::core::LastGoodFailurePolicy::PreserveOnceThenSurface => {
*count = count.saturating_add(1);
let mut surfaced = previous;
surfaced.error = snapshot.error;
surfaced.error_state = snapshot.error_state;
surfaced.fetch_duration_ms = snapshot.fetch_duration_ms;
surfaced
}
codexbar::core::LastGoodFailurePolicy::Replace => snapshot,
}
}

fn is_transient_claude_auth_error(error: Option<&str>) -> bool {
let Some(error) = error else {
return false;
};
let lower = error.to_ascii_lowercase();
lower.contains("unauthorized")
|| lower.contains("authentication required")
|| lower.contains("auth required")
}

fn is_hard_claude_auth_loss(error: Option<&str>) -> bool {
let Some(error) = error else {
return false;
};
let lower = error.to_ascii_lowercase();
// Credentials truly missing / login required — clear stale usage.
lower.contains("credentials not found")
|| lower.contains("run `claude` to authenticate")
|| (lower.contains("not installed") && lower.contains("claude"))
|| (lower.contains("subscription") && lower.contains("unavailable"))
}

fn is_claude_cli_usage_parse_failure(error: Option<&str>) -> bool {
let Some(error) = error else {
return false;
};
let lower = error.to_ascii_lowercase();
lower.contains("parse error")
|| lower.contains("empty output")
|| lower.contains("missing current session")
|| lower.contains("treated /usage as a normal prompt")
|| lower.contains("local activity stats")
|| lower.contains("could not parse")
}

fn is_claude_cli_rate_limit_failure(error: Option<&str>) -> bool {
let Some(error) = error else {
return false;
};
let lower = error.to_ascii_lowercase();
lower.contains("rate limit") || lower.contains("rate_limit") || lower.contains("ratelimited")
}

fn is_claude_timeout_failure(error: Option<&str>) -> bool {
let Some(error) = error else {
return false;
};
error.eq_ignore_ascii_case("timeout") || error.to_ascii_lowercase().contains("timed out")
}

async fn fetch_provider_snapshot(
id: ProviderId,
ctx: FetchContext,
Expand Down
Loading