Hmbown/CodeWhale · error · anyhow::Error
engine event channel closed before turn {turn_id} completed
Error message
engine event channel closed before turn {turn_id} completed What it means
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.
Source
Thrown at crates/tui/src/runtime_threads.rs:7068
loop {
let event = if let Some(event) = pending_event.take() {
Some(event)
} else if event_channel_closed {
None
} else {
let mut rx = engine.rx_event.write().await;
rx.recv().await
};
let Some(event) = event else {
if self
.is_interrupt_requested(&thread_id, &turn_id)
.await
.unwrap_or(false)
{
turn_status = Some(RuntimeTurnStatus::Interrupted);
break;
}
bail!("engine event channel closed before turn {turn_id} completed");
};
// SyncSession and configuration operations emit control status
// receipts on the same channel before SendMessage is processed.
// They belong to engine setup, not to the next claimed turn.
if !saw_turn_started
&& matches!(
&event,
EngineEvent::Status { .. }
| EngineEvent::SessionUpdated { .. }
| EngineEvent::AgentList { .. }
| EngineEvent::AgentSpawned { .. }
| EngineEvent::AgentProgress { .. }
| EngineEvent::AgentComplete { .. }
| EngineEvent::SubAgentMailbox { .. }
)
{
continue;View on GitHub (pinned to 0c42157ee5)
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)
Example fix
// before
let completed = wait_for_turn_completion(&thread_id, &turn_id).await?; // bails with channel closed
// after
match wait_for_turn_completion(&thread_id, &turn_id).await {
Ok(completed) => completed,
Err(err) if err.to_string().contains("event channel closed") => {
mark_turn_failed(&mut store, &turn_id).await?; // unstick persisted InProgress
runtime.reload_engine(&thread_id).await?; // fresh engine generation
runtime.send_message(&thread_id, last_user_prompt).await?
}
Err(err) => return Err(err),
} Defensive patterns
Strategy: try-catch
Validate before calling
// Before waiting on a turn, confirm the engine task is still alive.
if !runtime.engine_is_alive(thread_id).await {
return Err(anyhow::anyhow!("engine died before turn completion; restart required"));
}
wait_for_turn_completion(thread_id, turn_id).await Try / catch
match wait_for_turn_completion(thread_id, turn_id).await {
Ok(turn) => { /* completed normally */ }
Err(err) if err.to_string().contains("event channel closed") => {
// Engine died mid-turn: reconcile, restart, resend.
mark_turn_failed(&mut store, turn_id).await?; // clear stuck InProgress
runtime.reload_engine(thread_id).await?; // new engine generation
runtime.send_message(thread_id, &last_user_prompt).await?;
}
Err(err) => return Err(err),
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Thread engine changed while preparing steer; retry
- Turn {turn_id} is no longer in progress and cannot be steere
- ${name} is unavailable in Workflow scripts: runs must be det
- new Date()/Date() is unavailable in Workflow scripts: runs m
- parallel(): expected an array of thunks
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/862d68bc91986d23.
Report an issue: GitHub.