{"record":{"id":"db3f7a27227700a0","repo":"zeroclaw-labs/zeroclaw","slug":"serial-response-skip-limit-exceeded-maximum-max","errorCode":null,"errorMessage":"Serial response skip limit exceeded (maximum {MAX_SKIPPED_RESPONSE_FRAMES} frames)","messagePattern":"Serial response skip limit exceeded \\(maximum (.+?) frames\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-hardware/src/peripherals/serial.rs","lineNumber":66,"sourceCode":"    loop {\n        let mut buf = Vec::new();\n        let mut byte = [0u8; 1];\n        loop {\n            port.read_exact(&mut byte).await?;\n            if byte[0] == b'\\n' {\n                break;\n            }\n            buf.push(byte[0]);\n        }\n\n        if let Ok(resp) = serde_json::from_slice::<Value>(&buf)\n            && resp.get(\"id\").and_then(Value::as_str) == Some(id_str.as_str())\n        {\n            return Ok(resp);\n        }\n\n        if skipped_frames == MAX_SKIPPED_RESPONSE_FRAMES {\n            anyhow::bail!(\n                \"Serial response skip limit exceeded (maximum {MAX_SKIPPED_RESPONSE_FRAMES} frames)\"\n            );\n        }\n        skipped_frames += 1;\n    }\n}\n\n/// Shared serial transport for tools. Pub(crate) for capabilities tool.\npub struct SerialTransport {\n    port: Mutex<SerialStream>,\n}\n\nimpl SerialTransport {\n    pub(crate) async fn request(&self, cmd: &str, args: Value) -> anyhow::Result<ToolResult> {\n        let mut port = self.port.lock().await;\n        // One timeout covers the request and every skipped frame, so stale or\n        // malformed input cannot restart the deadline.\n        let resp = tokio::time::timeout(","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-hardware/src/peripherals/serial.rs#L48-L84","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Flash/verify the board runs ZeroClaw firmware, which only emits id-tagged responses","Stop other clients on the port (serial monitor, second zeroclaw instance) and retry — the stream recovers on the next request","If the device emits telemetry, disable it or move it off the protocol link; the 16-frame skip cap is a hard constant","After a timed-out request, issue a single ping to drain its late response instead of bursting retries"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// health probe before real work; a failing ping drains stale frames\nlet healthy = transport.request(\"ping\", serde_json::json!({})).await.is_ok();\nif !healthy {\n    anyhow::bail!(\"serial link not healthy; check firmware and other clients on the port\");\n}","typeGuard":null,"tryCatchPattern":"match transport.request(cmd, args).await {\n    Err(e) if format!(\"{e}\").contains(\"skip limit exceeded\") => {\n        // stream recovers on the next request: single ping to drain, then retry once\n        let _ = transport.request(\"ping\", serde_json::json!({})).await;\n        transport.request(cmd, args).await\n    }\n    rest => rest,\n}","preventionTips":["Keep exactly one client on the serial port — no monitors, no second runtime","Flash ZeroClaw firmware so responses carry the request id and nothing else is emitted","Never burst parallel retries after a timeout; late stale responses eat the next request's 16-frame skip budget"],"tags":["serial","protocol-desync","unsolicited-frames","embedded","skip-limit"],"backgroundTag":"serial-protocol-desync","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}