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
- Retry the send — the message was not applied; check whether it returned to the queue/composer.
- Read the inner {err}: a panic payload points at a bug in spawned_dispatch_inner or the apply closure to fix.
- Avoid quitting or cancelling the runtime during an in-flight dispatch; wait for the turn to start.
- 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
- Don't quit the TUI while a send is in flight; wait for the turn to start or explicitly cancel first.
- Fix panics inside spawned_dispatch_inner and its apply closures — a panic here always loses the dispatch.
- Requeue the user's message on this error so nothing is silently dropped.
- Distinguish cancelled vs panicked JoinErrors for clearer diagnostics.
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
- ChatGPT revoke task was lost
- MCP import failed
- sub-agent task should finish
- Task worker shutdown failed
- API timeout should publish an Interrupted mailbox lifecycle…
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)