zeroclaw-labs/zeroclaw · error

QQ WebSocket connection closed: heartbeat ACK timeout ({MAX_

Error message

QQ WebSocket connection closed: heartbeat ACK timeout ({MAX_MISSED_ACKS} consecutive missed ACKs)

What it means

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().

Source

Thrown at crates/zeroclaw-channels/src/qq.rs:1810

            }
            ExitReason::StreamEnded => {
                ::zeroclaw_log::record!(
                    WARN,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
                        .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
                    "WebSocket stream ended unexpectedly; resume will be attempted on reconnect"
                );
                anyhow::bail!("QQ WebSocket connection closed: stream ended unexpectedly")
            }
            ExitReason::HeartbeatTimeout => {
                ::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!({"MAX_MISSED_ACKS": MAX_MISSED_ACKS})),
                    "heartbeat timeout after consecutive missed ACKs; resume will be attempted on reconnect"
                );
                anyhow::bail!(
                    "QQ WebSocket connection closed: heartbeat ACK timeout \
                     ({MAX_MISSED_ACKS} consecutive missed ACKs)"
                )
            }
            ExitReason::WriteFailed => {
                ::zeroclaw_log::record!(
                    WARN,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
                        .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
                    "WebSocket write failed; resume will be attempted on reconnect"
                );
                anyhow::bail!("QQ WebSocket connection closed: write failed")
            }
            ExitReason::ChannelClosed => {
                anyhow::bail!("QQ WebSocket connection closed: internal message channel closed")
            }
        }
    }

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Rely on the reconnect: this exit exists precisely to replace zombie connections; Resume recovers the session
  2. If frequent, measure network loss to the gateway host and fix the underlying connectivity
  3. Avoid process suspension without reconnect logic — a paused process that wakes up almost always trips this path
Defensive patterns

Strategy: retry

Try / catch

if let Err(err) = channel.listen(&tx).await {
    if format!("{err:#}").contains("heartbeat ACK timeout") {
        tokio::time::sleep(backoff.next()).await;
        continue; // zombie connection replaced; Resume recovers session
    }
    return Err(err);
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/687106c9aadb19bc. Report an issue: GitHub.