Skip to content

feat(surface): die Realtime-Brücke — ein Server-Ereignis erreicht die Views, die es brauchen - #165

Merged
BechsteinDigital merged 1 commit into
mainfrom
feat/surface-realtime-bridge
Aug 6, 2026
Merged

feat(surface): die Realtime-Brücke — ein Server-Ereignis erreicht die Views, die es brauchen#165
BechsteinDigital merged 1 commit into
mainfrom
feat/surface-realtime-bridge

Conversation

@BechsteinDigital

Copy link
Copy Markdown
Owner

1. Why is this change necessary?

ProvidesContexts und RequiresContexts standen 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 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:

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 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 main gibt es keinen Weg, einen serverseitigen Wert in den Kontext-Kanal zu bekommen — ein
Plugin muss eine eigene WebSocket-Route anlegen und im Browser selbst parsen.

Auf diesem Branch:

// in einem Plugin, z. B. beim Eintreffen eines Anrufs
broadcaster.Publish(new SurfaceContextAddress(workspaceKey, "agent-desk"), "communication.active-call/v1", call);
// in einem Block — unverändert gegenüber dem lokalen Fall
scope.subscribe('communication.active-call/v1', (call) => { /* … */ })

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, und
ohne 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 ISurfaceContextBroadcaster in
docs-site dokumentiert 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

  • Geteilter Kontext über Surface-Grenzen (Baustein 4b): Anker 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.
  • Ablauf (§5.4): ein Wert bleibt, bis er überschrieben oder geleert wird. Für den
    Ein-Surface-Fall ist die Verbindung selbst die Lebensdauer — sie endet mit dem Tab.

… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant