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

  1. Nothing to fix per se — the stale dispatch was intentionally discarded; re-send the message in the current session
  2. If it appears unexpectedly, verify nothing is rotating `cost_status::scope_token()` outside of genuine turn/session boundaries
  3. 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

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


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_revisions

View on GitHub (pinned to 73e0f67d83)