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:
@@ -37,4 +37,58 @@ internal sealed class InMemoryAuditRepository : IAuditRepository
|
||||
=> Task.FromResult<IReadOnlyList<RunReceipt>>(Receipts.Where(r => r.TaskId == taskId).ToList());
|
||||
|
||||
public Task<int> CountAsync(CancellationToken ct) => Task.FromResult(Entries.Count);
|
||||
|
||||
/// <summary>Spiegelt die SQL-Regel: Started ohne späteren Abschluss derselben CallId.</summary>
|
||||
public Task<IReadOnlyList<AuditEntry>> ListOpenCallsAsync(CancellationToken ct)
|
||||
{
|
||||
var alle = Entries.ToList();
|
||||
|
||||
var abgeschlossen = alle
|
||||
.Where(e => e.Status != AuditStatus.Started && e.CallId.Length > 0)
|
||||
.Select(e => e.CallId)
|
||||
.ToHashSet();
|
||||
|
||||
var offen = alle
|
||||
.Where(e => e.Status == AuditStatus.Started
|
||||
&& e.CallId.Length > 0
|
||||
&& !abgeschlossen.Contains(e.CallId))
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<AuditEntry>>(offen);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AuditEntry>> ListForAgentAsync(
|
||||
string agentId, DateTime? seit, int limit, CancellationToken ct)
|
||||
=> Task.FromResult<IReadOnlyList<AuditEntry>>(
|
||||
Entries.Where(e => e.AgentId == agentId)
|
||||
.Where(e => seit is null || e.OccurredAt >= seit)
|
||||
.Reverse()
|
||||
.Take(limit)
|
||||
.ToList());
|
||||
|
||||
public Task<IReadOnlyList<ToolUsageStat>> SummarizeToolUsageAsync(
|
||||
string? agentId, DateTime? seit, CancellationToken ct)
|
||||
=> Task.FromResult<IReadOnlyList<ToolUsageStat>>(
|
||||
Entries.Where(e => agentId is null || e.AgentId == agentId)
|
||||
.Where(e => seit is null || e.OccurredAt >= seit)
|
||||
.GroupBy(e => (e.AgentId, e.Tool))
|
||||
.Select(g =>
|
||||
{
|
||||
var abgeschlossen = g.Where(e => e.Status != AuditStatus.Started).ToList();
|
||||
var begonnen = g.Count(e => e.Status == AuditStatus.Started);
|
||||
|
||||
return new ToolUsageStat
|
||||
{
|
||||
AgentId = g.Key.AgentId,
|
||||
Tool = g.Key.Tool,
|
||||
Calls = abgeschlossen.Count,
|
||||
Failed = abgeschlossen.Count(e => e.Status == AuditStatus.Error),
|
||||
Denied = abgeschlossen.Count(e => e.Status == AuditStatus.Denied),
|
||||
Unfinished = Math.Max(0, begonnen - abgeschlossen.Count),
|
||||
TotalDurationMs = abgeschlossen.Sum(e => e.DurationMs),
|
||||
MaxDurationMs = g.Max(e => e.DurationMs)
|
||||
};
|
||||
})
|
||||
.OrderByDescending(s => s.Calls)
|
||||
.ToList());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user