Hmbown/CodeWhale · warning
Turn moved on before the steer reached the model;
Error message
Turn {turn_id} moved on before the steer reached the model; {STEER_DROPPED_REASON} What it means
Thrown when the engine explicitly reports `SteerOutcome::Dropped` on the settlement channel — the turn moved on (finished or advanced past the point where the steer could be injected) before the queued steer reached the model. The caller already returned a `Queued` receipt, so this error is the honest async correction that the steer was not delivered.
Solutions
- Fall back to starting a new turn containing the steering message so the user's input is never lost.
- Handle the Dropped outcome in the caller by updating the UI receipt from Queued to Dropped and prompting the user to resend.
- Steer earlier in the turn lifecycle: send steering input while the turn is clearly mid-stream rather than near its end.
Example fix
// before
let turn = runtime.steer(thread_id, turn_id, item).await?;
// after
match runtime.steer(thread_id, turn_id, item).await {
Ok(turn) => turn,
Err(e) if e.to_string().contains("moved on before the steer") => {
runtime.start_turn(thread_id, item).await? // deliver as a new turn
}
Err(e) => return Err(e),
} Defensive patterns
Strategy: fallback
Try / catch
match runtime.steer(thread_id, turn_id, item).await {
Ok(turn) => Ok(turn),
Err(e) if e.to_string().contains("moved on before the steer") => {
runtime.start_turn(thread_id, item).await // resend as a fresh turn
}
Err(e) => Err(e),
} Prevention
- Steer early in a turn, not near its expected end.
- Always carry a fallback path from Queued steer receipts to a new-turn start.
- Reconcile UI receipts: update Queued to Dropped when this error surfaces.
When it happens
Trigger: Awaiting the steer settlement future (`settle_rx` with STEER_SETTLE_WAIT timeout) and receiving `(SteerOutcome::Dropped, _)` — happens when the turn completes or is interrupted between acceptance of the queued item and the engine's next mailbox check.
Common situations: Steer sent just as the model emits its final message; steer queued behind a turn that ends before the next API request; UI shows 'queued' then must reconcile with the drop.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Thread engine changed while preparing steer; retry
- Turn is no longer in progress and cannot be steered
- Steer settlement task ended before acknowledgement
- Thread execution settings changed while preparing…
- Turn is stopping and cannot be steered
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/eb030e844917aa2b.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/runtime_threads.rs:10492
}
};
// The reserved send has no await/failure point. From here the
// engine and durable record agree even if the API caller drops.
let outcome_rx = permit.send_with_outcome(prompt.clone());
touch_lru(&mut active.lru, thread_id);
queued_turn = turn.clone();
self.spawn_steer_settlement(turn, item, prompt, outcome_rx)
};
// The settler owns the verdict; this is only an observation of it.
// Steering a streaming turn settles in milliseconds, so the caller
// almost always gets the truth in-band. Behind a long tool call the
// engine will not look at its mailbox for minutes, and an API request
// must not hang that long — so the wait is bounded and the caller
// falls back to the honest `Queued` receipt it already has.
match tokio::time::timeout(STEER_SETTLE_WAIT, settle_rx).await {
Ok(Ok((SteerOutcome::Accepted, turn))) => Ok(turn),
Ok(Ok((SteerOutcome::Dropped, _))) => bail!(
"Turn {turn_id} moved on before the steer reached the model; {STEER_DROPPED_REASON}"
),
Ok(Err(_)) => bail!("Steer settlement task ended before acknowledgement"),
Err(_) => Ok(queued_turn),
}
}
pub async fn compact_thread(
&self,
thread_id: &str,
req: CompactThreadRequest,
) -> Result<TurnRecord> {
// Compaction carries a concrete provider route just like a normal
// turn. Keep the same reload/admission boundary through durable engine
// handoff so it cannot dispatch an old credential or endpoint after a
// successful config reload.
let _config_admission = self.config_admission.read().await;
self.ensure_accepting_execution()?;View on GitHub (pinned to 73e0f67d83)