fix: Windows-Kompatibilität - Streaming, Execute-Deadlock, Plan-Modus bei Choice

- 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
This commit is contained in:
Meleki committed 2026-03-09 17:01:18 +01:00
1 parent b40a872c29
commit 3e590a6d03
5 files changed
+19 -21

No files matched your search

+5 -13
View File
@@ -52,19 +52,7 @@ public static class ChatEndpoint
var userId = req.HttpContext.GetUserId(); var userId = req.HttpContext.GetUserId();
var maxIterations = config.GetValue<int>("MaxIterations", 100); var maxIterations = config.GetValue<int>("MaxIterations", 100);
// Bei Choice-Response: planMode vom vorherigen Chain übernehmen var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations, planMode: body.PlanMode);
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);
if (ctx == null) if (ctx == null)
{ {
res.StatusCode = 400; res.StatusCode = 400;
@@ -132,6 +120,10 @@ public static class ChatEndpoint
res.Headers["Cache-Control"] = "no-cache"; res.Headers["Cache-Control"] = "no-cache";
res.Headers["Connection"] = "keep-alive"; res.Headers["Connection"] = "keep-alive";
// Response-Buffering deaktivieren – wichtig für SSE unter Windows
var bufferingFeature = res.HttpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature>();
bufferingFeature?.DisableBuffering();
// ── Preparer-Agent Phase (vor dem Worker) ── // ── Preparer-Agent Phase (vor dem Worker) ──
if (ctx.Preparer != null && !isChoiceResponse) if (ctx.Preparer != null && !isChoiceResponse)
{ {
+3 -3
View File
@@ -92,7 +92,7 @@ public class AnthropicService
LogPayload(json, sessionId, chainId, iterations); LogPayload(json, sessionId, chainId, iterations);
var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); 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 }, new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent },
HttpCompletionOption.ResponseHeadersRead, HttpCompletionOption.ResponseHeadersRead,
cancellationToken cancellationToken
@@ -665,7 +665,7 @@ public class AnthropicService
LogPayload(json, sessionId, chainId, baseIterations + iterations); LogPayload(json, sessionId, chainId, baseIterations + iterations);
var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); 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 }, new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent },
cancellationToken: cancellationToken); cancellationToken: cancellationToken);
@@ -873,7 +873,7 @@ public class AnthropicService
LogPayload(json, ctx.SessionId, ctx.ChainId, iterations); LogPayload(json, ctx.SessionId, ctx.ChainId, iterations);
var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); 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 }); new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent });
var respBody = await httpResp.Content.ReadAsStringAsync(); var respBody = await httpResp.Content.ReadAsStringAsync();
+6 -2
View File
@@ -388,6 +388,10 @@ public class ToolService
// Timeout (180s) UND externen CancellationToken verknüpfen // Timeout (180s) UND externen CancellationToken verknüpfen
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(180)); 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 try
{ {
await proc.WaitForExitAsync(cts.Token); await proc.WaitForExitAsync(cts.Token);
@@ -399,8 +403,8 @@ public class ToolService
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
return "Fehler: Timeout nach 180 Sekunden - Prozess abgebrochen"; return "Fehler: Timeout nach 180 Sekunden - Prozess abgebrochen";
} }
var stdout = await proc.StandardOutput.ReadToEndAsync(); var stdout = await stdoutTask;
var stderr = await proc.StandardError.ReadToEndAsync(); var stderr = await stderrTask;
var result = new StringBuilder(); var result = new StringBuilder();
if (!string.IsNullOrEmpty(stdout)) result.Append(stdout); if (!string.IsNullOrEmpty(stdout)) result.Append(stdout);
if (!string.IsNullOrEmpty(stderr)) if (!string.IsNullOrEmpty(stderr))
+4 -2
View File
@@ -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', { return fetch('/api/chat', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId, choiceToolCallId, choiceResult }), body: JSON.stringify(payload),
signal signal
}); });
} }
+1 -1
View File
@@ -343,5 +343,5 @@ async function handleChoiceClick(toolCallId, choice, bubbleDiv) {
}); });
bubbleDiv.classList.add('choice-answered'); bubbleDiv.classList.add('choice-answered');
await _runStream((signal) => apiChatChoice(getCurrentSessionId(), toolCallId, choice, signal)); await _runStream((signal) => apiChatChoice(getCurrentSessionId(), toolCallId, choice, signal, _planModeActive));
} }