diff --git a/gateway/status.py b/gateway/status.py index 5a410f696f..0e131a3e87 100644 --- a/gateway/status.py +++ b/gateway/status.py @@ -842,6 +842,13 @@ def write_runtime_status( )) if platform is not _UNSET: platform_payload = payload["platforms"].get(platform, {}) + if platform_state == "connected": + # Every writer that publishes ``connected`` (startup stamp, adapter ``_mark_connected``, + # Telegram's in-place polling recovery) ends the retry episode; only the watcher's + # reconnect path used to say so, and a restart after a NEEDS_ATTENTION escalation + # carried the flag into a healthy record for weeks. + needs_attention = False if needs_attention is _UNSET else needs_attention + retrying_since = None if retrying_since is _UNSET else retrying_since _apply_set_fields(platform_payload, ( ("state", platform_state, None), ("error_code", error_code, None), ("error_message", error_message, None), diff --git a/tests/gateway/test_status.py b/tests/gateway/test_status.py index 50dab7ab56..ffbcd71ef1 100644 --- a/tests/gateway/test_status.py +++ b/tests/gateway/test_status.py @@ -412,6 +412,33 @@ class TestGatewayRuntimeStatus: assert payload["platforms"]["discord"]["error_code"] is None assert payload["platforms"]["discord"]["error_message"] is None + @pytest.mark.parametrize("via", ["startup_stamp", "adapter_mark_connected"]) + def test_connected_clears_needs_attention_from_any_writer(self, tmp_path, monkeypatch, via): + """The reconnect-loop escalation (needs_attention + retrying_since) must end on EVERY + ``connected`` write, not only the watcher's. A gateway restart after an escalation stamps + ``connected`` from the startup path / adapter, which left the flag sticky for weeks on a + healthy Telegram record.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + status.write_runtime_status( + platform="telegram", platform_state="retrying", needs_attention=True, + retrying_since="2026-08-30T07:53:47+00:00", + ) + if via == "startup_stamp": + status.write_runtime_status( + platform="telegram", platform_state="connected", error_code=None, error_message=None) + else: + from gateway.platforms.base import BasePlatformAdapter + adapter = object.__new__(type("_Adapter", (BasePlatformAdapter,), { + m: (lambda *a, **k: None) for m in ("connect", "disconnect", "get_chat_info", "send")})) + adapter._runtime_status_platform_key = "telegram" + adapter._fatal_error_code = adapter._fatal_error_message = None + adapter._fatal_error_retryable = True + adapter._mark_connected() + entry = status.read_runtime_status()["platforms"]["telegram"] + assert entry["state"] == "connected" + assert entry["needs_attention"] is False + assert entry["retrying_since"] is None + class TestGetProcessStartTime: """Start-time fingerprint backing the PID-reuse guard (#43846 / #50468).