Hmbown/CodeWhale · info
Message dispatch belongs to a previous session
Error message
Message dispatch belongs to a previous session
What it means
The dispatch error/rollback closure re-checks the cost-scope token captured at prepare time. If it changed, the failure belongs to a previous session, so the closure skips rollback/app-side retirement rather than mutating the current session with stale state. Thrown from `build_dispatch_error_closure` on the failure path only.
Solutions
- Nothing to fix per se — the stale dispatch was intentionally discarded; re-send the message in the current session
- If it appears unexpectedly, verify nothing is rotating `cost_status::scope_token()` outside of genuine turn/session boundaries
- Reproduce with a fresh turn instead of relying on queued state from the old session
Defensive patterns
Strategy: type-guard
Validate before calling
if prepare.cost_scope != crate::cost_status::scope_token() {
// failure belongs to a previous session; skip rollback, do not mutate app state
return;
} Type guard
fn failure_is_for_current_session(prepare: &Prepare) -> bool {
prepare.cost_scope == crate::cost_status::scope_token()
} Try / catch
if let Err(err) = send.await {
if prepare.cost_scope == crate::cost_status::scope_token() {
app.remote_control.fail_active_dispatch(&err);
rollback(prepare);
} // else: previous session; intentionally no-op
} Prevention
- Always compare the captured cost-scope token before mutating app state from async closures
- Design error closures to be no-ops when the session scope changed
- Keep turn/session boundaries the only place `scope_token()` rotates
When it happens
Trigger: A prepared dispatch fails (send error, closed channel) and, by the time its error closure runs, `crate::cost_status::scope_token()` no longer equals `prepare.cost_scope` — i.e. the session/turn scope changed between prepare and failure handling.
Common situations: Dispatch fails slowly (network) while the user already started a new turn or session; error closure fires after a session swap; test scenarios simulating cancellation ownership across turns.
Related errors
- Message dispatch belongs to a previous engine or session
- child-local search
- {err}
- Failed to steer turn
- fleet run is already terminal ( )
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/ee70f9dc578085f0.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/tui/ui/dispatch.rs:1022
pub(crate) fn build_dispatch_error_closure(
prepare: UserDispatchPrepare,
recovery: DispatchRecovery,
error: String,
) -> crate::tui::app::DispatchApplyFn {
Box::new(
move |app: &mut App,
_engine_handle: &EngineHandle,
_config: &Config|
-> anyhow::Result<()> {
app.dispatch_in_flight = false;
// No operation was admitted, including route/reservation failures:
// retire only cancellation introduced by this dispatch. A previous
// admitted turn may still need to suppress its queued events.
if !prepare.snapshot.suppress_stream_events_until_turn_complete {
app.suppress_stream_events_until_turn_complete = false;
}
if prepare.cost_scope != crate::cost_status::scope_token() {
anyhow::bail!("Message dispatch belongs to a previous session");
}
app.remote_control.fail_active_dispatch(&error);
// Roll back the optimistic sync prepare mutations.
app.is_loading = prepare.snapshot.is_loading;
app.runtime_turn_status = prepare.snapshot.runtime_turn_status.clone();
app.receipt_text = prepare.snapshot.receipt_text.clone();
app.receipt_started_at = prepare.snapshot.receipt_started_at;
app.tool_evidence = prepare.snapshot.tool_evidence.clone();
let keep_user_echo = is_missing_credential_dispatch_error(&error);
if keep_user_echo {
// Echo first: keep HistoryCell::User painted in prepare. Drop only
// the unsent api_messages append and loading chrome.
app.truncate_api_messages(prepare.snapshot.api_messages_len);
app.last_send_at = prepare.snapshot.last_send_at;
} else {
app.history.truncate(prepare.snapshot.history_len);
app.prune_transcript_index_state(prepare.snapshot.history_len);
app.history_revisionsView on GitHub (pinned to 73e0f67d83)