Hmbown/CodeWhale · error

ChatGPT revoke task was lost

Error message

ChatGPT revoke task was lost: {err}

What it means

Thrown when the background task that revokes Codewhale-owned ChatGPT OAuth tokens completes but its JoinHandle returns a JoinError instead of the revoke result (the task panicked or was cancelled). The OAuthProvider::revoke call is spawned via tokio::task::spawn and awaited; a JoinError means the outcome of the revocation is unknown — the token may or may not have been revoked server-side. The library surfaces this instead of silently treating an unknown revoke state as success.

Solutions

  1. Retry the revoke operation — the JoinError is usually transient (shutdown/cancel race), and re-running the revoke re-spawns the task.
  2. Check logs for a panic in the ChatGPT OAuth revoke path and fix the underlying panic cause (e.g. malformed stored token JSON at the config path).
  3. If the runtime is shutting down, re-run `codew` after startup completes rather than revoking during teardown.
  4. As a last resort, revoke tokens manually via the ChatGPT/OpenAI web session and then clear the stored OAuth state with config.clear_codewhale_owned_chatgpt_oauth().

Example fix

// before
let outcome = tokio::spawn(async move { revoke(...) }).await
    .map_err(|err| anyhow::anyhow!("ChatGPT revoke task was lost: {err}"))
    .and_then(|result| result);
// after: retry the join once before failing
let outcome = match tokio::spawn(async move { revoke(...) }).await
    .map_err(|err| anyhow::anyhow!("ChatGPT revoke task was lost: {err}"))
{
    Ok(result) => result,
    Err(err) => {
        tracing::warn!("revoke join failed, retrying: {err:#}");
        tokio::spawn(async move { revoke(...) }).await
            .map_err(|err| anyhow::anyhow!("ChatGPT revoke task was lost: {err}"))?
    }
};
Defensive patterns

Strategy: try-catch

Try / catch

match spawned_revoke.await {
    Ok(Ok(())) => config.clear_codewhale_owned_chatgpt_oauth(),
    Ok(Err(e)) => eprintln!("revoke failed: {e:#}"),
    Err(join_err) => {
        eprintln!("revoke task lost: {join_err}; retrying is safe");
        // retry once before giving up
    }
}

Prevention

When it happens

Trigger: Calling the Codewhale-owned ChatGPT token revoke flow (clearing ChatGPT OAuth) when the spawned revoke task panics or is cancelled before returning its Result, so `.await` on the JoinHandle yields Err(join_err) which is mapped to this message.

Common situations: Revoking ChatGPT tokens from the TUI while the runtime is shutting down and drops the task; a panic inside the OAuth revoke path (network layer, TLS, serde); running under a runtime configured with a short blocking/task timeout that cancels the spawn.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/4c84bc2138044c15. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/tui/ui/apply.rs:3541

    )
    .await
}

/// `/auth chatgpt-revoke`. The remote revoke is one blocking HTTP round trip
/// per stored token under the OAuth lifecycle lock, so it runs on the blocking
/// pool instead of the event loop. It targets the session's own config file
/// and clears the live route afterwards so the header stops claiming OAuth.
pub(crate) async fn run_chatgpt_revoke_from_tui(app: &mut App, config: &mut Config) {
    let config_path = app.config_path.clone();
    let outcome = tokio::task::spawn_blocking(move || {
        crate::oauth::revoke_owned_login(
            crate::oauth::OAuthProvider::Chatgpt,
            config_path.as_deref(),
            None,
        )
    })
    .await
    .map_err(|err| anyhow::anyhow!("ChatGPT revoke task was lost: {err}"))
    .and_then(|result| result);
    let message = match outcome {
        Ok(()) => {
            config.clear_codewhale_owned_chatgpt_oauth();
            "Revoked Codewhale-owned ChatGPT tokens. Codex CLI consent is unchanged.".to_string()
        }
        Err(err) => format!("ChatGPT revoke failed: {err:#}"),
    };
    app.add_message(HistoryCell::System {
        content: message.clone(),
    });
    app.status_message = Some(message);
    app.needs_redraw = true;
}

#[cfg(test)]
pub(crate) fn apply_loaded_session(
    app: &mut App,

View on GitHub (pinned to 73e0f67d83)