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

  1. Check logs/stderr for the engine panic or fatal error that closed the channel
  2. Reconcile state: mark the in-flight turn Failed/Interrupted in the store so it does not stay InProgress
  3. Restart/reload the engine for the thread and resend the message as a new turn
  4. If a specific input reliably triggers it, capture the payload and report the engine bug
  5. 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

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


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/862d68bc91986d23. Report an issue: GitHub.