Zielkette, Stufe 2: ruhende Aufgabenbaeume werden geprueft (#714)

- TaskTreeWatch findet offene Aufgaben, deren ganzer Teilbaum zur Ruhe
  gekommen ist: nichts laeuft, nichts wird gleich gestartet (eine todo an
  einen Menschen bewegt sich nicht von selbst). Nur der oberste ruhende Baum
  zaehlt. Rein und ohne Instanz pruefbar.
- Erkennen ist Code, Deuten ist Modell: Die Leitung der Einheit, zu der der
  Baum gehoert (sonst die darueber, sonst ein Mensch), bekommt eine
  Pruefaufgabe mit dem Baum und den typischen Fehlhalten. Sie darf Arbeit
  zurueckgeben, nicht selbst nachbessern.
- Einmal je Ruhezustand: Ein Fingerabdruck des Baums im StateStore. Wer den
  Stillstand hinnimmt, wird nicht erneut gefragt; erst wenn sich der Baum
  bewegt hat und wieder ruht. Die Pruefaufgabe steht neben dem Baum, nicht
  darin.
- Der Scanner ruft die Pruefung je Takt, sie schaut alle 30 Minuten.

Erster Baustein von #646 (Erkennen); die uebrigen Muster dort folgen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-24 20:19:51 +02:00
co-authored by Claude Opus 5.5
parent b22272345e
commit cd244b8cbd
4 changed files with 436 additions and 2 deletions
+8 -1
View File
@@ -137,6 +137,7 @@ public sealed class TaskScanner : IAsyncDisposable
private readonly ILateRunGate? _lateRunGate;
private readonly IBudgetOutlook? _outlook;
private readonly ITaskTreeWatch? _treeWatch;
/// <summary>How often the day ahead is looked at — a forecast, not a watchdog.</summary>
public static readonly TimeSpan OutlookInterval = TimeSpan.FromMinutes(30);
@@ -163,8 +164,10 @@ public sealed class TaskScanner : IAsyncDisposable
int maxParallelRuns = DefaultMaxParallelRuns,
Func<TaskItem, BudgetMissPolicy>? budgetMissPolicy = null,
ILateRunGate? lateRunGate = null,
IBudgetOutlook? budgetOutlook = null)
IBudgetOutlook? budgetOutlook = null,
ITaskTreeWatch? treeWatch = null)
{
_treeWatch = treeWatch;
_repo = repo;
_dispatcher = dispatcher;
_market = market ?? new AlwaysOpenMarketCalendar();
@@ -286,6 +289,10 @@ public sealed class TaskScanner : IAsyncDisposable
SummarizeMissesIfNewDay(now);
await WarnAboutShortfallsAsync(candidates, now, ct);
// #714: resting task trees are looked at — the watch decides itself how often.
if (_treeWatch is not null)
await _treeWatch.CheckAsync(now, ct);
var due = new List<(TaskItem Task, string Occurrence, bool Late)>();
foreach (var task in candidates)
{