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();
}
}