335 lines
19 KiB
Markdown
335 lines
19 KiB
Markdown
# Taskboard — Aufgaben statt Delay-Schleifen
|
|
|
|
Setzt A1 aus der [Roadmap](Roadmap.md) um. Das Taskboard ist das Fundament, auf dem
|
|
Audit (A3), Staging (A2), Marktkalender (C1) und das Ergebnisregister (C7/C8)
|
|
aufsetzen. Es löst zugleich sechs Altpunkte auf einmal (F-A5 Queue, F-A4 Run-Historie,
|
|
B8 Rekursion, B6 `Task.Delay`-Überlauf, B7 Cron in Lokalzeit, T7 `RunAsync` vs.
|
|
`ChatAsync`).
|
|
|
|
Aufbau analog zum [Memory-Konzept](Memory-Konzept.md): erst warum die vorhandenen
|
|
Mechanismen nicht reichen, dann Dateiformat, Wahrheitsaufteilung, Scanner-Verhalten,
|
|
Invarianten, Migration.
|
|
|
|
## Das Problem
|
|
|
|
Heute gibt es zwei getrennte, je für sich unzureichende Wege, einen Agenten Arbeit
|
|
tun zu lassen:
|
|
|
|
- **Der Cron-Scheduler** ([`AgentScheduler`](../src/ClawdDotNet.Core/Scheduling/AgentScheduler.cs),
|
|
[`ToolJobScheduler`](../src/ClawdDotNet.Core/Scheduling/ToolJobScheduler.cs)) hängt starr
|
|
am Agenten: eine Cron-Zeile je Agent, ausgeführt über ein `Task.Delay` bis zum
|
|
nächsten Termin. Ein jährlicher Termin bedeutet ein `Task.Delay` über Monate (B6).
|
|
Cron läuft in Lokalzeit ohne explizite Zone (B7). Ob mit oder ohne Kontext gelaufen
|
|
wird, entscheidet ein implizites Flag (`UseChatContext`, T7).
|
|
- **Die `coordination/*.md`-Dateien** im SharedWorkspace sind die improvisierte
|
|
Antwort der Agenten darauf, dass es kein Aufgabenmodell gibt: `task_*`-, `status_*`-
|
|
und `broadcast`-Dateien, per Konvention beschrieben, ohne Schema, ohne Claiming,
|
|
ohne Zustandsübergänge. Zwei Agenten, die dieselbe Datei „übernehmen", tun das ohne
|
|
jede Absicherung.
|
|
|
|
Delegation läuft heute über rekursives `send_message`/`spawn` — ein Agent ruft
|
|
synchron einen anderen, der wieder einen dritten (B8: Zyklengefahr, deshalb ein
|
|
`LoopGuard` als Notbremse). Es gibt keine Run-Historie am Auftrag und keine Queue.
|
|
|
|
## Warum ein neues Subsystem, nicht der vorhandene State-Store
|
|
|
|
Dieselbe Überlegung wie beim Gedächtnis: `IStateStore` ist eine Schlüssel-Wert-Tabelle
|
|
für kleine Marker. Ein Aufgabenmodell mit Status, Zuweisung, Abhängigkeiten und
|
|
Terminen darin abzulegen hieße, JSON in eine `Value`-Spalte zu schreiben — nicht
|
|
filterbar, nicht atomar claimbar, nicht auswertbar.
|
|
|
|
Der Kern ist eine **atomare Anspruchsnahme** (Claim). Genau das kann ein Dateisystem
|
|
nicht verlässlich und der Schlüssel-Wert-Store nicht ausdrücken, eine SQL-Zeile mit
|
|
einem bedingten `UPDATE` aber sehr wohl. Deshalb eine eigene Tabelle auf dem
|
|
vorhandenen [`SqliteStorage`](../src/ClawdDotNet.Core/Storage/SqliteStorage.cs) (WAL,
|
|
`busy_timeout`, prozessweite Schreib-Warteschlange) — dieselbe Grundlage, die schon
|
|
Gedächtnis, Zustand und Verbrauch teilen.
|
|
|
|
## Wahrheitsaufteilung — Datei ist Definition, DB ist Koordination
|
|
|
|
Die eine Entscheidung, an der alles hängt:
|
|
|
|
| Ebene | Wahrheit über | Wer schreibt |
|
|
|---|---|---|
|
|
| **Markdown-Datei** (Frontmatter + Rumpf) | die *Definition* der Aufgabe: Titel, Priorität, Assignee, Termin, Abnahme, Abhängigkeiten. Menschen- und agentenlesbar. | Mensch (Editor), Agent (`task_*`-Tool) |
|
|
| **SQLite-Tabelle `Tasks`** | den *Ausführungszustand*: Status, Claim, Lease, Last-Fired-Marker je Termin, Blocker-Auflösung. | ausschließlich das Taskboard selbst (Importer, Scanner, Tool) |
|
|
|
|
**Regel:** Für die Definition ist die Datei die Wahrheit. Für jede
|
|
Ausführungsentscheidung ist die DB die Wahrheit. Weichen beide ab (Absturz zwischen
|
|
DB-Claim und Datei-Schreiben), gewinnt die DB, und die Datei wird bei der
|
|
Reconciliation nachgezogen.
|
|
|
|
Warum nicht alles nur in die DB und die Datei als reine Projektion? Weil die Datei der
|
|
Bedienpunkt ist: Ein Mensch soll eine Aufgabe im Editor anlegen und ändern können, ein
|
|
Agent über sein Tool, und beides soll im SharedWorkspace sichtbar und versionierbar
|
|
bleiben. Warum nicht alles nur in Dateien? Weil das Claiming dort nicht atomar geht —
|
|
siehe oben. Die Aufteilung nimmt von beidem das Belastbare.
|
|
|
|
Der **Importer** ist die Brücke: Er liest die Frontmatter-Definition und spiegelt sie
|
|
idempotent in die DB-Zeile (`UPSERT` auf `task_id`). Er läuft beim Start (alle Dateien),
|
|
nach jeder `task_*`-Änderung (die betroffene Datei) und optional per
|
|
`FileSystemWatcher` mit Debounce (~500 ms, wie bei A4), damit von Hand editierte
|
|
Dateien zeitnah einfließen.
|
|
|
|
## Dateiformat
|
|
|
|
Aufgaben liegen als eine Datei je Aufgabe unter `SharedWorkspace/tasks/`. Der
|
|
Dateiname ist beschreibend (`recherche-nvda-earnings.md`); die stabile Identität ist
|
|
die `id` im Frontmatter, nicht der Name — so überlebt eine Aufgabe das Umbenennen.
|
|
|
|
```markdown
|
|
---
|
|
id: t-8f3a2c # stabil, beim Anlegen vergeben; Wahrheit der Identität
|
|
title: NVDA Earnings recherchieren
|
|
status: todo # backlog | todo | in_progress | in_review | done | canceled | blocked
|
|
type: work # work | approval | human_input
|
|
priority: 3 # 1 (niedrig) .. 5 (hoch)
|
|
assignee: "@crawler" # @new | @<agentId> | @human
|
|
when: # optional; fehlt = einmalige Aufgabe, sofort fällig
|
|
kind: cron # at | every | cron
|
|
value: "0 7 * * 1-5"
|
|
tz: Europe/Berlin # PFLICHT, wenn when gesetzt ist — kein Termin ohne Zone
|
|
require_approval: false # true = gilt erst nach Review (in_review) als done
|
|
acceptance: | # Abnahmekriterien, gegen die das Ergebnis geprüft wird
|
|
Aktuelle Zahlen mit Datum und Quelle, in SharedWorkspace/data/nvda.json abgelegt.
|
|
blocked_by: [t-4b1e] # diese Aufgabe startet erst, wenn alle Blocker done sind
|
|
onlyWhenMarketOpen: false # C1: Termin nur auslösen, wenn der Markt offen ist
|
|
---
|
|
|
|
Freitext-Rumpf: Auftragsbeschreibung, Kontext, Verweise. Geht als Aufgabenstellung
|
|
an den Agenten. Kommentare (Ergebnisse, Kritik, Reopen) werden unten angehängt.
|
|
```
|
|
|
|
Feldregeln:
|
|
|
|
- **`when.kind`**: `at` (einmaliger Zeitpunkt, ISO 8601), `every` (Intervall, z. B.
|
|
`30m`), `cron` (5-Felder-Ausdruck wie bisher). `tz` ist bei allen dreien Pflicht —
|
|
das ist die Antwort auf B7. Die vorhandene
|
|
[`CronExpression`](../src/ClawdDotNet.Core/Scheduling/CronExpression.cs) rechnet
|
|
heute zonen-blind in `DateTime.Now`; sie wird in einen zonen-bewussten Aufruf
|
|
gekapselt (nächsten Termin in `tz` bestimmen, dann in UTC vergleichen). Das
|
|
verzahnt sich mit R2 (`TimeProvider`) aus der [Teststrategie](Teststrategie.md).
|
|
- **`assignee`** ersetzt das `UseChatContext`-Flag durch eine explizite Angabe (T7):
|
|
- `@new` bzw. `@new:<agentId>` → frischer Lauf ohne Historie (`AgentEngine.RunAsync`).
|
|
Bloßes `@new` ist nur eindeutig, wenn die Instanz genau **einen** Agenten hat; sonst
|
|
benennt `@new:<agentId>` den Ziel-Agenten. Ist er nicht auflösbar, scheitert der
|
|
Dispatch mit klarer Meldung, statt einen falschen Agenten zu raten.
|
|
- `@<agentId>` → bestehender Agent mit seinem Kontext (`AgentEngine.ChatAsync`)
|
|
- `@human` → wartet auf einen Menschen; kein Modell-Lauf
|
|
- **`type`**: `work` (Standard), `approval` und `human_input`. Bei den letzten beiden
|
|
ist ein Mensch der Assignee; seine Antwort **ist** das Ergebnis und Input für
|
|
Folgeaufgaben. Kein Sonderpfad — nur ein Assignee, der kein Modell ist.
|
|
|
|
## Scanner-Verhalten
|
|
|
|
Ein einziger Takt (~60 s) statt vieler langer `Task.Delay`. Damit kennt das System
|
|
keine Monats-Delays mehr (B6), und ein verpasster Takt ist ein verpasster Termin, kein
|
|
Zeitbombe.
|
|
|
|
Je Takt:
|
|
|
|
1. **Fällige Termine bestimmen.** Aus jeder aktiven Task-Zeile den nächsten Termin in
|
|
ihrer `tz` berechnen und gegen „jetzt" prüfen. Ein Termin ist durch einen
|
|
**Occurrence-Key** eindeutig: `task_id` + geplante Feuerzeit (bei `at`/`cron`/`every`).
|
|
2. **Claim vor Lauf (at-most-once).** Bevor gelaufen wird, wird der Occurrence-Key
|
|
atomar beansprucht:
|
|
|
|
```sql
|
|
UPDATE Tasks
|
|
SET claim_token = @token, claimed_at = @now, last_occurrence = @occ, status = 'in_progress'
|
|
WHERE id = @id
|
|
AND status IN ('todo','backlog')
|
|
AND (last_occurrence IS NULL OR last_occurrence < @occ)
|
|
AND (claim_token IS NULL OR claimed_at < @leaseCutoff);
|
|
```
|
|
|
|
Genau eine Zeile betroffen = Anspruch gewonnen. Ein zweiter, gleichzeitiger Takt
|
|
findet die Bedingung nicht mehr erfüllt (Rowcount 0) und läuft **nicht** — so wird
|
|
ein doppelter Tick zu einem Lauf.
|
|
3. **Dispatch nach Assignee.** `@new` → `RunAsync`; `@<agent>` → `ChatAsync`; `@human`
|
|
→ kein Lauf, Status bleibt/wird `in_review` bzw. `todo`, die UI zeigt die Aufgabe
|
|
als wartend. Der Rumpf (+ Abnahmekriterien) ist die Nachricht an den Agenten.
|
|
4. **Abschluss verbuchen.** Last-Fired-Marker setzen (`last_occurrence = @occ`), Status
|
|
fortschreiben, Claim lösen. Bei `require_approval` → `in_review` statt `done`. Der
|
|
Lauf wird am Task verknüpft (Grundlage für A3-Receipts und die Run-Historie F-A4).
|
|
|
|
**Kein Retry-Sturm.** Der Marker wird auch bei einem **fehlgeschlagenen** Lauf gesetzt:
|
|
ein fehlgeschlagener Lauf bleibt der einzige Versuch für diesen Termin. Wiederholung
|
|
ist eine bewusste Entscheidung (neuer Termin oder Reopen), kein Automatismus. Das
|
|
schützt vor einem Agenten, der bei jedem Takt erneut in denselben Fehler läuft und
|
|
Budget verbrennt.
|
|
|
|
**Serialisierung mit den Chat-Läufen (B2).** Der Scanner ruft die Engine über
|
|
dieselben Einstiegspunkte wie WebView, ToolJob und AgentComm. `ChatAsync` ist bereits
|
|
je Agent über ein `SemaphoreSlim`-Gate serialisiert
|
|
([`AgentEngine`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs)) — der Scanner fügt
|
|
sich dort ein, statt einen zweiten, konkurrierenden Pfad in den geteilten
|
|
Konversationskontext aufzumachen. Der Claim ist eine **DB**-Grenze (welcher Termin wird
|
|
behandelt), das Agent-Gate eine **Kontext**-Grenze (kein verschränkter Nachrichtenstrom).
|
|
Beide werden gebraucht; keine ersetzt die andere.
|
|
|
|
### Startup-Reconciliation
|
|
|
|
Beim Start wird Soll (Frontmatter) gegen Ist (DB-Marker/Claims) abgeglichen, statt
|
|
verpasste Läufe still zu überspringen:
|
|
|
|
- Alle Task-Dateien importieren (UPSERT), gelöschte Dateien in der DB als `archived`
|
|
markieren.
|
|
- **Stale Claims freigeben:** Ein Claim, dessen `claimed_at` älter ist als die
|
|
Lease-Dauer (ein während des Laufs abgestürzter Prozess), wird verworfen — die
|
|
Bedingung `claimed_at < @leaseCutoff` im Claim-`UPDATE` erledigt das ohnehin, die
|
|
Reconciliation stellt den Status zusätzlich von `in_progress` auf `todo` zurück.
|
|
- **Verpasste Termine erkennen:** Liegt der letzte planmäßige Termin nach dem
|
|
Last-Fired-Marker, ist ein Lauf ausgefallen. Er wird als solcher gemeldet (Log, später
|
|
A5), nicht heimlich verschluckt. Ob nachgeholt wird, ist Politik — Standard: einmal
|
|
nachholen, sonst würde ein über Nacht ausgeschalteter Rechner beim Start eine Welle
|
|
auslösen.
|
|
|
|
## Die drei Invarianten
|
|
|
|
Der Scanner-Kern ist in der Roadmap als der heikle Teil markiert (die Fehlerklasse, die
|
|
hier schon einmal schiefging). Er wird strikt gegen diese Invarianten gebaut und mit
|
|
Property-Tests (FsCheck, siehe Teststrategie) abgesichert:
|
|
|
|
1. **Nie zwei Claims auf einen Task-Termin.** Garantiert durch das bedingte `UPDATE`:
|
|
Rowcount ≤ 1 pro Occurrence-Key. Test: N parallele Claims auf denselben Key → genau
|
|
einer gewinnt.
|
|
2. **Kein Dispatch bei offenem Blocker.** Eine Aufgabe mit unerfüllten `blocked_by`
|
|
ist nicht `todo`, sondern `blocked`, und die Claim-Bedingung (`status IN
|
|
('todo','backlog')`) greift nicht. Test: Blocker offen → kein Lauf; letzter Blocker
|
|
`done` → genau ein Auto-Dispatch.
|
|
3. **Doppelter Tick = ein Lauf.** Zwei Takte im selben Fenster konkurrieren um denselben
|
|
Occurrence-Key; der Claim lässt nur einen durch. Test: zwei gleichzeitige
|
|
`ScanOnce` → ein Lauf, ein Marker.
|
|
|
|
Diese drei sind keine Kür, sondern die Bedingung dafür, dass der Kern mit einem
|
|
schwächeren Modell umgesetzt werden darf.
|
|
|
|
## Abhängigkeiten und Eskalation
|
|
|
|
- **`blocked_by` mit Auto-Dispatch:** Wird eine Aufgabe `done`, sucht das Board alle
|
|
Aufgaben, deren `blocked_by` sie enthält. Sind für eine davon **alle** Blocker `done`,
|
|
wechselt sie `blocked → todo` und der Scanner nimmt sie beim nächsten Takt auf.
|
|
- **Blocker-Eskalation:** Meldet ein Agent einen Blocker (`task_update status=blocked`
|
|
mit Begründung), fällt die Aufgabe und der Zuständige (Lead/Benutzer) wird
|
|
benachrichtigt. Bis A5 (Matrix) steht, geht das über den vorhandenen Log-/UI-Weg.
|
|
|
|
## Zusammenspiel mit Staging (A2) und Reopen
|
|
|
|
- **`require_approval` / `in_review`:** Eine Aufgabe mit `require_approval: true` gilt
|
|
nach dem Lauf nicht als `done`, sondern als `in_review`. Das ist der natürliche
|
|
Andockpunkt für A2: Die Freigabe erzeugt einen Folge-Task, der den Agenten mit dem
|
|
eingefrorenen Aufruf weckt (so bleibt der Lauf regulär beendet, kein pausierter
|
|
In-Memory-Zustand — genau die Architektur-Vorgabe aus der Roadmap-Einstufung für A2).
|
|
- **Reopen/Feedback:** Ergebnis + Kritik gehen per `task_comment` an **denselben**
|
|
Agenten zurück (`ChatAsync` in dessen Kontext), statt eine neue Aufgabe von vorn zu
|
|
beginnen. Die Aufgabe kehrt nach `todo`/`in_progress` zurück, der Verlauf am Task
|
|
bleibt erhalten.
|
|
|
|
## Agenten-Tool
|
|
|
|
Ein Tool `Taskboard` im Muster von `MemoryTool` (eine Aktion je Aufruf), das über einen
|
|
neuen `ITaskRepository` auf dem `AgentToolContext` arbeitet (analog `IMemoryRepository?
|
|
Memory`):
|
|
|
|
| Aktion | Zweck |
|
|
|---|---|
|
|
| `task_create` | Aufgabe anlegen — schreibt Datei **und** DB-Zeile (über den Importer). Vergibt die `id`. |
|
|
| `task_list` | Aufgaben filtern (Status, Assignee, Betreff). Gekappte, kontextschonende Ausgabe wie bei Memory. |
|
|
| `task_update` | Status/Felder ändern; `blocked` melden; Ergebnis eintragen. |
|
|
| `task_comment` | Kommentar/Kritik anhängen; mit Reopen den Zuständigen erneut wecken. |
|
|
|
|
Agent-zu-Agent-Delegation läuft künftig hierüber: Statt rekursivem `send_message` legt
|
|
ein Agent eine Aufgabe mit `assignee: @<other>` an. Das ist strukturell zyklenfrei (B8) —
|
|
eine Aufgabe ist ein Datensatz, kein synchroner Aufruf-Stack.
|
|
|
|
**Sicherheit:** Ein Task-Rumpf ist Prompt-Input für den Assignee. Fremdbestimmte Inhalte
|
|
(Ergebnisse anderer Tools, die in einen Task fließen) werden als Daten gerahmt, nicht als
|
|
Anweisung — dieselbe Linie wie K2. Irreversibles, das ein Task auslöst, läuft über A2.
|
|
|
|
## Migration
|
|
|
|
Die `coordination/*.md`-Dateien gehen im Taskboard auf. Die Altdateien haben kein Schema
|
|
(freies Markdown wie `# Task: …`, `## Status: ASSIGNED`), deshalb bewusst konservativ
|
|
(`CoordinationMigration`, beim Start ausgeführt):
|
|
|
|
- **`task_*`-Dateien** → einmalig als `backlog`-Aufgaben übernommen: Titel aus der
|
|
`# Task:`-Überschrift (sonst erste Überschrift, sonst Dateiname), das ganze Markdown
|
|
als Rumpf, `assignee: @human` als sicherer Default, bis ein Mensch sie zuordnet.
|
|
`backlog` (nicht `todo`), damit der Scanner nichts unbesehen ausführt.
|
|
- **`status_*`, `broadcast`, Incident-Berichte, `*.json`-Artefakte** → keine Aufgaben;
|
|
bleiben unangetastet (später nach A5/Matrix bzw. verfallen als Altbestand).
|
|
|
|
Idempotent durch **Verschieben statt Löschen**: eine übernommene `task_*`-Datei wandert
|
|
nach `coordination/migrated/` — die Historie bleibt, ein zweiter Start findet sie nicht
|
|
mehr. Vor der Migration greift die übliche Regel: frisches Backup.
|
|
|
|
## Ein Takt für alles — Ablösung der Alt-Scheduler
|
|
|
|
Der Scanner ist der **einzige** periodische Treiber. Die früheren `AgentScheduler` und
|
|
`ToolJobScheduler` (zwei `Task.Delay`-Schleifen mit B6/B7, ungetestet) sind **gelöscht** —
|
|
alles Periodische ist jetzt ein Task:
|
|
|
|
- **Geplanter Agent-Lauf** (früher `scheduler`-Config) → ein normaler Task mit
|
|
`when: cron` und Assignee `@new:<agent>`.
|
|
- **Tool-Job-Poll** (früher `toolJobs`-Config, z. B. `telegram_poll`) → ein Task vom Typ
|
|
**`tool_job`** mit `tool_name`/`job_type`. Beim fälligen Termin tickt der Dispatcher den
|
|
`IToolJobProvider` und weckt den Zielagenten (Assignee) nur, wenn der Tick etwas meldet —
|
|
mit oder ohne Kontext, je nach `ToolJobResult`.
|
|
|
|
Zwei Feinheiten, die dabei geradegezogen wurden:
|
|
|
|
- **Wiederkehrende Tasks** (`cron`/`every`/`tool_job`) kehren nach dem Feuern auf `todo`
|
|
zurück statt auf `done` — sonst liefe ein Cron-Task nur ein einziges Mal. Der
|
|
Last-Fired-Marker verhindert weiterhin, dass **derselbe** Termin doppelt feuert.
|
|
- **`backlog` ist ein Halte-Status**: Der Scanner claimt nur `todo`. Eine Aufgabe in
|
|
`backlog` (frisch importiert, migriert, oder ein deaktivierter Poll) ruht, bis ein
|
|
Mensch sie auf `todo` setzt.
|
|
|
|
Die Alt-Konfiguration (`scheduler`, `toolJobs`) wird beim Start einmalig und
|
|
nicht-destruktiv in Tasks migriert (`SchedulerTaskMigration`, stabile Ids
|
|
`sched-<agent>` / `tj-<agent>-<job>`). Ein manuelles „Jetzt ausführen" im Host läuft über
|
|
`TaskScanner.RunTaskNowAsync`.
|
|
|
|
## Verzahnung
|
|
|
|
- **C1 Marktkalender:** `onlyWhenMarketOpen` gehört ins Frontmatter, nicht in einen
|
|
eigenen Mechanismus — der Scanner überspringt einen Termin, wenn der Markt zu ist.
|
|
- **A3 Audit/Receipts:** Jeder Lauf wird am Task verknüpft; der Abschluss-Beleg (Schritte,
|
|
Tokens, Kosten) fällt daraus ab und macht C7 weitgehend zum Abfallprodukt.
|
|
- **F-A5/F-A4:** Das Board **ist** die Queue; die verknüpften Läufe **sind** die Historie.
|
|
|
|
## Umsetzungsreihenfolge
|
|
|
|
Bewusst so geschnitten, dass der heikle Kern zuletzt und gegen grüne Invarianten kommt:
|
|
|
|
1. **Modelle + `ITaskRepository` + Schema + Frontmatter-Parser.** Reine, testbare
|
|
Bausteine. Der Parser wird eng gebaut (die Frontmatter ist ein kleiner, flacher
|
|
Satz aus Skalaren und kurzen Listen) — keine YAML-Bibliothek, passend zum
|
|
dependency-armen Stil des Projekts. 4.6-tauglich.
|
|
2. **`Taskboard`-Tool + Importer.** Datei ↔ DB, `task_*`-Aktionen. 4.6-tauglich.
|
|
3. **Scanner-Kern** (Claim, Dispatch, Reconciliation, Auto-Dispatch) — strikt gegen die
|
|
drei Invarianten, mit Property-Tests. Der in der Roadmap für Opus 5/Fable markierte
|
|
Teil; mit Opus 4.8 nur streng nach diesem Dokument und mit den Invarianten-Tests als
|
|
Netz.
|
|
4. **Migration** der `coordination/*.md`.
|
|
|
|
Neue Subsysteme kommen mit Tests nach der [Teststrategie](Teststrategie.md); die
|
|
Invarianten-Tests sind bei Punkt 3 die Absicherung, kein Nice-to-have.
|
|
|
|
## Offen
|
|
|
|
- **Nachhol-Politik verpasster Termine** — umgesetzt als „höchstens einmal nachholen":
|
|
der Scanner nimmt den jüngsten verpassten Termin, nicht jeden einzelnen. Eine frische
|
|
Aufgabe holt zudem keinen Termin von **vor** ihrer Anlage nach. Ob das je Task
|
|
abschaltbar sein soll (`catchUp: true|false`), ist offen.
|
|
- **Marktkalender (C1)** — der Scanner fragt eine `IMarketCalendar` (derzeit Platzhalter
|
|
„immer offen"). C1 liefert später den echten Kalender; die Verzahnung steht.
|
|
- **DST-Randfall** — eine bei der Zeitumstellung nicht existierende Ortszeit
|
|
(Frühjahr, „02:30") wird derzeit übersprungen statt verschoben. Für die geplanten
|
|
Termine unkritisch; die Härtung gehört zur Scheduler-Nacharbeit (Teststrategie R4/R5).
|
|
- **Priorität als Reihenfolge** — bei mehreren fälligen Aufgaben desselben Agenten
|
|
bestimmt `priority` die Reihenfolge; ob strikt oder gewichtet, ist noch offen.
|
|
- **Aufräumen** — `done`/`canceled`-Aufgaben nach einer Frist archivieren, damit
|
|
`tasks/` nicht zuwächst (dieselbe Überlegung wie „Verfall" beim Gedächtnis).
|