{"record":{"id":"dffacd4c0436fae4","repo":"Hmbown/CodeWhale","slug":"stream-stall","errorCode":"stream_stall","errorMessage":"Stream stalled: no data received for {timeout_secs}s, closing stream","messagePattern":"Stream stalled: no data received for (.+?)s, closing stream","errorType":"console","errorClass":"StreamError::Stall","httpStatus":null,"severity":"error","filePath":"crates/tui/src/core/engine/turn_loop.rs","lineNumber":4790,"sourceCode":"        let max_duration = self.config.stream_max_duration;\n        let max_duration_secs = max_duration.as_secs();\n        let max_content_bytes = self.config.stream_max_content_bytes;\n\n        // Process stream events\n        loop {\n            let poll_outcome = tokio::select! {\n                biased;\n                _ = self.cancel_token.cancelled() => None,\n                result = tokio::time::timeout(chunk_timeout, stream.next()) => {\n                    match result {\n                        Ok(Some(event_result)) => Some(event_result),\n                        Ok(None) => None, // stream ended normally\n                        Err(_) => {\n                            let envelope = StreamError::Stall {\n                                timeout_secs: chunk_timeout_secs,\n                            }\n                            .into_envelope();\n                            crate::logging::warn(&envelope.message);\n                            // A stall is a stream error like any other:\n                            // count it so the nothing-streamed retry can\n                            // fire, and record it so an unrecovered stall\n                            // fails the turn with the real reason instead\n                            // of ending \"Completed\" over a frozen block.\n                            stream_errors = stream_errors.saturating_add(1);\n                            stream_error.get_or_insert(envelope.message.clone());\n                            let _ = self.tx_event.send(Event::error(envelope)).await;\n                            None\n                        }\n                    }\n                }\n            };\n            let Some(event_result) = poll_outcome else {\n                break;\n            };\n            while let Some(pending) = self.next_turn_steer() {\n                if pending.content.trim().is_empty() {","sourceCodeStart":4772,"sourceCodeEnd":4808,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/core/engine/turn_loop.rs#L4772-L4808","documentation":"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.","triggerScenarios":"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 }.","commonSituations":"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.","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."],"exampleFix":"// before\nstall_timeout = 15s\n// after: allow slow time-to-first-token from a warmup-heavy provider\nstall_timeout = 60s","handlingStrategy":"retry","validationCode":"// preflight: ensure the provider endpoint responds before starting a turn\nif tokio::time::timeout(Duration::from_secs(10), health_check(endpoint)).await.is_err() {\n    return Err(\"provider unreachable; skipping turn\");\n}","typeGuard":null,"tryCatchPattern":"match result {\n    Err(e) if e.contains(\"Stream stalled\") => warn_and_retry_with_backoff(),\n    other => other,\n}","preventionTips":["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."],"tags":["network","streaming","timeout","llm"],"backgroundTag":"request-timeout","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T21:17:16.096Z"}