fix: SSE-Stream-Stabilität und History-Trimming tool_use/tool_result-Schutz

- Kestrel MinResponseDataRate per-Request deaktivieren für SSE-Streams
  (Default 240 bytes/s + 5s Grace → client_disconnected bei langen Tools)
- SSE-Heartbeat alle 15s während Stream (schützt gegen Browser/Netzwerk-Idle)
- History-Trimming: RemoveRange schneidet nicht mehr in tool_use/tool_result-Paare
- Reminder-Insertion: kein Einfügen zwischen tool_use und tool_result
This commit is contained in:
Meleki committed 2026-03-09 20:13:22 +01:00
1 parent e81983cab6
commit dd99216913
3 files changed
+101 -2

No files matched your search

+74 -2
View File
@@ -445,8 +445,67 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
}
// History auf maximal N Messages begrenzen (älteste entfernen)
// WICHTIG: Nicht mitten in einem tool_use/tool_result-Paar abschneiden!
if (history.Count > _maxHistory)
history.RemoveRange(0, history.Count - _maxHistory);
{
int removeCount = history.Count - _maxHistory;
// Nach vorne korrigieren bis wir an einer sicheren Stelle sind:
// Nicht abschneiden wenn die erste verbleibende Message ein tool_result ist
// oder die letzte entfernte Message ein assistant mit tool_use ist
while (removeCount < history.Count - 1)
{
var firstRemaining = history[removeCount];
bool isToolResult = false;
if (firstRemaining is Dictionary<string, object> frDict &&
frDict.TryGetValue("role", out var frRole) && frRole.ToString() == "user" &&
frDict.TryGetValue("content", out var frContent) && frContent is List<object> frList)
{
isToolResult = frList.Any(item =>
{
var json = JsonSerializer.Serialize(item);
return json.Contains("\"tool_result\"");
});
}
if (isToolResult)
{
// Die erste verbleibende Message ist ein tool_result → muss den vorherigen
// assistant[tool_use] auch behalten → ein Element weniger entfernen
removeCount--;
// Jetzt prüfen ob der neue Start auch sicher ist (könnte auch tool_result sein)
continue;
}
// Prüfen ob die letzte entfernte Message ein assistant mit tool_use war
bool lastRemovedHasToolUse = false;
if (removeCount > 0)
{
var lastRemoved = history[removeCount - 1];
if (lastRemoved is Dictionary<string, object> lrDict &&
lrDict.TryGetValue("role", out var lrRole) && lrRole.ToString() == "assistant" &&
lrDict.TryGetValue("content", out var lrContent) && lrContent is List<object> lrList)
{
lastRemovedHasToolUse = lrList.Any(item =>
{
var json = JsonSerializer.Serialize(item);
return json.Contains("\"tool_use\"");
});
}
}
if (lastRemovedHasToolUse)
{
// Letztes entferntes Element enthält tool_use → auch das tool_result entfernen
removeCount++;
continue;
}
break;
}
if (removeCount > 0 && removeCount < history.Count)
history.RemoveRange(0, removeCount);
}
// Reminder einfügen: alle ~30 user/assistant-Paare ein Reminder-Paar einfügen
var reminder = SystemPromptBuilder.BuildReminder(promptBlocks);
@@ -473,7 +532,20 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
{
// Position nach dem aktuellen Element – aber nur einfügen wenn es eine
// assistant-Message ist (damit user/assistant-Alternierung nicht bricht)
if (history[i] is Dictionary<string, object> d &&
// UND die nächste Message kein tool_result ist (sonst bricht tool_use/tool_result-Paar)
bool nextIsToolResult = false;
if (i + 1 < history.Count && history[i + 1] is Dictionary<string, object> nextDict &&
nextDict.TryGetValue("role", out var nextRole) && nextRole.ToString() == "user" &&
nextDict.TryGetValue("content", out var nextContent) && nextContent is List<object> nextList)
{
nextIsToolResult = nextList.Any(item =>
{
var json = JsonSerializer.Serialize(item);
return json.Contains("\"tool_result\"");
});
}
if (!nextIsToolResult && history[i] is Dictionary<string, object> d &&
d.TryGetValue("role", out var r) && r.ToString() == "assistant")
{
insertPositions.Add(i + 1);