diff --git a/gateway/status.py b/gateway/status.py index 9a1a3bce2b..9120495780 100644 --- a/gateway/status.py +++ b/gateway/status.py @@ -552,6 +552,12 @@ def _gateway_command_subcommand(command: str | None) -> str | None: # Gateway-dedicated entrypoints carry no subcommand to inspect. if any(t == "gateway/run.py" or t.endswith("/gateway/run.py") for t in tokens): return "run" + # Atomic Hermes' bundled desktop runner shares HERMES_HOME with the CLI; without this, + # `gateway run --replace` does not recognise it as a running gateway, skips the + # terminate-and-scoped-lock-handoff path, and collides with its still-held scoped locks + # (e.g. the Discord bot-token lock). See #22418. + if any(b == "desktop-gateway.py" for b in basenames): + return "run" if any(b in ("hermes-gateway", "hermes-gateway.exe") for b in basenames): return "run" joined = " ".join(tokens) diff --git a/tests/gateway/test_gateway_command_line_matcher.py b/tests/gateway/test_gateway_command_line_matcher.py index 88867a58a5..ddcae282a5 100644 --- a/tests/gateway/test_gateway_command_line_matcher.py +++ b/tests/gateway/test_gateway_command_line_matcher.py @@ -58,3 +58,18 @@ def test_accepts_real_gateway_run(cmd): assert matches(cmd) is True +# Atomic Hermes' bundled desktop runner (regression for #22418): it shares +# HERMES_HOME with the CLI and must be recognised as a gateway so +# ``gateway run --replace`` enters the replace/lock-handoff path instead of +# colliding with the desktop runner's still-held scoped locks. +ATOMIC_DESKTOP = ( + "/Applications/Atomic Hermes.app/Contents/Resources/python-server/python " + "/Applications/Atomic Hermes.app/Contents/Resources/python-server/desktop-gateway.py" +) + + +def test_accepts_atomic_desktop_gateway(): + assert matches(ATOMIC_DESKTOP) is True + assert matches_runtime(ATOMIC_DESKTOP) is True + +