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
- Restart the TUI session — a closed engine channel means the engine task is gone and cannot accept commands.
- Check engine logs/stderr for the panic or error that killed the engine task before the submit.
- Before sending, verify the engine handle is still connected and fall back to running the shell command locally instead of erroring.
- 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
- Keep the engine task alive for the lifetime of the UI event loop; never drop the receiver while the loop runs.
- Check engine health/connected state before enabling shell input in the composer.
- Monitor engine task panics and restart the engine rather than leaving a closed channel.
- Fall back to local shell execution when the engine channel is closed.
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
- agent profile may not request allow_shell=true
- allowlisted read-only executable
- API timeout should publish an Interrupted mailbox lifecycle…
- ChatGPT revoke task was lost
- classifier-approved Git read did not keep its subcommand in…
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)