zeroclaw-labs/zeroclaw · error

WeCom WS {command} response channel closed before ack (req_i

Error message

WeCom WS {command} response channel closed before ack (req_id={req_id})

What it means

Commands sent over the WeCom WS embed a req_id and register a oneshot response channel in pending_responses. If the sender half of that channel is dropped before the ack arrives — connection task shutting down, entry removed on reconnect, or a req_id collision — the receiver errors with RecvError and this bail fires, naming the command and req_id.

Source

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

        &self,
        frame: Value,
        req_id: &str,
        command: &str,
    ) -> Result<()> {
        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 {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Retry the command — a fresh req_id is generated and re-registered on the (new) connection
  2. During deliberate shutdown, cancel/ignore in-flight commands instead of treating the closed channel as an error
  3. If it repeats outside reconnects, grep logs for the req_id to check for id collisions or premature cleanup
Defensive patterns

Strategy: retry

Try / catch

Catch the 'response channel closed before ack' bail and re-issue the command (fresh req_id) after confirming the connection is up; suppress and cancel in-flight commands during deliberate shutdown instead of propagating.

Prevention

When it happens

Trigger: ws_send_respond_msg or send_markdown_chunks_to_scope in flight exactly when the WS connection shuts down or reconnects; the reader task clears pending_responses while a command awaits its ack; duplicate req_id registration evicting a live waiter.

Common situations: Reconnect racing an outbound command; deliberate channel shutdown with commands still awaiting acks.

Related errors


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