Bugtracker-Durchgang: 13 Bugs behoben, Build ohne Warnungen
Alle Behebungen auf der Linux-Test-VM gegengeprueft (Ubuntu, .NET 10.0.111): Build 0 Warnungen / 0 Fehler, 637 Tests gruen, 1 uebersprungen (WindowsFact). Sicherheit und Zielplattform #373 PathBoundary.Canonicalize fiel offen aus. File.ResolveLinkTarget wirft fuer einen fehlenden Abschnitt FileNotFoundException; der catch(IOException) verliess daraufhin die GANZE Aufloesung mit dem unaufgeloesten Pfad - und der besteht jede Praefixpruefung. Der Symlink-Schutz war damit bei jedem Schreibvorgang wirkungslos. Jetzt ueber FileSystemInfo.LinkTarget, und TryCanonicalize meldet "nicht bestimmbar", statt fail-open durchzureichen. #374 WorkspacePath weist den Backslash rundheraus ab, wie den Doppelpunkt. Eine Schreibweise statt einer Sonderregel je Windows-Form. #375 Log-Verzeichnis: leer heisst "Anwendungseinstellung"; ein relativer Wert zaehlt gegen das Instanzverzeichnis, nicht gegen das Arbeitsverzeichnis des Prozesses. Ein systemd-Dienst landet nicht mehr auf /Logs. Agentenkommunikation #206 Delegationskette gegen A->B->A (Test A13). Zyklusfrei per Bauart, dazu MaxDelegationDepth gegen die Staffel, die niemand beauftragt hat. #461 Zwei unabhaengige Ketten ueber Kreuz - beim Beheben von #206 gefunden und reproduziert. Ein delegierter Lauf wartet nur noch begrenzt auf ein fremdes Gate; ein Chat vom Menschen reiht sich weiter unbegrenzt ein. #234 Instanz-Id wird durchgereicht statt aus einem nur beilaeufig gesetzten Feld gelesen. Betrieb und Oberflaeche #376 Log-Ansicht chronologisch statt gruppiert je Moduldatei, mit Stacktraces an ihrer Meldung - auch ueber Taktgrenzen hinweg. #377 Info-Tabelle: feste Spaltenbreiten, damit ein langer Aenderungstext den anderen Spalten nicht den Platz nimmt. #378 Sicherung: Mindestabstand zwischen Ueberschrift und Schaltflaechenleiste. #379 Hellmodus: drei von vier gedaempften Textpinseln lagen unter 4,5:1. Die helle Palette ist als Ganzes verschoben, damit die Staffelung sichtbar bleibt statt auf einen Wert zusammenzufallen. Hygiene #233 stock_add: der Indexstand wird innerhalb der Sperre festgehalten. #235 Zeitumstellung: eine nicht existierende Ortszeit wird auf den ersten Zeitpunkt nach der Luecke verschoben statt uebersprungen (R4, R5). #236 45 Nullable-Warnungen und eine CA1416 - nicht unterdrueckt, sondern die Stellen so umgebaut, dass die Zusage nachvollziehbar wird. Mail meldet jetzt, WELCHER Zugangsschluessel fehlt, statt an einer KeyNotFoundException zu scheitern. #203 Kompaktiert wird vor dem API-Aufruf statt danach; die gemessenen Prompt-Groessen kalibrieren die Schaetzung je Modell. Neu: tests/ClawdDotNet.App.Tests - fuer #375 und #376 gab es keinen Ort fuer Tests. Dazu Regressionstests fuer jede Behebung; jeder wurde gegen den alten Stand geprueft, damit belegt ist, dass er den Fehler faengt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,12 +14,32 @@ public sealed class ContextCompactor
|
||||
private const int MaxToolResultChars = 2000;
|
||||
private const string TruncatedMarker = "\n\n[... Ergebnis gekürzt ...]";
|
||||
|
||||
/// <summary>
|
||||
/// Wie stark ein einzelner Messwert den Faktor verschiebt. Klein genug, dass ein
|
||||
/// Ausreißer die Schwelle nicht kippt, groß genug, dass ein Modellwechsel nach
|
||||
/// wenigen Aufrufen ankommt.
|
||||
/// </summary>
|
||||
private const double CalibrationWeight = 0.25;
|
||||
|
||||
/// <summary>
|
||||
/// Je Modell: um welchen Faktor die rohe Schätzung danebenlag. 1,0 heißt „noch nichts
|
||||
/// gemessen". Modellweise, weil verschiedene Tokenizer denselben Text verschieden
|
||||
/// zerlegen — ein gemeinsamer Faktor wäre der Mittelwert von etwas, das nichts
|
||||
/// miteinander zu tun hat.
|
||||
/// </summary>
|
||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, double> _calibration = new();
|
||||
|
||||
public ContextCompactor(IChatCompletionClient client, ILoggerFactory loggerFactory)
|
||||
{
|
||||
_client = client;
|
||||
_logger = loggerFactory.CreateLogger("ClawdDotNet.Core.Engine.ContextCompactor");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Die rohe Schätzung: vier Zeichen je Token. Grob, aber ohne Netzaufruf und ohne
|
||||
/// Kenntnis des Tokenizers — und darauf kommt es an, weil sie <b>vor</b> dem Senden
|
||||
/// gebraucht wird. <see cref="EstimateFor"/> zieht die gemessene Abweichung nach.
|
||||
/// </summary>
|
||||
public static int EstimateTokens(List<ChatMessage> messages)
|
||||
{
|
||||
var totalChars = 0;
|
||||
@@ -37,16 +57,51 @@ public sealed class ContextCompactor
|
||||
return totalChars / 4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Die Schätzung für ein bestimmtes Modell — die rohe Rechnung, korrigiert um das,
|
||||
/// was bei diesem Modell bisher tatsächlich gemessen wurde.
|
||||
/// </summary>
|
||||
public int EstimateFor(string model, List<ChatMessage> messages)
|
||||
=> (int)(EstimateTokens(messages) * _calibration.GetValueOrDefault(model, 1.0));
|
||||
|
||||
/// <summary>
|
||||
/// Meldet, wie groß der Prompt wirklich war, den eine Schätzung vorhergesagt hat.
|
||||
///
|
||||
/// <para>Vier Zeichen je Token stimmt für englische Fließtexte ungefähr und liegt
|
||||
/// bei deutschem Text, JSON und Code daneben — je nach Modell in beide Richtungen.
|
||||
/// Der Faktor zieht die Schätzung dorthin, wo dieses Modell tatsächlich liegt.</para>
|
||||
///
|
||||
/// <para>Gleitender Mittelwert statt letztem Messwert: Ein einzelner Ausreißer soll
|
||||
/// die Schwelle nicht verschieben. Begrenzt auf das Halbe bis Vierfache — was
|
||||
/// darüber hinausginge, ist kein Kalibrierungsfall mehr, sondern ein Fehler
|
||||
/// woanders, und den soll die Schätzung nicht kaschieren.</para>
|
||||
/// </summary>
|
||||
public void Calibrate(string model, int estimatedTokens, int actualPromptTokens)
|
||||
{
|
||||
if (estimatedTokens <= 0 || actualPromptTokens <= 0) return;
|
||||
|
||||
var gemessen = Math.Clamp((double)actualPromptTokens / estimatedTokens, 0.5, 4.0);
|
||||
|
||||
// Auch der erste Messwert zählt nur mit seinem Gewicht — Ausgangspunkt ist die
|
||||
// rohe Schätzung (Faktor 1,0). Setzte der erste Wert den Faktor voll, könnte ein
|
||||
// einzelner Ausreißer die Schwelle sofort verschieben, und genau das soll der
|
||||
// gleitende Mittelwert verhindern.
|
||||
static double Naechster(double bisher, double gemessen)
|
||||
=> bisher * (1 - CalibrationWeight) + gemessen * CalibrationWeight;
|
||||
|
||||
_calibration.AddOrUpdate(
|
||||
model,
|
||||
Naechster(1.0, gemessen),
|
||||
(_, bisher) => Naechster(bisher, gemessen));
|
||||
}
|
||||
|
||||
public async Task<bool> CompactIfNeededAsync(
|
||||
List<ChatMessage> messages,
|
||||
int lastPromptTokens,
|
||||
LoopGuardConfig guard,
|
||||
string model,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var contextTokens = lastPromptTokens > 0
|
||||
? lastPromptTokens
|
||||
: EstimateTokens(messages);
|
||||
var contextTokens = EstimateFor(model, messages);
|
||||
|
||||
var threshold = (int)(guard.MaxContextTokens * guard.CompactionThreshold);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user