Wiederaufnahme nach Absturz, Nachschau und Auswertung (#553-#556)

#553 FileRW schreibt atomar. Die Vorkehrung lag seit #241 im selben
Repository und wurde ueberall benutzt - Einstellungen, Sicherungen,
Chat-Zustand, Identity.md - nur der Weg, ueber den Agenten ihre
Arbeitsergebnisse ablegen, ging daran vorbei. Damit stand das Verhaeltnis
auf dem Kopf: geschuetzt war das Eigene, ungeschuetzt das Ergebnis.
append bleibt bewusst nicht atomar; das ginge nur, indem die ganze Datei
neu geschrieben wird, und bei einer grossen Protokolldatei waere das der
falsche Handel.

#554 Wiederaufnahme. Neuer Eintrag AuditStatus.Started, geschrieben VOR
der Wirkung und als einziger nicht "best effort" - auf ihn stuetzt sich
alles Weitere, also laeuft der Aufruf gar nicht erst, wenn er nicht
durchkommt. Neue Kennzeichnung IsRepeatable, getrennt von IsParallelSafe:
Das eine heisst "stoert keinen gleichzeitigen Aufruf", das andere
"richtet zweimal keinen Schaden an". CrashRecovery sichtet beim Start,
was offen blieb, und stuft ein - fuehrt aber nichts aus. Es sichtet und
berichtet; entscheiden soll der Aufrufer.

Dabei kippte eine alte Regel: Bisher wurde ein Abbruch bewusst NICHT
protokolliert. Das war richtig, solange es keinen Eintrag vor dem Aufruf
gab. Jetzt saehe sonst jeder Benutzerabbruch beim naechsten Start aus wie
ein Absturz - bis niemand mehr hinsieht. Deshalb AuditStatus.Cancelled.

#555 Optionale Zustandspruefung IToolStateProbe, umgesetzt in FileRW.
Unknown ist eine vollwertige Antwort, kein Fehlschlag: Ob ein SMTP-Server
angenommen hat, laesst sich nicht feststellen, und ein Werkzeug, das hier
raet, waere schlimmer als eines, das schweigt. Partial gilt ausdruecklich
nicht als geklaert - einen halb angehaengten Text sollte niemand
automatisch geraderuecken.

#556 Auswertung. ListForAgentAsync und SummarizeToolUsageAsync. Die Dauer
wird nur ueber abgeschlossene Aufrufe gemittelt; zaehlten die
Started-Eintraege mit ihrer Dauer 0 mit, saehe jedes Werkzeug doppelt so
schnell aus, wie es ist.

Nebenbei: SqliteStorage kann jetzt Spalten nachruesten. CREATE TABLE IF
NOT EXISTS allein genuegte nicht - bei einer bestehenden Datenbank tut die
Anweisung nichts, und erst das naechste INSERT waere gescheitert, im
Betrieb beim Anwender.

Vier Audit-Tests angepasst: Ein ausgefuehrter Aufruf erzeugt jetzt zwei
Eintraege statt einem. Ein Aufruf, der gar nicht erst laeuft - verweigert,
unbekannt, vorgelegt - hat weiterhin genau einen; der Beginn wird erst
nach Rechtepruefung und Staging geschrieben.

23 neue Tests, 912 gesamt, keine Regression. Build ohne Warnungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-12 21:48:18 +02:00
co-authored by Claude Opus 5
parent fb4b218e45
commit b568a45de3
14 changed files with 1177 additions and 30 deletions
+54 -1
View File
@@ -202,7 +202,10 @@ public sealed class SqliteStorage
Status TEXT NOT NULL,
Summary TEXT NOT NULL DEFAULT '',
DurationMs INTEGER NOT NULL DEFAULT 0,
OccurredAt TEXT NOT NULL
OccurredAt TEXT NOT NULL,
-- Paart den "angefangen"-Eintrag mit seinem Abschluss (#554).
-- Wird fuer bestehende Datenbanken in Nachruesten() ergaenzt.
CallId TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS IX_AuditLog_Run
@@ -261,5 +264,55 @@ public sealed class SqliteStorage
""";
cmd.ExecuteNonQuery();
Nachruesten(connection);
}
/// <summary>
/// Spalten, die nach der ersten Fassung einer Tabelle dazugekommen sind.
///
/// <para><c>CREATE TABLE IF NOT EXISTS</c> allein genügt dafür nicht: Bei einer
/// bestehenden Datenbank tut die Anweisung gar nichts, die neue Spalte fehlt, und
/// erst das nächste <c>INSERT</c>, das sie benennt, scheitert — im Betrieb, beim
/// Anwender, nicht beim Entwickler mit der frischen Datei.</para>
/// </summary>
private static void Nachruesten(SqliteConnection connection)
{
// #554: Paart den "angefangen"-Eintrag mit seinem Abschluss.
SpalteErgaenzen(connection, "AuditLog", "CallId", "TEXT NOT NULL DEFAULT ''");
// Findet offene Aufrufe beim Start, ohne die ganze Tabelle zu lesen.
using var index = connection.CreateCommand();
index.CommandText = """
CREATE INDEX IF NOT EXISTS IX_AuditLog_Call
ON AuditLog (CallId, Id);
CREATE INDEX IF NOT EXISTS IX_AuditLog_Agent
ON AuditLog (AgentId, Id DESC);
""";
index.ExecuteNonQuery();
}
/// <summary>
/// Ergänzt eine Spalte, falls sie fehlt. Ist sie da, geschieht nichts.
///
/// <para>SQLite kennt kein <c>ADD COLUMN IF NOT EXISTS</c>, deshalb wird erst
/// nachgesehen. Die Vorgabe ist Pflicht: Eine <c>NOT NULL</c>-Spalte ohne
/// Vorgabewert lässt sich einer gefüllten Tabelle nicht hinzufügen.</para>
/// </summary>
private static void SpalteErgaenzen(SqliteConnection connection, string tabelle, string spalte, string definition)
{
using (var pruefen = connection.CreateCommand())
{
pruefen.CommandText = $"SELECT COUNT(*) FROM pragma_table_info('{tabelle}') WHERE name = @spalte";
pruefen.Parameters.AddWithValue("@spalte", spalte);
if (Convert.ToInt32(pruefen.ExecuteScalar()) > 0)
return;
}
using var ergaenzen = connection.CreateCommand();
ergaenzen.CommandText = $"ALTER TABLE {tabelle} ADD COLUMN {spalte} {definition}";
ergaenzen.ExecuteNonQuery();
}
}