{"record":{"id":"226fc6670848328e","repo":"CoplayDev/unity-mcp","slug":"unity-sent-heartbeat-frames-without-payload-within","errorCode":null,"errorMessage":"Unity sent heartbeat frames without payload within configured threshold","messagePattern":"Unity sent heartbeat frames without payload within configured threshold","errorType":"exception","errorClass":"TimeoutError","httpStatus":null,"severity":"warning","filePath":"Server/src/transport/legacy/unity_connection.py","lineNumber":193,"sourceCode":"        if self.use_framing:\n            # Heartbeat semantics: the Unity editor emits zero-length frames while\n            # a long-running command is still executing. We tolerate a bounded\n            # number of these frames (or a small time window) before surfacing a\n            # timeout to the caller so tools can retry or fail gracefully.\n            heartbeat_limit = getattr(config, 'max_heartbeat_frames', 16)\n            heartbeat_window = getattr(config, 'heartbeat_timeout', 2.0)\n            heartbeat_started = time.monotonic()\n            heartbeat_count = 0\n            try:\n                while True:\n                    header = self._read_exact(sock, 8)\n                    payload_len = struct.unpack('>Q', header)[0]\n                    if payload_len == 0:\n                        heartbeat_count += 1\n                        logger.debug(\n                            f\"Received heartbeat frame #{heartbeat_count}\")\n                        if heartbeat_count >= heartbeat_limit or (time.monotonic() - heartbeat_started) > heartbeat_window:\n                            raise TimeoutError(\n                                \"Unity sent heartbeat frames without payload within configured threshold\"\n                            )\n                        continue\n                    if payload_len > FRAMED_MAX:\n                        raise ValueError(\n                            f\"Invalid framed length: {payload_len}\")\n                    payload = self._read_exact(sock, payload_len)\n                    logger.debug(\n                        f\"Received framed response ({len(payload)} bytes)\")\n                    return payload\n            except socket.timeout as exc:\n                logger.warning(\"Socket timeout during framed receive\")\n                raise TimeoutError(\"Timeout receiving Unity response\") from exc\n            except TimeoutError:\n                raise\n            except Exception as exc:\n                logger.error(f\"Error during framed receive: {exc}\")\n                raise","sourceCodeStart":175,"sourceCodeEnd":211,"githubUrl":"https://github.com/CoplayDev/unity-mcp/blob/c21bf496bca87d54e75bad048563c3adb1782081/Server/src/transport/legacy/unity_connection.py#L175-L211","documentation":"Raised in the framed receive loop (unity_connection.py:193). Unity emits zero-length frames as heartbeats while a long-running command is still executing. If only heartbeats arrive past max_heartbeat_frames (default 16) or heartbeat_timeout seconds (default 2.0s) without a real payload, the command is treated as wedged and a TimeoutError is raised.","triggerScenarios":"A tool whose Unity-side handler never returns — an infinite loop, a deadlock, or a blocking modal dialog in the editor — so Unity keeps sending heartbeats but never the result frame.","commonSituations":"Unity showing a blocking modal/input dialog, a PlayMode script stuck in a loop, or a genuinely heavy operation that legitimately exceeds the small default heartbeat budget.","solutions":["Check Unity for a blocking modal dialog or a hung operation and dismiss/cancel it, then retry.","If the operation is legitimately long, raise the budget via config.max_heartbeat_frames and/or config.heartbeat_timeout.","Restart the Unity editor / recompile if a handler appears truly deadlocked."],"exampleFix":"// before — default budget (16 frames / 2.0s)\n# config.max_heartbeat_frames = 16\n\n// after — allow longer-running commands\nfrom core.config import config\nconfig.heartbeat_timeout = 10.0\nconfig.max_heartbeat_frames = 80","handlingStrategy":"validation","validationCode":"from core.config import config\n\ndef heartbeat_budget_adequate(estimated_seconds: float) -> bool:\n    return (config.heartbeat_timeout >= estimated_seconds\n            and config.max_heartbeat_frames >= int(estimated_seconds / 0.1))","typeGuard":"def is_heartbeat_timeout(e: BaseException) -> bool:\n    return (isinstance(e, TimeoutError)\n            and 'heartbeat frames without payload' in str(e))","tryCatchPattern":"try:\n    resp = conn.send_command(cmd, params)\nexcept TimeoutError as e:\n    if 'heartbeat frames without payload' in str(e):\n        # likely a blocking dialog / stuck op in Unity; bump budget and retry once\n        config.heartbeat_timeout = max(config.heartbeat_timeout, 10.0)\n        resp = conn.send_command(cmd, params)\n    else:\n        raise","preventionTips":["Watch for Unity modal dialogs and dismiss them before long commands.","Size max_heartbeat_frames/heartbeat_timeout to your longest legitimate operation.","Avoid PlayMode scripts with unbounded loops."],"tags":["heartbeat","timeout","framed","wedge","long-running"],"backgroundTag":null,"analyzedSha":"c21bf496bca87d54e75bad048563c3adb1782081","analyzedAt":"2026-08-13T17:36:56.095Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}