{"record":{"id":"9566edd39a9ca77c","repo":"Hmbown/CodeWhale","slug":"message-dispatch-belongs-to-a-previous-engine-or-session","errorCode":null,"errorMessage":"Message dispatch belongs to a previous engine or session","messagePattern":"Message dispatch belongs to a previous engine or session","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/tui/src/tui/ui/dispatch.rs","lineNumber":869,"sourceCode":"        effective_provider_identity,\n        effective_provider_label,\n        effective_reasoning_effort: effective_reasoning_receipt,\n        auto_selection,\n    };\n    Box::new(move |app, current_engine, config| {\n        // Admission stays serialized by this flag until its callback retires,\n        // even if the user replaced the Engine/session while routing waited.\n        app.dispatch_in_flight = false;\n        // This request has no admitted Op and cannot emit TurnComplete. Retire\n        // its local cancellation even after replacement, but leave a previous\n        // admitted turn's suppression for that turn's terminal event to retire.\n        if !prepare.snapshot.suppress_stream_events_until_turn_complete {\n            app.suppress_stream_events_until_turn_complete = false;\n        }\n        if !engine_handle.tx_op.same_channel(&current_engine.tx_op)\n            || prepare.cost_scope != crate::cost_status::scope_token()\n        {\n            anyhow::bail!(\"Message dispatch belongs to a previous engine or session\");\n        }\n        if !app.is_loading || engine_handle.tx_op.is_closed() {\n            let error = if engine_handle.tx_op.is_closed() {\n                \"Engine stopped before accepting the message\"\n            } else {\n                \"Message dispatch was cancelled before it reached the engine\"\n            };\n            return build_dispatch_error_closure(prepare, recovery, error.to_string())(\n                app,\n                &engine_handle,\n                config,\n            );\n        }\n        build_dispatch_success_closure(prepare, outcome)(app, &engine_handle, config)?;\n        // Existing Engine admission binds cancellation controls and the Op in\n        // one FIFO. No await separates the UI checkpoint from this handoff.\n        engine_handle.send_reserved_op(permit, op);\n        drop(usage.batch.take());","sourceCodeStart":851,"sourceCodeEnd":887,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/tui/ui/dispatch.rs#L851-L887","documentation":"When the spawned dispatch finally executes, it verifies that the engine channel it captured still matches the TUI app's current engine and that the cost scope token is unchanged. If the engine was replaced (new session/turn) or the scope moved on, the message is refused instead of being delivered to a stale engine.","triggerScenarios":"Calling `spawned_dispatch_inner` (via spawned_dispatch_execute / run_prepared_dispatch) after the app swapped to a new engine instance (`engine_handle.tx_op` is on a different channel) or after `crate::cost_status::scope_token()` changed, e.g. the user cancelled and started a new turn or reopened a session while the dispatch task was pending.","commonSituations":"Queued message dispatched just as the user pressed Escape/new-session; slow async dispatch racing a session restart; dispatch tasks surviving a model switch that recreated the engine.","solutions":["Re-submit the message through the normal input path so it binds to the current engine","Avoid restarting the session/turn while a dispatch is pending; wait for the send to complete first","If it recurs, check for a bug recreating the engine on every input (channel identity churn)"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"let current = app.current_engine();\nif !engine_handle.tx_op.same_channel(&current.tx_op)\n    || prepare.cost_scope != crate::cost_status::scope_token() {\n    eprintln!(\"dispatch is stale; re-submitting\");\n    requeue(message);\n    return;\n}","typeGuard":"fn dispatch_is_current(h: &EngineHandle, prepare: &Prepare) -> bool {\n    h.tx_op.same_channel(&current_engine().tx_op)\n        && prepare.cost_scope == crate::cost_status::scope_token()\n}","tryCatchPattern":"match spawned_dispatch_execute(...).await {\n    Err(e) if e.to_string().contains(\"previous engine or session\") => {\n        requeue_for_current_engine(message); // surface as cancelled, not failed\n    }\n    r => r,\n}","preventionTips":["Don't cancel/restart the session while a queued dispatch is pending","Re-submit messages after any session or engine change instead of expecting old dispatches to land","Treat stale dispatch as cancellation; never retry the captured engine handle"],"tags":["rust","async","race-condition","session"],"backgroundTag":"invalid-state-transition","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}