zeroclaw-labs/zeroclaw · error

WeCom WS {command} ack timeout after {}s (req_id={req_id})

Error message

WeCom WS {command} ack timeout after {}s (req_id={req_id})

What it means

Every WeCom WS command waits at most WECOM_COMMAND_TIMEOUT_SECS = 10 seconds for its ack, matched by req_id; on timeout the pending entry is removed and this error bails with the command name and req_id. The frame was sent but WeCom never answered in time.

Source

Thrown at crates/zeroclaw-channels/src/wecom_ws.rs:415

        let (tx, rx) = tokio::sync::oneshot::channel();
        self.pending_responses
            .lock()
            .await
            .insert(req_id.to_string(), tx);

        if let Err(err) = self.ws_send_frame(frame).await {
            self.pending_responses.lock().await.remove(req_id);
            return Err(err);
        }

        match tokio::time::timeout(Duration::from_secs(WECOM_COMMAND_TIMEOUT_SECS), rx).await {
            Ok(Ok(result)) => result,
            Ok(Err(_)) => anyhow::bail!(
                "WeCom WS {command} response channel closed before ack (req_id={req_id})"
            ),
            Err(_) => {
                self.pending_responses.lock().await.remove(req_id);
                anyhow::bail!(
                    "WeCom WS {command} ack timeout after {}s (req_id={req_id})",
                    WECOM_COMMAND_TIMEOUT_SECS
                );
            }
        }
    }

    async fn maybe_handle_command_response(&self, frame: &Value) -> bool {
        let Some(req_id) = frame
            .get("headers")
            .and_then(|headers| headers.get("req_id"))
            .and_then(Value::as_str)
        else {
            return false;
        };

        let Some(errcode) = frame.get("errcode").and_then(Value::as_i64) else {
            return false;

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Retry the command once — transient upstream latency is the usual cause
  2. Reduce markdown chunk size/count so WeCom acks faster
  3. If timeouts are constant, force a WS reconnect — the socket is likely half-dead — and verify latency to the WeCom endpoint
  4. Correlate the req_id in logs to confirm whether the ack arrived late vs never

Example fix

// before
let r = channel.send_markdown_chunks(scope, chunks).await?;
// after
let r = retry_on("ack timeout", 2, ||
    channel.send_markdown_chunks(scope.clone(), chunks.clone())).await?;
Defensive patterns

Strategy: retry

Try / catch

Catch the ack-timeout bail, retry the command once after a short backoff; if the second attempt also times out, force a WS reconnect and try once more before dead-lettering the message.

Prevention

When it happens

Trigger: ws_send_respond_msg or send_markdown_chunks_to_scope when the WeCom server does not ack within 10s: upstream latency spikes, a half-dead socket that delivers frames but stalls responses, or slow processing of large chunked markdown payloads.

Common situations: Network degradation where the socket stays open but stalls; oversized markdown chunk sequences; WeCom service-side latency events.

Understand the failure class

Related errors


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