zeroclaw-labs/zeroclaw · error

Gemini auth profile is missing token set: {profile_id}

Error message

Gemini auth profile is missing token set: {profile_id}

What it means

Post-lock twin of error 673 in get_valid_gemini_access_token: after taking the per-profile refresh mutex and re-loading the store, the profile's token_set is now None. The profile changed between the two loads — a concurrent write replaced the OAuth token set with a bearer token or dropped it.

Source

Thrown at crates/zeroclaw-providers/src/auth/mod.rs:324

        if !token_set.is_expiring_within(Duration::from_secs(OPENAI_REFRESH_SKEW_SECS)) {
            return Ok(Some(token_set.access_token.clone()));
        }

        let Some(refresh_token) = token_set.refresh_token.clone() else {
            return Ok(Some(token_set.access_token.clone()));
        };

        let refresh_lock = refresh_lock_for_profile(&profile_id);
        let _guard = refresh_lock.lock().await;

        // Re-load after waiting for lock to avoid duplicate refreshes.
        let data = self.store.load().await?;
        let Some(latest_profile) = data.profiles.get(&profile_id) else {
            return Ok(None);
        };

        let Some(latest_tokens) = latest_profile.token_set.as_ref() else {
            anyhow::bail!("Gemini auth profile is missing token set: {profile_id}");
        };

        if !latest_tokens.is_expiring_within(Duration::from_secs(OPENAI_REFRESH_SKEW_SECS)) {
            return Ok(Some(latest_tokens.access_token.clone()));
        }

        let refresh_token = latest_tokens.refresh_token.clone().unwrap_or(refresh_token);

        if let Some(remaining) = refresh_backoff_remaining(&profile_id) {
            anyhow::bail!(
                "Gemini token refresh is in backoff for {remaining}s due to previous failures"
            );
        }

        let mut refreshed = match refresh_gemini_access_token_with_retries(
            &self.client,
            client_id,
            client_secret,

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Retry the credential resolution once the concurrent profile write completes
  2. Keep one writer for the auth profiles file (single instance or single state dir)
  3. Do not flip a gemini profile between OAuth and token kinds while requests are in flight
Defensive patterns

Strategy: retry

Validate before calling

// Single-writer profiles file avoids token_set disappearing between loads.
let data = auth.load_profiles().await?;
anyhow::ensure!(
    data.profiles.get(&profile_id).map(|p| p.token_set.is_some()).unwrap_or(false),
    "gemini profile lost its token set mid-flight"
);

Type guard

fn is_oauth_profile(p: &AuthProfile) -> bool {
    p.token_set.is_some()
}

Try / catch

match auth.get_valid_gemini_access_token(override_, cid, secret).await {
    Ok(tok) => tok,
    Err(e) if e.to_string().contains("missing token set") => {
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        auth.get_valid_gemini_access_token(override_, cid, secret).await?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: A concurrent auth paste-token / profile overwrite lands between the first store load and the post-lock re-load inside get_valid_gemini_access_token, while send_generate_content or warmup was resolving credentials.

Common situations: CLI auth commands and a running gateway share one profiles file; two instances point at the same state dir; external tooling rewrites the profiles JSON.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/93d7b9951b1e6a0b. Report an issue: GitHub.