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)); }