From cf4238859ed290be8f2a9288ff179c1c01dcfccd Mon Sep 17 00:00:00 2001 From: Meleki Date: Tue, 2 Jun 2026 08:32:51 +0200 Subject: [PATCH] Fix duplicate messages after entering a running chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the chat is opened while a chain is already streaming, the recovery poll (_streamingPollInterval) is started. Its forced pollHistory(true) renders the DB messages while the live SSE bubbles render them too — the live bubbles never advance lastKnownMessageId, so the poll re-fetches the same assistant message and renders a second bubble. Make live SSE and the recovery poll mutually exclusive: - Stop a lingering recovery poll at the start of a real live stream so the SSE renders alone. - Re-ensure the regular (isStreaming-guarded) history polling idempotently in the finally block, so it is not left off after entering a running chain. Co-Authored-By: Claude Opus 4.8 --- wwwroot/js/chat.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/wwwroot/js/chat.js b/wwwroot/js/chat.js index a5f4d69..e8c5289 100644 --- a/wwwroot/js/chat.js +++ b/wwwroot/js/chat.js @@ -279,6 +279,15 @@ async function _runStream(fetchPromise) { stopBtn.hidden = false; isStreaming = true; _abortController = new AbortController(); + // Eine Live-SSE-Antwort rendert ALLEIN. Einen evtl. noch laufenden Recovery-Poll + // (_streamingPollInterval – gestartet beim Betreten des Chats WAEHREND eine Chain + // lief) hier stoppen. Sonst rendert dessen pollHistory(true) dieselben DB-Nachrichten + // ein zweites Mal NEBEN den Live-Bubbles (die lastKnownMessageId nicht vorruecken) + // => Doppel-Nachrichten nach Eintritt in eine laufende Chain. + if (_streamingPollInterval) { + clearInterval(_streamingPollInterval); + _streamingPollInterval = null; + } _currentToolBox = null; _chatAbandoned = false; _stopClickCount = 0; @@ -345,6 +354,11 @@ async function _runStream(fetchPromise) { stopBtn.title = 'Antwort abbrechen'; isStreaming = false; _abortController = null; + // Regulaeres History-Polling idempotent wieder sicherstellen: Falls beim Eintritt + // in eine laufende Chain auf den Recovery-Poll umgeschaltet (und History-Polling + // gestoppt) wurde, ist es nach dem Live-Stream sonst aus. startHistoryPolling() + // hat selbst einen Guard (if (historyPollInterval) return) => kein Doppel-Intervall. + startHistoryPolling(); // Steering-Hinweis zurücksetzen (Eingabe ist wieder normales Senden) if (_steerInputEl) { _steerInputEl.placeholder = _prevPlaceholder;