{"repo":"NousResearch/hermes-agent","public_id":"56704","display_label":"Null pid/window_id guard in list_windows on Linux/X11","summary":"Multiple fixes and a bug report addressing a crash when list_windows returns None for pid or window_id on Linux, WSL, and X11 environments. The proposed solutions add guards or skip null-valued entries to prevent int(None) errors.","subsystem":"agent","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":3},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":58173},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-07-04T09:37:33+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.4},"fix_edges":[{"issue_number":56704,"pr_number":58173,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":56704,"title":"computer_use capture fails on Linux/WSL: int(None) on pid/window_id from list_windows","url":null,"state":"closed"},{"kind":"pr","number":56707,"title":"fix(computer-use): guard against None pid/window_id from list_windows","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52498","number":52498},{"kind":"pr","value":"#52498","number":52498}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":57392,"title":"fix(computer-use): guard None pid/window_id in list_windows parse (WSLg/X11 compatibility)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#58173","number":58173},{"kind":"pr","value":"#52498","number":52498}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":57426,"title":"fix(tools): guard computer_use against null pid/window_id from list_windows","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52498","number":52498},{"kind":"pr","value":"#57392","number":57392},{"kind":"pr","value":"#52498","number":52498}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":58173,"title":"fix(computer-use): skip null-pid/window_id windows from list_windows (X11)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["scripts/release.py","tests/tools/test_computer_use_null_pid_windows.py","tools/computer_use/cua_backend.py"],"fix_hunks":[{"file":"scripts/release.py","hunks":["@@ -261,6 +261,7 @@\n     \"[redacted:email]\": \"synapsesx\",\n     \"[redacted:email]\": \"itsflownium\",\n     \"[redacted:email]\": \"dirtyren\",\n+    \"[redacted:email]\": \"Marxb85\",\n     \"[redacted:email]\": \"yyzquwu\",  # PR #47567 salvage (Matrix: register inbound handlers with wait_sync=True so _dispatch_sync's gather awaits them; without it mautrix fire-and-forgets and inbound intake has no completion point)\n     \"[redacted:email]\": \"jearnest11\",  # PR #48700 salvage (multi-profile gateway flap: use node symlink's own parent, not .resolve() target, when building systemd/launchd service PATH so one profile's node path can't leak into every unit and force a perpetual daemon-reload restart loop)\n     \"[redacted:email]\": \"tgmerritt\",  # PR #43553 salvage (parse vLLM's token-based output-cap error format so over-cap max_tokens 400s reduce the output cap instead of death-looping into compression)"]},{"file":"tests/tools/test_computer_use_null_pid_windows.py","hunks":["@@ -0,0 +1,144 @@\n+\"\"\"Regression for the X11 null-PID `list_windows` crash.\n+\n+On X11 a window's PID comes from the *optional* ``_NET_WM_PID`` property, so\n+the cua-driver legitimately reports ``pid: null`` for windows that don't set\n+it (the desktop root, panels, override-redirect popups, …). Both\n+``capture()`` and ``focus_app()`` previously coerced *every* window's pid via\n+``int(w[\"pid\"])`` inside a list comprehension, so a single null-pid window\n+raised::\n+\n+    TypeError: int() argument must be a string, a bytes-like object or a\n+    real number, not 'NoneType'\n+\n+…aborting the whole enumeration before any screenshot was taken — i.e. the\n+agent could never capture the screen at all on an X11 desktop that had even\n+one such window.\n+\n+The fix routes both ingestion sites through ``_ingest_windows``, which skips\n+windows lacking a usable pid/window_id (uncapturable anyway) and coerces the\n+rest, so real targetable windows survive.\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+import base64\n+from unittest.mock import MagicMock\n+\n+# 8×8 transparent PNG — decodes cleanly so capture() can size it.\n+_PNG_B64 = (\n+    \"iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAADUlEQVR4nG\"\n+    \"NgGAUgAAABCAABgukLHQAAAABJRU5ErkJggg==\"\n+)\n+\n+\n+# ---------------------------------------------------------------------------\n+# _ingest_windows: the fix locus (pure function, no session needed)\n+# ---------------------------------------------------------------------------\n+\n+class TestIngestWindows:\n+    def test_skips_window_with_null_pid(self):\n+        from tools.computer_use.cua_backend import _ingest_windows\n+\n+        raw = [\n+            {\"app_name\": \"Desktop\", \"pid\": None, \"window_id\": 1, \"z_index\": 0},\n+            {\"app_name\": \"Firefox\", \"pid\": 4321, \"window_id\": 77, \"z_index\": 1},\n+        ]\n+\n+        out = _ingest_windows(raw)\n+\n+        assert [w[\"app_name\"] for w in out] == [\"Firefox\"]\n+        assert out[0][\"pid\"] == 4321\n+        assert out[0][\"window_id\"] == 77\n+\n+    def test_skips_window_with_null_window_id(self):\n+        from tools.computer_use.cua_backend import _ingest_windows\n+\n+        raw = [\n+            {\"app_name\": \"Panel\", \"pid\": 10, \"window_id\": None, \"z_index\": 0},\n+            {\"app_name\": \"Firefox\", \"pid\": 4321, \"window_id\": 77, \"z_index\": 1},\n+        ]\n+\n+        out = _ingest_windows(raw)\n+\n+        assert [w[\"app_name\"] for w in out] == [\"Firefox\"]\n+\n+    def test_coerces_numeric_strings_like_the_original_int_call(self):\n+        # The original `int(w[\"pid\"])` accepted numeric strings; preserve that.\n+        from tools.computer_use.cua_backend import _ingest_windows\n+\n+        out = _ingest_windows(\n+            [{\"app_name\": \"Term\", \"pid\": \"200\", \"window_id\": \"9\", \"z_index\": 0}]\n+        )\n+\n+        assert out[0][\"pid\"] == 200\n+        assert out[0][\"window_id\"] == 9\n+        assert isinstance(out[0][\"pid\"], int)\n+\n+    def test_preserves_fields_capture_relies_on(self):\n+        from tools.computer_use.cua_backend import _ingest_windows\n+\n+        out = _ingest_windows([\n+            {\n+                \"app_name\": \"Firefox\",\n+                \"pid\": 1,\n+                \"window_id\": 2,\n+                \"is_on_screen\": False,\n+                \"title\": \"Mozilla Firefox\",\n+                \"z_index\": 3,\n+            }\n+        ])\n+\n+        w = out[0]\n+        assert w[\"off_screen\"] is True          # derived from is_on_screen\n+        assert w[\"title\"] == \"Mozilla Firefox\"\n+        assert w[\"z_index\"] == 3\n+\n+\n+# ---------------------------------------------------------------------------\n+# capture(): end-to-end proof the null-pid window no longer crashes capture\n+# ---------------------------------------------------------------------------\n+\n+def _backend_with_windows(raw_windows):\n+    \"\"\"A CuaDriverBackend whose session returns `raw_windows` from\n+    list_windows and a valid PNG from screenshot.\"\"\"\n+    from tools.computer_use.cua_backend import CuaDriverBackend\n+\n+    backend = CuaDriverBackend()\n+    session = MagicMock()\n+    session.capabilities_discovered = True\n+    session._has_tool.return_value = True\n+\n+    def _call_tool(name, args, *a, **k):\n+        if name == \"list_windows\":\n+            return {\"structuredContent\": {\"windows\": raw_windows}}\n+        if name == \"screenshot\":\n+            return {\n+                \"structuredContent\": {\n+                    \"screenshot_png_b64\": _PNG_B64,\n+                    \"screenshot_mime_type\": \"image/png\",\n+                }\n+            }\n+        return {}\n+\n+    session.call_tool.side_effect = _call_tool\n+    backend._session = session\n+    return backend\n+\n+\n+def test_capture_vision_survives_null_pid_window():\n+    raw = [\n+        {\"app_name\": \"Desktop\", \"pid\": None, \"window_id\": 1, \"z_index\": 0},\n+        {\"app_name\": \"Firefox\", \"pid\": 4321, \"window_id\": 77,\n+         \"is_on_screen\": True, \"title\": \"Mozilla Firefox\", \"z_index\": 1},\n+    ]\n+    backend = _backend_with_windows(raw)\n+\n+    cap = backend.capture(mode=\"vision\")\n+\n+    # The real, targetable window is selected rather than the whole capture\n+    # crashing on the null-pid desktop window.\n+    assert cap.app == \"Firefox\"\n+    assert cap.png_b64 == _PNG_B64\n+    assert backend._active_pid == 4321\n+    assert backend._active_window_id == 77\n+    assert base64.b64decode(cap.png_b64)  # decodes cleanly"]},{"file":"tools/computer_use/cua_backend.py","hunks":["@@ -909,6 +909,41 @@ def _image_from_tool_result(out: Dict[str, Any]) -> tuple[Optional[str], Optiona\n     return None, None\n \n \n+def _ingest_windows(raw_windows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:\n+    \"\"\"Normalise cua-driver ``list_windows`` entries, dropping unusable ones.\n+\n+    Every downstream operation needs both an integer ``pid`` (for\n+    get_window_state / action tools) and ``window_id`` (for screenshot /\n+    element clicks), so a window missing either is uncapturable.\n+\n+    Crucially, on X11 a window's PID comes from the *optional*\n+    ``_NET_WM_PID`` property — the desktop root, panels, and\n+    override-redirect popups routinely omit it, so the driver reports\n+    ``pid: null`` for them. Coercing every entry unconditionally\n+    (``int(w[\"pid\"])``) let one such window abort enumeration of the real,\n+    targetable windows. We skip the unusable entries instead so capture()\n+    and focus_app() still find the windows that matter.\n+    \"\"\"\n+    windows: List[Dict[str, Any]] = []\n+    for w in raw_windows:\n+        pid, window_id = w.get(\"pid\"), w.get(\"window_id\")\n+        if pid is None or window_id is None:\n+            continue\n+        try:\n+            pid_int, window_id_int = int(pid), int(window_id)\n+        except (TypeError, ValueError):\n+            continue\n+        windows.append({\n+            \"app_name\": w.get(\"app_name\", \"\"),\n+            \"pid\": pid_int,\n+            \"window_id\": window_id_int,\n+            \"off_screen\": not w.get(\"is_on_screen\", True),\n+            \"title\": w.get(\"title\", \"\"),\n+            \"z_index\": w.get(\"z_index\", 0),\n+        })\n+    return windows\n+\n+\n # ---------------------------------------------------------------------------\n # The backend itself\n # ---------------------------------------------------------------------------","@@ -1028,17 +1063,7 @@ def capture(self, mode: str = \"som\", app: Optional[str] = None) -> CaptureResult\n             {\"on_screen_only\": True, \"session\": self._session_id},\n         )\n         raw_windows = (lw_out.get(\"structuredContent\") or {}).get(\"windows\") or []\n-        windows = [\n-            {\n-                \"app_name\": w.get(\"app_name\", \"\"),\n-                \"pid\": int(w[\"pid\"]),\n-                \"window_id\": int(w[\"window_id\"]),\n-                \"off_screen\": not w.get(\"is_on_screen\", True),\n-                \"title\": w.get(\"title\", \"\"),\n-                \"z_index\": w.get(\"z_index\", 0),\n-            }\n-            for w in raw_windows\n-        ]\n+        windows = _ingest_windows(raw_windows)\n         # Sort by z_index descending (lowest z_index = frontmost on macOS).\n         windows.sort(key=lambda w: w[\"z_index\"])\n ","@@ -1433,15 +1458,7 @@ def focus_app(self, app: str, raise_window: bool = False) -> ActionResult:\n             {\"on_screen_only\": True, \"session\": self._session_id},\n         )\n         raw_windows = (lw_out.get(\"structuredContent\") or {}).get(\"windows\") or []\n-        windows = [\n-            {\n-                \"app_name\": w.get(\"app_name\", \"\"),\n-                \"pid\": int(w[\"pid\"]),\n-                \"window_id\": int(w[\"window_id\"]),\n-                \"z_index\": w.get(\"z_index\", 0),\n-            }\n-            for w in raw_windows\n-        ]\n+        windows = _ingest_windows(raw_windows)\n         windows.sort(key=lambda w: w[\"z_index\"])\n \n         app_lower = app.lower()\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}