{"record":{"id":"174cdf9a3f5b1488","repo":"zeroclaw-labs/zeroclaw","slug":"qq-websocket-connection-closed-stream-ended-unexp","errorCode":null,"errorMessage":"QQ WebSocket connection closed: stream ended unexpectedly","messagePattern":"QQ WebSocket connection closed: stream ended unexpectedly","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/qq.rs","lineNumber":1800,"sourceCode":"            }\n            ExitReason::Close(ref frame) => {\n                let (code, reason) = frame\n                    .as_ref()\n                    .map(|f| (f.code.to_string(), f.reason.to_string()))\n                    .unwrap_or_else(|| (\"unknown\".into(), \"none\".into()));\n                ::zeroclaw_log::record!(WARN, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note).with_outcome(::zeroclaw_log::EventOutcome::Unknown).with_attrs(::serde_json::json!({\"code\": code.to_string(), \"reason\": reason.to_string()})), \"WebSocket closed with code=, reason=\\\"\\\"; resume will be attempted on reconnect\");\n                anyhow::bail!(\n                    \"QQ WebSocket connection closed: close_code={code}, reason=\\\"{reason}\\\"\"\n                )\n            }\n            ExitReason::StreamEnded => {\n                ::zeroclaw_log::record!(\n                    WARN,\n                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)\n                        .with_outcome(::zeroclaw_log::EventOutcome::Unknown),\n                    \"WebSocket stream ended unexpectedly; resume will be attempted on reconnect\"\n                );\n                anyhow::bail!(\"QQ WebSocket connection closed: stream ended unexpectedly\")\n            }\n            ExitReason::HeartbeatTimeout => {\n                ::zeroclaw_log::record!(\n                    WARN,\n                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)\n                        .with_outcome(::zeroclaw_log::EventOutcome::Unknown)\n                        .with_attrs(::serde_json::json!({\"MAX_MISSED_ACKS\": MAX_MISSED_ACKS})),\n                    \"heartbeat timeout after consecutive missed ACKs; resume will be attempted on reconnect\"\n                );\n                anyhow::bail!(\n                    \"QQ WebSocket connection closed: heartbeat ACK timeout \\\n                     ({MAX_MISSED_ACKS} consecutive missed ACKs)\"\n                )\n            }\n            ExitReason::WriteFailed => {\n                ::zeroclaw_log::record!(\n                    WARN,\n                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)","sourceCodeStart":1782,"sourceCodeEnd":1818,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/qq.rs#L1782-L1818","documentation":"Raised by QQChannel::listen when the WebSocket stream terminates without a Close frame (read.next() returned None, ExitReason::StreamEnded). This means the TCP/TLS connection dropped abruptly — the server or an intermediary vanished rather than closing politely — and a WARN is logged stating resume will be attempted. Session state is preserved, so the next listen() tries Resume.","triggerScenarios":"The select arm 'msg = read.next()' yields None during the gateway loop: connection reset by the server, NAT/idle timeout, VPN or proxy dropping the socket, or the process's network interface going away.","commonSituations":"Bots running behind NAT gateways or corporate proxies with aggressive idle timeouts (heartbeats should normally prevent this); flaky container networking; QQ gateway restarts that kill sockets without a close frame; laptops suspending mid-session.","solutions":["Let the supervisor reconnect — Resume usually restores the session without event loss","If it recurs frequently, verify heartbeats are flowing (see the heartbeat path) so intermediaries do not idle the connection out","Check network stability between the host and the QQ gateway (proxy, NAT, firewall timeouts)"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"if let Err(err) = channel.listen(&tx).await {\n    if format!(\"{err:#}\").contains(\"stream ended unexpectedly\") {\n        tokio::time::sleep(backoff.next()).await;\n        continue; // session preserved; Resume on reconnect\n    }\n    return Err(err);\n}","preventionTips":["Heartbeats keep the connection alive — make sure the heartbeat interval negotiated with the gateway is respected","Avoid NAT/proxy idle timeouts shorter than the heartbeat interval","Handle process suspend/resume by tearing the listener down cleanly instead of letting the stream die mid-read"],"tags":["qq","websocket","connection-dropped","reconnect","resume","network"],"backgroundTag":"websocket-connection-dropped","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}