# NousResearch/hermes-agent · WhatsApp disable config and bridge process leak

## Open now

- 0 issues, 1 PR open on GitHub.
- PR #19999: fix(gateway): honor WhatsApp config disable

These issues and fixes address two related WhatsApp bugs: the inability to properly disable the WhatsApp platform via UI or CLI, and a bridge process leak caused by asymmetry in the disable configuration handling.

## TL;DR

- 4 member(s): 0 open issue(s), 1 closed issue(s), 1 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #21229. Verify it addresses your case.
- **Recommended:** read PR #21229 (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: `resolved` · deciding PR(s): `pr-21229`. 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.50`: 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

- #19124: WhatsApp platform cannot be disabled via UI/CLI + bridge process leak

## Prior & competing PRs

- **#21229: fix(whatsapp): bridge process leak + disable config asymmetry (salvage #19138)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `gateway/config.py`, `gateway/platforms/whatsapp.py`
  - fix hunks:
  - `gateway/config.py`

    ```diff
    @@ -1152,10 +1152,17 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
         
         # WhatsApp (typically uses different auth mechanism)
         whatsapp_enabled = os.getenv("WHATSAPP_ENABLED", "").lower() in ("true", "1", "yes")
    -    if whatsapp_enabled:
    -        if Platform.WHATSAPP not in config.platforms:
    -            config.platforms[Platform.WHATSAPP] = PlatformConfig()
    -        config.platforms[Platform.WHATSAPP].enabled = True
    +    whatsapp_disabled_explicitly = os.getenv("WHATSAPP_ENABLED", "").lower() in ("false", "0", "no")
    +    if Platform.WHATSAPP in config.platforms:
    +        # YAML config exists — respect explicit disable
    +        wa_cfg = config.platforms[Platform.WHATSAPP]
    +        if whatsapp_disabled_explicitly:
    +            wa_cfg.enabled = False
    +        elif whatsapp_enabled:
    +            wa_cfg.enabled = True
    +        # else: keep whatever the YAML set
    +    elif whatsapp_enabled:
    +        config.platforms[Platform.WHATSAPP] = PlatformConfig(enabled=True)
         whatsapp_home = os.getenv("WHATSAPP_HOME_CHANNEL")
         if whatsapp_home and Platform.WHATSAPP in config.platforms:
             config.platforms[Platform.WHATSAPP].home_channel = HomeChannel(
    ```
  - `gateway/platforms/whatsapp.py`

    ```diff
    @@ -21,6 +21,7 @@
     import os
     import platform
     import re
    +import signal
     import subprocess
     
     _IS_WINDOWS = platform.system() == "Windows"
    ```

    ```diff
    @@ -54,19 +55,77 @@ def _kill_port_process(port: int) -> None:
                             except subprocess.SubprocessError:
                                 pass
             else:
    -            result = subprocess.run(
    -                ["fuser", f"{port}/tcp"],
    -                capture_output=True, timeout=5,
    -            )
    -            if result.returncode == 0:
    -                subprocess.run(
    -                    ["fuser", "-k", f"{port}/tcp"],
    +            # Try fuser first (Linux), fall back to lsof (macOS / WSL2)
    +            killed = False
    +            try:
    +                result = subprocess.run(
    +                    ["fuser", f"{port}/tcp"],
                         capture_output=True, timeout=5,
                     )
    +                if result.returncode == 0:
    +                    subprocess.run(
    +                        ["fuser", "-k", f"{port}/tcp"],
    +                        capture_output=True, timeout=5,
    +                    )
    +                    killed = True
    +            except FileNotFoundError:
    +                pass  # fuser not installed
    +
    +            if not killed:
    +                try:
    +                    result = subprocess.run(
    +                        ["lsof", "-ti", f":{port}"],
    +                        capture_output=True, text=True, timeout=5,
    +                    )
    +                    for pid_str in result.stdout.strip().splitlines():
    +                        try:
    +                            os.kill(int(pid_str), signal.SIGTERM)
    +                        except (ValueError, ProcessLookupError, PermissionError):
    +                            pass
    +                except FileNotFoundError:
    +                    pass  # lsof not installed either
         except Exception:
             pass
     
     
    +def _kill_stale_bridge_by_pidfile(session_path: Path) -> None:
    +    """Kill a bridge process recorded in a PID file from a previous run.
    +
    +    The bridge writes ``bridge.pid`` into the session directory when it
    +    starts.  If the gateway crashed without a clean shutdown the old bridge
    +    process becomes orphaned — this helper finds and kills it.
    +    """
    +    pid_file = session_path / "bridge.pid"
    +    if not pid_file.exists():
    +        return
    +    try:
    +        pid = int(pid_file.read_text().strip())
    +    except (ValueError, OSError, TypeError):
    +        try:
    +            pid_file.unlink()
    +        except OSError:
    +            pass
    +        return
    +    try:
    +        os.kill(pid, 0)  # check existence
    +        os.kill(pid, signal.SIGTERM)
    +        logger.info("[whatsapp] Killed stale bridge PID %d from pidfile", pid)
    +    except (ProcessLookupError, PermissionError, OSError):
    +        pass
    +    try:
    +        pid_file.unlink()
    +    except OSError:
    +        pass
    +
    +
    +def _write_bridge_pidfile(session_path: Path, pid: int) -> None:
    +    """Write the bridge PID to a file for later cleanup."""
    +    try:
    +        (session_path / "bridge.pid").write_text(str(pid))
    +    except OSError:
    +        pass
    +
    +
     def _terminate_bridge_process(proc, *, force: bool = False) -> None:
         """Terminate the bridge process using process-tree semantics where possible."""
         if _IS_WINDOWS:
    ```

    ```diff
    @@ -428,6 +487,7 @@ async def connect(self) -> bool:
                     pass  # Bridge not running, start a new one
                 
                 # Kill any orphaned bridge from a previous gateway run
    +            _kill_stale_bridge_by_pidfile(self._session_path)
                 _kill_port_process(self._bridge_port)
                 await asyncio.sleep(1)
                 
    ```

    ```diff
    @@ -459,6 +519,7 @@ async def connect(self) -> bool:
                     preexec_fn=None if _IS_WINDOWS else os.setsid,
                     env=bridge_env,
                 )
    +            _write_bridge_pidfile(self._session_path, self._bridge_process.pid)
                 
                 # Wait for the bridge to connect to WhatsApp.
                 # Phase 1: wait for the HTTP server to come up (up to 15s).
    ```

    ```diff
    @@ -609,6 +670,12 @@ async def disconnect(self) -> None:
                 # Bridge was not started by us, don't kill it
                 print(f"[{self.name}] Disconnecting (external bridge left running)")
     
    +        # Clean up PID file
    +        try:
    +            (self._session_path / "bridge.pid").unlink(missing_ok=True)
    +        except OSError:
    +            pass
    +
             # Cancel the poll task explicitly
             if self._poll_task and not self._poll_task.done():
                 self._poll_task.cancel()
    ```
- **#19138: fix: WhatsApp bridge process leak and disable config asymmetry**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#19999: fix(gateway): honor WhatsApp config disable**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `gateway/config.py`, `tests/gateway/test_config.py`

## Fix status

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