Mehrere Instanzen sollen unabhaengig voneinander laufen. Heute tun sie das
nicht: Sie teilen sich Protokollverzeichnis, Sicherungsordner und
Sicherungszeitplan. In einer gemeinsamen Startup.log standen abwechselnd
Zeilen zweier Instanzen, ohne Kennzeichnung, welche gerade spricht -- im
Mehrinstanzbetrieb ist damit jede Fehlersuche Ratearbeit. Genau in dieser
Lage ist der Aufhaenger aus #529 entstanden.
Fuenf Schluessel wandern aus AppSettings nach InstanceSettings.json:
logDirectory, backupDirectory, backupKeepCount, autoBackupEnabled,
autoBackupTime. Leer heisst jetzt <Instanz>/Logs beziehungsweise
<Instanz>/Backups; ein relativer Wert zaehlt weiterhin gegen das
Instanzverzeichnis, nicht gegen das Arbeitsverzeichnis des Prozesses
(#375 bleibt gewahrt).
autoBackupEnabled und autoBackupTime standen in der Planung nicht, sind
aber mitgegangen: Ein Schalter, der die taegliche Sicherung fuer eine
Instanz einschaltet und sie damit fuer alle einschaltet, ist derselbe
Fehler wie ein Ordner fuer alle -- und zwei von vier Sicherungsfeldern zu
verschieben hinterliesse eine Einstellungsseite, auf der die Haelfte
anwendungsweit gilt.
Keine Uebernahme der Altwerte, aber eine Ansage: SettingsManager.
AbandonedKeys meldet die aufgegebenen Schluessel, solange sie noch in
Settings.json stehen, und der Start protokolliert einmal, was nicht mehr
gilt und wohin diese Instanz stattdessen schreibt. Ein anwendungsweiter
Pfad, in alle Instanzen kopiert, waere wieder ein Ordner fuer alle.
webServerPort bleibt vorerst stehen. Es entfaellt mit Stufe 2, sobald
Kestrel auf dem Socket hoert; vorher wegzunehmen hiesse, die
Weboberflaeche ohne Ersatz abzuschalten.
Korrektur an der eigenen Befundlage: "Instanz A wirft die Sicherungen von
Instanz B weg" war zu breit. Beide Rotationen filtern bereits nach dem
Praefix backup_<Instanzname>_. Der reale Defekt ist enger, und ein Praefix
ist keine Grenze: backup_Test_2_*.zip faengt mit backup_Test_ an, also
zaehlt Test im gemeinsamen Ordner die Archive von Test_2 mit. Belegt in
BackupSchedulerTests, das den alten Zustand eigens wiederherstellt.
Nachgeprueft an der laufenden Installation, nicht nur im Test: Nach dem
Umbau schreibt Instance-Test nach Instances/Instance-Test/Logs, das alte
gemeinsame Verzeichnis bekommt keine Zeile mehr, und die Warnung zu den
aufgegebenen Schluesseln erscheint genau einmal.
Enthaelt ausserdem, was seit der Weboberflaeche liegengeblieben war:
- #526: Instanzgeheimnisse wurden beim Speichern verschluesselt und beim
Laden nie entschluesselt. LoadInstanceConfig hatte kein
ConfigSecrets.Unprotect; ConfigLoader.LoadAsync macht es richtig, wird
zur Laufzeit aber gar nicht aufgerufen. Der OpenRouter-Schluessel ging
so verschluesselt an die API.
- Ein offener Ereignisstrom hielt das Beenden fuenf Sekunden auf. Die
Stroeme hingen nur an RequestAborted, das beim geordneten Herunterfahren
nicht feuert; jetzt zusaetzlich an ApplicationStopping. Der Fang in
WebUiHost gilt dazu ohne Bedingung auf RequestAborted -- mit ihr waere
jeder offene Strom beim Beenden als Fehler im Bugtracker gelandet.
- Nach der Anmeldung blieb die Weboberflaeche leer: #anmeldung
{ display:flex } schlaegt als ID-Selektor das hidden-Attribut.
- docs/Instanzmanager-Konzept.md: Befund, die Entscheidungen E-M1 bis
E-M6, der Schnitt zwischen Manager und Instanz und die sechs Stufen.
Build ohne Warnungen, 709 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- ClawdDotNet.Web: Kestrel im Prozess der Anwendung, Token-Absicherung (TokenGate mit FixedTimeEquals), Endpunkte unter /api, Ereignisstrom unter /events (Server-Sent Events) und fortlaufendes Log-Streaming.
- Weboberflaeche (wwwroot/): Vollstaendiges Single-Page-Frontend mit nativem HTML5, CSS3 und ES-Modulen ohne Buildschritt (strikt textContent, #-Routing, sofortiger Login-Sprung bei fehlendem Token, Light/Dark-Theme, lokale Barlow-Schriften).
- ClawdDotNet.App: Fachlogik aus den Avalonia-ViewModels in Dienste herausgeloest (AgentAdministration, JobAdministration, ServiceAdministration, RunCoordinator) und ueber InstanceChange synchronisiert.
- Tests: ClawdDotNet.Web.Tests und ClawdDotNet.App.Tests hinzugefuegt, alle 684 Tests erfolgreich.
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>
Es gab keine Obergrenze. Ein Agent in einer Schleife — etwa durch gegenseitige
send_message-Aufrufe — konnte unbeaufsichtigt Guthaben verbrennen; die
Credits-Anzeige war rein informativ.
Verbrauchsdaten in der Datenbank
Voraussetzung dafuer ist eine belastbare Erfassung. Bisher lag der Verbrauch in
TokenUsage.json: Bei JEDEM Agenten-Lauf wurde die gesamte Datei geladen, ergaenzt
und neu geschrieben, unter einem globalen Lock. Das waechst quadratisch und ist
der eigentliche Engpass bei vielen Agenten — unabhaengig davon, welche Datenbank
darunter liegt.
RunUsage liegt jetzt in einer eigenen Tabelle mit Ortsdatum, damit ein Tagesbudget
der Wahrnehmung des Benutzers folgt und die Abfrage ohne Zeitzonenrechnerei
auskommt. Betraege werden als Text abgelegt und als decimal gelesen: Ueber REAL zu
gehen wuerde bei Cent-Betraegen Rundungsfehler einsammeln, die sich ueber tausende
Laeufe summieren. Ein Test weist das mit 1000 Buchungen zu je 0,0001 USD nach.
BudgetGuard
Zwei Arten von Grenzen, weil sich Kosten nicht immer beziffern lassen: Liefert der
Anbieter fuer ein Modell keine Preise, greift die Kostengrenze nicht — die
Token-Grenze dagegen immer. Wer sich absichern will, setzt beide. Ist die Summe
wegen fehlender Preise unvollstaendig, steht das in der Begruendung; sonst wirkte
ein niedriger Verbrauch wie ein noch offener Spielraum.
Grenzen gibt es je Agent und je Instanz. Geprueft wird vor der ersten Anfrage,
damit ein erschoepftes Budget gar nichts mehr kostet. Neuer Endzustand
BudgetExceeded.
Nebenbei: Der Namespace Usage kollidierte mit der gleichnamigen Modellklasse fuer
Token-Angaben und heisst jetzt Accounting.
357 Tests gruen (209 Core, 148 Tools).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OpenRouter-Schluessel, Datenbank-Verbindungszeichenfolgen samt Passwort,
Mail-Zugangsdaten und das Telegram-2FA-Passwort lagen im Klartext in
AgentSettings.json und InstanceSettings.json. Wer die Dateien lesen konnte — ein
Backup, eine Dateifreigabe, ein versehentlicher Commit — hatte alle Zugaenge.
SecretProtector nutzt DPAPI im Benutzerkontext: Die Werte lassen sich nur vom
selben Windows-Benutzer auf demselben Rechner lesen. Das schuetzt gegen
Weitergabe der Datei, nicht gegen einen Angreifer, der bereits als dieser
Benutzer laeuft — fuer einen lokal laufenden Dienst die angemessene Stufe.
Verschluesselte Werte tragen ein Praefix. Dadurch bleibt Klartext aus bestehenden
Konfigurationen lesbar und wird beim naechsten Speichern automatisch uebernommen;
vorhandene Installationen laufen ohne Zutun weiter.
Ein Wert, der sich nicht entschluesseln laesst — etwa nach Benutzer- oder
Rechnerwechsel — wird gemeldet statt stillschweigend als Klartext
durchgereicht. Sonst ginge ein unbrauchbarer Schluessel an die API und der
Fehler waere schwer zuzuordnen.
ConfigSecrets entscheidet anhand der Feldnamen, welche Werte betroffen sind. Das
ist noetig, weil die Tool-Konfiguration ein freies Woerterbuch ist. Beim Speichern
werden die Werte nur fuer den Schreibvorgang verschluesselt und danach wieder
entschluesselt, damit die laufende Instanz weiterarbeiten kann.
309 Tests gruen (161 Core, 148 Tools).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
T1 — Prompt-Caching. Bisher wurde bei jedem Schritt eines Runs der komplette
Prompt neu berechnet, inklusive Tool-Definitionen und System-Prompt, die sich nie
aendern. Bei zehn Schritten und einem 15k-Praefix sind das 150.000 statt 15.000
Eingabe-Tokens.
ChatMessage bekommt dafuer einen eigenen JsonConverter: Der Inhalt geht weiterhin
als String raus, bei gesetztem CacheBreakpoint jedoch als Blockarray mit
cache_control. Beim Lesen werden beide Formate akzeptiert, damit bestehende
ChatContext.json weiter geladen werden koennen.
PromptCache setzt zwei Breakpoints: einen auf den System-Prompt (deckt
Tool-Definitionen und System-Prompt ab) und einen rollierenden auf die letzte
Nachricht mit Inhalt. Vorherige Markierungen werden vorher entfernt, damit sie
sich nicht ansammeln. Aktivierung ueber promptCaching: auto (Default, aktiv fuer
Modelle mit Unterstuetzung), on oder off.
Der wichtigste Test dazu prueft die Praefix-Stabilitaet: Der System-Prompt muss
ueber alle Schritte zeichengleich serialisiert werden. Ein einziger Zeitstempel
darin wuerde den Cache still verwerfen — die Kosten blieben unveraendert, ohne
dass es irgendwo auffiele.
T9 — Usage liest prompt_tokens_details.cached_tokens; die Zahl wird bis in
AgentRunResult durchgereicht. Ohne sie liesse sich die Wirkung nicht belegen.
T2 — Tool-Ergebnisse werden jetzt zentral in ExecuteToolCallAsync gekappt
(maxToolResultChars, Default 16.000). Bisher konnte ein einzelner WebFetch mit
dem 512-KB-Standardlimit rund 130.000 Tokens in EINER Antwort erzeugen; die
Compaction griff erst danach, bezahlt war der Request laengst.
T3 — Die Zusammenfassung beim Kompaktieren laeuft ueber ein konfigurierbares
summaryModel (Default gemini-2.5-flash) statt ueber das teure Agentenmodell.
B4 — AgentRunResult fuehrt Prompt- und Completion-Tokens getrennt; die
Kostenanzeige schaetzte bisher 50/50, real liegt das Verhaeltnis eher bei 95:5.
Die veraltete Preistabelle bleibt offen.
Neue Einstellungen sind im PropertyGrid sichtbar und werden vom AgentEditor bei
neuen Agenten mitgeschrieben.
Alle 91 Tests gruen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
B1 — Die Compaction behielt blind die letzten 6 Nachrichten. Fiel diese Grenze
mitten in eine Tool-Sequenz, entstand eine tool-Antwort ohne zugehoerigen
assistant-tool_call; die API lehnt das mit HTTP 400 ab. FindSafeTailStart
verschiebt die Grenze jetzt rueckwaerts auf eine Blockgrenze.
B14 — Bei Konversationen mit hoechstens 6 Nachrichten enthielt der Tail auch die
system-Nachricht, die anschliessend ein zweites Mal angehaengt wurde. Ergebnis
war ein doppelter System-Prompt und eine duplizierte Konversation — die
Compaction vergroesserte den Kontext, statt ihn zu verkleinern. Der Tail beginnt
nun grundsaetzlich hinter dem System-Prompt; liegt davor nichts Nennenswertes,
wird die Kompaktierung uebersprungen. Gefunden durch den Property-Test.
Nebeneffekt: Zusammengefasst wird nur noch der Teil, der tatsaechlich wegfaellt.
Der Tail bleibt woertlich erhalten und musste bisher doppelt bezahlt werden.
B3 — maxTokens zaehlte kumulativ ueber alle Schritte, wurde aber wie eine
Kontextgrenze konfiguriert. Da jeder Schritt den vollen Kontext erneut sendet,
brach ein Chat mit 20k Kontext nach vier Schritten ab. Aufgeteilt in
maxCumulativeTokens (Kostenbudget, Default 500k) und maxContextTokens
(Kontextgroesse). Alte Konfigurationen werden beim Laden migriert, die
Fehlermeldungen unterscheiden jetzt Schritt- und Kostenlimit.
Alle 31 Tests gruen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>