fix: Agent bricht bei Page-Refresh nicht mehr ab + SSE-Writes crash-sicher

Ursache: WriteSseEvent in ProcessStream warf OperationCanceledException
bei Client-Disconnect (Page-Refresh) → sseCt war gelinkt mit dem
ASP.NET cancellationToken → Exception wurde im catch-Block als
'stopped' behandelt → Agent brach ab obwohl nur der Browser refreshed wurde.

Fixes:
- ProcessStream: Alle WriteSseEvent durch TryWriteSse ersetzt
  SSE-Writes bei Client-Disconnect werden jetzt zu stillen No-Ops
  statt den gesamten Agent-Flow per Exception zu killen
- catch-Blöcke: OperationCanceledException nur bei workCt/hardCt als
  'stopped' behandeln, neuer catch für Client-Disconnect als
  'client_disconnected' (Fallback, sollte nicht mehr auftreten)
- done-Event nach Stop: sseCt → CancellationToken.None damit das
  done-Event trotz gecancelltem sseCt noch gesendet wird
- Frontend: done-Handler rendert Chain-Badge direkt im apiGetChain-
  Callback statt Race-Condition mit finally-Block
- Frontend: 'stopped' Badge wieder angezeigt (war nur fälschlich bei
  Page-Refresh, jetzt korrekt nur bei echtem Stop)
This commit is contained in:
Meleki committed 2026-03-10 11:21:28 +01:00
1 parent b1d449b997
commit 6499864f13
2 files changed
+55 -13

No files matched your search

+10 -4
View File
@@ -121,11 +121,13 @@ function handleStreamEvent(eventType, data) {
if (data.model) {
_lastDoneChainData = data;
} else if (data.chainId) {
// Fallback: Chain-Daten vom Server nachladen und Badge direkt rendern
// (async – _lastDoneChainData wäre im finally schon gelesen)
apiGetChain(data.chainId).then(chain => {
if (chain) _lastDoneChainData = chain;
if (chain) addChainMetaBubble(chain, false);
}).catch(() => {});
}
setMeta('Bereit');
setMeta(data.stopReason === 'stopped' ? 'Gestoppt' : 'Bereit');
break;
}
}
@@ -238,8 +240,7 @@ async function _runStream(fetchPromise) {
if (_chatAbandoned) return;
endStreamBubble();
if (e.name === 'AbortError') {
setMeta('Abgebrochen');
await new Promise(r => setTimeout(r, 500));
setMeta('Gestoppt');
} else {
try {
const status = await apiIsStreaming(sid);
@@ -261,6 +262,11 @@ async function _runStream(fetchPromise) {
stopBtn.title = 'Antwort abbrechen';
isStreaming = false;
_abortController = null;
// Meta zurücksetzen falls kein done-Event kam (z.B. nach Stop)
var metaEl = $('#meta');
if (metaEl && metaEl.textContent.includes('Streaming')) {
setMeta(_stopClickCount > 0 ? 'Gestoppt' : 'Bereit');
}
}
endStreamBubble();
removeStreamedElements();