NousResearch/hermes-agent

Null pid/window_id guard in list_windows on Linux/X11

Cluster 56704 · agent. Data as of

Nothing open on GitHub. Fix merged in #58173. Check it covers your case.

The short answer

0open issues
0open PRs
5items grouped
Known?
Yes. 5 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #58173. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #58173 fix(computer-use): skip null-pid/window_id windows from list_windows (X11)

    merged to main

    files: scripts/release.py, tests/tools/test_computer_use_null_pid_windows.py, tools/computer_use/cua_backend.py

    Show the diff
    • scripts/release.py
      @@ -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
      @@ -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
      @@ -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
       # ---------------------------------------------------------------------------
      @@ -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"])
       
      @@ -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()

3 PRs closed without merging

From the close comments: superseded by another PR 2 · done elsewhere 1

  • #56707 fix(computer-use): guard against None pid/window_id from list_windows · superseded by another PR → #52498
  • #57392 fix(computer-use): guard None pid/window_id in list_windows parse (WSLg/X11 compatibility) · done elsewhere → #58173, #52498
  • #57426 fix(tools): guard computer_use against null pid/window_id from list_windows · superseded by another PR → #52498, #57392
Closed issues · 1
  • Closed#56704computer_use capture fails on Linux/WSL: int(None) on pid/window_id from list_windows
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.40: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#58173
Closed issues
1
Attempts
3 PRs closed without merging
Fix link checked
4 Jul 2026

For your agent

The same facts, typed.