zeroclaw-labs/zeroclaw · error
Email OAuth2 profile is missing token set: {profile_id}
Error message
Email OAuth2 profile is missing token set: {profile_id} What it means
Raised by get_valid_email_oauth2_token after it acquires the per-profile refresh lock and re-loads the auth store: the profile still exists but its token_set field is now None. The first (pre-lock) load saw a token set, so between the two loads another writer cleared it (e.g. profile removal/re-creation, a downgrade to a non-OAuth profile kind, or a partial store overwrite). The refresh path refuses to continue without tokens to refresh.
Source
Thrown at crates/zeroclaw-providers/src/auth/mod.rs:538
return Ok(Some(token_set.access_token.clone()));
}
let Some(refresh_token) = token_set.refresh_token.clone() else {
// No refresh token; return the (possibly expired) access token and
// let the IMAP auth failure surface as a log event.
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 acquiring 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!("Email OAuth2 profile is missing token set: {profile_id}");
};
if !latest_tokens.is_expiring_within(Duration::from_secs(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!(
"Email OAuth2 token refresh is in backoff for {remaining}s due to previous failures"
);
}
let mut refreshed = match refresh_email_access_token_with_retries(
&self.client,
token_url,
client_id,
&refresh_token,View on GitHub (pinned to 88bb9c8533)
Solutions
- Stop concurrent writers: only mutate email OAuth profiles via the auth store APIs (store_email_oauth2_tokens / update_profile) so token_set is never cleared by partial writes
- Re-run `zeroclaw auth login` for the email channel (or re-do the email OAuth2 consent flow) so the profile has a valid token_set again
- If another long-running zeroclaw instance shares the same state dir, stop it or give each instance its own state dir
- Inspect the profile in the auth store JSON and delete the broken profile entry so it is re-created cleanly on next login
Example fix
// before: overwriting the whole profile drops token_set
store.upsert_profile(AuthProfile::new_bare(alias, name), true).await?;
// after: mutate in place so the existing token_set survives
store.update_profile(&profile_id, |p| {
p.kind = AuthProfileKind::OAuth;
Ok(())
}).await?; Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-check the profile before asking for a token
let data = auth_store.load().await?;
if let Some(id) = select_profile_id(&data, alias, None) {
let ok = data.profiles.get(&id)
.and_then(|p| p.token_set.as_ref())
.is_some();
anyhow::ensure!(ok, "profile {id} has no token set; re-run email OAuth login");
} Try / catch
match auth.get_valid_email_oauth2_token(alias, None, url, id, &scopes).await {
Err(e) if e.to_string().contains("missing token set") => {
// treat as stale profile: trigger re-login flow, do not retry the call
start_email_oauth_login(alias).await?;
}
result => result?,
} Prevention
- Never rewrite whole profile records; use update_profile so token_set survives edits
- One zeroclaw instance per state dir to avoid concurrent store writers
- Monitor for WARN auth events and re-run login when profiles lose tokens
When it happens
Trigger: Two concurrent get_valid_email_oauth2_token calls for the same channel alias racing with a store update that writes profile.token_set = None; an external process (zeroclaw auth logout / profile edit) mutating the same auth store JSON while an IMAP connect is refreshing; a profile replaced with a bare profile that has no token_set.
Common situations: Running multiple channel workers against one state dir, re-importing or resetting a profile while email polling is live, or a hand-edited/corrupted auth store where a profile object lost its token_set key.
Related errors
- email channel '{}' has oauth2 configured but no auth service
- Email OAuth2 token refresh is in backoff for {remaining}s du
- oauth2 configured for '{}' but no auth service provided
- rename_agent not supported by this memory backend
- Email channel requires the `channel-email` feature
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/1d940bd5035a239f.
Report an issue: GitHub.