Hmbown/CodeWhale · error · StreamError::Stall
stream_stall
stream_stall
Error message
Stream stalled: no data received for {timeout_secs}s, closing stream What it means
The engine's stream watchdog closed a model stream because no data arrived within chunk_timeout_secs seconds. It constructs a StreamError::Stall envelope, logs it, and increments stream_errors so the nothing-streamed retry can fire or the turn can fail with the real reason. This prevents turns hanging forever on a frozen connection.
Solutions
- Retry the request; the engine treats stalls like any stream error and may retry automatically.
- Increase the stall/chunk timeout setting if the provider legitimately has high time-to-first-token.
- Check network path (VPN, proxy, firewall) that may silently drop idle SSE connections.
- Check provider status pages for ongoing incidents and fail over to another model/provider.
Example fix
// before stall_timeout = 15s // after: allow slow time-to-first-token from a warmup-heavy provider stall_timeout = 60s
Defensive patterns
Strategy: retry
Validate before calling
// preflight: ensure the provider endpoint responds before starting a turn
if tokio::time::timeout(Duration::from_secs(10), health_check(endpoint)).await.is_err() {
return Err("provider unreachable; skipping turn");
} Try / catch
match result {
Err(e) if e.contains("Stream stalled") => warn_and_retry_with_backoff(),
other => other,
} Prevention
- Tune the stall timeout to your provider's worst-case time-to-first-token.
- Avoid VPN/proxy setups that silently drop idle SSE connections.
- Enable keepalive at the network layer for long-lived streams.
When it happens
Trigger: While reading stream chunks in run_turn's stream pump, a read times out after chunk_timeout_secs of silence, producing Ok(None)/timeout that maps to StreamError::Stall { timeout_secs }.
Common situations: Slow or overloaded provider APIs; corporate proxies or VPNs dropping idle connections; long server-side model warmups exceeding the stall window; very large prompts with long time-to-first-token.
Understand the failure class
Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Model stream ended with no answer or tool call.
- SSE stream idle timeout after
- SSE stream idle timeout after
- SSE stream idle timeout after
- SSE stream idle timeout after
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/dffacd4c0436fae4.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/core/engine/turn_loop.rs:4790
let max_duration = self.config.stream_max_duration;
let max_duration_secs = max_duration.as_secs();
let max_content_bytes = self.config.stream_max_content_bytes;
// Process stream events
loop {
let poll_outcome = tokio::select! {
biased;
_ = self.cancel_token.cancelled() => None,
result = tokio::time::timeout(chunk_timeout, stream.next()) => {
match result {
Ok(Some(event_result)) => Some(event_result),
Ok(None) => None, // stream ended normally
Err(_) => {
let envelope = StreamError::Stall {
timeout_secs: chunk_timeout_secs,
}
.into_envelope();
crate::logging::warn(&envelope.message);
// A stall is a stream error like any other:
// count it so the nothing-streamed retry can
// fire, and record it so an unrecovered stall
// fails the turn with the real reason instead
// of ending "Completed" over a frozen block.
stream_errors = stream_errors.saturating_add(1);
stream_error.get_or_insert(envelope.message.clone());
let _ = self.tx_event.send(Event::error(envelope)).await;
None
}
}
}
};
let Some(event_result) = poll_outcome else {
break;
};
while let Some(pending) = self.next_turn_steer() {
if pending.content.trim().is_empty() {View on GitHub (pinned to 73e0f67d83)