Hmbown/CodeWhale · error

translation route changed after turn dispatch; refusing to…

Error message

translation route changed after turn dispatch; refusing to reuse a different provider client

What it means

Before reusing a provider client for a translation request, `exact_translation_client` validates the freshly resolved route against the route recorded at turn dispatch time: provider identity key, model, and endpoint base_url must all match exactly. Any drift means the code refuses to send credentials/completions through a different client than the turn was started with.

Solutions

  1. Let the current turn finish/fail, then start a new turn so the route is re-captured; do not change models/endpoints mid-turn
  2. Revert the config/model change made during the turn and retry
  3. If an alias changed unexpectedly, pin the provider/model explicitly instead of relying on auto resolution
Defensive patterns

Strategy: validation

Validate before calling

let validated = provider_client.validate().map_err(anyhow::Error::msg)?;
if validated.identity.key != route.provider_identity
    || validated.model != route.model
    || validated.candidate.endpoint().base_url != route.base_url {
    eprintln!("route changed; start a new turn instead of reusing the client");
    return;
}

Type guard

fn route_matches(v: &Validated, r: &TurnRoute) -> bool {
    v.identity.key == r.provider_identity
        && v.model == r.model
        && v.candidate.endpoint().base_url == r.base_url
}

Try / catch

match exact_translation_client(...).await {
    Err(e) if e.to_string().contains("route changed") => {
        abort_turn_and_prompt_restart();
    }
    r => r,
}

Prevention

When it happens

Trigger: Calling `exact_translation_client` (from `run_event_loop`) when re-resolving the route yields a different `identity.key`, `model`, or `candidate.endpoint().base_url` than the `route` captured when the turn was dispatched — e.g. config/model profile changed mid-turn.

Common situations: User edits config or switches model profile while a turn is streaming; provider alias now resolves to another endpoint; persisted route cache updated between dispatch and completion.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at crates/tui/src/tui/ui/event_loop.rs:313

    let identity = config
        .resolve_persisted_provider_identity(
            Some(route.provider.as_str()),
            Some(&route.provider_identity),
        )
        .map_err(anyhow::Error::msg)?;
    let validated = crate::route_runtime::resolve_runtime_route_for_identity(
        config,
        &identity,
        Some(&route.model),
    )
    .map_err(anyhow::Error::msg)?
    .validate()
    .map_err(anyhow::Error::msg)?;
    if validated.identity.key != route.provider_identity
        || validated.model != route.model
        || validated.candidate.endpoint().base_url != route.base_url
    {
        anyhow::bail!(
            "translation route changed after turn dispatch; refusing to reuse a different provider client"
        );
    }
    if let Some(receipt) = route.receipt.as_ref()
        && &validated
            .client
            .turn_route_receipt(&route.provider_identity)
            != receipt
    {
        anyhow::bail!(
            "translation credential or endpoint changed after turn dispatch; refusing stale completion ownership"
        );
    }
    Ok(Arc::new(validated.client))
}

/// Bind the Runtime thread store to a session before the process-owner lock
/// is taken, so a second Codewhale on the same machine does not collide on

View on GitHub (pinned to 73e0f67d83)