From a7e9233917abc458fedf9057f76c98606bcbd140 Mon Sep 17 00:00:00 2001 From: Erick Date: Fri, 12 Jun 2026 12:39:13 -0700 Subject: [PATCH] fix(adapter): fire session.close in daemon thread to unblock event loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit on_session_end() called bridge.request("session.close") inline with a 30 s blocking urlopen(). gateway/run.py calls this synchronously from _handle_reset_command (an async fn), so the blocking I/O ran on the asyncio event loop thread, preventing Discord heartbeats from firing and causing forced reconnection after 10–30 s. The session.close response is unused and errors are already suppressed, so the call is semantically fire-and-forget. Moving it to a daemon thread is the correct fix: the event loop is never blocked, the request still goes out, and a 5 s timeout keeps it bounded if the bridge is dead. Reproducer: Violet 2026-06-12 09:54 (agent.log lines 3629–3791). Spec: ~/specs/memos-bridge-blocking-shutdown-spec.md --- .../adapters/hermes/memos_provider/__init__.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py b/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py index fd9d0d827..45c7c1f6c 100644 --- a/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py +++ b/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py @@ -1506,8 +1506,20 @@ def on_session_end(self, messages: list[dict[str, Any]]) -> None: # type: ignor # the core will pause or finalize the open episode according to # topic-boundary rules so interrupted Hermes sessions can resume # into the same task later. - with contextlib.suppress(Exception): - self._bridge.request("session.close", {"sessionId": self._session_id}) + # + # Fire session.close in a daemon thread — the response is unused, so + # this is semantically fire-and-forget. Calling urlopen() inline blocks + # the asyncio event loop (gateway/run.py calls us synchronously from + # _handle_reset_command) and causes Discord heartbeat timeouts when the + # bridge is unresponsive. 5 s timeout keeps it bounded. + _bridge = self._bridge + _sid = self._session_id + + def _close() -> None: + with contextlib.suppress(Exception): + _bridge.request("session.close", {"sessionId": _sid}, timeout=5.0) + + threading.Thread(target=_close, daemon=True).start() def shutdown(self) -> None: # type: ignore[override] self._bridge_keepalive_stop.set()