Mehrere Instanzen sollen unabhaengig voneinander laufen. Heute tun sie das nicht: Sie teilen sich Protokollverzeichnis, Sicherungsordner und Sicherungszeitplan. In einer gemeinsamen Startup.log standen abwechselnd Zeilen zweier Instanzen, ohne Kennzeichnung, welche gerade spricht -- im Mehrinstanzbetrieb ist damit jede Fehlersuche Ratearbeit. Genau in dieser Lage ist der Aufhaenger aus #529 entstanden. Fuenf Schluessel wandern aus AppSettings nach InstanceSettings.json: logDirectory, backupDirectory, backupKeepCount, autoBackupEnabled, autoBackupTime. Leer heisst jetzt <Instanz>/Logs beziehungsweise <Instanz>/Backups; ein relativer Wert zaehlt weiterhin gegen das Instanzverzeichnis, nicht gegen das Arbeitsverzeichnis des Prozesses (#375 bleibt gewahrt). autoBackupEnabled und autoBackupTime standen in der Planung nicht, sind aber mitgegangen: Ein Schalter, der die taegliche Sicherung fuer eine Instanz einschaltet und sie damit fuer alle einschaltet, ist derselbe Fehler wie ein Ordner fuer alle -- und zwei von vier Sicherungsfeldern zu verschieben hinterliesse eine Einstellungsseite, auf der die Haelfte anwendungsweit gilt. Keine Uebernahme der Altwerte, aber eine Ansage: SettingsManager. AbandonedKeys meldet die aufgegebenen Schluessel, solange sie noch in Settings.json stehen, und der Start protokolliert einmal, was nicht mehr gilt und wohin diese Instanz stattdessen schreibt. Ein anwendungsweiter Pfad, in alle Instanzen kopiert, waere wieder ein Ordner fuer alle. webServerPort bleibt vorerst stehen. Es entfaellt mit Stufe 2, sobald Kestrel auf dem Socket hoert; vorher wegzunehmen hiesse, die Weboberflaeche ohne Ersatz abzuschalten. Korrektur an der eigenen Befundlage: "Instanz A wirft die Sicherungen von Instanz B weg" war zu breit. Beide Rotationen filtern bereits nach dem Praefix backup_<Instanzname>_. Der reale Defekt ist enger, und ein Praefix ist keine Grenze: backup_Test_2_*.zip faengt mit backup_Test_ an, also zaehlt Test im gemeinsamen Ordner die Archive von Test_2 mit. Belegt in BackupSchedulerTests, das den alten Zustand eigens wiederherstellt. Nachgeprueft an der laufenden Installation, nicht nur im Test: Nach dem Umbau schreibt Instance-Test nach Instances/Instance-Test/Logs, das alte gemeinsame Verzeichnis bekommt keine Zeile mehr, und die Warnung zu den aufgegebenen Schluesseln erscheint genau einmal. Enthaelt ausserdem, was seit der Weboberflaeche liegengeblieben war: - #526: Instanzgeheimnisse wurden beim Speichern verschluesselt und beim Laden nie entschluesselt. LoadInstanceConfig hatte kein ConfigSecrets.Unprotect; ConfigLoader.LoadAsync macht es richtig, wird zur Laufzeit aber gar nicht aufgerufen. Der OpenRouter-Schluessel ging so verschluesselt an die API. - Ein offener Ereignisstrom hielt das Beenden fuenf Sekunden auf. Die Stroeme hingen nur an RequestAborted, das beim geordneten Herunterfahren nicht feuert; jetzt zusaetzlich an ApplicationStopping. Der Fang in WebUiHost gilt dazu ohne Bedingung auf RequestAborted -- mit ihr waere jeder offene Strom beim Beenden als Fehler im Bugtracker gelandet. - Nach der Anmeldung blieb die Weboberflaeche leer: #anmeldung { display:flex } schlaegt als ID-Selektor das hidden-Attribut. - docs/Instanzmanager-Konzept.md: Befund, die Entscheidungen E-M1 bis E-M6, der Schnitt zwischen Manager und Instanz und die sechs Stufen. Build ohne Warnungen, 709 Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
17 KiB
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_<Instanzname>_ 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
(<Instanz>/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+instanceals 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 | <Instanz>/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.
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 <Instanz>/Logs und <Instanz>/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, wenn Kestrel auf dem Socket
hört — also mit Stufe 2. Vorher wegzunehmen hieße, die Weboberfläche ohne Ersatz
abzuschalten. Der Portstreit zweier Instanzen ist bis dahin gedeckelt, aber nicht gelöst:
WebUiHost.TryStart gibt bei belegtem Port null zurück und protokolliert es, statt den
Start zu verhindern.
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.