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:
1 parent
b40a872c29
commit
3e590a6d03
5 files changed
+19
-21
No files matched your search
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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));
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user