Audit und Laufbelege: Tageswerte je Agent und Einheit, Monatsarchiv statt Loeschen (#700)
Nach der Vorgabe vom 2026-09-22 (so wenig loeschen wie moeglich): Laeufe aelter als hotDays (Vorgabe 180, 0 = alles bleibt) wandern aus AuditLog und RunReceipts in Archive/audit-JJJJ-MM.db - kopiert und entfernt in einer Transaktion, Zeilen-IDs bleiben, spaeter dazugekommene Spalten werden im Archiv ergaenzt. Vorher wird jeder Tag in RunDaily verdichtet (Laeufe, Fehlschlaege, Schrittgrenze, Tokens, Kosten, Werkzeugaufrufe, Fehler, Verweigert, Freigaben) mit der Einheit des Agenten; die Tageswerte bleiben. Nachschlagen eines Laufs findet ihn auch im Archiv. API /usage/days, /usage/archive; Karte 'Tageswerte' in der Verbrauchsansicht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0dd8a761b8
commit
095722ae9c
@@ -83,22 +83,72 @@ public sealed class SqliteAuditRepository : IAuditRepository
|
||||
|
||||
public async Task<IReadOnlyList<AuditEntry>> ListForRunAsync(string runId, CancellationToken ct)
|
||||
{
|
||||
await using var conn = await _storage.OpenConnectionAsync(ct);
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT " + AuditColumns + " FROM AuditLog WHERE RunId = @runId ORDER BY Id";
|
||||
cmd.Parameters.AddWithValue("@runId", runId);
|
||||
return await ReadEntriesAsync(cmd, ct);
|
||||
await using (var conn = await _storage.OpenConnectionAsync(ct))
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT " + AuditColumns + " FROM AuditLog WHERE RunId = @runId ORDER BY Id";
|
||||
cmd.Parameters.AddWithValue("@runId", runId);
|
||||
var hot = await ReadEntriesAsync(cmd, ct);
|
||||
if (hot.Count > 0)
|
||||
return hot;
|
||||
}
|
||||
|
||||
// #700: an older run moved to the archive — it is still evidence and still found.
|
||||
foreach (var archive in ArchiveConnections())
|
||||
await using (archive)
|
||||
{
|
||||
using var cmd = archive.CreateCommand();
|
||||
cmd.CommandText = "SELECT " + AuditColumns + " FROM AuditLog WHERE RunId = @runId ORDER BY Id";
|
||||
cmd.Parameters.AddWithValue("@runId", runId);
|
||||
var found = await ReadEntriesAsync(cmd, ct);
|
||||
if (found.Count > 0)
|
||||
return found;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public async Task<RunReceipt?> GetReceiptForRunAsync(string runId, CancellationToken ct)
|
||||
{
|
||||
await using var conn = await _storage.OpenConnectionAsync(ct);
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT " + ReceiptColumns + " FROM RunReceipts WHERE RunId = @runId ORDER BY Id DESC LIMIT 1";
|
||||
cmd.Parameters.AddWithValue("@runId", runId);
|
||||
await using (var conn = await _storage.OpenConnectionAsync(ct))
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT " + ReceiptColumns + " FROM RunReceipts WHERE RunId = @runId ORDER BY Id DESC LIMIT 1";
|
||||
cmd.Parameters.AddWithValue("@runId", runId);
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
if (await reader.ReadAsync(ct))
|
||||
return ReadReceipt(reader);
|
||||
}
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
return await reader.ReadAsync(ct) ? ReadReceipt(reader) : null;
|
||||
foreach (var archive in ArchiveConnections())
|
||||
await using (archive)
|
||||
{
|
||||
using var cmd = archive.CreateCommand();
|
||||
cmd.CommandText = "SELECT " + ReceiptColumns + " FROM RunReceipts WHERE RunId = @runId ORDER BY Id DESC LIMIT 1";
|
||||
cmd.Parameters.AddWithValue("@runId", runId);
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
if (await reader.ReadAsync(ct))
|
||||
return ReadReceipt(reader);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #700: The month archives, newest first, opened read-only one at a time. A lookup by run
|
||||
/// is rare and asks for one run — opening a few small files for it is cheap.
|
||||
/// </summary>
|
||||
private IEnumerable<SqliteConnection> ArchiveConnections()
|
||||
{
|
||||
foreach (var file in AuditArchive.FilesIn(AuditArchive.ArchiveDirectoryOf(_storage)))
|
||||
{
|
||||
var connection = new SqliteConnection(new SqliteConnectionStringBuilder
|
||||
{
|
||||
DataSource = Path.Combine(AuditArchive.ArchiveDirectoryOf(_storage), file.FileName),
|
||||
Mode = SqliteOpenMode.ReadOnly,
|
||||
Pooling = false
|
||||
}.ToString());
|
||||
connection.Open();
|
||||
yield return connection;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<RunReceipt>> ListReceiptsAsync(DateTime fromUtc, DateTime toUtc, int limit, CancellationToken ct)
|
||||
|
||||
Reference in New Issue
Block a user