From e2567a466fde41f51446bc7a32e2829c6159fb6f Mon Sep 17 00:00:00 2001 From: Richard Date: Sat, 12 Sep 2026 21:51:46 +0200 Subject: [PATCH] Sichtung offener Werkzeugaufrufe beim Start verdrahtet (#554) AppHost.StartAsync ruft jetzt CrashRecovery. Ein geordnetes Ende hinterlaesst nichts; ist etwas da, ist die Instanz gestorben, waehrend Werkzeuge liefen. Bewusst nur sichten und protokollieren, nicht ausfuehren. Ob die Wirkung eines Aufrufs eintrat, weiss auch dieser Dienst nicht - ein automatisches "dann eben nochmal" versendete im Zweifel die Mail ein zweites Mal. Scheitert die Sichtung selbst, haelt sie den Start nicht auf. Eine Instanz, die wegen einer misslungenen Nachschau nicht hochkommt, waere schlimmer als eine, die mit einer Wissensluecke startet. Der Aufruf sitzt in StartAsync statt in BuildCore: BuildCore ist synchron, und die Signatur zu aendern haette durch alle Aufrufer gezogen. Blockendes Warten in einer Fensteranwendung waere die schlechtere Loesung gewesen. Build ohne Warnungen, 912 Tests, keine Regression. Co-Authored-By: Claude Opus 5 --- src/ClawdDotNet.App/AppHost.cs | 85 ++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) 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,