Hmbown/CodeWhale · critical

dispatch task was lost

Error message

dispatch task was lost: {err}

What it means

run_prepared_dispatch spawns spawned_dispatch_inner via tokio::spawn and awaits the JoinHandle. If the join fails, the spawned dispatch task panicked, was aborted, or the runtime shut down before it finished — the JoinError is wrapped as "dispatch task was lost: {err}". The user's message dispatch never reached its apply step, so the turn did not start.

Solutions

  1. Retry the send — the message was not applied; check whether it returned to the queue/composer.
  2. Read the inner {err}: a panic payload points at a bug in spawned_dispatch_inner or the apply closure to fix.
  3. Avoid quitting or cancelling the runtime during an in-flight dispatch; wait for the turn to start.
  4. Wrap the dispatch task body in a panic boundary (catch_unwind / spawn with supervision) if the engine should survive dispatch panics.

Example fix

// before
.map_err(|err| anyhow::anyhow!("dispatch task was lost: {err}"))?;
// after
.map_err(|err| {
    if err.is_cancelled() {
        anyhow::anyhow!("dispatch cancelled before starting; message returned to queue")
    } else {
        anyhow::anyhow!("dispatch task was lost: {err}")
    }
})?;
Defensive patterns

Strategy: retry

Try / catch

match run_prepared_dispatch(terminal, app, engine_handle, config, prepare, recovery).await {
    Ok(()) => {},
    Err(err) if err.to_string().starts_with("dispatch task was lost") => {
        requeue_message(app, message); // let the user retry the send
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: Awaiting the JoinHandle of spawned_dispatch_inner when the task panics (bug inside dispatch application), is aborted (shutdown/cancel), or the tokio runtime terminates mid-dispatch.

Common situations: Quitting the TUI while a send is being prepared; a panic in the dispatch apply code path; runtime teardown races during rapid quit-after-submit; recovered dispatches whose recovery closure itself panics.

Understand the failure class

Background: "API request failed": what wrapped HTTP errors from external APIs mean and how to find the real cause — this error's family across 29 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/96ff02b813c0c0f6. Report an issue: GitHub.

Appendix: source

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

pub(crate) async fn run_prepared_dispatch(
    app: &mut App,
    config: &Config,
    engine_handle: &EngineHandle,
    prepare: UserDispatchPrepare,
    recovery: DispatchRecovery,
) -> Result<()> {
    // Unit tests that intentionally omit the production completion mailbox
    // apply the result inline. Run the owned async phase as a task just like
    // production does so its large future is polled from a clean executor
    // stack instead of nesting under the test helper's call chain.
    let apply = tokio::spawn(spawned_dispatch_inner(
        prepare,
        recovery,
        engine_handle.clone(),
    ))
    .await
    .map_err(|err| anyhow::anyhow!("dispatch task was lost: {err}"))?;
    apply(app, engine_handle, config)
}

pub(crate) async fn run_xai_device_login_from_tui(
    terminal: &mut AppTerminal,
    app: &mut App,
    engine_handle: &mut EngineHandle,
    config: &mut Config,
) -> Result<bool> {
    pause_terminal(
        terminal,
        app.use_alt_screen(),
        app.use_mouse_capture,
        app.use_bracketed_paste,
    )?;
    let login_result = crate::oauth::login(crate::oauth::OAuthProvider::Xai).await;
    resume_terminal(
        terminal,

View on GitHub (pinned to 433685b202)