zeroclaw-labs/zeroclaw · error

xAI auth profile is missing token set: {profile_id}

Error message

xAI auth profile is missing token set: {profile_id}

What it means

Post-lock twin of error 676 in get_valid_xai_access_token: after acquiring the per-profile refresh lock, the re-loaded profile no longer carries a token_set. A concurrent write changed the profile from OAuth to token kind (or removed the set) between the two store loads inside this resolver.

Source

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

        };

        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;

        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!("xAI 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!(
                "xAI token refresh is in backoff for {remaining}s due to previous failures"
            );
        }

        let mut refreshed =
            match refresh_xai_access_token_with_retries(&self.client, &refresh_token).await {
                Ok(tokens) => {
                    clear_refresh_backoff(&profile_id);
                    tokens
                }

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Retry once the profile write settles
  2. Use one state dir per process so the profiles file has a single writer
  3. Keep the xai profile on one credential kind instead of switching it while traffic runs
Defensive patterns

Strategy: retry

Validate before calling

let data = auth.load_profiles().await?;
anyhow::ensure!(
    data.profiles.get(&profile_id).map(|p| p.token_set.is_some()).unwrap_or(false),
    "xai 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_xai_access_token(override_).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_xai_access_token(override_).await?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: resolve_credential runs while the same xai profile is overwritten by a concurrent paste-token or external edit; the first load sees a token_set, the second does not, and the call bails.

Common situations: Shared state dir between CLI and long-running process; parallel scripts rewriting the profiles file.

Related errors


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