# Instanzmanager — Befund und Konzept Warum mehrere Instanzen heute nicht unabhängig voneinander laufen, und wie es stattdessen aussehen soll. Stand: 2026-09-08. Anlass: Beim Instanzwechsel hat sich die Anwendung aufgehängt und wurde von Windows beendet. Vorhaben: **#528**, zugehöriger Fehler: **#529**. Die Entscheidungen in Abschnitt 2 sind getroffen. Was danach kommt, folgt aus ihnen. --- ## 1 — Der Befund ### 1.1 Was passiert ist Am 2026-09-08 wurde aus der laufenden Anwendung heraus eine zweite Instanz angelegt und auf sie gewechselt: | Zeit | Quelle | Eintrag | |---|---|---| | 17:26:48 | Dateisystem | `Instance-Test2` angelegt | | 17:26:50 | `Lifetime.log` | `Application is shutting down...` | | 17:26:55 | `Diagnostics.log` | Zwei Ereignisströme beendet, 33 s und 776 s Laufzeit | | 17:27:45 | Windows-Ereignisprotokoll | `AppHangB1` — „hat aufgehört mit Windows zu interagieren und wurde geschlossen" | **Die Anwendung wurde nicht beendet, sie wurde erschlagen.** Bemerkenswert ist, was dabei *nicht* geschah: Zu diesem Prozess steht **keine einzige Protokollzeile** in den Instanzprotokollen — der Schreibpuffer wurde nie geleert. Keine Fehlermeldung, kein Bugtracker-Eintrag, kein Watchdog-Alarm. Der Vorfall ist ausschließlich rekonstruierbar, weil das Betriebssystem ihn festgehalten hat. **Das ist der Kern:** Meldeweg, Watchdog und Fehler-Stream leben *in* dem Prozess, über dessen Zustand sie berichten sollen. Fällt er hart, fällt der Meldeweg mit — genau in dem Fall, der gemeldet werden müsste. ### 1.2 Was heute geteilt ist, obwohl es getrennt sein müsste | Geteilt | Wirkung | Beleg | |---|---|---| | **Ein Protokollverzeichnis** | Alle Instanzen schreiben in dieselben Dateien, ohne Kennzeichnung, welche spricht | `Startup.log` enthält abwechselnd Zeilen von `Test` und `Test2` | | **Eine `Settings.json`** | Vier Stellen schreiben sie im laufenden Betrieb. Zwei Instanzen: die letzte gewinnt, stillschweigend | `LicenseGate`, `MainWindowViewModel`, `AgentsPageViewModel`, `SettingsPageViewModel` | | **Ein Sicherungsverzeichnis** | Ein Ordner für alle Archive. Die Aufbewahrung räumt dort nach dem Präfix `backup__` auf — und ein Präfix ist keine Grenze: `backup_Test_2_….zip` fängt mit `backup_Test_` an, `Test` wirft es also weg | `AppSettings.BackupDirectory` | | **Ein Sicherungs-Schalter** | Wer die tägliche Sicherung für eine Instanz einschaltet, schaltet sie für alle ein | `AppSettings.AutoBackupEnabled` | | **Ein Web-Port als Vorgabe** | Jede Instanz bringt `webServerPort: 7001` mit; die zweite kann nicht binden | `InstanceConfig.WebServerPort` | ### 1.3 Der Wechsel kann gar nicht anders enden Der Instanzmanager ist heute ein Fenster *innerhalb* einer laufenden Instanz. Wechseln heißt deshalb: Der Prozess fährt sich selbst herunter, startet sich mit `--instance` neu und beendet sich. Wer die Instanz wechselt, beendet zwangsläufig die, aus der heraus er es tut. --- ## 2 — Die Entscheidungen Getroffen am 2026-09-08. ### E-M1 — Der Weg zwischen Manager und Instanz **Entschieden: HTTP über einen Unix-Domain-Socket je Instanz, unter Windows über eine Named Pipe.** Der Socket liegt im Instanzverzeichnis (`/manager.sock`), Rechte 0600. Warum nicht TCP: Ports müssten verwaltet, vergeben und gegen Kollisionen gesichert werden — genau das Problem, an dem die heutige Vorbelegung `7001` scheitert. Ein Socket im Instanzverzeichnis braucht keine Vergabe, und die Zugriffsrechte stehen schon am Dateisystem. Warum trotzdem HTTP darüber: Weil der Manager Anfragen an die Instanzen **weiterleitet** (E-M5). Bleibt das Protokoll HTTP, ist das Weiterleiten eine Weiterleitung und keine Übersetzung — und die bestehenden Endpunkte, DTOs und der Ereignisstrom aus #494 gelten unverändert weiter. Kestrel kann auf beidem hören, `SocketsHttpHandler` kann auf beidem verbinden. ### E-M2 — Die Oberflächen **Entschieden: Jede Instanz behält ihr eigenes Desktop-Fenster, der Manager bekommt ein eigenes.** Die Instanzfenster laufen im Hintergrund und werden beim Wechsel nach vorn geholt. Ein Wechsel ist damit kein Neustart mehr, sondern ein Fensterwechsel. ### E-M3 — Lizenz **Entschieden: Die Lizenz liegt im Manager und wird dort geprüft.** Instanzprozesse starten nicht ohne aktiven Manager. Eine Lizenz erlaubt beliebig viele Instanzen. Folge: `LicenseGate` und `LicenseWatch` wandern aus dem Instanzprozess in den Manager. `AppHost.Callbacks.License` entfällt für Instanzen. ### E-M4 — Einstellungen **Entschieden: Der Manager kennt nur das Nötigste, alles andere gehört der Instanz.** Die Aufteilung steht in Abschnitt 6. ### E-M5 — Die Weboberfläche **Entschieden: Eine Adresse, ein Zugang, alle Instanzen.** Der Manager besitzt Port, Zugang und Rahmen. Anfragen zu Chat, Agenten, Aufgaben und Freigaben reicht er über E-M1 an die betroffene Instanz weiter. **Er leitet weiter, er implementiert nicht.** Keine Fachlogik im Manager — die Weiterleitung kennt Kennungen und Pfade, sonst nichts. ### E-M6 — Wenn der Manager ausfällt **Entschieden: Laufende Instanzen laufen weiter.** Der neu gestartete Manager findet sie über eine Datei im Instanzverzeichnis wieder und übernimmt die Aufsicht. Damit wird der Manager nicht zu dem Einzelpunkt, gegen den dieses ganze Vorhaben gebaut wird. Die Prüfung „gibt es einen Manager?" findet **beim Start** einer Instanz statt, nicht fortlaufend. Ausnahme: Ein **Lizenzwiderruf** ist kein Ausfall, sondern ein Urteil. Erfährt der Manager davon, hält er die Instanzen an. --- ## 3 — Der Schnitt ``` ┌──────────────────────────────────────────────────────────────┐ │ Instanzmanager (ein Prozess je Maschine) │ │ · eigenes Fenster + die eine Weboberfläche │ │ · Instanzen anlegen, starten, anhalten, entfernen │ │ · Aufsicht: lebt sie? stürzt sie wiederholt ab? │ │ · Lizenz · Update · DC-Master-Token → Sub-Token je Instanz │ │ · KEINE Datenbank. KEINE Fachlogik. KEINE Werkzeuge. │ └──────┬───────────────────────────────────────────────────────┘ │ manager.sock (HTTP über UDS / Named Pipe) ┌───┴────────┬────────────────┬───────────────┐ ▼ ▼ ▼ ▼ ┌────────┐ ┌────────┐ ┌────────┐ Fenster je Instanz │Instanz │ │Instanz │ │Instanz │ (laufen im Hinter- │ A │ │ B │ │ C │ grund, kommen beim │ │ │ │ │ │ Wechsel nach vorn) │ Engine · Werkzeuge · Taskboard · Staging · state.db │ │ eigenes Protokoll · eigenes Sub-Token · eigener Meldeweg │ │ Telegram · Rocket.Chat · Mail · FTP — ausschliesslich hier │ └────────┘ └────────┘ └────────┘ ``` ### 3.1 Der Manager hat keine Datenbank Alles, was er braucht, liegt als JSON im Projektverzeichnis: die Instanzliste, seine drei Einstellungen, je Instanz eine kleine Zustandsdatei (läuft sie? seit wann? mit welcher Prozesskennung? wie oft zuletzt abgestürzt?). `state.db` einer Instanz fasst er **nicht** an — die Datei gehört genau einem Prozess, und prozessübergreifend gilt die Schreibsperre von `SqliteStorage` nicht. ### 3.2 Werkzeuge laufen ausschließlich in der Instanz Telegram, Rocket.Chat, Mail, FTP, Datenbank — alle Werkzeuge gehören der Instanz und nur ihr. Der Manager kennt sie nicht und spricht nie mit ihnen. **Das gilt auch für die Telegram-Anmeldung.** Sie ist ein Rückruf in `AppHost.Callbacks`; mit E-M2 beantwortet ihn das **eigene Fenster der Instanz**. Über den Manager läuft davon nichts. (Eine frühere Fassung dieses Papiers hat den Rückruf als Beleg für Kopflosigkeit angeführt — das war missverständlich: Der Rückruf zeigt nur, dass die Anwendungsschicht ihre Umgebung nicht festschreibt.) --- ## 4 — Was der Manager protokolliert und meldet **Nur, was aus seiner Sicht relevant ist:** | Ereignis | Protokoll | Ans Deploymentcenter | |---|---|---| | Instanz gestartet | ja | Zustandswechsel | | Instanz geordnet beendet | ja | Zustandswechsel | | **Instanz abgestürzt** | ja | **Fehlermeldung stellvertretend** | | Instanz startet wiederholt neu | ja | Fehlermeldung | | Instanz antwortet nicht mehr auf dem Socket | ja | Fehlermeldung | | Lizenz ungültig, Update fehlgeschlagen | ja | Fehlermeldung | **Was die Instanz betrifft, bleibt in der Instanz.** Ein fehlgeschlagener Werkzeugaufruf, ein abgebrochener Agentenlauf, eine überschrittene Budgetgrenze — das protokolliert und meldet die Instanz selbst, mit ihrem eigenen Sub-Token. Der Manager sieht davon nichts und soll davon nichts sehen. Der Fall aus 1.1 ist genau der, für den es den Manager gibt: Die Instanz kann nichts mehr sagen, also sagt er es an ihrer Stelle — mit Prozesskennung, Beendigungsgrund und dem Ausschnitt aus ihrem Protokoll, der noch auf der Platte liegt. ### 4.1 Jede Instanz meldet weiterhin selbst Zwei Wege, mit Absicht: - Die **Instanz** meldet, solange sie lebt — eigener Fehler-Stream, eigener Heartbeat, eigener Watchdog-Monitor. Das Deploymentcenter führt Instanzen bereits über `source` + `instance` als eigene Monitore. - Der **Manager** meldet, was die Instanz nicht mehr melden kann. Jeder Weg deckt den Ausfall des anderen ab. Ein Manager, der jede Meldung durchreicht, wäre der neue Einzelpunkt. --- ## 5 — Lizenz und Token **Der Manager hält das Master-Token, die Instanzen halten Sub-Token.** Das Verfahren ist gebaut: `TokenProvisioner` tauscht über `POST /api/tokens/v1/provision` das Master-Token gegen ein Sub-Token je Instanz, mit den Rechten `watchdog:ping` und `bugtracker:report`. Rechte lassen sich dabei nur einschränken, nie erweitern; jedes Sub-Token ist einzeln widerrufbar. Zu tun bleibt der Umzug: Heute wird bei der Einrichtung provisioniert (`setup.json`, `"source": "provision"`), künftig beim Anlegen einer Instanz durch den Manager. **Achtung bei der Projektbindung.** Seit SDK 2.8.1 erben Watchdog-Monitore das Projekt ihres Ping-Tokens; ein Monitor mit `project_slug = NULL` erzeugt Befunde im Scheinprojekt „default", und `dc_findings(project_slug=…)` liefert für eine laufende, meldende Instanz dann **null Befunde**. Die Sub-Token müssen also projektgebunden ausgestellt werden — sonst sieht die Aufsicht aus wie „nichts auffällig". Wir stehen auf **SDK 2.6.0**, aktuell ist **2.8.2**. **#218 ist damit Voraussetzung, nicht Nacharbeit.** --- ## 6 — Einstellungen: die Aufteilung ### Der Manager (`manager.json`) | Feld | Warum es nicht in die Instanz kann | |---|---| | `webUiPort`, `webUiBinding`, `webUiToken` | Eine Weboberfläche für alle | | `licenseKey` | E-M3: die Lizenz gilt der Installation | | `deploymentcenterUrl`, `deploymentcenterMasterToken` | Aus ihm entstehen die Sub-Token | | `instancesDirectory` | Wo die Instanzen liegen — das muss er wissen, bevor es eine gibt | | `updateChannel` | Aktualisiert wird das Programm, nicht die Instanz | Mehr nicht. Farbschema, Teilerstellungen und Protokollstufe sind Werte einer Oberfläche und gehören zu der, die sie anzeigt. ### Die Instanz (`InstanceSettings.json`) Alles Übrige, und drei Felder ziehen aus den Anwendungseinstellungen dorthin um — sie waren nie anwendungsweit, sie sahen nur so aus: | Feld | Heute | Künftig | |---|---|---| | `logDirectory` | geteilt, alle Instanzen in denselben Dateien | `/Logs`, Vorbelegung | | `backupDirectory` + `backupKeepCount` | geteilt, Präfixkollision bei ineinanderliegenden Namen | je Instanz | | `autoBackupEnabled` + `autoBackupTime` | geteilt, ein Schalter für alle | je Instanz | | `webServerPort` | je Instanz, Vorbelegung 7001, Kollision | **entfällt** — es gibt nur noch den Socket | `autoBackupEnabled` und `autoBackupTime` standen in der ersten Fassung dieser Tabelle nicht. Sie sind mitgegangen, weil die Hälfte eines Blocks zu verschieben eine Einstellungsseite hinterlässt, auf der zwei von vier Sicherungsfeldern für alle Instanzen gelten und zwei nur für eine — und weil ein Schalter, der die Sicherung für alle einschaltet, derselbe Fehler ist wie ein Ordner für alle. `deploymentcenterEnvironment` bleibt bei der Instanz: Eine Maschine kann eine Produktions- und eine Testinstanz nebeneinander betreiben. --- ## 7 — Noch zu klären **Sicherungen im Manager?** Dafür spricht viel: Die gemeinsame Rotation aus 1.2 verschwände, und eine Sicherung ließe sich auch von einer Instanz ziehen, die gerade nicht läuft. Dagegen spricht eine Falle — **eine Sicherung zurückzuspielen heißt, in ein Instanzverzeichnis zu schreiben, während die Instanz womöglich läuft.** Das muss den Prozess erst anhalten, sonst schreibt man unter einer offenen `state.db` das Dateisystem um. Wenn es kommt, dann mit dieser Reihenfolge: anhalten, zurückspielen, starten — und nie ohne Rückfrage. **Wie stellt eine Instanz fest, dass ein Manager da ist?** Vorschlag: Der Manager startet sie und gibt ihr den Socketpfad mit; die Instanz verbindet sich einmal zurück und beendet sich, wenn das nicht gelingt. Kein Selbstzweck-Handschlag, sondern die Prüfung aus E-M3. **Startet der Manager eine abgestürzte Instanz wieder?** Bisher nicht — er merkt es und meldet es, mehr nicht. Dafür spricht, dass eine Instanz, die nachts um drei abstürzt, bis zum Morgen tot bleibt. Dagegen spricht die Schleife: Eine Instanz, die an einer kaputten Konfiguration scheitert, stürzt beim zehnten Versuch genauso ab wie beim ersten, und dann hat man statt einer toten Instanz eine tote Instanz und ein volles Protokoll. Wenn es kommt, dann mit Wartezeitverdopplung und einer Obergrenze, nach der er es sein lässt und das meldet. **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 „vermutlich" ist kein Befund. Zwei unbelegte Verdachtsmomente: `BackupScheduler` hat sechs `await` und kein einziges `ConfigureAwait(false)` und fängt sich damit den Oberflächenfaden ein; `ShutdownRequested` ist ein `async void`-Behandler. --- ## 8 — Was aus #494 wird Die Weboberfläche ist nicht umsonst gebaut. Sie zerfällt in zwei Teile, die beide bleiben: | Teil | Wohin | |---|---| | Endpunkte, DTOs, Maskierung, Ereignisstrom | **In die Instanz.** Statt auf einem TCP-Port hört Kestrel auf dem Socket. Der Code bleibt, die Bindung ändert sich | | `wwwroot` (Gemini), Anmeldung, Token, Rahmen | **In den Manager.** Dazu die Instanzauswahl und die Verwaltungsansichten | | Weiterleitung `/api/instanzen/{id}/…` → Instanz | **Neu im Manager** | Der Ereignisstrom ist die eine Stelle, die wirklich neu gedacht werden muss: N Instanzen, ein Strom zum Browser. Der Manager hält je laufender Instanz eine Verbindung und führt sie zusammen, mit der Instanzkennung an jedem Ereignis. --- ## 9 — Reihenfolge | Stufe | Inhalt | Woran man es misst | |---|---|---| | 1 ✔ | Einstellungen und Protokolle je Instanz trennen (Abschnitt 6) | Zwei Instanzen gleichzeitig, keine schreibt der anderen ins Protokoll | | 2 ✔ | Instanzprozess kopflos lauffähig, Kestrel auf dem Socket | Eine Instanz läuft ohne Fenster, der Socket antwortet | | 3 ◑ | Manager: anlegen, starten, anhalten, Prozessaufsicht, Lizenz, Sub-Token | Eine Instanz abschießen — der Manager merkt es und meldet es | | 4 ◑ | Weboberfläche zieht um, Weiterleitung, ein Ereignisstrom | Eine Adresse, ein Zugang, Wechsel ohne Neustart | | 5 | Instanzfenster als eigener Prozess, Wechsel holt das Fenster nach vorn | Kein Neustart mehr beim Wechseln | | 6 | Sicherungen im Manager (falls Abschnitt 7 dafür entscheidet) | Zurückspielen hält die Instanz an und startet sie wieder | **Stufe 1 ist die, die man überspringen möchte und nicht überspringen darf.** Solange alle Instanzen in dieselben Protokolldateien schreiben, ist jede Fehlersuche im Mehrinstanzbetrieb Ratearbeit — und genau in dieser Lage ist der Vorfall aus 1.1 entstanden. ### 9.1 Was Stufe 1 gebracht hat Fünf Schlüssel sind aus `AppSettings` in `InstanceSettings.json` gewandert: `logDirectory`, `backupDirectory`, `backupKeepCount`, `autoBackupEnabled`, `autoBackupTime`. Leer heißt jetzt `/Logs` und `/Backups`; ein relativer Wert zählt weiterhin gegen das Instanzverzeichnis (#375). **Keine Übernahme der Altwerte, aber eine Ansage.** Ein anwendungsweiter Pfad, in alle Instanzen kopiert, wäre wieder derselbe Ordner für alle. Stehen die Schlüssel noch in einer alten `Settings.json`, meldet `SettingsManager.AbandonedKeys` sie, und der Start protokolliert einmal, was nicht mehr gilt und wohin diese Instanz stattdessen schreibt. Beim nächsten Speichern verschwinden sie aus der Datei. **`webServerPort` bleibt vorerst stehen.** Es entfällt erst mit **Stufe 4**, wenn der Manager den Port übernimmt — nicht schon, sobald Kestrel den Socket hat. Eine frühere Fassung dieses Absatzes sagte „mit Stufe 2"; das war falsch gerechnet: Solange es keinen Manager gibt, der die Weboberfläche trägt, hieße Wegnehmen, sie ersatzlos abzuschalten. --- ## 10 — Was das für Vorhandenes bedeutet **„Kern als Dienst, Oberfläche als Client"** aus dem Roadmap-Rückstau wird überholt: Dort ging es um *einen* Kern und *eine* Oberfläche. Der dort notierte Auslöser — „lohnt sich, sobald der erste UI-Absturz eine Nacht Agentenarbeit kostet" — ist mit 1.1 eingetreten. **#214 (24/7-Betriebsform)** sagt heute „Kein kopfloser Betrieb: Die Oberfläche läuft laut Zielbetrieb immer mit". Das gilt nach E-M2 in anderer Form: Der Manager läuft immer mit, die Instanzfenster laufen mit, aber eine Instanz *kann* ohne Fenster laufen. **#218 (SDK 2.6.0 → aktuell)** wird Voraussetzung — siehe Abschnitt 5. ### 9.2 Was Stufe 2 gebracht hat **Kestrel hört auf dem Socket** — `/manager.sock` unter Linux, unter Windows eine Named Pipe, deren Name aus einem Fingerabdruck des Instanzpfades entsteht (`InstanceEndpoint`). Zwei Instanzen bekommen zwangsläufig zwei Adressen, ohne dass jemand etwas vergeben müsste. **Der Socket ist immer da, auch ohne Weboberfläche.** Er ist nicht Teil von #494, sondern die Steuerfläche der Instanz: `webUiEnabled` schaltet den TCP-Zugang, nicht den Socket. Was aus `WebUiHost` wurde, heißt deshalb jetzt `InstanceHttpHost` — ein Kestrel, zwei Zugänge. **Der Torwächter unterscheidet die Zugänge.** Über TCP gilt Token-Pflicht, über den Socket nicht: Dort steht die Grenze am Dateisystem (0600 beziehungsweise `CurrentUserOnly`), und wer die Socketdatei öffnen darf, sieht ohnehin in das Verzeichnis, in dem alles liegt, was ein Token schützen würde. **Woran die beiden auseinandergehalten werden, ist der Punkt, an dem ein Irrtum teuer wäre:** Bei Socket-Verbindungen stehen in `HttpContext.Connection` keine IP-Adressen — daraus zu schließen „keine IP, also Socket" wäre eine Vermutung an der Stelle, an der ein Torwächter steht, und ließe im Zweifel eine TCP-Anfrage ohne Token durch. Der Socket-Zuhörer hängt deshalb jeder Verbindung selbst eine Marke an (`LocalTransport`); wo sie fehlt, gilt Token-Pflicht. **Ein belegter Port kostet nur noch die Weboberfläche.** Vorher gab `TryStart` in dem Fall `null` zurück und die ganze HTTP-Fläche war weg. Das ist seit dem Socket die falsche Rangfolge: Ihn zu verlieren, weil eine zweite Instanz schon läuft, hieße die Instanz unerreichbar zu machen — in genau dem Fall, für den es den Manager gibt. **Kopfloser Betrieb** über `--headless --instance `. Die Verzweigung sitzt in `Program.Main` vor `BuildAvaloniaApp`, damit auf einem Server ohne Bildschirmsitzung kein Fenstersystem angefasst wird. Gefragt wird nichts: Instanz aus dem Argument, Lizenz über `NonInteractiveLicensePrompt`, Telegram-Anmeldung aus. Nachgemessen, nicht angenommen: Eine kopflose Instanz auf `Instance-Test` lief ohne Fenster, und eine rohe HTTP-Anfrage aus einem fremden Prozess (PowerShell, direkt auf die Pipe) bekam `200 OK` auf `/api/info` — ohne Token. **Zwei Lücken, die offen bleiben.** Die Socketdatei-Fälle (liegengebliebene Leiche, Aufräumen beim Beenden) sind unter Windows gegenstandslos und werden dort mit Begründung übersprungen — der Unix-Domain-Socket selbst ist auf der Entwicklungsmaschine also ungeprüft, obwohl er die Zielplattform ist. Und das geordnete Ende auf `SIGTERM` ließ sich hier nicht auslösen: Windows kennt das Signal für einen fensterlosen Prozess nicht. Beides gehört auf die Linux-Testmaschine. ### 9.3 Was Stufe 3 bisher gebracht hat **Die Aufsicht steht, Lizenz und Sub-Token fehlen noch.** Neu ist das Projekt `ClawdDotNet.Manager` mit genau **einem** Projektverweis: `ClawdDotNet.Core`. Das ist keine Sparsamkeit, sondern die halbe Architektur — `ClawdDotNet.App` schleppt sechzehn Werkzeugprojekte hinter sich her, und „keine Werkzeuge im Manager" soll eine Eigenschaft des Bauplans sein und keine Absichtserklärung. Damit `InstanceDirectoryManager` von beiden Seiten erreichbar ist, ist er nach `Core/Instances` gezogen; dorthin gehört er ohnehin, denn er beschreibt, wie eine Instanz auf der Platte liegt. **Der Socket ist die Auskunftsquelle, nicht die Prozessliste.** Das ist die eine Entscheidung, an der alles hängt. Die Prozessliste sagt, *dass* etwas da ist — sie sagt nicht, ob es noch arbeitet, und genau das war der Zustand aus 1.1: Die Anwendung stand einwandfrei in der Prozessliste, bis Windows sie als „reagiert nicht mehr" beendet hat. Ein Aufsichtsdurchgang fragt deshalb den Socket und kennt drei Antworten: | Socket | Prozess | Befund | |---|---|---| | antwortet | — | läuft | | schweigt | ist weg | **Absturz**, mit Abschiedscode | | schweigt | lebt | **verstummt** — nach genügend Fehlversuchen in Folge | Nicht beim ersten Aussetzer: Eine einzelne verpasste Antwort ist Alltag, und ein Alarm bei jedem Aussetzer ist nach einer Woche der Alarm, den niemand mehr liest. **Ein Start gilt erst, wenn die Instanz antwortet.** Zwischen „der Prozess läuft" und „die Instanz läuft" liegen Datenbank, Agenten und Kestrel. Wer nur auf `Process.Start` wartet, hält eine Instanz für gestartet, die gleich an einer kaputten Konfiguration scheitert. **Angehalten wird durch Bitten, nicht durch Schießen.** Neu auf der Instanzseite: `POST /api/instanz/beenden`, **ausschließlich über den Socket** — über TCP gibt es den Endpunkt nicht, auch mit gültigem Token nicht, und er antwortet dort mit 404 statt 403. Die Lebensdauer einer Instanz gehört dem Manager; eine Browsersitzung soll den Prozess, der sie bedient, nicht beenden können. Erst nach Ablauf der Frist wird hart beendet. **Wiederfinden nach einem Managerausfall (E-M6).** Die Zustandsdatei `/instance-state.json` ist der *Hinweis*, der Socket ist der *Beweis*: Erst wird gefragt, wer da antwortet, und nur wenn dieselbe Instanzkennung zurückkommt, wird die Prozesskennung aus der Datei überhaupt angefasst. Andersherum — Prozess suchen, finden, für die eigene Instanz halten — führt nach einem Neustart der Maschine geradewegs dazu, ein fremdes Programm zu beaufsichtigen und beim Anhalten zu beenden. Startzeit und Prozesskennung zusammen entlarven eine wiederverwendete Nummer. **Selbsttätiger Neustart ist bewusst nicht gebaut.** Die Abnahmebedingung lautet „merkt es und meldet es", und zum Neustarten trifft dieses Papier keine Entscheidung. Eine Instanz in einer Schleife wieder hochzufahren, die jedes Mal abstürzt, wäre die falsche Vorgabe für etwas, das niemand entschieden hat. **Das ist eine offene Frage** — siehe Abschnitt 7. **Das Register und die stellvertretende Meldung.** `InstanceRegistry` hält je Instanz eine Aufsicht und hat zwei Zusagen, die beide aus E-M6 folgen: Beim Start wird *übernommen*, nicht gestartet — ein Manager, der beim Hochkommen alles hochfährt, was er findet, startet nach einem Neustart der Maschine auch das, was jemand mit Absicht angehalten hat. Und beim Beenden wird nichts beendet: Wer den Manager schließt, will den Manager schließen. Die einzige Ausnahme ist `StopAllAsync` — für einen Grund, der alle betrifft, und der heißt Lizenzwiderruf. `SupervisionReporter` macht aus einem Befund eine Meldung im Bugtracker, **stellvertretend**: mit Prozesskennung, Abschiedscode und dem Ausschnitt aus dem Protokoll der Instanz, der noch auf der Platte liegt. Der Idempotenzschlüssel ist stabil je Instanz und Art — stürzt dieselbe Instanz wiederholt ab, zählt der Server hoch, statt den Bugtracker mit Zwillingen zu füllen; wiederholte Abstürze sind ein Befund und nicht ein Dutzend gleicher. **Ein leerer Protokollauszug ist die aussagekräftigste Zeile der Meldung.** Genau das war der Zustand aus 1.1: Der Prozess wurde erschlagen, bevor sein Schreibpuffer geleert war, und hinterließ nichts. Die Meldung sagt das ausdrücklich, statt eine leere Stelle zu lassen. **`manager.json`** trägt die acht Felder aus Abschnitt 6 und sonst nichts. Lizenzschlüssel, Master-Token und Web-Token liegen darin verschlüsselt und zur Laufzeit im Klartext — wie in den Anwendungseinstellungen, mit Absicht dieselbe Bauart. Unsinnige Aufsichtswerte (Abfragetakt 0, Alarm nach null Fehlversuchen) werden eingefangen: Die Datei lässt sich von Hand bearbeiten. **Ein Defekt, den erst die Tests gezeigt haben.** Eine angehaltene Instanz erschien als `Instance-Alpha` statt `Alpha` — die Aufsicht kennt den Namen erst, wenn die Instanz einmal geantwortet hat, und eine angehaltene tut das nie. Genau diese Ansicht zeigt der Manager aber an. Das Register führt den Namen jetzt aus `InstanceSettings.json` mit. **Abnahme, mit echten Prozessen gemessen.** Der Manager hat die Instanz `Test` in 0,7 s gestartet und über die Named Pipe identifiziert, die Zustandsdatei geschrieben (`pid=54476`, `id=d68af6fd`), dann wurde der Prozess von außen abgeschossen. Der nächste Durchgang meldete `Crashed`, Abschiedscode −1, meldenswert, und räumte die Zustandsdatei weg. Dazu 20 Tests gegen gesteuerte Prozesse — ein Absturz zum gewünschten Zeitpunkt und eine wiederverwendete Prozesskennung sind mit einem echten Programm nicht herzustellen. **Ein Hinweis zur Messumgebung.** Die Abnahme lief über einen eigens gebauten Treiber außerhalb der Werkzeugumgebung. Ein Prozess, den ein Agentenwerkzeug hier startet, landet in einem Job-Objekt, das seine Kinder sofort mitnimmt; der Instanzprozess starb dadurch, bevor er die erste Protokollzeile schreiben konnte. Das ist ein Artefakt der Umgebung und keine Eigenschaft des Managers — aber es heißt, dass ein Abnahmetest dieser Art nicht in die gewöhnliche Testreihe gehört. ### 9.4 Lizenz und Sub-Token im Manager **Ein neues Projekt: `ClawdDotNet.Licensing`.** `LicenseGate`, `LicenseWatch`, `LicenseInfo` und `ILicensePrompt` sind aus `ClawdDotNet.App` dorthin gezogen. Der Grund ist der Schnitt: Zwei Prozesse brauchen dieselbe Lizenzprüfung, und keiner von beiden darf den anderen kennen. In `Core` konnte es nicht landen — dann zöge jedes der sechzehn Werkzeugprojekte das Deploymentcenter-SDK mit. Dabei ist die Kopplung an `SettingsManager` verschwunden: `LicenseGate` kennt jetzt nur `ILicenseStore` — Serveradresse lesen, Schlüssel lesen und schreiben, speichern. Die Anwendung führt ihn in `Settings.json`, der Manager in `manager.json`. Der Torwächter prüft Schlüssel; Dateien verwaltet er nicht. Die Anwendungsversion kommt als Parameter statt aus einem globalen Zugriff — diese Schicht weiß nicht, welche Anwendung sie benutzt. **`ManagerCore` ist die Klammer:** eigene Einstellungen, Lizenz, Register, Meldeweg. Was darüber liegt — ein Fenster, die eine Weboberfläche für alle — setzt darauf auf. - **Ohne gültige Lizenz startet er keine Instanz.** Laufende werden trotzdem in die Liste genommen: Man soll sie sehen — und anhalten können. - **Ein Widerruf hält alle an.** Der einzige Fall, in dem der Manager das von sich aus tut. Ein Serverausfall gehört ausdrücklich nicht dazu; das SDK trennt Urteil und gescheiterten Versuch, und nur das Urteil kommt hier an. Sonst nähme ein Netzproblem sämtliche Installationen gleichzeitig mit. - **Ohne hinterlegte Lizenzprüfung sperrt er nichts.** Sonst gäbe es eine Henne-Ei-Sperre, bevor überhaupt jemand einen Schlüssel eintragen kann. **Sub-Token je Instanz.** Der Manager hält das Master-Token und tauscht es beim **Anlegen** einer Instanz gegen ein eigenes — `watchdog:ping` und `bugtracker:report`, einzeln widerrufbar. Beim Anlegen und nicht beim ersten Start, weil die Instanzdatei dann zweifelsfrei niemandem gehört; sie später zu schreiben, während der Instanzprozess läuft, hieße zwei Schreiber auf einer Datei. Für Instanzen, die es vor dem Manager schon gab, wird es vor dem Start nachgeholt. Danach wird nachgefragt (`whoami`), ob das neue Token an ein Projekt gebunden ist — seit SDK 2.8.1 erbt ein Watchdog-Monitor das Projekt seines Ping-Tokens, und ohne Bindung landen die Befunde im Scheinprojekt „default". Ein fehlgeschlagener Tokenbezug hält die Instanz nicht auf: Monitoring, das nur bei perfekter Rechtelage läuft, ist genau dann still, wenn man es braucht. **Was noch fehlt, damit E-M3 vollständig ist:** Die Instanzen prüfen die Lizenz weiterhin selbst. Das lässt sich erst abschalten, wenn eine Instanz feststellen kann, ob ein Manager da ist (Abschnitt 7) — vorher entstünde eine Fassung ganz ohne Durchsetzung. Bis dahin führen beide ihren eigenen Schlüssel. **Ein Testfehler, der zweimal derselbe war.** In den Manager-Tests hatte ich die Antwort des Sockets hinterlegt, *bevor* der Manager hochfuhr — damit sagt man „diese Instanz läuft bereits", der Manager übernimmt sie (E-M6), der Starter wird nie angefasst, und der Test prüft etwas anderes, als er behauptet. Es gibt jetzt eine Hilfsmethode, die genau das ausschließt. ### 9.5 Was Stufe 4 gebracht hat **Den Managerprozess.** `ClawdDotNet.Instanzmanager` ist eine Konsolenanwendung: Sie prüft die Lizenz, übernimmt, was läuft, hängt die Weboberfläche auf und wartet. Sie beendet beim Herunterfahren keine Instanz. **Zwei neue Projekte.** `ClawdDotNet.Manager.Web` trägt die Weboberfläche — ohne jeden Verweis auf `ClawdDotNet.Web`, denn dort hängt `App` und daran sechzehn Werkzeugprojekte. `ClawdDotNet.ManagerApp` ist der Einstiegspunkt. `wwwroot` ist aus der Instanz in den Manager gezogen (Abschnitt 8). **Der Schnitt der HTTP-Fläche.** Unter `/api/verwaltung` steht, was den Manager angeht: Instanzen auflisten, anlegen, starten, anhalten. **Alles andere unter `/api` geht unverändert an eine Instanz** — er leitet weiter, er implementiert nicht. Welche Instanz gemeint ist, sagt die Kopfzeile `X-Instanz`; läuft genau eine, ist sie gemeint, und laufen mehrere, wird nachgefragt statt geraten. Eine Anfrage an die falsche Instanz ist schlimmer als eine abgelehnte. **Der zusammengeführte Ereignisstrom.** Der Manager hält je laufender Instanz eine Zuleitung und schreibt an jedes Ereignis, von wem es kommt (`instanz`, `instanzName`). Ergänzt statt verpackt: Ein Umschlag wäre einfacher gewesen, hätte aber jede Ansicht der Weboberfläche zum Umschreiben gezwungen. **Abnahme, gegen laufende Prozesse gemessen:** | Geprüft | Ergebnis | |---|---| | `/api/verwaltung/instanzen` ohne Token | 401 | | Oberfläche ohne Token | 200 — die leere Hülle, wie vorgesehen | | Instanz über den Manager starten | `Running`, PID 18104 | | `/api/info` durchgereicht | Antwort **der Instanz** (`instanceId d68af6fd`) | | `/api/status` durchgereicht | 503 der Instanz, unverfälscht | | `/events/logs` | Zeilen der Instanz, mit `"instanz":"Instance-Test"` daran | | Anhalten über den Manager | `Stopped` | | **Manager abgeschossen** | Instanz läuft weiter (E-M6) | | **Manager neu gestartet** | „1 Instanzen bekannt, 1 laufen bereits" — übernommen | **Drei Defekte, die erst der Lauf gezeigt hat:** 1. **Der Manager hing an einer Konsolenabfrage.** `ConsoleLicensePrompt` wartete auf eine Eingabe, die niemand geben konnte; die Weboberfläche kam nie hoch, und im Protokoll stand nichts davon, weil die Frage auf der Standardausgabe steht. Ein Test auf `Console.IsInputRedirected` reicht dafür **nicht**: Er beantwortet „ist die Eingabe umgeleitet", nicht „kann jemand antworten" — ein Prozess ganz ohne Konsole, der gefährlichste Fall, ist nicht umgeleitet. Der Manager fragt jetzt gar nicht; ein Schlüssel gehört in `manager.json`. 2. **Eine unlesbare `manager.json` wurde stillschweigend durch Vorgaben ersetzt.** Der Manager suchte dann im falschen Verzeichnis, hörte auf einem anderen Port und fragte nach einer Lizenz, die woanders stand — und nichts sagte, woran es lag. Es kostete eine halbe Stunde und einen fehlenden Backslash. `LoadProblem` meldet den Grund jetzt. 3. **Der Instanzprozess erbte `CLAWD_CONFIG_DIR` vom Manager** und legte sich im Konfigurationsverzeichnis des Managers eine eigene `Settings.json` an. Eine Umgebungsvariable vererbt sich, ein Argument nicht — deshalb nimmt der Manager sein Konfigurationsverzeichnis jetzt über `--config`. Für eine systemd-Unit ist das ohnehin der geradere Weg. **Was in Stufe 4 noch fehlt:** Die Weboberfläche selbst kennt noch keine Instanzen — sie spricht `/api/…` an und bekommt die eine laufende. Instanzauswahl, `X-Instanz` und die Verwaltungsansicht sind Arbeit an `wwwroot` und gehören damit in Geminis Spur. Und `webServerPort` steht noch in der Instanz: Ihr TCP-Zugang ist erst überflüssig, wenn die Oberfläche im Manager vollständig ist.