From 3e590a6d03ee94c43ebf4d68adc9e92a3e69a4ec Mon Sep 17 00:00:00 2001 From: Meleki Date: Mon, 9 Mar 2026 17:01:18 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20Windows-Kompatibilit=C3=A4t=20-=20Stream?= =?UTF-8?q?ing,=20Execute-Deadlock,=20Plan-Modus=20bei=20Choice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HttpResponseMessage mit using disposen (3 Stellen in AnthropicService) verhindert Socket-Exhaustion unter Windows (2 Connections/Host Limit) - SSE DisableBuffering() in ChatEndpoint für sofortigen Flush unter Windows - Execute: stdout/stderr vor WaitForExitAsync lesen statt danach verhindert Pipe-Deadlock bei vollem Buffer (Windows: 4KB) - Plan-Modus: Frontend sendet planMode bei Choice-Response mit Backend-Fallback auf vorherigen Chain entfernt - User-Entscheidung gilt --- Endpoints/ChatEndpoint.cs | 18 +++++------------- Services/AnthropicService.cs | 6 +++--- Services/ToolService.cs | 8 ++++++-- wwwroot/js/api.js | 6 ++++-- wwwroot/js/chat.js | 2 +- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index e888435..4c9df0a 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -52,19 +52,7 @@ public static class ChatEndpoint var userId = req.HttpContext.GetUserId(); var maxIterations = config.GetValue("MaxIterations", 100); - // Bei Choice-Response: planMode vom vorherigen Chain übernehmen - var effectivePlanMode = body.PlanMode; - if (isChoiceResponse && !effectivePlanMode) - { - var prevChainId = await chatService.FindChainWithToolCall(sessionId, body.ChoiceToolCallId); - if (prevChainId != null) - { - var prevChain = await db.GetChain(prevChainId); - if (prevChain != null) effectivePlanMode = prevChain.PlanMode; - } - } - - var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations, planMode: effectivePlanMode); + var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations, planMode: body.PlanMode); if (ctx == null) { res.StatusCode = 400; @@ -132,6 +120,10 @@ public static class ChatEndpoint res.Headers["Cache-Control"] = "no-cache"; res.Headers["Connection"] = "keep-alive"; + // Response-Buffering deaktivieren – wichtig für SSE unter Windows + var bufferingFeature = res.HttpContext.Features.Get(); + bufferingFeature?.DisableBuffering(); + // ── Preparer-Agent Phase (vor dem Worker) ── if (ctx.Preparer != null && !isChoiceResponse) { diff --git a/Services/AnthropicService.cs b/Services/AnthropicService.cs index da7e3a6..06794d7 100644 --- a/Services/AnthropicService.cs +++ b/Services/AnthropicService.cs @@ -92,7 +92,7 @@ public class AnthropicService LogPayload(json, sessionId, chainId, iterations); var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); - var httpResp = await client.SendAsync( + using var httpResp = await client.SendAsync( new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent }, HttpCompletionOption.ResponseHeadersRead, cancellationToken @@ -665,7 +665,7 @@ public class AnthropicService LogPayload(json, sessionId, chainId, baseIterations + iterations); var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); - var httpResp = await client.SendAsync( + using var httpResp = await client.SendAsync( new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent }, cancellationToken: cancellationToken); @@ -873,7 +873,7 @@ public class AnthropicService LogPayload(json, ctx.SessionId, ctx.ChainId, iterations); var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); - var httpResp = await client.SendAsync( + using var httpResp = await client.SendAsync( new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent }); var respBody = await httpResp.Content.ReadAsStringAsync(); diff --git a/Services/ToolService.cs b/Services/ToolService.cs index 325d053..5e2d02e 100644 --- a/Services/ToolService.cs +++ b/Services/ToolService.cs @@ -388,6 +388,10 @@ public class ToolService // Timeout (180s) UND externen CancellationToken verknüpfen using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(TimeSpan.FromSeconds(180)); + // stdout/stderr VOR WaitForExit lesen – verhindert Deadlock + // wenn Pipe-Buffer volllaufen (Windows: nur 4KB!) + var stdoutTask = proc.StandardOutput.ReadToEndAsync(); + var stderrTask = proc.StandardError.ReadToEndAsync(); try { await proc.WaitForExitAsync(cts.Token); @@ -399,8 +403,8 @@ public class ToolService cancellationToken.ThrowIfCancellationRequested(); return "Fehler: Timeout nach 180 Sekunden - Prozess abgebrochen"; } - var stdout = await proc.StandardOutput.ReadToEndAsync(); - var stderr = await proc.StandardError.ReadToEndAsync(); + var stdout = await stdoutTask; + var stderr = await stderrTask; var result = new StringBuilder(); if (!string.IsNullOrEmpty(stdout)) result.Append(stdout); if (!string.IsNullOrEmpty(stderr)) diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js index eafccb0..c9a9dde 100644 --- a/wwwroot/js/api.js +++ b/wwwroot/js/api.js @@ -52,11 +52,13 @@ async function apiChat(content, sessionId, signal, planMode) { }); } -async function apiChatChoice(sessionId, choiceToolCallId, choiceResult, signal) { +async function apiChatChoice(sessionId, choiceToolCallId, choiceResult, signal, planMode) { + var payload = { sessionId, choiceToolCallId, choiceResult }; + if (planMode) payload.planMode = true; return fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ sessionId, choiceToolCallId, choiceResult }), + body: JSON.stringify(payload), signal }); } diff --git a/wwwroot/js/chat.js b/wwwroot/js/chat.js index e3384f9..5d2f132 100644 --- a/wwwroot/js/chat.js +++ b/wwwroot/js/chat.js @@ -343,5 +343,5 @@ async function handleChoiceClick(toolCallId, choice, bubbleDiv) { }); bubbleDiv.classList.add('choice-answered'); - await _runStream((signal) => apiChatChoice(getCurrentSessionId(), toolCallId, choice, signal)); + await _runStream((signal) => apiChatChoice(getCurrentSessionId(), toolCallId, choice, signal, _planModeActive)); }