From 2ffda752ad662876d2c7f42041b87e9e3cdfbc75 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:52:10 +0800 Subject: [PATCH] fix: send SSE heartbeat to prevent WebChat disconnect during compression (#7003) * fix: send SSE heartbeat to prevent WebChat disconnect during compression When context compression triggers with slow reasoning models (e.g. deepseek-reasoner), the backend can go 30+ seconds without pushing any SSE data. The client-side EventSource / browser then assumes the connection is dead and disconnects, causing the WebUI to hang indefinitely since it never receives the eventual response. Fix: yield an SSE comment (`: heartbeat`) on every empty poll cycle. Comment lines are defined in the SSE spec as keep-alive signals -- the EventSource API ignores them but the HTTP connection stays open. Fixes #6938 * chore(dashboard): extract SSE heartbeat to constant --------- Co-authored-by: Yufeng He <40085740+universeplayer@users.noreply.github.com> Co-authored-by: RC-CHN <1051989940@qq.com> --- astrbot/dashboard/routes/chat.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index c79ad1e35..a4173ed84 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -26,6 +26,9 @@ from astrbot.core.utils.datetime_utils import to_utc_isoformat from .route import Response, Route, RouteContext +# SSE heartbeat message to keep the connection alive during long-running operations +SSE_HEARTBEAT = ": heartbeat\n\n" + @asynccontextmanager async def track_conversation(convs: dict, conv_id: str): @@ -40,6 +43,9 @@ async def _poll_webchat_stream_result(back_queue, username: str): try: result = await asyncio.wait_for(back_queue.get(), timeout=1) except asyncio.TimeoutError: + # Return a sentinel so the caller can send an SSE heartbeat to + # keep the connection alive during long-running operations (e.g. + # context compression with reasoning models). See #6938. return None, False except asyncio.CancelledError: logger.debug(f"[WebChat] 用户 {username} 断开聊天长连接。") @@ -364,6 +370,11 @@ class ChatRoute(Route): client_disconnected = True break if not result: + # Send an SSE comment as keep-alive so the client + # doesn't time out during slow backend ops like + # context compression with reasoning models (#6938). + if not client_disconnected: + yield SSE_HEARTBEAT continue if (