Budgetende, Stufe 1: Ausfall vermerken, Regel je Agent, mitnehmen statt anrennen (#716)

Bisher teilte sich eine Budgetablehnung den Ausgang NichtGelaufen mit dem
Herunterfahren. Einmalige Aufgaben wurden den Rest des Tages alle 30 Minuten
erneut abgelehnt (jedes Mal ein Beleg) und liefen nach Mitternacht still zu
Lasten des neuen Tages. Wiederkehrende verfielen still.

- TaskLauf.KeinBudget als eigener Ausgang; der Dispatcher bildet
  BudgetExceeded darauf ab.
- Vier Felder an der Aufgabe: BudgetMissCount, LastBudgetMissAt,
  DeferredUntil, CatchUpPending (Spalten werden fuer bestehende Datenbanken
  nachgeruestet).
- Einmalige Aufgaben werden mitgenommen: Der Termin bleibt erhalten, faellig
  ist die Aufgabe wieder ab der naechsten lokalen Mitternacht, nicht vorher.
- Wiederkehrende Aufgaben folgen der Regel des Agenten
  (budget.whenExhausted): expire vermerkt den Ausfall und laesst den Termin
  verfallen; catchUpOnce fasst alle verpassten Termine zu einem Nachholauf am
  naechsten Tag zusammen. Ein regulaer faelliger Termin deckt das Nachholen
  mit ab, es bleibt bei einem Lauf.
- Der neue Tag hat Vorfahrt: Mitgenommenes und Nachholaeufe kommen nach allem
  regulaer Faelligen, unabhaengig von der Prioritaet.
- Beim ersten Takt eines neuen Tages eine Zusammenfassung je Agent im Log.
- Sichtbar: Die Aufgabenkarte der WebUI zeigt Ausfaelle, Mitnahme und
  Nachholen; die Regel ist im Desktop je Agent einstellbar. Nebenbei sind
  "Next:"/"Last:" auf der Karte ins Sprachpaket gewandert.

Stufen 2 bis 4 (Reservierung, Spielraum statt pauschalem Anteil, Vorbeugen)
folgen.

Tests: BudgetMissTests (9 neu). 1.238 gruen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-24 12:03:15 +02:00
co-authored by Claude Opus 5.5
parent b53396a754
commit 5f3a85595b
18 changed files with 624 additions and 15 deletions
@@ -1,3 +1,4 @@
using ClawdDotNet.Core.Tasks;
using System.Collections.ObjectModel;
using Avalonia.Threading;
using ClawdDotNet.App;
@@ -77,6 +78,13 @@ public sealed partial class AgentItemViewModel : ViewModelBase
[ObservableProperty]
private long _dailyTokens;
/// <summary>#716: What happens to a recurring task's run when the budget is exhausted.</summary>
[ObservableProperty]
private string _whenExhausted = "expire";
/// <summary>The values <see cref="AgentBudget.WhenExhausted"/> understands.</summary>
public IReadOnlyList<string> WhenExhaustedOptions { get; } = ["expire", "catchUpOnce"];
public ObservableCollection<ToolItemViewModel> AvailableTools { get; } = [];
/// <summary>
@@ -101,6 +109,7 @@ public sealed partial class AgentItemViewModel : ViewModelBase
_timeoutSeconds = config.LoopGuard.TimeoutSeconds;
_dailyCostUsd = config.Budget.DailyCostUsd;
_dailyTokens = config.Budget.DailyTokens;
_whenExhausted = config.Budget.MissPolicy == BudgetMissPolicy.CatchUpOnce ? "catchUpOnce" : "expire";
foreach (var tool in KnownTools)
{
@@ -137,6 +146,7 @@ public sealed partial class AgentItemViewModel : ViewModelBase
Config.LoopGuard.TimeoutSeconds = TimeoutSeconds;
Config.Budget.DailyCostUsd = DailyCostUsd;
Config.Budget.DailyTokens = DailyTokens;
Config.Budget.WhenExhausted = WhenExhausted;
}
}
@@ -208,6 +208,15 @@
<TextBox Text="{Binding DailyTokens}" />
</StackPanel>
</Grid>
<StackPanel>
<TextBlock Classes="label" Text="{Binding L[desktop.agents.whenExhausted]}" />
<ComboBox HorizontalAlignment="Stretch"
ItemsSource="{Binding WhenExhaustedOptions}"
SelectedItem="{Binding WhenExhausted}" />
<TextBlock Classes="caption" TextWrapping="Wrap"
Text="{Binding L[desktop.agents.whenExhaustedHint]}" />
</StackPanel>
</StackPanel>
</StackPanel>
+2
View File
@@ -64,6 +64,8 @@
"desktop.agents.maxToolResultChars": "Max. Tool-Ergebnis (Zeichen)",
"desktop.agents.dailyCost": "Tagesbudget ($ USD)",
"desktop.agents.dailyTokens": "Tagesbudget (Tokens)",
"desktop.agents.whenExhausted": "Wenn das Budget erschöpft ist (wiederkehrende Aufgaben)",
"desktop.agents.whenExhaustedHint": "expire: Der verpasste Lauf verfällt und wird an der Aufgabe vermerkt. catchUpOnce: Alle verpassten Läufe werden am nächsten Tag durch einen Lauf nachgeholt, nach der regulären Arbeit des Tages. Einmalige Aufgaben werden immer auf den nächsten Tag mitgenommen.",
"desktop.agents.allowedToolsFor": "ZUGELASSENE WERKZEUGE FÜR {0}",
"desktop.agents.thTool": "WERKZEUG",
"desktop.agents.thDescription": "BESCHREIBUNG",
+2
View File
@@ -64,6 +64,8 @@
"desktop.agents.maxToolResultChars": "Max Tool Result (chars)",
"desktop.agents.dailyCost": "Daily Budget ($ USD)",
"desktop.agents.dailyTokens": "Daily Budget (Tokens)",
"desktop.agents.whenExhausted": "When the budget is exhausted (recurring tasks)",
"desktop.agents.whenExhaustedHint": "expire: the missed run lapses and is recorded on the task. catchUpOnce: all missed runs are caught up by one run the next day, after the day's regular work. One-time tasks are always carried over to the next day.",
"desktop.agents.allowedToolsFor": "ALLOWED TOOLS FOR {0}",
"desktop.agents.thTool": "TOOL",
"desktop.agents.thDescription": "DESCRIPTION",