Skip to content

fix(gateway/whatsapp): hide node.exe console on Windows via CREATE_NO_WINDOW (#29715) - #29807

Closed
xxxigm wants to merge 2 commits into
NousResearch:mainfrom
xxxigm:fix/29715-whatsapp-bridge-no-windows-console
Closed

fix(gateway/whatsapp): hide node.exe console on Windows via CREATE_NO_WINDOW (#29715)#29807
xxxigm wants to merge 2 commits into
NousResearch:mainfrom
xxxigm:fix/29715-whatsapp-bridge-no-windows-console

Conversation

@xxxigm

@xxxigm xxxigm commented May 21, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #29715 — on Windows, every time the gateway started the WhatsApp adapter under pythonw.exe, Windows allocated a fresh visible node.exe console window for the Node bridge. Closing the empty window killed the bridge with 0xC000013A (CTRL_LOGOFF_EVENT-shaped exit); the gateway reconnection watcher then relaunched it, making the popup come back.

Root cause.
gateway/platforms/whatsapp.py::WhatsAppAdapter.connect() launched the Node bridge with:

subprocess.Popen(
    ["node", str(bridge_path), …],
    stdout=bridge_log_fh, stderr=bridge_log_fh,
    preexec_fn=None if _IS_WINDOWS else os.setsid,
    env=bridge_env,
)

No creationflags. preexec_fn is a no-op on Windows, so when a console-subsystem child (node.exe) is launched from a parent that has no console (pythonw.exe -m hermes_cli.main gateway run --replace), Windows allocates a brand-new console window for the child.

Fix.
Introduce a tiny _bridge_popen_extra_kwargs() helper that returns:

  • {"creationflags": windows_hide_flags()} on Windows — i.e. CREATE_NO_WINDOW (0x08000000) via the project's blessed cross-platform helper from hermes_cli/_subprocess_compat.py.
  • {"preexec_fn": os.setsid} on POSIX — unchanged behaviour, bridge gets its own session so Ctrl+C in the gateway terminal doesn't propagate.

The Popen call site unpacks **_bridge_popen_extra_kwargs() so the kwargs land cleanly without overriding stdout/stderr/env.

Deliberately uses windows_hide_flags() (CREATE_NO_WINDOW only) rather than the sibling windows_detach_flags() (DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW) because DETACHED_PROCESS severs stdio handles, which would break the stdout=bridge_log_fh redirect the adapter relies on for QR-code and connection diagnostics.

The helper resolves _IS_WINDOWS lazily inside the function body (the public arg accepts None and re-reads the module-level constant at call time) so tests can simulate the Windows branch on a non-Windows host via monkeypatch.setattr.

Related Issue

Closes #29715WhatsApp bridge opens blank node.exe console window on Windows.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • gateway/platforms/whatsapp.py

    • Import windows_hide_flags from hermes_cli._subprocess_compat (the project's blessed Windows-creationflags helper, already used in cron/scheduler.py, tools/process_registry.py, etc.).
    • New _bridge_popen_extra_kwargs(is_windows=None) helper that returns the platform-correct Popen kwargs. Lazy _IS_WINDOWS resolution so the helper picks up monkeypatch.setattr(_IS_WINDOWS, …) in tests.
    • Replace the inline preexec_fn=None if _IS_WINDOWS else os.setsid argument at the bridge Popen site with **_bridge_popen_extra_kwargs().
  • tests/gateway/test_whatsapp_bridge_no_console_window.py (new, +405 lines, 13 cases across three classes):

    • TestBridgePopenExtraKwargsHelper (5) — pure-helper contract: POSIX → preexec_fn=os.setsid only; simulated Windows (via monkeypatch.setattr(_subprocess_compat, "IS_WINDOWS", True)) → creationflags with CREATE_NO_WINDOW set and DETACHED_PROCESS explicitly NOT set; default no-arg call follows module-level _IS_WINDOWS; returned dict only contains expected keys, never both at once.
    • TestConnectPlumbsExtraKwargsToPopen (3) — end-to-end: drive WhatsAppAdapter.connect() through every preflight with subprocess.Popen patched to record kwargs, then assert the recorded call shape. Third case verifies the Windows branch still routes stdout/stderr to the same bridge.log handle (the whole reason we avoid DETACHED_PROCESS).
    • TestWhatsAppSourceGuardrail (5) — static asserts on gateway/platforms/whatsapp.py so a future refactor can't quietly drop the fix: helper defined, Popen call site unpacks **_bridge_popen_extra_kwargs(), pre-fix preexec_fn=None if _IS_WINDOWS else os.setsid inline pattern is not reintroduced, windows_hide_flags is imported, windows_detach_flags( is never called.

How to Test

  1. Check out the branch and activate the venv:
    git fetch origin fix/29715-whatsapp-bridge-no-windows-console
    git checkout fix/29715-whatsapp-bridge-no-windows-console
    source .venv/bin/activate   # or: python3 -m venv .venv && source .venv/bin/activate && pip install -e ".[all,dev]"
    
  2. Run the new regression file on its own:
    scripts/run_tests.sh tests/gateway/test_whatsapp_bridge_no_console_window.py -v
    
    Expected: 13 passed.
  3. Run the combined WhatsApp suite to confirm no cross-file regressions:
    scripts/run_tests.sh tests/gateway/test_whatsapp_bridge_no_console_window.py tests/gateway/test_whatsapp_connect.py tests/gateway/test_whatsapp_group_gating.py tests/gateway/test_whatsapp_formatting.py tests/gateway/test_whatsapp_reply_prefix.py tests/hermes_cli/test_whatsapp_setup_ordering.py
    
    Expected: 102 passed.
  4. On a real Windows host, enable the WhatsApp platform and start the gateway under pythonw.exe. Confirm:
    • No blank node.exe console window appears.
    • bridge.log still receives QR codes / whatsapp-web.js startup logs.
    • Closing any unrelated console (PowerShell, etc.) no longer kills the bridge.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(gateway/whatsapp): … and test(gateway/whatsapp): …)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix (no unrelated commits)
  • I've run scripts/run_tests.sh tests/gateway/test_whatsapp_bridge_no_console_window.py tests/gateway/test_whatsapp_connect.py tests/gateway/test_whatsapp_group_gating.py tests/gateway/test_whatsapp_formatting.py tests/gateway/test_whatsapp_reply_prefix.py tests/hermes_cli/test_whatsapp_setup_ordering.py and all 102 tests pass
  • I've added tests for my changes (13 new regression cases — 5 pure-helper, 3 end-to-end connect() wire-level, 5 source guardrail)
  • I've tested on my platform: macOS 15.2 (Darwin 24.6.0), Python 3.12; Windows behaviour is validated via monkeypatch-simulated tests on the helper and end-to-end paths

Documentation & Housekeeping

  • I've updated relevant documentation — the contract is documented in _bridge_popen_extra_kwargs's docstring; no user-facing docs reference the bridge launch shape
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A (the fix follows the existing Windows-creationflags pattern already used in cron/scheduler.py, tools/code_execution_tool.py, tools/browser_tool.py, etc.)
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — POSIX behaviour is unchanged; Windows gets CREATE_NO_WINDOW
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

Screenshots / Logs

$ scripts/run_tests.sh tests/gateway/test_whatsapp_bridge_no_console_window.py -v
4 workers [13 items]
............. [100%]
============================== 13 passed in 1.09s ==============================

$ scripts/run_tests.sh tests/gateway/test_whatsapp_bridge_no_console_window.py tests/gateway/test_whatsapp_connect.py tests/gateway/test_whatsapp_group_gating.py tests/gateway/test_whatsapp_formatting.py tests/gateway/test_whatsapp_reply_prefix.py tests/hermes_cli/test_whatsapp_setup_ordering.py
4 workers [102 items]
============================= 102 passed in 2.35s ==============================

xxxigm added 2 commits May 21, 2026 15:56
…_WINDOW (NousResearch#29715)

On Windows, ``WhatsAppAdapter._connect`` launched the Node bridge with:

    subprocess.Popen([...node, bridge.js, ...],
                     stdout=bridge_log_fh, stderr=bridge_log_fh,
                     preexec_fn=None if _IS_WINDOWS else os.setsid,
                     env=bridge_env)

— no ``creationflags``. ``preexec_fn`` is a no-op on Windows, so launching
the console-subsystem ``node.exe`` from a parent that has no console
(e.g. the gateway under ``pythonw.exe -m hermes_cli.main gateway run
--replace``) caused Windows to allocate a fresh, visible console window
just for the bridge. Closing that empty window killed the bridge with
``0xC000013A`` (``CTRL_LOGOFF_EVENT``-shaped exit), and the gateway
reconnection watcher relaunched it, making the blank window pop back.

Add a tiny ``_bridge_popen_extra_kwargs`` helper that returns
``{"creationflags": windows_hide_flags()}`` on Windows (i.e.
``CREATE_NO_WINDOW``) and ``{"preexec_fn": os.setsid}`` on POSIX, then
plumb it into the existing ``subprocess.Popen`` call. The helper is
exported as a stable seam so tests on a non-Windows host can simulate
the Windows path via ``monkeypatch.setattr(_subprocess_compat,
"IS_WINDOWS", True)``.

Deliberately uses ``windows_hide_flags()`` (``CREATE_NO_WINDOW`` only)
rather than ``windows_detach_flags()`` (which also sets
``DETACHED_PROCESS``) — ``DETACHED_PROCESS`` severs stdio handles,
which would break the ``stdout=bridge_log_fh`` redirect the adapter
relies on for QR-code and connection diagnostics.

POSIX behaviour is unchanged.
…ousResearch#29715)

Adds tests/gateway/test_whatsapp_bridge_no_console_window.py — 13
cases across three classes:

- TestBridgePopenExtraKwargsHelper (5): pure-helper contract of the
  new ``_bridge_popen_extra_kwargs`` — POSIX gets ``preexec_fn=
  os.setsid`` with no ``creationflags``; Windows (simulated via
  ``monkeypatch.setattr(_subprocess_compat, "IS_WINDOWS", True)``)
  gets ``creationflags`` with ``CREATE_NO_WINDOW`` (0x08000000) set
  and ``DETACHED_PROCESS`` (0x00000008) explicitly NOT set; default
  no-arg call follows the module-level ``_IS_WINDOWS`` constant; the
  returned dict only contains ``creationflags``/``preexec_fn`` keys
  and never both at once.

- TestConnectPlumbsExtraKwargsToPopen (3): end-to-end — drive
  ``WhatsAppAdapter.connect()`` through every preflight (Node
  requirements, ``creds.json``, npm install short-circuit, health
  probe miss, stale-pidfile / port-killer mocks) with
  ``subprocess.Popen`` patched to record kwargs, then assert the
  recorded call shape. Includes a third case verifying that the
  Windows branch still routes stdout/stderr to the same
  ``bridge.log`` handle (the whole reason we avoid
  ``DETACHED_PROCESS``).

- TestWhatsAppSourceGuardrail (5): static asserts on
  ``gateway/platforms/whatsapp.py`` so a future refactor can't
  quietly drop the fix — helper is defined, ``Popen`` call site
  unpacks it via ``**_bridge_popen_extra_kwargs()``, the pre-fix
  ``preexec_fn=None if _IS_WINDOWS else os.setsid`` inline pattern
  isn't reintroduced, ``windows_hide_flags`` is imported from the
  shared compat module, and ``windows_detach_flags(`` is never
  called (it would re-introduce stdio severing).
@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery platform/whatsapp WhatsApp Business adapter P2 Medium — degraded but workaround exists labels May 21, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related to #29725 — both fix #29715 (WhatsApp bridge console window on Windows). This PR is a cleaner implementation that uses the project's blessed windows_hide_flags() helper and doesn't bundle unrelated changes.

@teknium1

teknium1 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Thanks for this PR — the underlying bug (WhatsApp bridge spawning Node with a visible console window on Windows) is now fixed on main via #60924, a salvage of #60647 which swaps the bare start_new_session=True for the shared windows_detach_popen_kwargs() helper. Several PRs targeted this same symptom; we merged the minimal variant that reuses the sanctioned helper. Your report/fix helped confirm the bug class — appreciated. Closing as superseded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/whatsapp WhatsApp Business adapter type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WhatsApp bridge opens blank node.exe console window on Windows

3 participants