Hmbown/CodeWhale · error
Failed to start turn: engine operation channel closed
Error message
Failed to start turn: engine operation channel closed
What it means
To start a turn, the runtime reserves a slot on the engine's operation channel via engine.tx_op.clone().reserve_owned().await. If the channel is closed (receiver dropped — the engine task is gone), it maps the error to 'Failed to start turn: engine operation channel closed'. This means no engine task is alive to accept the turn operation.
Solutions
- Ensure no Op::Shutdown was sent to this engine before starting the turn
- Check engine task logs for a panic/abort that closed the channel
- Reinstall the engine for the thread and retry the turn start
- Serialize shutdown against turn starts so starts cannot race teardown
Example fix
// before
let permit = engine.tx_op.clone().reserve_owned().await
.map_err(|_| anyhow!("Failed to start turn: engine operation channel closed"))?;
// after
if !engine.is_alive() {
bail!("engine for {thread_id} is not running; reinstall before starting a turn");
}
let permit = engine.tx_op.clone().reserve_owned().await
.map_err(|_| anyhow!("Failed to start turn: engine operation channel closed"))?; Defensive patterns
Strategy: try-catch
Validate before calling
if !engine_is_alive(&engine) { bail!("engine already shut down; cannot start turn"); } Type guard
fn engine_is_alive(engine: &EngineHandle) -> bool { !engine.tx_op.is_closed() } Try / catch
match start_turn(req).await {
Err(e) if e.to_string().contains("operation channel closed") => {
// engine task died: reinstall and retry once
manager.install_engine(&req.thread_id).await?;
start_turn(req).await
}
other => other,
} Prevention
- Sequence turn starts before Op::Shutdown
- Alert on engine task panics — they close the op channel
- Check tx_op.is_closed() as a cheap liveness probe before starting turns
When it happens
Trigger: Calling start_turn after the engine task for the thread has terminated (crashed, was aborted, or already received Op::Shutdown); racing shutdown while a turn is being started; engine worker death that was not cleaned out of active.engines.
Common situations: Tearing down the runtime while a caller still starts turns; an engine panic that dropped the op receiver; sequencing bugs where Shutdown was sent before the last turn start.
Related errors
- engine event channel closed before turn
- Runtime engine shutdown failed
- background hook supervisor is unavailable
- Failed to sync thread session
- Failed to trigger compaction: engine operation channel…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/a402a654a9668a64.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/runtime_threads.rs:10204
auto_approve,
translation_enabled: false,
allowed_tools,
dynamic_tools: req.dynamic_tools,
hook_executor: None,
approval_mode: policy.permission,
verbosity,
provenance: input_source.provenance(),
});
// Reserve mailbox capacity before claiming or persisting anything.
// If the caller is cancelled while capacity is unavailable, no
// durable or in-memory turn state has changed.
let permit = engine
.tx_op
.clone()
.reserve_owned()
.await
.map_err(|_| anyhow!("Failed to start turn: engine operation channel closed"))?;
let acceptance_rx = {
// Lock order is active -> thread_mutation. Neither guard crosses
// an await, and spawning the owned lifecycle task is synchronous.
// The operation claim is an OS-backed file lock, so another
// Runtime process sharing this store cannot pass the replay check
// or persist a competing turn for the same operation key.
let mut active = self.active.lock().await;
let mut operation_claim = operation
.as_ref()
.map(|operation| {
self.store.open_turn_operation_claim_lock(
&operation.binding.operation_key_fingerprint,
)
})
.transpose()?
.map(fd_lock::RwLock::new);
let operation_claim_guard = operation_claimView on GitHub (pinned to 73e0f67d83)