mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
feat(gateway): per-platform gateway_restart_notification flag
Adds an opt-out toggle on PlatformConfig that gates both restart
lifecycle pings: the "♻ Gateway restarted" message sent to the chat
that issued /restart, and the "♻️ Gateway online" home-channel
startup notification. Defaults to True so existing deployments are
unaffected.
The motivating split is operator vs. end-user surfaces: a back-channel
like Telegram should keep these pings, while a Slack workspace shared
with end users should not surface gateway lifecycle noise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
33bf5f6292
commit
b71f80e6ce
4 changed files with 120 additions and 4 deletions
|
|
@ -271,15 +271,23 @@ class PlatformConfig:
|
|||
# - "first": Only first chunk threads to user's message (default)
|
||||
# - "all": All chunks in multi-part replies thread to user's message
|
||||
reply_to_mode: str = "first"
|
||||
|
||||
|
||||
# Whether the gateway is allowed to send "♻️ Gateway online" /
|
||||
# "♻ Gateway restarted" lifecycle notifications on this platform.
|
||||
# Default True preserves prior behavior. Set False on platforms used
|
||||
# by end users (e.g. Slack) where operator-flavored restart pings are
|
||||
# noise; keep True for back-channels where the operator wants them.
|
||||
gateway_restart_notification: bool = True
|
||||
|
||||
# Platform-specific settings
|
||||
extra: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
result = {
|
||||
"enabled": self.enabled,
|
||||
"extra": self.extra,
|
||||
"reply_to_mode": self.reply_to_mode,
|
||||
"gateway_restart_notification": self.gateway_restart_notification,
|
||||
}
|
||||
if self.token:
|
||||
result["token"] = self.token
|
||||
|
|
@ -288,19 +296,22 @@ class PlatformConfig:
|
|||
if self.home_channel:
|
||||
result["home_channel"] = self.home_channel.to_dict()
|
||||
return result
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "PlatformConfig":
|
||||
home_channel = None
|
||||
if "home_channel" in data:
|
||||
home_channel = HomeChannel.from_dict(data["home_channel"])
|
||||
|
||||
|
||||
return cls(
|
||||
enabled=_coerce_bool(data.get("enabled"), False),
|
||||
token=data.get("token"),
|
||||
api_key=data.get("api_key"),
|
||||
home_channel=home_channel,
|
||||
reply_to_mode=data.get("reply_to_mode", "first"),
|
||||
gateway_restart_notification=_coerce_bool(
|
||||
data.get("gateway_restart_notification"), True
|
||||
),
|
||||
extra=data.get("extra", {}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -11386,6 +11386,14 @@ class GatewayRunner:
|
|||
)
|
||||
return None
|
||||
|
||||
platform_cfg = self.config.platforms.get(platform)
|
||||
if platform_cfg is not None and not platform_cfg.gateway_restart_notification:
|
||||
logger.info(
|
||||
"Restart notification suppressed: %s has gateway_restart_notification=false",
|
||||
platform_str,
|
||||
)
|
||||
return None
|
||||
|
||||
metadata = {"thread_id": thread_id} if thread_id else None
|
||||
result = await adapter.send(
|
||||
str(chat_id),
|
||||
|
|
@ -11437,6 +11445,14 @@ class GatewayRunner:
|
|||
if not home or not home.chat_id:
|
||||
continue
|
||||
|
||||
platform_cfg = self.config.platforms.get(platform)
|
||||
if platform_cfg is not None and not platform_cfg.gateway_restart_notification:
|
||||
logger.info(
|
||||
"Home-channel startup notification suppressed: %s has gateway_restart_notification=false",
|
||||
platform.value,
|
||||
)
|
||||
continue
|
||||
|
||||
target = (platform.value, str(home.chat_id), str(home.thread_id) if home.thread_id else None)
|
||||
if target in skipped or target in delivered:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -57,6 +57,19 @@ class TestPlatformConfigRoundtrip:
|
|||
restored = PlatformConfig.from_dict({"enabled": "false"})
|
||||
assert restored.enabled is False
|
||||
|
||||
def test_gateway_restart_notification_defaults_true(self):
|
||||
assert PlatformConfig().gateway_restart_notification is True
|
||||
assert PlatformConfig.from_dict({}).gateway_restart_notification is True
|
||||
|
||||
def test_gateway_restart_notification_roundtrip_false(self):
|
||||
pc = PlatformConfig(enabled=True, gateway_restart_notification=False)
|
||||
restored = PlatformConfig.from_dict(pc.to_dict())
|
||||
assert restored.gateway_restart_notification is False
|
||||
|
||||
def test_gateway_restart_notification_coerces_quoted_false(self):
|
||||
restored = PlatformConfig.from_dict({"gateway_restart_notification": "false"})
|
||||
assert restored.gateway_restart_notification is False
|
||||
|
||||
|
||||
class TestGetConnectedPlatforms:
|
||||
def test_returns_enabled_with_token(self):
|
||||
|
|
|
|||
|
|
@ -496,6 +496,82 @@ async def test_send_restart_notification_logs_warning_on_sendresult_failure(
|
|||
assert not notify_path.exists()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_home_channel_startup_notification_skipped_when_flag_disabled(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
"""Per-platform opt-out: gateway_restart_notification=False mutes the home-channel ping."""
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
|
||||
|
||||
runner, adapter = make_restart_runner()
|
||||
runner.config.platforms[Platform.TELEGRAM].home_channel = HomeChannel(
|
||||
platform=Platform.TELEGRAM,
|
||||
chat_id="home-42",
|
||||
name="Ops Home",
|
||||
)
|
||||
runner.config.platforms[Platform.TELEGRAM].gateway_restart_notification = False
|
||||
adapter.send = AsyncMock()
|
||||
|
||||
delivered = await runner._send_home_channel_startup_notifications()
|
||||
|
||||
assert delivered == set()
|
||||
adapter.send.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_home_channel_startup_notification_default_flag_true(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
"""Default behavior is unchanged: missing flag means notifications still fire."""
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
|
||||
|
||||
runner, adapter = make_restart_runner()
|
||||
# Sanity-check the dataclass default — guards against future refactors
|
||||
# silently flipping the default to False.
|
||||
assert runner.config.platforms[Platform.TELEGRAM].gateway_restart_notification is True
|
||||
|
||||
runner.config.platforms[Platform.TELEGRAM].home_channel = HomeChannel(
|
||||
platform=Platform.TELEGRAM,
|
||||
chat_id="home-42",
|
||||
name="Ops Home",
|
||||
)
|
||||
adapter.send = AsyncMock(return_value=SendResult(success=True, message_id="home"))
|
||||
|
||||
delivered = await runner._send_home_channel_startup_notifications()
|
||||
|
||||
assert delivered == {("telegram", "home-42", None)}
|
||||
adapter.send.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_restart_notification_skipped_when_flag_disabled(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
"""The /restart originator's notification also honors the per-platform flag.
|
||||
|
||||
Slack used by end users → flag off → no "Gateway restarted" message even
|
||||
when an end user accidentally triggers /restart. The marker file is still
|
||||
cleaned up so the notification doesn't leak into the next boot.
|
||||
"""
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
|
||||
|
||||
notify_path = tmp_path / ".restart_notify.json"
|
||||
notify_path.write_text(json.dumps({
|
||||
"platform": "telegram",
|
||||
"chat_id": "42",
|
||||
}))
|
||||
|
||||
runner, adapter = make_restart_runner()
|
||||
runner.config.platforms[Platform.TELEGRAM].gateway_restart_notification = False
|
||||
adapter.send = AsyncMock()
|
||||
|
||||
delivered_target = await runner._send_restart_notification()
|
||||
|
||||
assert delivered_target is None
|
||||
adapter.send.assert_not_called()
|
||||
assert not notify_path.exists()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_restart_notification_logs_info_on_sendresult_success(
|
||||
tmp_path, monkeypatch, caplog
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue