{"record":{"id":"687106c9aadb19bc","repo":"zeroclaw-labs/zeroclaw","slug":"qq-websocket-connection-closed-heartbeat-ack-time","errorCode":null,"errorMessage":"QQ WebSocket connection closed: heartbeat ACK timeout ({MAX_MISSED_ACKS} consecutive missed ACKs)","messagePattern":"QQ WebSocket connection closed: heartbeat ACK timeout \\((.+?) consecutive missed ACKs\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/qq.rs","lineNumber":1810,"sourceCode":"            }\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)\n                        .with_outcome(::zeroclaw_log::EventOutcome::Unknown),\n                    \"WebSocket write failed; resume will be attempted on reconnect\"\n                );\n                anyhow::bail!(\"QQ WebSocket connection closed: write failed\")\n            }\n            ExitReason::ChannelClosed => {\n                anyhow::bail!(\"QQ WebSocket connection closed: internal message channel closed\")\n            }\n        }\n    }","sourceCodeStart":1792,"sourceCodeEnd":1828,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/qq.rs#L1792-L1828","documentation":"Raised by QQChannel::listen when MAX_MISSED_ACKS (3) consecutive heartbeats go unacknowledged (ExitReason::HeartbeatTimeout). The loop sends op 1 heartbeats on the interval negotiated with the gateway, increments missed_ack_count on each beat, and resets it on op 11 ACKs; only after 3 consecutive misses does it declare the connection zombied and bail. A WARN log with MAX_MISSED_ACKS as an attr precedes the bail, and Resume is attempted on the next listen().","triggerScenarios":"The hb tick fires three times with no op 11 ACK in between: a half-open TCP connection (peer gone but no RST received), severe network congestion or packet loss, or the QQ gateway stalled. missed_ack_count >= MAX_MISSED_ACKS sets ExitReason::HeartbeatTimeout at qq.rs:1502.","commonSituations":"Mobile or unstable links where heartbeats are lost but the socket stays 'open'; virtualization/host sleep pausing the process while the timer keeps firing on wake; asymmetric network failures behind NAT where ACKs are dropped but the connection table survives.","solutions":["Rely on the reconnect: this exit exists precisely to replace zombie connections; Resume recovers the session","If frequent, measure network loss to the gateway host and fix the underlying connectivity","Avoid process suspension without reconnect logic — a paused process that wakes up almost always trips this path"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"if let Err(err) = channel.listen(&tx).await {\n    if format!(\"{err:#}\").contains(\"heartbeat ACK timeout\") {\n        tokio::time::sleep(backoff.next()).await;\n        continue; // zombie connection replaced; Resume recovers session\n    }\n    return Err(err);\n}","preventionTips":["Monitor how often heartbeat timeouts occur — frequent ones mean underlying network instability","Keep hosts from suspending the process while the gateway loop runs","Trust the 3-miss design: do not add your own shorter timeout that tears down healthy connections"],"tags":["qq","websocket","heartbeat-timeout","zombie-connection","reconnect","network"],"backgroundTag":"websocket-heartbeat-timeout","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}