Files
ClawdDotNet/src/ClawdDotNet.Core/Config/InstanceConfig.cs
T
RichardandClaude Opus 5.5 8df5d004e4 Scanner: Laeufe deckeln, stille Kappung bei 500 beseitigen (#682)
Drei Fehler im Taktgeber des Taskboards, zwei gemeldet, einer beim Bau gefunden:

1. Unbegrenzte Parallelitaet. Jede faellige Aufgabe startete sofort. Bei
   Cron-Terminen auf runden Zeiten hiess das: alle Agenten gleichzeitig, und
   alle Laeufe passierten die Budgetpruefung gemeinsam, weil sie je Lauf vor
   dem Start laeuft. Neu: maxParallelTaskRuns je Instanz (Vorgabe 4). Der Rest
   wartet, sortiert nach Prioritaet und dann nach Faelligkeit.

2. Stille Kappung. Die Kandidaten kamen ueber ListAsync mit Limit 500; darueber
   fielen Termine lautlos aus dem Takt. Neu: ListRunnableAsync liefert alle
   Aufgaben auf todo, ungekappt. Muessen Aufgaben warten, steht das im Log,
   allerdings nur, wenn sich die Zahl aendert.

3. Der Takt wartete auf jeden Lauf, den er startete. Ein zehnminuetiger Lauf
   hielt damit jeden anderen Termin zehn Minuten an, und mit einem Deckel
   haetten die freien Plaetze leer gestanden. Neu: Die Schleife startet und
   wartet nicht; DisposeAsync wartet stattdessen auf die laufenden Laeufe.

Beansprucht wird nur, was sofort starten kann. Alles andere bleibt auf todo,
sonst liefe beim Warten der Lease ab und die Aufgabe wuerde ein zweites Mal
beansprucht. Ein Cron-Termin geht dadurch nicht verloren, weil DueOccurrence
bis zum letzten Lauf zuruecksucht. Ein von Hand gestarteter Lauf wartet auf
keinen Platz, belegt aber einen.

Bleibt offen: Bis zu maxParallelTaskRuns Laeufe koennen die Budgetpruefung
weiterhin gemeinsam passieren. Die Luecke ist damit begrenzt, aber nicht
geschlossen; dafuer braeuchte es eine Reservierung vor dem Lauf.

Tests: ScannerCapacityTests (9 neu). 1.229 gruen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 10:24:51 +02:00

193 lines
8.4 KiB
C#

using System.Text.Json.Serialization;
namespace ClawdDotNet.Core.Config;
public sealed class InstanceConfig
{
[JsonPropertyName("instanceId")]
public string InstanceId { get; set; } = Guid.NewGuid().ToString("N")[..8];
[JsonPropertyName("instanceName")]
public string InstanceName { get; set; } = "Default";
[JsonPropertyName("openRouterApiKey")]
public string OpenRouterApiKey { get; set; } = "";
[JsonPropertyName("workingDirectory")]
public string WorkingDirectory { get; set; } = "./data/";
/// <summary>
/// Wohin diese Instanz protokolliert. Leer heißt <c>&lt;Instanzverzeichnis&gt;/Logs</c>.
///
/// <para><b>Je Instanz und nicht anwendungsweit</b> (#528, Stufe 1). Vorher galt bei
/// „leer" das Log-Verzeichnis aus den Anwendungseinstellungen — ein Ort für alle
/// Instanzen. In <c>Startup.log</c> standen dann abwechselnd Zeilen zweier Instanzen
/// ohne Kennzeichnung, welche gerade spricht; im Mehrinstanzbetrieb war jede
/// Fehlersuche damit Ratearbeit.</para>
///
/// <para>Ein relativer Wert zählt gegen das Instanzverzeichnis, nicht gegen das
/// Arbeitsverzeichnis des Prozesses (siehe <c>AppHost.ResolveLogDirectory</c>).</para>
/// </summary>
[JsonPropertyName("logDirectory")]
public string LogDirectory { get; set; } = "";
/// <summary>
/// Ob der Instanzmanager diese Instanz am Laufen hält (#528).
///
/// <para><b>Zwei Dinge in einem Schalter, weil es dieselbe Zusage ist:</b> Der Manager
/// startet diese Instanz, wenn er selbst hochkommt, und er startet sie wieder, wenn
/// sie abstürzt. Wer das nicht will, lässt ihn aus — dann startet sie nur, wenn jemand
/// es verlangt.</para>
///
/// <para><b>Ausgeschaltet vorbelegt</b>, und das ist die vorsichtige Seite: Ein
/// Manager, der nach einem Neustart der Maschine alles hochfährt, was er findet,
/// startet auch das, was jemand mit Absicht angehalten hat.</para>
///
/// <para>Der Neustart nach einem Absturz ist begrenzt: Nach mehreren Versuchen in
/// kurzer Folge setzt der Manager aus und meldet es. Was an einer kaputten
/// Konfiguration scheitert, scheitert beim zehnten Versuch genauso, und dann hat man
/// statt einer toten Instanz eine tote Instanz und ein volles Protokoll.</para>
/// </summary>
[JsonPropertyName("autostartByManager")]
public bool AutostartByManager { get; set; }
/// <summary>
/// Wohin die Sicherungen dieser Instanz gehen. Leer heißt
/// <c>&lt;Instanzverzeichnis&gt;/Backups</c>.
///
/// <para><b>Je Instanz</b> (#528, Stufe 1). Vorher lagen die Archive aller Instanzen
/// in einem Ordner, und die Aufbewahrung (<see cref="BackupKeepCount"/>) räumt dort
/// nach dem Präfix <c>backup_&lt;Instanzname&gt;_</c> auf. Liegen zwei Namen
/// ineinander — <c>Test</c> und <c>Test_2</c> —, zählt die eine Instanz die Archive
/// der anderen mit und wirft die überzähligen weg.</para>
/// </summary>
[JsonPropertyName("backupDirectory")]
public string BackupDirectory { get; set; } = "";
/// <summary>Erstellt täglich zur eingestellten Uhrzeit eine Sicherung dieser Instanz.</summary>
[JsonPropertyName("autoBackupEnabled")]
public bool AutoBackupEnabled { get; set; }
/// <summary>Tageszeit der automatischen Sicherung, <c>HH:mm</c>.</summary>
[JsonPropertyName("autoBackupTime")]
public string AutoBackupTime { get; set; } = "03:00";
/// <summary>
/// Wie viele Sicherungen dieser Instanz aufbewahrt werden. 0 = alle behalten.
/// </summary>
[JsonPropertyName("backupKeepCount")]
public int BackupKeepCount { get; set; } = 14;
/// <summary>
/// Port der Weboberfläche. Entfällt mit dem Instanzmanager (E-M5): Instanzen hören
/// künftig ausschließlich auf ihrem Socket/Named Pipe; die Weboberfläche trägt der Manager.
///
/// <para><b>Noch ohne <c>[Obsolete]</c>.</b> Das Attribut stand hier kurz und hat
/// sechs Warnungen erzeugt, ohne dass eine davon jemand hätte abstellen können — der
/// Port ist bis zum Abschluss von E-M5 der einzige Weg, auf dem eine eigenständige
/// Instanz ihre Oberfläche anbietet. Eine Warnung, die nur wegsehen lässt, ist
/// schlechter als keine; sie kommt zurück, wenn der Ersatz steht.</para>
/// </summary>
[JsonPropertyName("webServerPort")]
public int WebServerPort { get; set; } = 7001;
/// <summary>
/// Ob die Weboberfläche mitläuft. Ausgeschaltet vorbelegt — sie macht aus einer
/// lokalen Anwendung einen Netzwerkdienst, der Agenten mit Datei-, Mail- und
/// FTP-Zugriff steuert. Das schaltet man bewusst ein.
/// </summary>
[JsonPropertyName("webUiEnabled")]
public bool WebUiEnabled { get; set; }
/// <summary>
/// <c>loopback</c> (Vorgabe) oder <c>any</c>. Bei <c>any</c> ist
/// <see cref="WebUiToken"/> Pflicht, sonst startet die Weboberfläche nicht.
/// </summary>
[JsonPropertyName("webUiBinding")]
public string WebUiBinding { get; set; } = "loopback";
/// <summary>
/// Zugangstoken der Weboberfläche. Steht in <c>ConfigSecrets</c> und liegt damit
/// geschützt in der Instanzdatei — wie der OpenRouter-Schlüssel daneben.
/// </summary>
[JsonPropertyName("webUiToken")]
public string WebUiToken { get; set; } = "";
[JsonPropertyName("telegramClient")]
public TelegramClientConfig? TelegramClient { get; set; }
/// <summary>Anbindung an das Watchdog-Modul des Deploymentcenters (Instanz-Heartbeat).</summary>
[JsonPropertyName("watchdog")]
public WatchdogConfig Watchdog { get; set; } = new();
/// <summary>Tagesgrenzen über alle Agenten der Instanz hinweg. 0 = keine Grenze.</summary>
[JsonPropertyName("budget")]
public InstanceBudget Budget { get; set; } = new();
/// <summary>
/// #660: How long prompt and response of every run are kept. Default for all agents;
/// each agent can override it.
/// </summary>
[JsonPropertyName("promptHistory")]
public Transcripts.PromptHistorySettings PromptHistory { get; set; } = new();
/// <summary>
/// Wie viele Weitergaben eine Delegationskette höchstens haben darf (B8, #206).
/// A→B ist die Tiefe 1. Zyklen fängt die Kette selbst ab; dieser Wert begrenzt die
/// Staffel A→B→C→D, die niemand beauftragt hat und die jeden Schritt einen vollen
/// Lauf kostet.
///
/// Instanzweit und nicht je Agent: Eine Kette gehört keinem der Beteiligten.
/// </summary>
[JsonPropertyName("maxDelegationDepth")]
public int MaxDelegationDepth { get; set; } = 3;
/// <summary>
/// Wie lange ein delegierter Aufruf höchstens auf einen belegten Zielagenten
/// wartet, in Sekunden (#461). Danach kommt eine Absage statt weiterer Wartezeit.
///
/// Gilt nur für Delegationen. Ein vom Menschen angestoßener Chat reiht sich
/// weiterhin ein und wartet, so lange es dauert.
/// </summary>
[JsonPropertyName("delegationGateTimeoutSeconds")]
public int DelegationGateTimeoutSeconds { get; set; } = 120;
/// <summary>
/// #682: How many scheduled task runs may be in flight at the same time. Due tasks
/// beyond this wait in order of priority and due time and start as soon as a run
/// finishes.
///
/// <para><b>Why a cap at all.</b> Cron expressions pile up on round times. Without a
/// cap, fifty due tasks meant fifty simultaneous model calls — provider rate limits,
/// a cost spike, and all fifty passing the instance budget check together, because
/// that check runs per run before it starts.</para>
///
/// <para>Per instance for now; per unit comes with #685. Values below 1 count as 1.
/// A run the human starts by hand does not wait for a free slot, but it occupies one
/// while it runs.</para>
/// </summary>
[JsonPropertyName("maxParallelTaskRuns")]
public int MaxParallelTaskRuns { get; set; } = 4;
[JsonPropertyName("agents")]
public List<AgentConfig> Agents { get; set; } = new();
[JsonPropertyName("services")]
public List<ServiceConfig> Services { get; set; } = new();
}
public sealed class TelegramClientConfig
{
[JsonPropertyName("apiId")]
public int ApiId { get; set; }
[JsonPropertyName("apiHash")]
public string ApiHash { get; set; } = "";
[JsonPropertyName("phoneNumber")]
public string PhoneNumber { get; set; } = "";
[JsonPropertyName("password2FA")]
public string? Password2FA { get; set; }
}