using System.Text; using ClawdDotNet.Core.Deploymentcenter; using Microsoft.Extensions.Logging; namespace ClawdDotNet.Manager; /// /// Meldet ans Deploymentcenter, was die Instanz selbst nicht mehr melden kann. /// /// Das ist der Grund, warum es den Manager gibt. Am 2026-09-08 hat sich eine /// Instanz aufgehängt und wurde von Windows beendet: keine Protokollzeile, keine /// Fehlermeldung, kein Bugtracker-Eintrag, kein Watchdog-Alarm. Bauartbedingt — Meldeweg /// und Watchdog lebten in dem Prozess, über den sie berichten sollten. Fällt er hart, /// fällt der Meldeweg mit, und zwar genau im Fall, der gemeldet werden müsste. /// /// Zwei Wege, mit Absicht. Die Instanz meldet, solange sie lebt — eigener /// Fehler-Stream, eigener Heartbeat, eigenes Sub-Token. Der Manager meldet, was sie nicht /// mehr melden kann. Jeder Weg deckt den Ausfall des anderen ab; ein Manager, der jede /// Meldung durchreichte, wäre der neue Einzelpunkt. /// /// Und er meldet wenig. Nur Absturz, Verstummen und ein gescheiterter Start — /// das, was aus seiner Sicht relevant ist. Ein fehlgeschlagener Werkzeugaufruf, ein /// abgebrochener Agentenlauf, eine überschrittene Budgetgrenze bleiben in der Instanz. /// Der Manager sieht davon nichts und soll davon nichts sehen (Konzept, Abschnitt 4). /// public sealed class SupervisionReporter( BugtrackerClient bugtracker, ILogger logger, Func>? logExcerpt = null) { private readonly Func> _logExcerpt = logExcerpt ?? (dir => InstanceLogExcerpt.Read(dir)); /// /// Meldet ein Vorkommnis, wenn es meldenswert ist — sonst nicht. /// /// Wirft nie. Ein Meldeweg, der selbst wirft, wäre die schlechteste aller /// Welten: Der ursprüngliche Befund ginge dabei verloren, und die Aufsicht, die ihn /// gefunden hat, stürbe an ihrem eigenen Bericht. /// /// true, wenn eine Meldung rausgegangen ist. public async Task ReportAsync(SupervisionEvent ereignis, CancellationToken ct = default) { if (!ereignis.WorthReporting) return false; try { await bugtracker.ReportAsync( type: BugtrackerItemType.Bug, title: Title(ereignis), description: Description(ereignis), severity: ereignis.Kind == InstanceEventKind.StartFailed ? "medium" : "high", // Stabil je Instanz und Art: Stuerzt dieselbe Instanz wiederholt ab, zaehlt // der Server hoch, statt den Bugtracker mit Zwillingen zu fuellen. Genau // dafuer gibt es occurrence_count - und wiederholte Abstuerze sind ein // eigener Befund, kein Dutzend gleicher. clientRef: $"manager-{ereignis.Kind}-{Path.GetFileName(ereignis.InstanceDirectory)}" .ToLowerInvariant(), context: new Dictionary { ["quelle"] = "instanzmanager", ["instanz"] = ereignis.InstanceName, ["instanzverzeichnis"] = ereignis.InstanceDirectory, ["prozesskennung"] = ereignis.ProcessId, ["abschiedscode"] = ereignis.ExitCode, ["beobachtet"] = ereignis.At.ToString("O") }, ct: ct).ConfigureAwait(false); logger.LogInformation( "Stellvertretend gemeldet: {Art} an Instanz {Name}.", ereignis.Kind, ereignis.InstanceName); return true; } catch (Exception ex) { logger.LogWarning(ex, "Die stellvertretende Meldung für Instanz {Name} ging nicht raus. " + "Der Befund steht im Protokoll des Managers.", ereignis.InstanceName); return false; } } private static string Title(SupervisionEvent e) => e.Kind switch { InstanceEventKind.Crashed => $"Instanz '{e.InstanceName}' unerwartet beendet", InstanceEventKind.Unresponsive => $"Instanz '{e.InstanceName}' laeuft, antwortet aber nicht mehr", InstanceEventKind.StartFailed => $"Instanz '{e.InstanceName}' liess sich nicht starten", _ => $"Instanz '{e.InstanceName}': {e.Kind}" }; private string Description(SupervisionEvent e) { var text = new StringBuilder(); text.AppendLine(e.Message).AppendLine(); text.AppendLine("Gemeldet vom Instanzmanager, weil die Instanz es selbst nicht mehr konnte."); text.AppendLine(); text.AppendLine($"Instanz : {e.InstanceName}"); text.AppendLine($"Verzeichnis : {e.InstanceDirectory}"); text.AppendLine($"Prozesskennung : {e.ProcessId?.ToString() ?? "unbekannt"}"); text.AppendLine($"Abschiedscode : {e.ExitCode?.ToString() ?? "unbekannt"}"); text.AppendLine($"Beobachtet : {e.At:yyyy-MM-dd HH:mm:ss} UTC"); text.AppendLine(); var zeilen = _logExcerpt(e.InstanceDirectory); if (zeilen.Count == 0) { // Kein Versehen, sondern die aussagekraeftigste Zeile der ganzen Meldung. text.AppendLine( "Letzte Protokollzeilen: KEINE. Die Instanz hat nichts hinterlassen — " + "ihr Schreibpuffer wurde nie geleert. Genau dieser Zustand (#529) ist " + "der Grund, warum diese Meldung von aussen kommt."); } else { text.AppendLine($"Letzte {zeilen.Count} Protokollzeilen der Instanz:"); foreach (var zeile in zeilen) text.AppendLine(" " + zeile); } return text.ToString(); } }