Doppel-Klick Hard-Stop + ReadLineAsync Cancellation-Fix
Stop-Button: Zwei Stufen statt einer:
- 1. Klick: Graceful Stop (workCt) - laufende API-Calls/Tools abbrechen
- 2. Klick: Hard Stop (hardCt) - sofortiger Abbruch aus while-Loop
StreamingTracker: Zwei separate CancellationTokenSources (StopCts + HardCts),
Cancel() gibt 'graceful'/'hard'/null zurück je nach Stufe.
ReadLineAsync Cancellation-Fix: StreamReader.ReadLineAsync propagiert den
CancellationToken nicht zuverlässig an den underlying NetworkStream.
Bei extended thinking blockiert ReadLineAsync minutenlang ohne Reaktion
auf Cancellation. Fix: workCt.Register() disposed den HTTP-Stream,
damit ReadLineAsync sofort mit Exception abbricht.
Bug-Fix: OperationCanceledException in RunAgentChat/ChatNonStreaming wurde
vom generischen catch(Exception) geschluckt - Agent lief trotz Stop weiter.
Jetzt: catch(OperationCanceledException) { throw; } vor catch(Exception).
Frontend: Stop-Button zeigt Hard-Stop visuell (rot + Pulse-Animation),
SSE-Stream wird erst beim 2. Klick clientseitig abgebrochen.
This commit is contained in:
1 parent
eb32a2526a
commit
b1d449b997
5 files changed
+92
-23
No files matched your search
@@ -175,14 +175,16 @@ public static class ChatEndpoint
|
||||
// Bei Choice-Response: toolChoice auf "auto" setzen, damit Claude frei antworten kann
|
||||
var effectiveToolChoice = isChoiceResponse ? "auto" : ctx.ToolChoice;
|
||||
|
||||
// Zwei separate CancellationTokens:
|
||||
// - workCt: Nur Stop-Button → für Anthropic API-Calls, Tool-Ausführungen, DB-Writes
|
||||
// Drei separate CancellationTokens:
|
||||
// - workCt: Nur Stop-Button (graceful) → für Anthropic API-Calls, Tool-Ausführungen, DB-Writes
|
||||
// Agent-Arbeit läuft bei Client-Disconnect weiter (Client pollt ja)
|
||||
// - hardCt: Hard-Stop (Doppel-Klick) → sofortiger Abbruch aus while-Loop
|
||||
// - sseCt: Stop-Button + Client-Disconnect → für SSE-Writes an den Browser
|
||||
// Bei Client-Disconnect werden SSE-Writes zu stillen No-Ops
|
||||
var stopCts = streamingTracker.Start(sessionId, ctx.ChainId);
|
||||
var workCt = stopCts.Token;
|
||||
using var sseLinkCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, stopCts.Token);
|
||||
var tokens = streamingTracker.Start(sessionId, ctx.ChainId);
|
||||
var workCt = tokens.StopToken;
|
||||
var hardCt = tokens.HardToken;
|
||||
using var sseLinkCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, tokens.StopToken);
|
||||
var sseCt = sseLinkCts.Token;
|
||||
|
||||
// SSE-Heartbeat: Alle 15s ein Kommentar senden, damit die Verbindung
|
||||
@@ -204,7 +206,7 @@ public static class ChatEndpoint
|
||||
|
||||
try
|
||||
{
|
||||
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, workCt: workCt, sseCt: sseCt);
|
||||
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, workCt: workCt, hardCt: hardCt, sseCt: sseCt);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -225,7 +227,8 @@ public static class ChatEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
var cancelled = streamingTracker.Cancel(sessionId);
|
||||
await res.WriteAsJsonAsync(new { stopped = cancelled });
|
||||
// Erster Klick = "graceful" (laufende Iteration fertig), zweiter = "hard" (sofort)
|
||||
var stopLevel = streamingTracker.Cancel(sessionId);
|
||||
await res.WriteAsJsonAsync(new { stopped = stopLevel != null, level = stopLevel ?? "none" });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user