From a8fd7257b1738f89eadbe7015a613da64a2e02b1 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:15:47 -0700 Subject: [PATCH] feat(gateway): WSL-aware gateway with smart systemd detection (#7510) - Add shared is_wsl() to hermes_constants (like is_termux) - Update supports_systemd_services() to verify systemd is actually running on WSL before returning True - Add WSL-specific guidance in gateway install/start/setup/status for both cases: WSL+systemd and WSL without systemd - Improve help strings: 'run' now says recommended for WSL/Docker, 'start'/'install' now mention systemd/launchd explicitly - Add WSL gateway FAQ section with tmux/nohup/Task Scheduler tips - Update CLI commands docs with WSL tip - Deduplicate _is_wsl() from clipboard.py to shared hermes_constants - Fix clipboard tests to reset hermes_constants cache - 20 new WSL-specific tests covering detection, systemd check, supports_systemd_services integration, and command output Motivated by user feedback: took 1 hour to figure out run vs start on WSL, Telegram bot kept disconnecting due to flaky WSL systemd. --- hermes_cli/clipboard.py | 18 +- hermes_cli/gateway.py | 77 ++++++- hermes_cli/main.py | 6 +- hermes_constants.py | 21 ++ tests/hermes_cli/test_gateway_wsl.py | 279 +++++++++++++++++++++++++ tests/tools/test_clipboard.py | 7 +- website/docs/reference/cli-commands.md | 12 +- website/docs/reference/faq.md | 36 ++++ 8 files changed, 421 insertions(+), 35 deletions(-) create mode 100644 tests/hermes_cli/test_gateway_wsl.py diff --git a/hermes_cli/clipboard.py b/hermes_cli/clipboard.py index 622c087f3..fd81ed4c8 100644 --- a/hermes_cli/clipboard.py +++ b/hermes_cli/clipboard.py @@ -19,10 +19,9 @@ import subprocess import sys from pathlib import Path -logger = logging.getLogger(__name__) +from hermes_constants import is_wsl as _is_wsl -# Cache WSL detection (checked once per process) -_wsl_detected: bool | None = None +logger = logging.getLogger(__name__) def save_clipboard_image(dest: Path) -> bool: @@ -217,19 +216,6 @@ def _windows_save(dest: Path) -> bool: # ── Linux ──────────────────────────────────────────────────────────────── -def _is_wsl() -> bool: - """Detect if running inside WSL (1 or 2).""" - global _wsl_detected - if _wsl_detected is not None: - return _wsl_detected - try: - with open("/proc/version", "r") as f: - _wsl_detected = "microsoft" in f.read().lower() - except Exception: - _wsl_detected = False - return _wsl_detected - - def _linux_save(dest: Path) -> bool: """Try clipboard backends in priority order: WSL → Wayland → X11.""" if _is_wsl(): diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index 548f7b452..609bb5b9b 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -226,11 +226,33 @@ def is_linux() -> bool: return sys.platform.startswith('linux') -from hermes_constants import is_termux +from hermes_constants import is_termux, is_wsl + + +def _wsl_systemd_operational() -> bool: + """Check if systemd is actually running as PID 1 on WSL. + + WSL2 with ``systemd=true`` in wsl.conf has working systemd. + WSL2 without it (or WSL1) does not — systemctl commands fail. + """ + try: + result = subprocess.run( + ["systemctl", "is-system-running"], + capture_output=True, text=True, timeout=5, + ) + # "running", "degraded", "starting" all mean systemd is PID 1 + status = result.stdout.strip().lower() + return status in ("running", "degraded", "starting", "initializing") + except (FileNotFoundError, subprocess.TimeoutExpired, OSError): + return False def supports_systemd_services() -> bool: - return is_linux() and not is_termux() + if not is_linux() or is_termux(): + return False + if is_wsl(): + return _wsl_systemd_operational() + return True def is_macos() -> bool: @@ -2244,7 +2266,8 @@ def gateway_setup(): print() if supports_systemd_services() or is_macos(): platform_name = "systemd" if supports_systemd_services() else "launchd" - if prompt_yes_no(f" Install the gateway as a {platform_name} service? (runs in background, starts on boot)", True): + wsl_note = " (note: services may not survive WSL restarts)" if is_wsl() else "" + if prompt_yes_no(f" Install the gateway as a {platform_name} service?{wsl_note} (runs in background, starts on boot)", True): try: installed_scope = None did_install = False @@ -2269,16 +2292,21 @@ def gateway_setup(): print_info(" You can install later: hermes gateway install") if supports_systemd_services(): print_info(" Or as a boot-time service: sudo hermes gateway install --system") - print_info(" Or run in foreground: hermes gateway") + print_info(" Or run in foreground: hermes gateway run") + elif is_wsl(): + print_info(" WSL detected but systemd is not running.") + print_info(" Run in foreground: hermes gateway run") + print_info(" For persistence: tmux new -s hermes 'hermes gateway run'") + print_info(" To enable systemd: add systemd=true to /etc/wsl.conf, then 'wsl --shutdown'") else: if is_termux(): from hermes_constants import display_hermes_home as _dhh print_info(" Termux does not use systemd/launchd services.") - print_info(" Run in foreground: hermes gateway") - print_info(f" Or start it manually in the background (best effort): nohup hermes gateway >{_dhh()}/logs/gateway.log 2>&1 &") + print_info(" Run in foreground: hermes gateway run") + print_info(f" Or start it manually in the background (best effort): nohup hermes gateway run >{_dhh()}/logs/gateway.log 2>&1 &") else: print_info(" Service install not supported on this platform.") - print_info(" Run in foreground: hermes gateway") + print_info(" Run in foreground: hermes gateway run") else: print() print_info("No platforms configured. Run 'hermes gateway setup' when ready.") @@ -2319,9 +2347,23 @@ def gateway_command(args): print("Run manually: hermes gateway") sys.exit(1) if supports_systemd_services(): + if is_wsl(): + print_warning("WSL detected — systemd services may not survive WSL restarts.") + print_info(" Consider running in foreground instead: hermes gateway run") + print_info(" Or use tmux/screen for persistence: tmux new -s hermes 'hermes gateway run'") + print() systemd_install(force=force, system=system, run_as_user=run_as_user) elif is_macos(): launchd_install(force) + elif is_wsl(): + print("WSL detected but systemd is not running.") + print("Either enable systemd (add systemd=true to /etc/wsl.conf and restart WSL)") + print("or run the gateway in foreground mode:") + print() + print(" hermes gateway run # direct foreground") + print(" tmux new -s hermes 'hermes gateway run' # persistent via tmux") + print(" nohup hermes gateway run > ~/.hermes/logs/gateway.log 2>&1 & # background") + sys.exit(1) else: print("Service installation not supported on this platform.") print("Run manually: hermes gateway run") @@ -2354,6 +2396,16 @@ def gateway_command(args): systemd_start(system=system) elif is_macos(): launchd_start() + elif is_wsl(): + print("WSL detected but systemd is not available.") + print("Run the gateway in foreground mode instead:") + print() + print(" hermes gateway run # direct foreground") + print(" tmux new -s hermes 'hermes gateway run' # persistent via tmux") + print(" nohup hermes gateway run > ~/.hermes/logs/gateway.log 2>&1 & # background") + print() + print("To enable systemd: add systemd=true to /etc/wsl.conf and run 'wsl --shutdown' from PowerShell.") + sys.exit(1) else: print("Not supported on this platform.") sys.exit(1) @@ -2488,6 +2540,10 @@ def gateway_command(args): if is_termux(): print("Termux note:") print(" Android may stop background jobs when Termux is suspended") + elif is_wsl(): + print("WSL note:") + print(" The gateway is running in foreground/manual mode (recommended for WSL).") + print(" Use tmux or screen for persistence across terminal closes.") else: print("To install as a service:") print(" hermes gateway install") @@ -2502,9 +2558,12 @@ def gateway_command(args): print(f" {line}") print() print("To start:") - print(" hermes gateway # Run in foreground") + print(" hermes gateway run # Run in foreground") if is_termux(): - print(" nohup hermes gateway > ~/.hermes/logs/gateway.log 2>&1 & # Best-effort background start") + print(" nohup hermes gateway run > ~/.hermes/logs/gateway.log 2>&1 & # Best-effort background start") + elif is_wsl(): + print(" tmux new -s hermes 'hermes gateway run' # persistent via tmux") + print(" nohup hermes gateway run > ~/.hermes/logs/gateway.log 2>&1 & # background") else: print(" hermes gateway install # Install as user service") print(" sudo hermes gateway install --system # Install as boot-time system service") diff --git a/hermes_cli/main.py b/hermes_cli/main.py index e1c8cb1cc..81850fdfe 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -4447,7 +4447,7 @@ For more help on a command: gateway_subparsers = gateway_parser.add_subparsers(dest="gateway_command") # gateway run (default) - gateway_run = gateway_subparsers.add_parser("run", help="Run gateway in foreground") + gateway_run = gateway_subparsers.add_parser("run", help="Run gateway in foreground (recommended for WSL, Docker, Termux)") gateway_run.add_argument("-v", "--verbose", action="count", default=0, help="Increase stderr log verbosity (-v=INFO, -vv=DEBUG)") gateway_run.add_argument("-q", "--quiet", action="store_true", @@ -4456,7 +4456,7 @@ For more help on a command: help="Replace any existing gateway instance (useful for systemd)") # gateway start - gateway_start = gateway_subparsers.add_parser("start", help="Start gateway service") + gateway_start = gateway_subparsers.add_parser("start", help="Start the installed systemd/launchd background service") gateway_start.add_argument("--system", action="store_true", help="Target the Linux system-level gateway service") # gateway stop @@ -4474,7 +4474,7 @@ For more help on a command: gateway_status.add_argument("--system", action="store_true", help="Target the Linux system-level gateway service") # gateway install - gateway_install = gateway_subparsers.add_parser("install", help="Install gateway as service") + gateway_install = gateway_subparsers.add_parser("install", help="Install gateway as a systemd/launchd background service") gateway_install.add_argument("--force", action="store_true", help="Force reinstall") gateway_install.add_argument("--system", action="store_true", help="Install as a Linux system-level service (starts at boot)") gateway_install.add_argument("--run-as-user", dest="run_as_user", help="User account the Linux system service should run as") diff --git a/hermes_constants.py b/hermes_constants.py index 09274a8ef..7d149f404 100644 --- a/hermes_constants.py +++ b/hermes_constants.py @@ -168,6 +168,27 @@ def is_termux() -> bool: return bool(os.getenv("TERMUX_VERSION") or "com.termux/files/usr" in prefix) +_wsl_detected: bool | None = None + + +def is_wsl() -> bool: + """Return True when running inside WSL (Windows Subsystem for Linux). + + Checks ``/proc/version`` for the ``microsoft`` marker that both WSL1 + and WSL2 inject. Result is cached for the process lifetime. + Import-safe — no heavy deps. + """ + global _wsl_detected + if _wsl_detected is not None: + return _wsl_detected + try: + with open("/proc/version", "r") as f: + _wsl_detected = "microsoft" in f.read().lower() + except Exception: + _wsl_detected = False + return _wsl_detected + + OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" OPENROUTER_MODELS_URL = f"{OPENROUTER_BASE_URL}/models" diff --git a/tests/hermes_cli/test_gateway_wsl.py b/tests/hermes_cli/test_gateway_wsl.py new file mode 100644 index 000000000..ea5bf40ca --- /dev/null +++ b/tests/hermes_cli/test_gateway_wsl.py @@ -0,0 +1,279 @@ +"""Tests for WSL detection and WSL-aware gateway behavior.""" + +import io +import subprocess +import sys +from types import SimpleNamespace +from unittest.mock import patch, MagicMock, mock_open + +import pytest + +import hermes_cli.gateway as gateway +import hermes_constants + + +# ============================================================================= +# is_wsl() in hermes_constants +# ============================================================================= + +class TestIsWsl: + """Test the shared is_wsl() utility.""" + + def setup_method(self): + # Reset cached value between tests + hermes_constants._wsl_detected = None + + def test_detects_wsl2(self): + fake_content = ( + "Linux version 5.15.146.1-microsoft-standard-WSL2 " + "(gcc (GCC) 11.2.0) #1 SMP Thu Jan 11 04:09:03 UTC 2024\n" + ) + with patch("builtins.open", mock_open(read_data=fake_content)): + assert hermes_constants.is_wsl() is True + + def test_detects_wsl1(self): + fake_content = ( + "Linux version 4.4.0-19041-Microsoft " + "(Microsoft@Microsoft.com) (gcc version 5.4.0) #1\n" + ) + with patch("builtins.open", mock_open(read_data=fake_content)): + assert hermes_constants.is_wsl() is True + + def test_native_linux(self): + fake_content = ( + "Linux version 6.5.0-44-generic (buildd@lcy02-amd64-015) " + "(x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0) #44\n" + ) + with patch("builtins.open", mock_open(read_data=fake_content)): + assert hermes_constants.is_wsl() is False + + def test_no_proc_version(self): + with patch("builtins.open", side_effect=FileNotFoundError): + assert hermes_constants.is_wsl() is False + + def test_result_is_cached(self): + """After first detection, subsequent calls return the cached value.""" + hermes_constants._wsl_detected = True + # Even with open raising, cached value is returned + with patch("builtins.open", side_effect=FileNotFoundError): + assert hermes_constants.is_wsl() is True + + +# ============================================================================= +# _wsl_systemd_operational() in gateway +# ============================================================================= + +class TestWslSystemdOperational: + """Test the WSL systemd check.""" + + def test_running(self, monkeypatch): + monkeypatch.setattr( + gateway.subprocess, "run", + lambda *a, **kw: SimpleNamespace( + returncode=0, stdout="running\n", stderr="" + ), + ) + assert gateway._wsl_systemd_operational() is True + + def test_degraded(self, monkeypatch): + monkeypatch.setattr( + gateway.subprocess, "run", + lambda *a, **kw: SimpleNamespace( + returncode=1, stdout="degraded\n", stderr="" + ), + ) + assert gateway._wsl_systemd_operational() is True + + def test_starting(self, monkeypatch): + monkeypatch.setattr( + gateway.subprocess, "run", + lambda *a, **kw: SimpleNamespace( + returncode=1, stdout="starting\n", stderr="" + ), + ) + assert gateway._wsl_systemd_operational() is True + + def test_offline_no_systemd(self, monkeypatch): + monkeypatch.setattr( + gateway.subprocess, "run", + lambda *a, **kw: SimpleNamespace( + returncode=1, stdout="offline\n", stderr="" + ), + ) + assert gateway._wsl_systemd_operational() is False + + def test_systemctl_not_found(self, monkeypatch): + monkeypatch.setattr( + gateway.subprocess, "run", + MagicMock(side_effect=FileNotFoundError), + ) + assert gateway._wsl_systemd_operational() is False + + def test_timeout(self, monkeypatch): + monkeypatch.setattr( + gateway.subprocess, "run", + MagicMock(side_effect=subprocess.TimeoutExpired("systemctl", 5)), + ) + assert gateway._wsl_systemd_operational() is False + + +# ============================================================================= +# supports_systemd_services() WSL integration +# ============================================================================= + +class TestSupportsSystemdServicesWSL: + """Test that supports_systemd_services() handles WSL correctly.""" + + def test_wsl_with_systemd(self, monkeypatch): + """WSL + working systemd → True.""" + monkeypatch.setattr(gateway, "is_linux", lambda: True) + monkeypatch.setattr(gateway, "is_termux", lambda: False) + monkeypatch.setattr(gateway, "is_wsl", lambda: True) + monkeypatch.setattr(gateway, "_wsl_systemd_operational", lambda: True) + assert gateway.supports_systemd_services() is True + + def test_wsl_without_systemd(self, monkeypatch): + """WSL + no systemd → False.""" + monkeypatch.setattr(gateway, "is_linux", lambda: True) + monkeypatch.setattr(gateway, "is_termux", lambda: False) + monkeypatch.setattr(gateway, "is_wsl", lambda: True) + monkeypatch.setattr(gateway, "_wsl_systemd_operational", lambda: False) + assert gateway.supports_systemd_services() is False + + def test_native_linux(self, monkeypatch): + """Native Linux (not WSL) → True without checking systemd.""" + monkeypatch.setattr(gateway, "is_linux", lambda: True) + monkeypatch.setattr(gateway, "is_termux", lambda: False) + monkeypatch.setattr(gateway, "is_wsl", lambda: False) + assert gateway.supports_systemd_services() is True + + def test_termux_still_excluded(self, monkeypatch): + """Termux → False regardless of WSL status.""" + monkeypatch.setattr(gateway, "is_linux", lambda: True) + monkeypatch.setattr(gateway, "is_termux", lambda: True) + assert gateway.supports_systemd_services() is False + + +# ============================================================================= +# WSL messaging in gateway commands +# ============================================================================= + +class TestGatewayCommandWSLMessages: + """Test that WSL users see appropriate guidance.""" + + def test_install_wsl_no_systemd(self, monkeypatch, capsys): + """hermes gateway install on WSL without systemd shows guidance.""" + monkeypatch.setattr(gateway, "is_linux", lambda: True) + monkeypatch.setattr(gateway, "is_termux", lambda: False) + monkeypatch.setattr(gateway, "is_wsl", lambda: True) + monkeypatch.setattr(gateway, "supports_systemd_services", lambda: False) + monkeypatch.setattr(gateway, "is_macos", lambda: False) + monkeypatch.setattr(gateway, "is_managed", lambda: False) + + args = SimpleNamespace( + gateway_command="install", force=False, system=False, + run_as_user=None, + ) + with pytest.raises(SystemExit) as exc_info: + gateway.gateway_command(args) + assert exc_info.value.code == 1 + + out = capsys.readouterr().out + assert "WSL detected" in out + assert "systemd is not running" in out + assert "hermes gateway run" in out + assert "tmux" in out + + def test_start_wsl_no_systemd(self, monkeypatch, capsys): + """hermes gateway start on WSL without systemd shows guidance.""" + monkeypatch.setattr(gateway, "is_linux", lambda: True) + monkeypatch.setattr(gateway, "is_termux", lambda: False) + monkeypatch.setattr(gateway, "is_wsl", lambda: True) + monkeypatch.setattr(gateway, "supports_systemd_services", lambda: False) + monkeypatch.setattr(gateway, "is_macos", lambda: False) + + args = SimpleNamespace(gateway_command="start", system=False) + with pytest.raises(SystemExit) as exc_info: + gateway.gateway_command(args) + assert exc_info.value.code == 1 + + out = capsys.readouterr().out + assert "WSL detected" in out + assert "hermes gateway run" in out + assert "wsl.conf" in out + + def test_install_wsl_with_systemd_warns(self, monkeypatch, capsys): + """hermes gateway install on WSL with systemd shows warning but proceeds.""" + monkeypatch.setattr(gateway, "is_linux", lambda: True) + monkeypatch.setattr(gateway, "is_termux", lambda: False) + monkeypatch.setattr(gateway, "is_wsl", lambda: True) + monkeypatch.setattr(gateway, "supports_systemd_services", lambda: True) + monkeypatch.setattr(gateway, "is_macos", lambda: False) + monkeypatch.setattr(gateway, "is_managed", lambda: False) + + # Mock systemd_install to capture call + install_called = [] + monkeypatch.setattr( + gateway, "systemd_install", + lambda **kwargs: install_called.append(kwargs), + ) + + args = SimpleNamespace( + gateway_command="install", force=False, system=False, + run_as_user=None, + ) + gateway.gateway_command(args) + + out = capsys.readouterr().out + assert "WSL detected" in out + assert "may not survive WSL restarts" in out + assert len(install_called) == 1 # install still proceeded + + def test_status_wsl_running_manual(self, monkeypatch, capsys): + """hermes gateway status on WSL with manual process shows WSL note.""" + monkeypatch.setattr(gateway, "supports_systemd_services", lambda: False) + monkeypatch.setattr(gateway, "is_macos", lambda: False) + monkeypatch.setattr(gateway, "is_termux", lambda: False) + monkeypatch.setattr(gateway, "is_wsl", lambda: True) + monkeypatch.setattr(gateway, "find_gateway_pids", lambda: [12345]) + monkeypatch.setattr(gateway, "_runtime_health_lines", lambda: []) + # Stub out the systemd unit path check + monkeypatch.setattr( + gateway, "get_systemd_unit_path", + lambda system=False: SimpleNamespace(exists=lambda: False), + ) + monkeypatch.setattr( + gateway, "get_launchd_plist_path", + lambda: SimpleNamespace(exists=lambda: False), + ) + + args = SimpleNamespace(gateway_command="status", deep=False, system=False) + gateway.gateway_command(args) + + out = capsys.readouterr().out + assert "WSL note" in out + assert "tmux or screen" in out + + def test_status_wsl_not_running(self, monkeypatch, capsys): + """hermes gateway status on WSL with no process shows WSL start advice.""" + monkeypatch.setattr(gateway, "supports_systemd_services", lambda: False) + monkeypatch.setattr(gateway, "is_macos", lambda: False) + monkeypatch.setattr(gateway, "is_termux", lambda: False) + monkeypatch.setattr(gateway, "is_wsl", lambda: True) + monkeypatch.setattr(gateway, "find_gateway_pids", lambda: []) + monkeypatch.setattr(gateway, "_runtime_health_lines", lambda: []) + monkeypatch.setattr( + gateway, "get_systemd_unit_path", + lambda system=False: SimpleNamespace(exists=lambda: False), + ) + monkeypatch.setattr( + gateway, "get_launchd_plist_path", + lambda: SimpleNamespace(exists=lambda: False), + ) + + args = SimpleNamespace(gateway_command="status", deep=False, system=False) + gateway.gateway_command(args) + + out = capsys.readouterr().out + assert "hermes gateway run" in out + assert "tmux" in out diff --git a/tests/tools/test_clipboard.py b/tests/tools/test_clipboard.py index e8171fe1b..fab80b4bc 100644 --- a/tests/tools/test_clipboard.py +++ b/tests/tools/test_clipboard.py @@ -205,9 +205,9 @@ class TestMacosOsascript: class TestIsWsl: def setup_method(self): - # Reset cached value before each test - import hermes_cli.clipboard as cb - cb._wsl_detected = None + # _is_wsl is now hermes_constants.is_wsl — reset its cache + import hermes_constants + hermes_constants._wsl_detected = None def test_wsl2_detected(self): content = "Linux version 5.15.0 (microsoft-standard-WSL2)" @@ -229,6 +229,7 @@ class TestIsWsl: assert _is_wsl() is False def test_result_is_cached(self): + import hermes_constants content = "Linux version 5.15.0 (microsoft-standard-WSL2)" with patch("builtins.open", mock_open(read_data=content)) as m: assert _is_wsl() is True diff --git a/website/docs/reference/cli-commands.md b/website/docs/reference/cli-commands.md index 132da079c..c430d3ba8 100644 --- a/website/docs/reference/cli-commands.md +++ b/website/docs/reference/cli-commands.md @@ -140,15 +140,19 @@ Subcommands: | Subcommand | Description | |------------|-------------| -| `run` | Run the gateway in the foreground. | -| `start` | Start the installed gateway service. | -| `stop` | Stop the service. | +| `run` | Run the gateway in the foreground. Recommended for WSL, Docker, and Termux. | +| `start` | Start the installed systemd/launchd background service. | +| `stop` | Stop the service (or foreground process). | | `restart` | Restart the service. | | `status` | Show service status. | -| `install` | Install as a user service (`systemd` on Linux, `launchd` on macOS). | +| `install` | Install as a systemd (Linux) or launchd (macOS) background service. | | `uninstall` | Remove the installed service. | | `setup` | Interactive messaging-platform setup. | +:::tip WSL users +Use `hermes gateway run` instead of `hermes gateway start` — WSL's systemd support is unreliable. Wrap it in tmux for persistence: `tmux new -s hermes 'hermes gateway run'`. See [WSL FAQ](/docs/reference/faq#wsl-gateway-keeps-disconnecting-or-hermes-gateway-start-fails) for details. +::: + ## `hermes setup` ```bash diff --git a/website/docs/reference/faq.md b/website/docs/reference/faq.md index 6db208718..6950fb1e9 100644 --- a/website/docs/reference/faq.md +++ b/website/docs/reference/faq.md @@ -375,6 +375,42 @@ lsof -i :8080 hermes config show ``` +#### WSL: Gateway keeps disconnecting or `hermes gateway start` fails + +**Cause:** WSL's systemd support is unreliable. Many WSL2 installations don't have systemd enabled, and even when enabled, services may not survive WSL restarts or Windows idle shutdowns. + +**Solution:** Use foreground mode instead of the systemd service: + +```bash +# Option 1: Direct foreground (simplest) +hermes gateway run + +# Option 2: Persistent via tmux (survives terminal close) +tmux new -s hermes 'hermes gateway run' +# Reattach later: tmux attach -t hermes + +# Option 3: Background via nohup +nohup hermes gateway run > ~/.hermes/logs/gateway.log 2>&1 & +``` + +If you want to try systemd anyway, make sure it's enabled: + +1. Open `/etc/wsl.conf` (create it if it doesn't exist) +2. Add: + ```ini + [boot] + systemd=true + ``` +3. From PowerShell: `wsl --shutdown` +4. Reopen your WSL terminal +5. Verify: `systemctl is-system-running` should say "running" or "degraded" + +:::tip Auto-start on Windows boot +For reliable auto-start, use Windows Task Scheduler to launch WSL + the gateway on login: +1. Create a task that runs `wsl -d Ubuntu -- bash -lc 'hermes gateway run'` +2. Set it to trigger on user logon +::: + #### macOS: Node.js / ffmpeg / other tools not found by gateway **Cause:** launchd services inherit a minimal PATH (`/usr/bin:/bin:/usr/sbin:/sbin`) that doesn't include Homebrew, nvm, cargo, or other user-installed tool directories. This commonly breaks the WhatsApp bridge (`node not found`) or voice transcription (`ffmpeg not found`).