diff --git a/src/ClawdDotNet.App/AppHost.cs b/src/ClawdDotNet.App/AppHost.cs index f8d59ab..2ab982a 100644 --- a/src/ClawdDotNet.App/AppHost.cs +++ b/src/ClawdDotNet.App/AppHost.cs @@ -60,6 +60,15 @@ public sealed class AppHost : IAsyncDisposable public required ILoggerFactory LoggerFactory { get; init; } public required ToolRegistry Tools { get; init; } + /// + /// Zustandsspeicher und Audit-Log der Instanz. Nicht oeffentlich — sie gehoeren dem + /// Aufbau, nicht den Oberflaechen; gebraucht werden sie hier fuer die Sichtung + /// offener Werkzeugaufrufe beim Start (#554). + /// + internal IStateStore? StateStore { get; private init; } + + internal IAuditRepository? AuditRepository { get; private init; } + /// /// Das geordnete Ende dieser Instanz (#528). /// @@ -389,6 +398,15 @@ public sealed class AppHost : IAsyncDisposable var host = BuildCore(settings, directories, instance, instancePath, logDirectory, loggerFactory, tools, telegram, logger); + // #554: Nachsehen, was beim letzten Ende offen geblieben ist. Ein geordnetes Ende + // hinterlaesst nichts; ist hier etwas, ist die Instanz gestorben, waehrend + // Werkzeuge liefen. + // + // Bewusst nur sichten und protokollieren, nicht ausfuehren: Ob die Wirkung eines + // Aufrufs eingetreten ist, weiss auch dieser Dienst nicht. Ein automatisches + // "dann eben nochmal" versendete im Zweifel die Mail ein zweites Mal. + await host.SichteOffeneAufrufeAsync(logger); + // Nichts freizugeben: Der Torwächter nutzt den gemeinsamen HttpClient des SDK. host.License = license; host.IsManaged = gefuehrt; @@ -539,6 +557,8 @@ public sealed class AppHost : IAsyncDisposable LogDirectory = logDirectory, LoggerFactory = loggerFactory, Tools = tools, + StateStore = stateStore, + AuditRepository = audit, Engine = engine, Scanner = scanner, Staging = staging, @@ -569,6 +589,71 @@ public sealed class AppHost : IAsyncDisposable /// Taskboard und Scanner. Der Scanner ist der einzige periodische Treiber (A1) — /// geplante Agentenläufe wie Tool-Job-Polls sind Tasks. /// + /// + /// Sichtet beim Start die Werkzeugaufrufe, die beim letzten Ende offen blieben (#554), + /// und schreibt das Ergebnis ins Protokoll. + /// + /// Scheitert die Sichtung selbst, hält sie den Start nicht auf. Eine Instanz, + /// die wegen einer misslungenen Nachschau gar nicht erst hochkommt, wäre schlimmer + /// als eine, die mit einer Wissenslücke startet. + /// + private async Task SichteOffeneAufrufeAsync(ILogger logger) + { + // Ohne Protokoll gibt es nichts zu sichten, ohne Zustandsspeicher keine + // Nachschau. Beides fehlt nur beim Aufbau ohne Datenbank — dann gab es auch + // keine Werkzeugaufrufe, die offen sein könnten. + if (AuditRepository is not { } audit || StateStore is not { } stateStore) + return; + + var instance = Instance; + var tools = Tools; + var loggerFactory = LoggerFactory; + + try + { + // Die Nachschau braucht die Arbeitsverzeichnisse des Agenten — die kennt nur + // die Instanzkonfiguration. Ist der Agent inzwischen weg, gibt es keinen + // Kontext und damit keine Nachschau; die Einstufung bleibt trotzdem. + AgentToolContext? KontextFuer(string agentId, string toolName) + { + var agent = instance.Agents.FirstOrDefault(a => a.AgentId == agentId); + if (agent is null) + return null; + + var toolConfig = agent.Tools.TryGetValue(toolName, out var cfg) + ? cfg.AsReadOnly() + : new Dictionary().AsReadOnly(); + + return new AgentToolContext( + agent.AgentId, + instance.InstanceId, + toolConfig, + stateStore, + loggerFactory.CreateLogger($"ClawdDotNet.Tools.{toolName}.Probe"), + CancellationToken.None, + agent.WorkspacePath, + agent.SharedWorkspacePath); + } + + var recovery = new CrashRecovery(audit, tools, loggerFactory, KontextFuer); + var faelle = await recovery.SichtenAsync(CancellationToken.None); + + var offen = faelle.Where(f => !f.Erledigt).ToList(); + if (offen.Count > 0) + { + logger.LogWarning( + "{Anzahl} Werkzeugaufruf(e) brauchen eine Entscheidung. Sie wurden NICHT " + + "wiederholt — ob ihre Wirkung eintrat, ist nicht feststellbar.", + offen.Count); + } + } + catch (Exception ex) + { + logger.LogWarning(ex, + "Die Sichtung offener Werkzeugaufrufe ist gescheitert. Der Start geht weiter."); + } + } + private static (TaskScanner?, StagingService?, TaskboardService?) BuildTaskboard( InstanceConfig instance, SqliteTaskRepository taskRepository,