{"record":{"id":"862d68bc91986d23","repo":"Hmbown/CodeWhale","slug":"engine-event-channel-closed-before-turn-turn-id","errorCode":null,"errorMessage":"engine event channel closed before turn {turn_id} completed","messagePattern":"engine event channel closed before turn (.+?) completed","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":7068,"sourceCode":"        loop {\n            let event = if let Some(event) = pending_event.take() {\n                Some(event)\n            } else if event_channel_closed {\n                None\n            } else {\n                let mut rx = engine.rx_event.write().await;\n                rx.recv().await\n            };\n            let Some(event) = event else {\n                if self\n                    .is_interrupt_requested(&thread_id, &turn_id)\n                    .await\n                    .unwrap_or(false)\n                {\n                    turn_status = Some(RuntimeTurnStatus::Interrupted);\n                    break;\n                }\n                bail!(\"engine event channel closed before turn {turn_id} completed\");\n            };\n\n            // SyncSession and configuration operations emit control status\n            // receipts on the same channel before SendMessage is processed.\n            // They belong to engine setup, not to the next claimed turn.\n            if !saw_turn_started\n                && matches!(\n                    &event,\n                    EngineEvent::Status { .. }\n                        | EngineEvent::SessionUpdated { .. }\n                        | EngineEvent::AgentList { .. }\n                        | EngineEvent::AgentSpawned { .. }\n                        | EngineEvent::AgentProgress { .. }\n                        | EngineEvent::AgentComplete { .. }\n                        | EngineEvent::SubAgentMailbox { .. }\n                )\n            {\n                continue;","sourceCodeStart":7050,"sourceCodeEnd":7086,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/runtime_threads.rs#L7050-L7086","documentation":"The turn-completion wait loop received None from engine.rx_event: every sender for the engine's event channel was dropped, so the engine shut down before emitting the event that finishes turn_id, and no interrupt was requested to explain it (crates/tui/src/runtime_threads.rs:7068). In practice this means the engine task exited abnormally mid-turn (panic, fatal teardown, channel closed on unrecoverable error) and the turn's completion event will never arrive.","triggerScenarios":"Engine worker panics on a malformed provider response or bug; runtime shutdown tears engines down while a turn runs; provider client hits a fatal error and drops tx_event without a TurnCompleted; engine task aborted by supervision logic.","commonSituations":"Long streaming turns surviving into app shutdown; a panic loop triggered by specific tool output; drop of the engine handle by refactoring; OOM/cgroup kill of the engine task.","solutions":["Check logs/stderr for the engine panic or fatal error that closed the channel","Reconcile state: mark the in-flight turn Failed/Interrupted in the store so it does not stay InProgress","Restart/reload the engine for the thread and resend the message as a new turn","If a specific input reliably triggers it, capture the payload and report the engine bug","Avoid shutting down the runtime while turns are active (drain first)"],"exampleFix":"// before\nlet completed = wait_for_turn_completion(&thread_id, &turn_id).await?; // bails with channel closed\n\n// after\nmatch wait_for_turn_completion(&thread_id, &turn_id).await {\n    Ok(completed) => completed,\n    Err(err) if err.to_string().contains(\"event channel closed\") => {\n        mark_turn_failed(&mut store, &turn_id).await?;   // unstick persisted InProgress\n        runtime.reload_engine(&thread_id).await?;        // fresh engine generation\n        runtime.send_message(&thread_id, last_user_prompt).await?\n    }\n    Err(err) => return Err(err),\n}","handlingStrategy":"try-catch","validationCode":"// Before waiting on a turn, confirm the engine task is still alive.\nif !runtime.engine_is_alive(thread_id).await {\n    return Err(anyhow::anyhow!(\"engine died before turn completion; restart required\"));\n}\nwait_for_turn_completion(thread_id, turn_id).await","typeGuard":null,"tryCatchPattern":"match wait_for_turn_completion(thread_id, turn_id).await {\n    Ok(turn) => { /* completed normally */ }\n    Err(err) if err.to_string().contains(\"event channel closed\") => {\n        // Engine died mid-turn: reconcile, restart, resend.\n        mark_turn_failed(&mut store, turn_id).await?;      // clear stuck InProgress\n        runtime.reload_engine(thread_id).await?;            // new engine generation\n        runtime.send_message(thread_id, &last_user_prompt).await?;\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["Drain or refuse shutdown while any turn is active","Persist enough turn state to reconcile InProgress turns after engine death","Capture engine panics with a panic hook that logs the payload (see panic_payload_message)","Treat channel-closed during a turn as an engine crash: investigate logs, do not silently retry on the dead engine","Keep last user prompt available so the message can be resent on a fresh engine"],"tags":["rust","engine-lifecycle","channel","crash","turn-lifecycle"],"backgroundTag":"event-channel-closed","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}