# NousResearch/hermes-agent · Null pid/window_id guard in list_windows on Linux/X11

## Open now

- Nothing open on GitHub.

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.

## TL;DR

- 5 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 3 PR(s) closed without merging.
- A fix was merged to main in PR #58173. Verify it addresses your case.
- **Recommended:** read PR #58173 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.40`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #56704: computer_use capture fails on Linux/WSL: int(None) on pid/window_id from list_windows

## Prior & competing PRs

- **#56707: fix(computer-use): guard against None pid/window_id from list_windows**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#52498`, `#52498`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#57426: fix(tools): guard computer_use against null pid/window_id from list_windows**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#52498`, `#57392`, `#52498`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#58173: fix(computer-use): skip null-pid/window_id windows from list_windows (X11)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `scripts/release.py`, `tests/tools/test_computer_use_null_pid_windows.py`, `tools/computer_use/cua_backend.py`
  - fix hunks:
  - `scripts/release.py`

    ```diff
    @@ -261,6 +261,7 @@
         "[redacted:email]": "synapsesx",
         "[redacted:email]": "itsflownium",
         "[redacted:email]": "dirtyren",
    +    "[redacted:email]": "Marxb85",
         "[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)
         "[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)
         "[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)
    ```
  - `tests/tools/test_computer_use_null_pid_windows.py`

    ```diff
    @@ -0,0 +1,144 @@
    +"""Regression for the X11 null-PID `list_windows` crash.
    +
    +On X11 a window's PID comes from the *optional* ``_NET_WM_PID`` property, so
    +the cua-driver legitimately reports ``pid: null`` for windows that don't set
    +it (the desktop root, panels, override-redirect popups, …). Both
    +``capture()`` and ``focus_app()`` previously coerced *every* window's pid via
    +``int(w["pid"])`` inside a list comprehension, so a single null-pid window
    +raised::
    +
    +    TypeError: int() argument must be a string, a bytes-like object or a
    +    real number, not 'NoneType'
    +
    +…aborting the whole enumeration before any screenshot was taken — i.e. the
    +agent could never capture the screen at all on an X11 desktop that had even
    +one such window.
    +
    +The fix routes both ingestion sites through ``_ingest_windows``, which skips
    +windows lacking a usable pid/window_id (uncapturable anyway) and coerces the
    +rest, so real targetable windows survive.
    +"""
    +
    +from __future__ import annotations
    +
    +import base64
    +from unittest.mock import MagicMock
    +
    +# 8×8 transparent PNG — decodes cleanly so capture() can size it.
    +_PNG_B64 = (
    +    "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAADUlEQVR4nG"
    +    "NgGAUgAAABCAABgukLHQAAAABJRU5ErkJggg=="
    +)
    +
    +
    +# ---------------------------------------------------------------------------
    +# _ingest_windows: the fix locus (pure function, no session needed)
    +# ---------------------------------------------------------------------------
    +
    +class TestIngestWindows:
    +    def test_skips_window_with_null_pid(self):
    +        from tools.computer_use.cua_backend import _ingest_windows
    +
    +        raw = [
    +            {"app_name": "Desktop", "pid": None, "window_id": 1, "z_index": 0},
    +            {"app_name": "Firefox", "pid": 4321, "window_id": 77, "z_index": 1},
    +        ]
    +
    +        out = _ingest_windows(raw)
    +
    +        assert [w["app_name"] for w in out] == ["Firefox"]
    +        assert out[0]["pid"] == 4321
    +        assert out[0]["window_id"] == 77
    +
    +    def test_skips_window_with_null_window_id(self):
    +        from tools.computer_use.cua_backend import _ingest_windows
    +
    +        raw = [
    +            {"app_name": "Panel", "pid": 10, "window_id": None, "z_index": 0},
    +            {"app_name": "Firefox", "pid": 4321, "window_id": 77, "z_index": 1},
    +        ]
    +
    +        out = _ingest_windows(raw)
    +
    +        assert [w["app_name"] for w in out] == ["Firefox"]
    +
    +    def test_coerces_numeric_strings_like_the_original_int_call(self):
    +        # The original `int(w["pid"])` accepted numeric strings; preserve that.
    +        from tools.computer_use.cua_backend import _ingest_windows
    +
    +        out = _ingest_windows(
    +            [{"app_name": "Term", "pid": "200", "window_id": "9", "z_index": 0}]
    +        )
    +
    +        assert out[0]["pid"] == 200
    +        assert out[0]["window_id"] == 9
    +        assert isinstance(out[0]["pid"], int)
    +
    +    def test_preserves_fields_capture_relies_on(self):
    +        from tools.computer_use.cua_backend import _ingest_windows
    +
    +        out = _ingest_windows([
    +            {
    +                "app_name": "Firefox",
    +                "pid": 1,
    +                "window_id": 2,
    +                "is_on_screen": False,
    +                "title": "Mozilla Firefox",
    +                "z_index": 3,
    +            }
    +        ])
    +
    +        w = out[0]
    +        assert w["off_screen"] is True          # derived from is_on_screen
    +        assert w["title"] == "Mozilla Firefox"
    +        assert w["z_index"] == 3
    +
    +
    +# ---------------------------------------------------------------------------
    +# capture(): end-to-end proof the null-pid window no longer crashes capture
    +# ---------------------------------------------------------------------------
    +
    +def _backend_with_windows(raw_windows):
    +    """A CuaDriverBackend whose session returns `raw_windows` from
    +    list_windows and a valid PNG from screenshot."""
    +    from tools.computer_use.cua_backend import CuaDriverBackend
    +
    +    backend = CuaDriverBackend()
    +    session = MagicMock()
    +    session.capabilities_discovered = True
    +    session._has_tool.return_value = True
    +
    +    def _call_tool(name, args, *a, **k):
    +        if name == "list_windows":
    +            return {"structuredContent": {"windows": raw_windows}}
    +        if name == "screenshot":
    +            return {
    +                "structuredContent": {
    +                    "screenshot_png_b64": _PNG_B64,
    +                    "screenshot_mime_type": "image/png",
    +                }
    +            }
    +        return {}
    +
    +    session.call_tool.side_effect = _call_tool
    +    backend._session = session
    +    return backend
    +
    +
    +def test_capture_vision_survives_null_pid_window():
    +    raw = [
    +        {"app_name": "Desktop", "pid": None, "window_id": 1, "z_index": 0},
    +        {"app_name": "Firefox", "pid": 4321, "window_id": 77,
    +         "is_on_screen": True, "title": "Mozilla Firefox", "z_index": 1},
    +    ]
    +    backend = _backend_with_windows(raw)
    +
    +    cap = backend.capture(mode="vision")
    +
    +    # The real, targetable window is selected rather than the whole capture
    +    # crashing on the null-pid desktop window.
    +    assert cap.app == "Firefox"
    +    assert cap.png_b64 == _PNG_B64
    +    assert backend._active_pid == 4321
    +    assert backend._active_window_id == 77
    +    assert base64.b64decode(cap.png_b64)  # decodes cleanly
    ```
  - `tools/computer_use/cua_backend.py`

    ```diff
    @@ -909,6 +909,41 @@ def _image_from_tool_result(out: Dict[str, Any]) -> tuple[Optional[str], Optiona
         return None, None
     
     
    +def _ingest_windows(raw_windows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    +    """Normalise cua-driver ``list_windows`` entries, dropping unusable ones.
    +
    +    Every downstream operation needs both an integer ``pid`` (for
    +    get_window_state / action tools) and ``window_id`` (for screenshot /
    +    element clicks), so a window missing either is uncapturable.
    +
    +    Crucially, on X11 a window's PID comes from the *optional*
    +    ``_NET_WM_PID`` property — the desktop root, panels, and
    +    override-redirect popups routinely omit it, so the driver reports
    +    ``pid: null`` for them. Coercing every entry unconditionally
    +    (``int(w["pid"])``) let one such window abort enumeration of the real,
    +    targetable windows. We skip the unusable entries instead so capture()
    +    and focus_app() still find the windows that matter.
    +    """
    +    windows: List[Dict[str, Any]] = []
    +    for w in raw_windows:
    +        pid, window_id = w.get("pid"), w.get("window_id")
    +        if pid is None or window_id is None:
    +            continue
    +        try:
    +            pid_int, window_id_int = int(pid), int(window_id)
    +        except (TypeError, ValueError):
    +            continue
    +        windows.append({
    +            "app_name": w.get("app_name", ""),
    +            "pid": pid_int,
    +            "window_id": window_id_int,
    +            "off_screen": not w.get("is_on_screen", True),
    +            "title": w.get("title", ""),
    +            "z_index": w.get("z_index", 0),
    +        })
    +    return windows
    +
    +
     # ---------------------------------------------------------------------------
     # The backend itself
     # ---------------------------------------------------------------------------
    ```

    ```diff
    @@ -1028,17 +1063,7 @@ def capture(self, mode: str = "som", app: Optional[str] = None) -> CaptureResult
                 {"on_screen_only": True, "session": self._session_id},
             )
             raw_windows = (lw_out.get("structuredContent") or {}).get("windows") or []
    -        windows = [
    -            {
    -                "app_name": w.get("app_name", ""),
    -                "pid": int(w["pid"]),
    -                "window_id": int(w["window_id"]),
    -                "off_screen": not w.get("is_on_screen", True),
    -                "title": w.get("title", ""),
    -                "z_index": w.get("z_index", 0),
    -            }
    -            for w in raw_windows
    -        ]
    +        windows = _ingest_windows(raw_windows)
             # Sort by z_index descending (lowest z_index = frontmost on macOS).
             windows.sort(key=lambda w: w["z_index"])
     
    ```

    ```diff
    @@ -1433,15 +1458,7 @@ def focus_app(self, app: str, raise_window: bool = False) -> ActionResult:
                 {"on_screen_only": True, "session": self._session_id},
             )
             raw_windows = (lw_out.get("structuredContent") or {}).get("windows") or []
    -        windows = [
    -            {
    -                "app_name": w.get("app_name", ""),
    -                "pid": int(w["pid"]),
    -                "window_id": int(w["window_id"]),
    -                "z_index": w.get("z_index", 0),
    -            }
    -            for w in raw_windows
    -        ]
    +        windows = _ingest_windows(raw_windows)
             windows.sort(key=lambda w: w["z_index"])
     
             app_lower = app.lower()
    ```
- **#57392: fix(computer-use): guard None pid/window_id in list_windows parse (WSLg/X11 compatibility)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#58173`, `#52498`

## Fix status

- Merged to main in PR #58173 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-04`.
- Subsystem: `agent`
