Hmbown/CodeWhale · error

engine channel closed

Error message

engine channel closed

What it means

Raised when a `!`-prefixed shell command typed in the composer is forwarded to the engine over a tokio mpsc channel and the send fails with TrySendError::Closed — the receiving end (the engine task) has been dropped. Full channels produce a friendly 'Engine busy' status, but a closed channel is unrecoverable for this input, so it propagates as an anyhow error.

Solutions

  1. Restart the TUI session — a closed engine channel means the engine task is gone and cannot accept commands.
  2. Check engine logs/stderr for the panic or error that killed the engine task before the submit.
  3. Before sending, verify the engine handle is still connected and fall back to running the shell command locally instead of erroring.
  4. If it reproduces, report with the log: a closed receiver while the event loop runs indicates an engine lifecycle bug.

Example fix

// before
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
    return Err(anyhow::anyhow!("engine channel closed"));
}
// after: degrade gracefully to local execution
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
    app.status_message =
        Some("Engine unavailable — running shell command locally".to_string());
    run_shell_locally(&command)?;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// nothing to pre-check portably; match the send result instead
let is_closed = matches!(
    engine_tx.try_send(cmd),
    Err(tokio::sync::mpsc::error::TrySendError::Closed(_))
);

Try / catch

match engine_tx.try_send(cmd) {
    Ok(()) => {}
    Err(TrySendError::Full(_)) => status("Engine busy — try again"),
    Err(TrySendError::Closed(_)) => {
        status("Engine unavailable — restart the session");
        // optionally run the shell command locally instead
    }
}

Prevention

When it happens

Trigger: handle_bang_shell_input calls try_send on the engine command channel; the engine task has exited (crashed, panicked, or the event loop shut down) so the receiver is dropped when the user submits a bang-shell command.

Common situations: Engine process crashed mid-session from a provider or panic error; app teardown racing with a final composer submit; a bug that closed/dropped the engine handle while the UI event loop (run_event_loop) is still alive.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/9430ff6fa3f8b083. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/tui/ui/handlers.rs:495

            engine_handle.send_reserved_op(
                permit,
                Op::RunShellCommand {
                    command: command.to_string(),
                    mode: app.mode,
                    allow_shell: app.allow_shell,
                    trust_mode: app.trust_mode,
                    auto_approve: app_auto_approve_enabled(app),
                    approval_mode: app.approval_mode,
                },
            );
            app.status_message = Some(format!("Shell command submitted: {command}"));
        }
        Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
            app.status_message =
                Some("Engine busy — shell command not sent; try again".to_string());
        }
        Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
            return Err(anyhow::anyhow!("engine channel closed"));
        }
    }
    Ok(true)
}

fn report_mcp_login(app: &mut App, message: String, level: StatusToastLevel) {
    app.push_status_toast(message.clone(), level, Some(12_000));
    add_mcp_message(app, message);
    app.needs_redraw = true;
}

fn start_mcp_login(app: &mut App, config: &Config, name: String, scopes: Vec<String>) {
    use crate::tui::app::{McpLoginProgress, PendingMcpLogin};

    if let Some(pending) = &app.mcp_login {
        let server = pending.server.clone();
        report_mcp_login(
            app,

View on GitHub (pinned to 73e0f67d83)