Instanzmanager: Weboberflaeche kennt Instanzen, geordnetes Ende, Puls (#528, #531)

Weboberflaeche (#531): X-Instanz an der einen fetch-Stelle, Instanzauswahl in der
Kopfzeile, Verwaltungsansicht mit Anlegen/Starten/Anhalten/Fenster, Herkunft der
Ereignisse, Sicherungs- und Einstellungsansicht. 402/409/502 werden eigen behandelt.

Sicherungen im Manager (#530): Auflisten, Anlegen, Wiederherstellen, Loeschen. Das
Zurueckspielen haelt die Instanz vorher an und startet sie danach wieder.

Geordnetes Ende (Konzept 9.8). Zwei Befunde waren der Anlass:

Der Manager konnte eine Instanz MIT FENSTER nie geordnet beenden.
Control.ShutdownRequested war nur in HeadlessRun verdrahtet, nicht in der
Avalonia-Anwendung - und ein Fenster ist die Vorgabe. Die Bitte wurde mit 202
beantwortet, niemand hoerte zu, und der Manager schoss den Prozess nach Ablauf
seiner Frist jedes Mal ab.

InstanceSupervisor.StopAsync hielt Socket-Schweigen fuer einen Todesnachweis und
meldete "beendet", ohne den Prozess anzufassen. Bei einer verstummten Instanz ist
der Socket von vornherein still - der Neustart haette einen zweiten Prozess auf
dieselbe state.db gesetzt. Genau der Schaden, gegen den #528 gebaut ist.

- ShutdownCoordinator: erst die Tuer schliessen, dann auf offene Arbeiten warten,
  Frist zwei Minuten. Vorher brach TaskScanner.DisposeAsync mitten in ProcessAsync
  ab, sodass die Aufgabe bis zum Lease-Ablauf auf "running" stehenblieb.
- GET /api/instanz/puls hinter dem Socket-Gate: Phase und offene Arbeiten. Weiter
  gefragt statt gemeldet - ein Puls, den die Instanz von sich aus schickt, fehlt
  genau dann, wenn sie haengt.
- Eskalationsleiter: drei Durchgaenge ohne Antwort, einmal um ein Ende bitten,
  nach 30s beenden und den Vollzug abwarten, gemeldet als Absturz.
- EnsureDeadAsync vor jedem Start.

Weiteres aus dem Review:
- .gitignore verankert: "Instances/" passte auch auf src/ClawdDotNet.Core/Instances/.
  InstanceEndpoint.cs und InstanceLayout.cs waren dadurch nie eingecheckt.
- Das WebUI-Token stand im Protokoll des Managers. Konsole ja, Datei nein.
- Ein Klick auf "Instanz anlegen" loeste requestSubmit() zweimal aus.
- [Obsolete] auf WebServerPort wieder entfernt: sechs Warnungen, die niemand
  abstellen konnte, solange E-M5 nicht fertig ist.
- Die Wiederherstellung bricht nicht mehr ab, wenn der Browser weggeht.
- Entwicklungspfade zum Instanzmanager aus Segmenten statt mit Rueckstrichen, und
  der Port kommt aus manager.json statt fest aus der Zeile.

Offen und gemeldet: --managed hebelt die Lizenzpruefung aus (#535).

806 Tests gruen, 0 Warnungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-10 11:15:59 +02:00
co-authored by Claude Opus 5
parent d44bbeac1d
commit dcffcdcff5
48 changed files with 4852 additions and 171 deletions
+84 -5
View File
@@ -290,11 +290,9 @@ sondern die Prüfung aus E-M3.
ja, aber nur, wenn die Instanz es verlangt. Der Schalter heißt „Autostart durch
Manager" und steht in der Instanzdatei; siehe Abschnitt 9.7.
**Was ist mit einer Instanz, die verstummt?** Der Autostart greift nur bei einem
Absturz. Eine stumme Instanz lebt noch — sie neu zu starten hieße, sie erst
abzuschießen, und ob der Manager das von sich aus tun darf, ist nicht entschieden.
Bis dahin meldet er sie und lässt sie stehen. Genau dieser Zustand war der Anlass
(1.1), insofern ist die Frage nicht akademisch.
**Was ist mit einer Instanz, die verstummt? — Entschieden** (2026-09-10): Ja, der
Manager darf sie abschießen — aber erst am Ende einer Leiter, und nie ohne
Nachweis. Siehe Abschnitt 9.8.
**Der Aufhänger aus 1.1 ist nicht geklärt** (#529). Er verschwindet mit dem
Umbau vermutlich von selbst, weil es den Selbst-Neustart nicht mehr gibt — aber
@@ -835,3 +833,84 @@ Diese drei Punkte sind **C#** und warten auf den Kern-Agenten. Nicht anfangen:
2. **Lizenzprüfung aus der Instanz nehmen** (E-M3 vollständig). Braucht vorher einen
Handschlag, an dem eine Instanz erkennt, ob ein Manager da ist.
3. **Verstummte Instanzen neu starten?** Nicht entschieden — siehe Abschnitt 7.
### 9.8 Geordnetes Ende und Puls
Entschieden am 2026-09-10, aus zwei Befunden derselben Sitzung.
**Der erste: Jedes Beenden war ein Abschneiden.** `TaskScanner.DisposeAsync` bricht den
laufenden Takt über sein Token ab — mitten in `ProcessAsync`, also *nachdem* eine Aufgabe
beansprucht wurde und *bevor* `CompleteClaimAsync` sie wieder freigibt. Sie steht danach
auf `running` und bleibt dort, bis das Lease abläuft. Ein Agentenlauf mitten im
Werkzeugaufruf verlor seine Antwort ganz.
**Der zweite, schlimmere: Der Manager konnte eine Instanz mit Fenster überhaupt nicht
geordnet beenden.** `Control.ShutdownRequested` war nur kopflos verdrahtet
(`HeadlessRun`), nicht in der Avalonia-Anwendung — und ein Fenster ist die Vorgabe
(`InstanceWindows`). Die Bitte über den Socket wurde angenommen, mit 202 beantwortet, und
niemand hörte zu. Der Manager wartete seine Frist ab und schoss den Prozess dann **jedes
Mal** ab. Das geordnete Ende gab es auf dem Papier.
#### Erst die Tür, dann warten
`ShutdownCoordinator` (in `Core/Instances`) schließt zuerst alle Arbeitsquellen und wartet
danach, bis nichts mehr offen ist. Die Reihenfolge ist der ganze Trick: Andersherum
beansprucht der Scanner im nächsten Takt die nächste Aufgabe, und die Zahl der offenen
Arbeiten geht nie auf null.
Zwei Quellen sind angemeldet — Agentenläufe und Aufgaben. Beide können genau zweierlei:
keine neue Arbeit mehr annehmen und sagen, wie viel noch läuft. Was daraus folgt,
entscheidet der Koordinator. Die Frist (zwei Minuten) ist eine Zusage, keine Hoffnung:
Danach endet die Instanz, ob fertig oder nicht — sonst hinge sie an einem Agentenlauf, der
auf ein Modell wartet, das nicht antwortet. Was dann noch offen war, steht im Protokoll.
Beide Wege dorthin gibt es wirklich: Der Manager bittet über den Socket, und gleichzeitig
kann jemand am Fenster auf das Kreuz klicken. Ein zweiter Aufruf wartet deshalb auf
denselben Vorgang, statt einen neuen anzustoßen.
#### Der Puls
Der Manager fragte bisher `/api/info` und wusste danach nur, dass überhaupt jemand
geantwortet hat. Das reicht, um „läuft" von „tot" zu unterscheiden — aber nicht für die
beiden Fälle, die gleich aussehen und gegensätzlich zu behandeln sind: **eine Instanz, die
hängt, und eine, die gerade aufräumt.** Die erste gehört nach einer Frist abgeschossen,
die zweite in Ruhe gelassen.
`GET /api/instanz/puls` liegt hinter dem Socket-Gate, gehört also dem Manager allein, und
nennt zusätzlich Phase und offene Arbeiten. Bewusst weiter **gefragt und nicht gemeldet**:
Ein Puls, den die Instanz von sich aus schickt, fehlt genau dann, wenn sie hängt — und
dann kann der Manager „hängt" nicht von „hat es vergessen" unterscheiden. Wer fragt, weiß,
wann er keine Antwort bekommen hat.
#### Die Leiter
| Stufe | Was passiert |
|---|---|
| 3 Durchgänge ohne Antwort | `Unresponsive`, gemeldet. Vorher nicht — ein Agentenlauf, der die Maschine auslastet, ist kein Vorfall |
| sofort danach | Einmal über den Socket um ein Ende bitten. **Ein Versuch, mehr nicht** — wer eben auf demselben Socket geschwiegen hat, hört wahrscheinlich auch das nicht. Es kann ein einzelner hängender Endpunkt sein und nicht der ganze Prozess; gewartet wird auf die Uhr, nicht auf die Antwort |
| nach 30 s (`silentKillGraceSeconds`) | Der Prozess wird beendet — und der Vollzug **abgewartet**. `Kill()` ist eine Bitte an das Betriebssystem, keine vollzogene Tatsache |
| danach | Gemeldet als **Absturz**, nicht als geordnetes Ende. Gewollt hat es niemand — und nur so greift der Autostart |
| vor jedem Start | `EnsureDeadAsync`: Antwortet noch jemand auf dem Socket? Lebt das Prozesshandle noch? Solange ja, wird **nicht** gestartet |
Antwortet die Instanz zwischendurch wieder, war es ein Aussetzer, und niemand wird
abgeschossen (`Recovered`).
#### Der Fehlschluss, der hier begraben liegt
`StopAsync` brach seine Warteschleife ab, sobald der Socket nicht mehr antwortete, meldete
„beendet" und gab `true` zurück — **ohne den Prozess anzufassen.** Bei einer verstummten
Instanz ist der Socket aber von vornherein still: Der Aufruf meldete sofort Erfolg, der
Prozess lief weiter, und der Neustart setzte einen zweiten auf dieselbe `state.db`.
Schweigen ist nur dann ein Todesnachweis, wenn es keinen anderen Zeugen gibt. Hält der
Manager ein Prozesshandle und sagt das „lebt noch", dann lebt der Prozess — er antwortet
bloß nicht mehr. Nur bei einer wiederaufgenommenen Instanz ohne Handle muss das Schweigen
reichen, weil mehr nicht zu haben ist.
#### Und die Fristen greifen ineinander
Die Instanz gibt sich zwei Minuten zum Auslaufen, der Manager wartet bis zu fünf
(`maxDrainWaitSeconds`), solange sie **meldet**, dass sie aufräumt. Wäre seine Frist
kürzer, schösse er mitten in das Aufräumen hinein, um das er gerade gebeten hat. Und
trotzdem endlich: Eine Instanz, die dauerhaft „ich räume noch auf" sagt, hängt genauso wie
eine, die schweigt.