zeroclaw-labs/zeroclaw · error

Serial response skip limit exceeded (maximum {MAX_SKIPPED_RE

Error message

Serial response skip limit exceeded (maximum {MAX_SKIPPED_RESPONSE_FRAMES} frames)

What it means

send_request writes a JSON request with a unique numeric id, then reads newline-terminated frames and returns the first frame that is valid JSON with a matching id. Any frame that fails to parse or carries a different id counts toward MAX_SKIPPED_RESPONSE_FRAMES = 16; when the 17th non-matching frame arrives, the request fails instead of skipping further. The whole exchange is also bounded by a 5-second deadline (SERIAL_TIMEOUT_SECS) that unsolicited frames cannot extend.

Source

Thrown at crates/zeroclaw-hardware/src/peripherals/serial.rs:66

    loop {
        let mut buf = Vec::new();
        let mut byte = [0u8; 1];
        loop {
            port.read_exact(&mut byte).await?;
            if byte[0] == b'\n' {
                break;
            }
            buf.push(byte[0]);
        }

        if let Ok(resp) = serde_json::from_slice::<Value>(&buf)
            && resp.get("id").and_then(Value::as_str) == Some(id_str.as_str())
        {
            return Ok(resp);
        }

        if skipped_frames == MAX_SKIPPED_RESPONSE_FRAMES {
            anyhow::bail!(
                "Serial response skip limit exceeded (maximum {MAX_SKIPPED_RESPONSE_FRAMES} frames)"
            );
        }
        skipped_frames += 1;
    }
}

/// Shared serial transport for tools. Pub(crate) for capabilities tool.
pub struct SerialTransport {
    port: Mutex<SerialStream>,
}

impl SerialTransport {
    pub(crate) async fn request(&self, cmd: &str, args: Value) -> anyhow::Result<ToolResult> {
        let mut port = self.port.lock().await;
        // One timeout covers the request and every skipped frame, so stale or
        // malformed input cannot restart the deadline.
        let resp = tokio::time::timeout(

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Flash/verify the board runs ZeroClaw firmware, which only emits id-tagged responses
  2. Stop other clients on the port (serial monitor, second zeroclaw instance) and retry — the stream recovers on the next request
  3. If the device emits telemetry, disable it or move it off the protocol link; the 16-frame skip cap is a hard constant
  4. After a timed-out request, issue a single ping to drain its late response instead of bursting retries
Defensive patterns

Strategy: retry

Validate before calling

// health probe before real work; a failing ping drains stale frames
let healthy = transport.request("ping", serde_json::json!({})).await.is_ok();
if !healthy {
    anyhow::bail!("serial link not healthy; check firmware and other clients on the port");
}

Try / catch

match transport.request(cmd, args).await {
    Err(e) if format!("{e}").contains("skip limit exceeded") => {
        // stream recovers on the next request: single ping to drain, then retry once
        let _ = transport.request("ping", serde_json::json!({})).await;
        transport.request(cmd, args).await
    }
    rest => rest,
}

Prevention

When it happens

Trigger: The device streams unsolicited telemetry/status frames faster than responses; a previous request timed out and its late response is still buffered, so the next request inherits stale frames; another client (serial monitor, second runtime) is talking on the same port; line noise produces JSON frames with foreign ids.

Common situations: Firmware logging status on the same serial link as the request protocol; sharing /dev/ttyACM0 with a monitor; rapid retries after a timeout piling up stale responses; non-ZeroClaw firmware that does not tag responses with the request id.

Related errors


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