fix sse HANGING tasks

This commit is contained in:
team 1
2026-04-27 08:44:00 +02:00
parent edf6720940
commit ed139d577b
3 changed files with 51 additions and 12 deletions

View File

@@ -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.

View File

@@ -232,16 +232,11 @@ document.addEventListener('DOMContentLoaded', () => {
return false; return false;
} }
function cleanupThinkSpans(container, final = false) { function cleanupThinkSpans(container) {
if (!container) { if (!container) {
return; return;
} }
if (final) {
removeThinkSpansOnly(container);
return;
}
const thinkSpans = Array.from(container.querySelectorAll('.think')); const thinkSpans = Array.from(container.querySelectorAll('.think'));
if (thinkSpans.length === 0) { if (thinkSpans.length === 0) {
@@ -259,9 +254,9 @@ document.addEventListener('DOMContentLoaded', () => {
removeThinkSpansOnly(container); removeThinkSpansOnly(container);
} }
function renderBubbleContent(bubble, raw, final = false) { function renderBubbleContent(bubble, raw) {
bubble.innerHTML = renderMarkdown(raw); bubble.innerHTML = renderMarkdown(raw);
cleanupThinkSpans(bubble, final); cleanupThinkSpans(bubble);
enhanceChatLinks(bubble); enhanceChatLinks(bubble);
scrollChatToBottom(); scrollChatToBottom();
} }
@@ -338,8 +333,11 @@ document.addEventListener('DOMContentLoaded', () => {
function finalizeStream(bubble, raw) { function finalizeStream(bubble, raw) {
clearScheduledRender(); clearScheduledRender();
bubble.innerHTML = renderMarkdown(raw);
removeThinkSpansOnly(bubble);
bubble.classList.remove('loader'); bubble.classList.remove('loader');
renderBubbleContent(bubble, raw, true); enhanceChatLinks(bubble);
scrollChatToBottom();
} }
async function releaseStreamResources() { async function releaseStreamResources() {
@@ -573,7 +571,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (raw.trim() !== '') { if (raw.trim() !== '') {
const formattedMessage = `<em>${userMessage}</em>`; const formattedMessage = `<em>${userMessage}</em>`;
raw += raw.trim() === '' ? formattedMessage : `\n\n${formattedMessage}`; raw += raw.trim() === '' ? formattedMessage : `\n\n${formattedMessage}`;
renderBubbleContent(bubble, raw, true); renderBubbleContent(bubble, raw);
} else { } else {
bubble.innerHTML = `<em>${userMessage}</em>`; bubble.innerHTML = `<em>${userMessage}</em>`;
enhanceChatLinks(bubble); enhanceChatLinks(bubble);

View File

@@ -526,8 +526,7 @@ final readonly class AskSseController
$status = (string) ($claim['status'] ?? ''); $status = (string) ($claim['status'] ?? '');
return $status === self::JOB_STATUS_RUNNING return $status === self::JOB_STATUS_COMPLETED;
|| $status === self::JOB_STATUS_COMPLETED;
} }
/** /**