diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index 7229601f53..62873d7a04 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -1112,10 +1112,17 @@ async def get_event_emitter(request_info, update_db=True): # will no longer replay against. outer_type = event_data.get('type') if isinstance(event_data, dict) else None inner = event_data.get('data') if isinstance(event_data, dict) else None + # Only the specific chat:completion-with-error shape is terminal; + # a generic `error` field on some other event type could be a + # transient warning and shouldn't age the keys out early. is_terminal = ( (isinstance(inner, dict) and inner.get('done') is True) or outer_type == 'chat:tasks:cancel' - or (isinstance(inner, dict) and inner.get('error')) + or ( + outer_type == 'chat:completion' + and isinstance(inner, dict) + and inner.get('error') + ) ) if is_terminal and REDIS is not None and user_id and message_id: try: diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index e2ef682f50..b8daeb288f 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -724,7 +724,11 @@ if (expected && payload?.request_id !== expected) { return; } - resumeActiveRequestIdByMessageId.delete(messageId); + // Don't delete the active request_id here: a NEWER request could + // start during replay and set its own id. The finally below only + // clears the fence if we're still the active request; if a newer + // request has taken over, let its own lifecycle handle cleanup. + const ownRequestId = payload?.request_id; const envelopes = Array.isArray(payload?.envelopes) ? payload.envelopes : []; try { for (const envelope of envelopes) { @@ -733,8 +737,10 @@ await chatEventHandler(envelope); } } finally { - // Finally-clear so a mid-replay throw can't freeze live updates. - await clearResumeFence(messageId); + const current = resumeActiveRequestIdByMessageId.get(messageId); + if (current === ownRequestId || current == null) { + await clearResumeFence(messageId); + } } }; @@ -745,10 +751,12 @@ }; // Iterate all in-flight assistants so arena siblings aren't missed. + // Strict `done === false` so legacy messages with a missing `done` + // field don't get fanned out as unnecessary resume requests. const requestResumeForAllInProgress = () => { if (!history?.messages) return; for (const message of Object.values(history.messages)) { - if (message && message.role === 'assistant' && !message.done) { + if (message && message.role === 'assistant' && message.done === false) { requestResumeForMessage(message); } }