diff --git a/RETRIEX_SSE_RUNNING_RECONNECT_FINALIZE_FIX_README.md b/RETRIEX_SSE_RUNNING_RECONNECT_FINALIZE_FIX_README.md
new file mode 100644
index 0000000..383ee06
--- /dev/null
+++ b/RETRIEX_SSE_RUNNING_RECONNECT_FINALIZE_FIX_README.md
@@ -0,0 +1,42 @@
+# RetrieX SSE running reconnect / final cleanup fix
+
+Patch-only fix for the streamed answer UI after the job-based SSE flow.
+
+## Problem
+
+After introducing stream jobs, an EventSource reconnect to an already running job could receive:
+
+```text
+retry: 15000
+
+: duplicate-or-finished-stream
+
+event: done
+data: [DONE]
+```
+
+Because `running` jobs were treated like harmless duplicate/finished streams, the browser could finalize an incomplete answer as if it had completed successfully. In addition, the final `done` cleanup still used the streaming cleanup path, which can keep the last `.think` block visible.
+
+## Changes
+
+- `running` duplicate/reconnect streams are no longer silently closed as successful `[DONE]` completions.
+- Only already `completed` duplicate streams are silently closed with `done`.
+- A reconnect to a still-running job now follows the existing explicit error path, so the UI can show a clear interruption message instead of silently accepting a partial answer.
+- Final stream completion now removes all `.think` blocks and the loader class.
+
+## Changed files
+
+- `src/Controller/AskSseController.php`
+- `public/assets/js/base.js`
+
+## After installing
+
+Run:
+
+```bash
+php bin/console cache:clear
+php bin/console mto:agent:config:validate
+php bin/console mto:agent:regression:test
+```
+
+Also hard-refresh the browser or clear browser cache because `public/assets/js/base.js` is client-side JavaScript.
diff --git a/public/assets/js/base.js b/public/assets/js/base.js
index 6ce80e6..17d267e 100644
--- a/public/assets/js/base.js
+++ b/public/assets/js/base.js
@@ -232,16 +232,11 @@ document.addEventListener('DOMContentLoaded', () => {
return false;
}
- function cleanupThinkSpans(container, final = false) {
+ function cleanupThinkSpans(container) {
if (!container) {
return;
}
- if (final) {
- removeThinkSpansOnly(container);
- return;
- }
-
const thinkSpans = Array.from(container.querySelectorAll('.think'));
if (thinkSpans.length === 0) {
@@ -259,9 +254,9 @@ document.addEventListener('DOMContentLoaded', () => {
removeThinkSpansOnly(container);
}
- function renderBubbleContent(bubble, raw, final = false) {
+ function renderBubbleContent(bubble, raw) {
bubble.innerHTML = renderMarkdown(raw);
- cleanupThinkSpans(bubble, final);
+ cleanupThinkSpans(bubble);
enhanceChatLinks(bubble);
scrollChatToBottom();
}
@@ -338,8 +333,11 @@ document.addEventListener('DOMContentLoaded', () => {
function finalizeStream(bubble, raw) {
clearScheduledRender();
+ bubble.innerHTML = renderMarkdown(raw);
+ removeThinkSpansOnly(bubble);
bubble.classList.remove('loader');
- renderBubbleContent(bubble, raw, true);
+ enhanceChatLinks(bubble);
+ scrollChatToBottom();
}
async function releaseStreamResources() {
@@ -573,7 +571,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (raw.trim() !== '') {
const formattedMessage = `${userMessage}`;
raw += raw.trim() === '' ? formattedMessage : `\n\n${formattedMessage}`;
- renderBubbleContent(bubble, raw, true);
+ renderBubbleContent(bubble, raw);
} else {
bubble.innerHTML = `${userMessage}`;
enhanceChatLinks(bubble);
diff --git a/src/Controller/AskSseController.php b/src/Controller/AskSseController.php
index 924b002..6855877 100644
--- a/src/Controller/AskSseController.php
+++ b/src/Controller/AskSseController.php
@@ -526,8 +526,7 @@ final readonly class AskSseController
$status = (string) ($claim['status'] ?? '');
- return $status === self::JOB_STATUS_RUNNING
- || $status === self::JOB_STATUS_COMPLETED;
+ return $status === self::JOB_STATUS_COMPLETED;
}
/**