feat(surface): die Realtime-Brücke — ein Server-Ereignis erreicht die Views, die es brauchen - #165
Merged
Merged
Conversation
… Views, die es brauchen
ProvidesContexts und RequiresContexts standen bisher als Metadaten in der
View-Registrierung, ohne dass irgendetwas sie einlöste. Ein Block, der auf einen
eingehenden Anruf reagieren sollte, musste selbst eine WebSocket öffnen, ein Format
kennen und einen Reconnect schreiben — also genau das, was der Vertrag zu ersparen
versprach.
Jetzt hält die Runtime eine Verbindung, und ein Plugin publiziert serverseitig:
broadcaster.Publish(
new SurfaceContextAddress("acme", "agent-desk", "employees", "anna"),
"communication.active-call/v1",
call);
Der Block-Code ist danach derselbe, ob der Wert aus einer Insel im selben Tab kam oder
vom Server. Das ist der Punkt: Die Topologie wird eine Konfigurationsentscheidung des
Kunden, statt in jeden Block hineinzuwirken.
WER WAS SIEHT, ENTSCHEIDET DER SERVER
Die Adresse verengt schrittweise: Workspace, Surface, Subject. Ein aktiver Anruf gehört
der Agentin, die ihn führt, nicht jedem mit derselben Fläche offen. Und eine Subject-Id
allein ist keine Identität (ADR-017) — ohne Issuer-Vergleich läse "anna" beim
Kunden-Identitätsanbieter mit, was "anna" beim Mitarbeiter-Anbieter sieht. Beides steht
als Test, nicht als Vorsatz.
Clientseitig zu filtern gäbe es nichts: Was ein Tab nicht empfängt, kann er nicht lesen,
und alles, was er empfängt, steht in den DevTools. Deshalb ist die Brücke auch
EINBAHN — ein Frame aus dem Browser wird ignoriert, gelesen wird nur, um das Schließen
zu bemerken. Ein Wert von dort trüge keine Autorität.
DIE VORRANGREGEL FÄLLT AUS DEM KANAL
Die Brücke fordert einen Publisher an wie jeder andere. Besitzt eine Insel den Key
bereits, weist der Kanal sie ab, und sie lässt es dabei. „Lokal schlägt Server" braucht
damit keine Regel, die es ausspricht — und beim Schließen gibt die Brücke ihre Keys
frei, sonst sperrte sie die Insel aus, die sie bedienen könnte.
CONTROLLER STATT MINIMAL-API
Der erste Entwurf war ein Minimal-API-Endpunkt und lief in die Architekturregel:
"Bestandsaufnahme, kein Freibrief: neue Einträge gehören nicht hinzugefügt." Also ein
Controller. Der braucht seine eigene ApplicationPart — AddControllers() scannt nur die
Entry-Assembly, und ohne sie hätte die Route schlicht nicht existiert: kein Fehler,
keine Logzeile, nur ein 404, den niemand erklären kann.
Der Extension Point ist in docs-site dokumentiert; der Katalog-Test hat darauf bestanden.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
1. Why is this change necessary?
ProvidesContextsundRequiresContextsstanden als Metadaten in der View-Registrierung, ohnedass irgendetwas sie einlöste. Ein Block, der auf einen eingehenden Anruf reagieren sollte, musste
selbst eine WebSocket öffnen, ein Nachrichtenformat kennen und einen Reconnect schreiben — also
genau das, was der Vertrag zu ersparen versprach.
Baustein 4a des Umsetzungsschnitts
(
ops/specs/2026-08-06-admin-sdk-und-surface-composer-design.md§5.3, §8).2. What does this change do, exactly?
Die Runtime hält eine Verbindung je offener Surface, ein Plugin publiziert serverseitig:
Der Block-Code ist danach derselbe, ob der Wert aus einer Insel im selben Tab kam oder vom
Server. Das ist der Punkt: Die Topologie wird eine Konfigurationsentscheidung des Kunden, statt
in jeden Block hineinzuwirken.
Wer was sieht, entscheidet der Server. Die Adresse verengt schrittweise — Workspace, Surface,
Subject. Ein aktiver Anruf gehört der Agentin, die ihn führt, nicht jedem mit derselben Fläche
offen. Und eine Subject-Id allein ist keine Identität (ADR-017): ohne Issuer-Vergleich läse
„anna" beim Kunden-Identitätsanbieter mit, was „anna" beim Mitarbeiter-Anbieter sieht. Beides
steht als Test, nicht als Vorsatz.
Clientseitig zu filtern gäbe es nichts: Was ein Tab nicht empfängt, kann er nicht lesen, und
alles, was er empfängt, steht in den DevTools. Deshalb ist die Brücke Einbahn — ein Frame aus
dem Browser wird ignoriert, gelesen wird nur, um das Schließen zu bemerken. Ein Wert von dort
trüge keine Autorität.
Die Vorrangregel fällt aus dem Kanal. Die Brücke fordert einen Publisher an wie jeder andere;
besitzt eine Insel den Key bereits, weist der Kanal sie ab, und sie lässt es dabei. „Lokal
schlägt Server" braucht damit keine Regel, die es ausspricht. Beim Schließen gibt die Brücke ihre
Keys frei — sonst sperrte sie die Insel aus, die sie bedienen könnte.
Reconnect mit gedeckeltem exponentiellem Backoff (1 s → 30 s). Eine Surface, die durch einen
Ausfall im Sekundentakt neu verbindet, macht aus der schlechten Minute eines Servers eine
Lawine.
3. Describe each step to reproduce the issue or behaviour.
Auf
maingibt es keinen Weg, einen serverseitigen Wert in den Kontext-Kanal zu bekommen — einPlugin muss eine eigene WebSocket-Route anlegen und im Browser selbst parsen.
Auf diesem Branch:
4. Please link to the relevant issues (if any).
Setzt §8 Baustein 4a um. Der erste Konsument (Communication publiziert bei eingehendem Anruf)
folgt separat.
Zwei Regeln, die während der Arbeit gegriffen haben
Der erste Entwurf war ein Minimal-API-Endpunkt und lief in die Architekturregel:
„Bestandsaufnahme, kein Freibrief: neue Einträge gehören nicht hinzugefügt." Also ein Controller.
Der braucht seine eigene ApplicationPart —
AddControllers()scannt nur die Entry-Assembly, undohne sie hätte die Route schlicht nicht existiert: kein Fehler, keine Logzeile, nur ein 404, den
niemand erklären kann.
Und der Extension-Point-Katalog-Test bestand darauf, dass
ISurfaceContextBroadcasterindocs-sitedokumentiert wird, bevor er existieren darf.Zahlen
14 Dateien, 995 Zeilen. .NET 1886 + 29 Analyzer, Surface-Runtime 99 (vorher 89) — alle grün,
mehrfach wiederholt.
Noch nicht drin
subject/conversation,serverseitige Key-Deklaration mit Feld-Sichtbarkeit, Projektion je Abonnent. Der Ein-Surface-Fall
trägt ohne das vollständig, und kein Block muss dafür umgeschrieben werden.
Ein-Surface-Fall ist die Verbindung selbst die Lebensdauer — sie endet mit dem Tab.