# Novaberg — Node: Dispatcher **Projekt:** Novaberg — The Nova Anima Resonance System **Dokument:** Node-Referenz Dispatcher **Stand:** 24. Oktober 2026, 13:43 UTC (`date -u`; §3.1, Schritt 9: Trigger 2 an der Verwendung committet als `570cdda` und im Betrieb gemessen). Davor 24. Oktober 2026, 11:07 UTC (`date -u`; §3.1, Schritt 9 neu: die Verstärkung der hergenommenen Erinnerungen, seit dem Code-Tree `3a5ccab` mit Trigger 2 und den Feldern `kanten_neu`, `kanten_fehler`, `kanten_dauer_ms`, `error`). Davor 22. Oktober 2026, 05:27 UTC (`date -u`; §3.1, §5: der passive Ereignis-Auftrag aus der Sachlage läuft vor dem Fakten-Auftrag, ausgesondert nach Ziel und Aktion; ein Manager-Write mit `ziel="timeline"` geht weiter an die Registry). Davor 21. Oktober 2026, 00:40 UTC (`date -u`; das Fakten-Plugin gelöscht). Davor 20. Oktober 2026, 22:39 UTC (`date -u`; Schritt 2: der kurze Weg hängt an den Writes ohne den Fakten-Auftrag; der Zweig `fakten` im Verteil-Loop). Davor 19. September 2026, 13:12 UTC (das Angebot trägt die Dienste seiner Sache nach Nähe, `e451226`). Davor 18. September 2026, 21:14 UTC (der Dispatcher hält Angebote fest, vermerkt ein Nein am Objekt und den Ausgang am Turn). Davor 9. September 2026, 20:43 UTC (**der Turn-Abschluss haengt nicht mehr an den `pending_writes`, und der Knoten belegt seinen Lauf.** Bis heute stand in Schritt 1 ein `return state`, das `_session_turn_schreiben` und `_turn_roh_schreiben` uebersprang — zwei Schritte, die mit den Writes nichts zu tun haben; **15 von 227 Turns mit erzeugter Antwort** verloren so ihre `turn_roh`-Zeile (6,6 %, 14 davon nach einem gescheiterten Salienz-JSON). Dazu eine Eingangszeile mit den vier Groessen, an denen die Schreibpfade entscheiden — der Knoten schrieb zuvor **ausschliesslich bei Erfolg** und war damit an genau der Stelle unbeobachtbar, deren Ausbleiben er protokollieren soll. `TURN-ROH-FEHLT-BEI-ERZEUGTER-ANTWORT`. Davor 5. September 2026, 10:20 UTC (**die Verwendungszeile sagt jetzt, warum nichts verstaerkt wurde.** Der erste Betriebsbeleg war ein Nullbefund, und **sechs Ausgaenge sahen in der Zeile gleich aus** — fuenf davon Ausfaelle. Sie traegt seither `naehen_alle` (jede geprueften Naehe, auch die verworfene), `knappster_verworfener`, `schwelle`, `deckel` und `ausgang`. **Betriebsbeleg vollstaendig** `[gemessen 05.09.2026]`: 1 von 3, dann **3 von 3** und **3 von 3** verstaerkt — und ein Kandidat lag bei **0,5483 gegen die Schwelle 0,55**, also 0,0017 darunter. Die zwei stummen Zeilen davor sind einmalig nachgerechnet: knappste verworfene **0,2935** und **0,4334**, beide unter dem Maximum der Zufallsverteilung (0,5305) — **der Nullbefund war richtig, die Schwelle steht nicht zu hoch**.). Davor 4. September 2026, 13:00 UTC (**Verstaerkung setzt Verwendung voraus — gebaut am 04.09.2026.** `memory/usage_reinforcement.py`, gerufen vom Dispatcher nach `_turn_roh_schreiben`. Schwelle **0,55** aus 75.975 Paaren abgeleitet (P99,9 = 0,5412), Deckel 3. `[gemessen]` gegen den Bestand: **13 von 15 Turns** verstaerken, **24 von 30** nahen Kandidaten genommen, **0 von 45 fremden**. Die Segmentregel ist **vor dem Bau widerlegt** (25 von 25 Antworten ergeben ein Segment, Segmentierung 1,53 s im Turn). `tests/test_usage_reinforcement.py` (16), Suite **2966**. **Betriebsbeleg steht aus.** Der Knoten traegt dafuer `_verwendung_verstaerken`). Davor 4. September 2026 (**der Dispatcher war der vorgesehene Ort der Knoten-Verstaerkung und tat es nicht** — `novaberg-memory-synapsen_k.md` §7.1a: Verstaerkt wird, was Nova in der Antwort hergenommen hat, und zwar hier, weil der Responder formuliert und nicht persistiert. Gemessen: `knoten_verstaerken` **0 Aufrufe**, `lzg_resonanz` **0-mal gelesen**. Beide Eingaenge liegen bereit — `state["antwort_inhalt"]` und `state["lzg_resonanz"]["erinnerungen"]`. Beleg und Kennungen: `novaberg-memory-synapsen_k.md` §7.1a, `KZG-EINTRAG-BLEIBT-NACH-PROMOTION`, `KZG-THEMA-VERSTAERKT-NACHBARN`, `VERSTAERKUNG-OHNE-VERWENDUNG`.). Davor 11. Juli 2026, Chat 104 (Turn-Rohdaten-Schreiber ergänzt) **Pfad:** novaberg/docs/novaberg-node-dispatcher.md **Quellen:** nova-01-m-h.md **Datei:** `graph/nodes/dispatcher.py` --- ## 1. Aufgabe Der Dispatcher ist der letzte Node in beiden Graphen (HumanGraph und CharacterGraph). Er nimmt die `pending_writes` aus dem State, verteilt sie an Manager/Agenten, schreibt den Session-Turn vollständig (seit Chat 60) und legt seit Chat 104 die **Turn-Rohdaten** dauerhaft ab (§8). Er ist reiner Arbeiter — keine Bewertung, keine Entscheidung, keine LLM-Calls. **Zwei Agent-Dispatcher.** Seit Chat 29 ruft der Dispatcher für das Ziel `"kzg"` direkt `dispatch_kzg()` auf. Seit Chat 32 feuert er zusätzlich den DelegationsAgent (`dispatch_delegation()`) über eine ODER-Trigger-Prüfung auf den emotionalen State. --- ## 2. Position im Graph ``` HumanGraph (Pfad 1): ... → EI-Calc → Salienz → ▶ Dispatcher ◀ → END CharacterGraph (Pfad 2): ... → Tribunal → Evaluate → ok → Salienz → ▶ Dispatcher ◀ → END ``` **Seit Chat 60 wieder im Graph.** Der Dispatcher ist letzter Node beider Graphen. Salienz und Dispatcher laufen nicht mehr asynchron — sie sind Teil des jeweiligen Graph-Durchlaufs. **Kein LLM-Call:** Der Dispatcher selbst macht nichts am LLM — er ruft nur Manager-Plugins und Agent-Dispatcher auf. Der `graph_run_lock` wird hier nicht erworben. > **Umbenannt am 25.08.2026: `llm_lock` heisst jetzt `graph_run_lock`.** Der Name sagte *Sperre vor dem Sprachmodell* und meinte *ein Graphenlauf zur Zeit*; seit dem Vormittag desselben Tages traegt `services/llm_riegel.py` den echten Modell-Riegel, und die Verwechslung waere teuer geworden. **Input:** State mit `pending_writes` (befüllt von Salienz; Planner-Writes sind bereits vor dem Dispatcher geflossen — deren Dispatch passiert weiterhin implizit im Planner-Pfad). **Output:** State mit geleerten `pending_writes` und vollständig geschriebenem Session-Turn. Die Manager/Agenten haben ihre Schreiboperationen ausgeführt. --- ## 3. Verarbeitung ### 3.1 Ablauf 1. **Eingangszeile schreiben** (`schritt='eingang'`, seit 09.09.2026) — Zahl der Writes, ob `response`, `external`, `internal` und das Paar vorliegen. Sie steht **vor** jeder Verzweigung, weil der Knoten zuvor nur bei Erfolg schrieb und *nicht gelaufen* von *erfolglos gelaufen* nicht zu trennen war 2. `pending_writes` aus State lesen — wenn leer **oder nur ~~der Fakten-Auftrag~~ → passive Aufträge** (seit 20.10.2026: Writes mit `ziel != "fakten"` entscheiden, der Fakten-Auftrag läuft dann zusätzlich über `_run_fact_orders`; → seit dem 22.10.2026 entscheiden die Writes, die kein passiver Auftrag sind — Fakten-Auftrag und Ereignis-Auftrag `timeline`/`ereignis` laufen dann zusätzlich über `_run_passive_orders`, das Ereignis zuerst), **Durchlauf ohne Verteilung**: Drive-Schnappschuss, Session-Turn und Turn-Rohdaten laufen trotzdem, nur die Verstaerkung faellt aus (sie setzt voraus, dass Nova etwas hergenommen hat). ~~Durchlauf ohne Aktion~~ — der frueher hier stehende Ruecksprung kostete **15 von 227** Turns ihre `turn_roh`-Zeile 3. Nach `ziel` gruppieren (z.B. alle `"kzg"`-Writes zusammen) 4. Pro Ziel: Zuständigen Handler bestimmen - `"kzg"` → `dispatch_kzg(state, writes)` (KZG-Agent-Subgraph) — ~~`dispatch_kzg(state, writes, embed_client, embed_model)`~~ **überholt:** die beiden Embedding-Parameter waren tote Defaults (`embed_client=None`, `embed_model: str = ""`) und sind mit der Umstellung auf den EmbedWorker entfallen - Alle anderen → Manager aus der Plugin-Registry, `manager.execute(...)` aufrufen — **außer `"fakten"`:** seit 20.10.2026 an den Fakten-Agenten über `_run_fact_orders` (das Fakten-Plugin ist am 21.10.2026 gelöscht, nicht committet) → **seit dem 22.10.2026 laufen die passiven Aufträge vor der Verteilung** (`_run_passive_orders`): zuerst der Ereignis-Auftrag `timeline`/`ereignis` an `agents/timeline/ereignis.py::process_event_orders`, dann die Fakten-Aufträge mit dessen Abbildung `{kandidat: timeline_id}` in `daten["ereignisse"]` — nur wenn es eine gibt, sonst bleibt der Fakten-Auftrag, wie die Sachlage ihn stellte. Ausgesondert nach Ziel **und** Aktion (`_is_event_order`); ein Manager-Write mit `ziel="timeline"` geht weiter an die Registry. Ein Fehler der Ereignis-Seite geht ins Log, die Fakten laufen dann ohne Abbildung ([`novaberg-agent-timeline.md`](novaberg-agent-timeline.md) §5.5) 5. Handler aufrufen, Rückgabe: Anzahl ausgeführter Operationen pro Handler — **Ausnahme KZG:** `dispatch_kzg()` gibt ein Dict zurück, nicht nur eine Zahl (§3.2) 6. **DelegationsAgent prüfen** (unabhängig von pending_writes, siehe §3.4) 7. `pending_writes` im State leeren 8. **Session-Turn und Turn-Rohdaten schreiben** — jeder Ausstieg von `_turn_roh_schreiben` hinterlaesst seinen Grund als eigene Zeile (`schritt='turn_roh_uebersprungen'`): `paar_unvollstaendig`, `personality_fehlt`, `response_leer` — und `kein_antwortpfad` fuer den HumanGraph, der konstruktiv keine Antwort hat und sonst bei **jedem** Turn einen Ausfall meldete 9. **Die hergenommenen Erinnerungen verstärken** (`_verwendung_verstaerken`, seit dem 04.09.2026, nach `_turn_roh_schreiben`) — `memory/usage_reinforcement.py::reinforce_used` verstärkt die Erinnerungen, die die Antwort hergenommen hat ([`novaberg-memory-synapsen_t.md`](novaberg-memory-synapsen_t.md) §7.1a), und schreibt, wenn es gelesene Erinnerungen und eine Antwort gibt, eine Zeile ins `pipeline_log` mit `schritt='verwendung_verstaerkung'`; eine Ausnahme beendet den Turn nicht, sie steht im Log (`logger.exception`). → Seit dem Code-Tree `3a5ccab` (24.10.2026, Kennung wird nachgetragen → committet als `570cdda`; im Betrieb gemessen am 24.10.2026: `kanten_neu` 1044, `kanten_dauer_ms` 188,1 in einem Turn mit zwei Verstärkungen, [`novaberg-memory-synapsen_m.md`](novaberg-memory-synapsen_m.md) *Aus §7.9.2*) rechnet `reinforce_used` nach jeder gelungenen Verstärkung die Kanten des Knotens neu (Trigger 2, `memory/lzg_kanten.py::kanten_neuberechnen_mit_befund`, synchron nach der Zustellung, §7.9.2), und die Zeile trägt dazu `kanten_neu`, `kanten_fehler`, `kanten_dauer_ms` und `error`; die Infozeile im Log nennt Kantenzahl und Dauer ### 3.2 Manager-Signatur Alle Manager haben die gleiche `execute()`-Signatur: ```python def execute(self, writes, user_id, redis_client, postgres_url, embed_client, embed_model): ``` Jeder Manager bekommt alle Parameter, nimmt sich was er braucht. ~~FaktenManager braucht PostgreSQL,~~ TimelineManager braucht den Zeitparser, etc. (das Fakten-Plugin ist am 21.10.2026 gelöscht, nicht committet) **Ausnahme KZG:** Das Ziel `"kzg"` wird nicht über die Manager-Signatur geroutet. `dispatch_kzg()` hat eine eigene Signatur und führt den KZG-Agent-Subgraph aus — **fünf Nodes**: `schwelle_pruefen` → `magnete_aufloesen` → `verdichten` → `speichern` → `queues` (`agents/kzg/agent.py`, `build_graph`). ~~(Schwelle → Verdichtung → **Ähnlichkeit** → Store → Queues)~~ **überholt:** Einen Node `aehnlichkeit_pruefen` gibt es nicht mehr — das Modul ist gelöscht, im Paket `agents/kzg/` liegt keine `aehnlichkeit.py`. An seine Stelle ist `magnete_aufloesen` getreten (Entitäts-/Timeline-Auflösung), das **vor** der Verdichtung läuft. Signatur: ```python def dispatch_kzg( state: dict, writes: list[dict], ) -> dict: ``` **Rückgabekontrakt — drei Schlüssel:** | Schlüssel | Inhalt | |---|---| | `kzg_verarbeitet` | `int` — Anzahl der in diesem Lauf verarbeiteten Salienz-Segmente. | | `kzg_neue_keys` | `list[str]` — Redis-Keys der in diesem Lauf **neu angelegten** KZG-Einträge, in Segment-Reihenfolge. | | `kzg_verstaerkte_keys` | `list[str]` — Redis-Keys der **thematisch verstärkten** Nachbar-Einträge, über alle Segmente des Laufs gesammelt. | **Beide Rückgabepfade tragen dieselben drei Schlüssel** — auch der Registry-Miss-Pfad (`AgentRegistry.finden("kzg")` liefert nichts): Er gibt `kzg_verarbeitet: 0` mit zwei leeren Listen zurück, nicht ein leeres oder verkürztes Dict. Ein Aufrufer mit direktem Index-Zugriff läuft damit **auf keinem Pfad in einen `KeyError`**. **Neuanlage und Verstärkung kommen als zwei getrennte Listen an.** Sie werden nicht zusammengeführt. Der Dispatcher nimmt beide entgegen und protokolliert sie (Anzahlen für beide, die neuen Keys zusätzlich im Klartext); **verwendet werden sie bisher nicht** — der Transport steht, der Konsument fehlt noch. **Kardinalität:** Die Listen enthalten die Keys **eines** Graph-Laufs — je Salienz-Segment ein Subgraph-Durchlauf (`agents/kzg/dispatch.py:68`, Schleife über die `writes`). Pro Konversations-Turn wird `dispatch_kzg()` **zweimal** aufgerufen, einmal aus dem HumanGraph und einmal aus dem CharacterGraph, mit **unabhängiger Segmentzahl je Lauf**: Pfad 1 bewertet den Nutzer-Prompt, Pfad 2 Novas Antwort (`graph/nodes/salience.py:120-121` — zwei verschiedene Texte). **Log-Verhalten bei fehlendem Key — die Unterscheidung ist die Aussage:** - Fehlt der Key nach **regulärer Ablehnung** (`status == "abgelehnt"`, Segment unter der Salienz-Schwelle, der Speicher-Node lief nie): `logger.info`. Das ist der **Normalfall und kein Defektsignal.** - Fehlt der Key **ohne** diesen Status: `logger.warning` mit `status` und `speicher_status`. **Das ist das Defektsignal** — hier hätte ein Key entstehen müssen. - Ein verstärkter Eintrag ohne `key`-Feld erzeugt ebenfalls `logger.warning`. Wer die `info`-Zeilen für Fehler hält, sucht einen Defekt, der keiner ist. Die Schwelle abzulehnen ist die Aufgabe des Filters, nicht sein Versagen. ### 3.3 Fehlerbehandlung Wenn kein Manager für ein Ziel registriert ist: Warning-Log, Writes werden verworfen. Wenn ein Manager eine Exception wirft: Error-Log, andere Manager laufen weiter. Kein Abbruch des gesamten Dispatch-Vorgangs bei einem einzelnen Fehler. ### 3.4 DelegationsAgent (VENT1, Chat 32) Nach der Verarbeitung aller pending_writes prüft der Dispatcher ob der DelegationsAgent feuern soll. Die Prüfung ist eine **ODER-Verknüpfung** über drei Kriterien: | Kriterium | Bedingung | Trigger-Label | |-----------|-----------|---------------| | Effektivwert | `gewicht × arousal ** EI_AROUSAL_DOMINANZ ≥ DELEGATION_EFFEKTIVWERT_SCHWELLE` (Top-Emotion aus `emotions_verlauf`) | `"effektivwert"` | | Emotions-Vektor | Vektor ≠ `"plateau"` UND Valenz ≠ `"neutral"` | `"vektor"` | | Salienz | `salienz_score ≥ DELEGATION_SALIENZ_SCHWELLE` UND Valenz ≠ `"neutral"` | `"salienz"` | **Ausschluss:** `user_id == "nova"` → kein Delegation-Trigger (Novas eigene KZG-Einträge lösen keine Delegation aus). > **`DELEGATION_EFFEKTIVWERT_SCHWELLE` steht seit dem 31.08.2026 auf 0,20** (vorher 0,15), > mitgezogen mit der Reizstärke-Kalibrierung (`novaberg-ei.md` §Reizstärke). `[gemessen]` über > 33 echte Verlaufszustände: 18 Auslösungen vorher, 6 danach unverändert, **4 bei 0,20**. Der > Kipppunkt wanderte von arousal 0,546 auf **0,594** — die Perzeption vergibt für interessante, > aber unbelastete Sachverhalte bis 0,60, das obere Drittel des Alltagsbandes wurde also > mitgefangen. *Der Mensch ist keine Glasfigur.* > > **Die Zahl sitzt auf dem breitesten Plateau der Umgebung** (0,1726–0,2088). Zwei Auslösungen > wären bei 0,23 erreichbar, das Fenster dafür ist aber nur 0,011 breit — 0,24 stellt das Tor > auf null. Eine Schwelle auf einer Kante ist keine Schwelle. > > **Und sie steuert nur Kriterium 1 von dreien.** Kriterium 3 feuert bei einer Salienz über > `DELEGATION_SALIENZ_SCHWELLE`, und das trifft **93,5 %** von 2771 gemessenen Läufen. Wer den > DelegationsAgenten insgesamt seltener anschlagen lassen will, dreht dort — nicht hier. Wenn ein Trigger greift, wird `_delegation_trigger` und `salienz_obj_aktuell` in den State geschrieben und `dispatch_delegation(state, embed_client, embed_model)` aufgerufen. --- ## 4. State-Felder ### Gelesen | Feld | Quelle | Beschreibung | |------|--------|-------------| | `pending_writes` | Planner / Salienz | Liste von PendingWrite-Dicts | | `user_id` | API | Für Manager-Aufrufe + Delegation-Ausschluss | | `emotions_verlauf` | Enricher | Top-Emotion für Effektivwert-Berechnung | | `emotions_vektor` | Enricher | Vektor-Richtung (eskalation/deeskalation/plateau) | | `user_prompt` | API | Turn-Rohdaten (a) — §8 | | `response` | Responder | Turn-Rohdaten (c) + Rollen-Erkennung Session-Turn — §8 | | `external` / `internal` | db_zugriff / ei_calc(_persist) | Turn-Rohdaten (b)/(d), Emotions-Paar — §8 | | `character_id` | API / `create_state` | Paar-Scope aller Writes — §8 | ### Geschrieben | Feld | Beschreibung | |------|-------------| | `pending_writes` | Auf `[]` geleert nach Verarbeitung | | `_delegation_trigger` | Trigger-Label (`"effektivwert"`, `"vektor"`, `"salienz"`) — nur wenn Delegation feuert | | `salienz_obj_aktuell` | Salienz-Objekt des aktuellen Turns — nur wenn Delegation feuert | --- ## 5. PendingWrite-Routing | Ziel | Handler | Typische Aktionen | |------|---------|-------------------| | `"kzg"` | KZG-Agent (`dispatch_kzg()`) | Schwellwert prüfen, Magnete auflösen, kern verdichten, Redis-Store neu/verstärken, Queues befüllen — ~~Ähnlichkeit prüfen~~ (Node gelöscht), ~~Session-Turn annotieren~~ (macht der Dispatcher, siehe unten) | | `"fakten"` | ~~FaktenManager~~ → Fakten-Agent (`_run_fact_orders` → `agents/fakten/dispatch.py::process_fact_orders`) | Entitäten + Fakten-Tripel speichern, Entity Resolution — der Auftrag kommt aus der Sachlage; das Fakten-Plugin ist am 21.10.2026 gelöscht (nicht committet) | | `"timeline"` | TimelineManager — **außer der Aktion `ereignis`** (seit 22.10.2026): der passive Dienst `agents/timeline/ereignis.py::process_event_orders`, vor den Fakten (`_run_passive_orders`) | Termin anlegen, verschieben, löschen; mit `ereignis` das Ereignis einer anderen Person aus der Sachlage, über der Grenze der Salienz | | `"notizen"` | NotizenManager | Notiz anlegen, löschen | **Wer den Session-Turn schreibt.** Nicht der KZG-Agent — **der Dispatcher.** Der Agent verdichtet nur und legt den Kern des Segments mit der höchsten Salienz in `state["session_turn_kern"]` ab (`agents/kzg/dispatch.py`); der Dispatcher liest ihn dort ab und schreibt den Session-Turn vollständig (`graph/nodes/dispatcher.py`, `kern = state.get("session_turn_kern", "")`). Eine Funktion `session_turn_annotate()` existiert im ganzen Server nicht mehr. Die frühere Angabe „Session-Turn annotieren" in der Tabelle oben war die Aufgabenteilung **vor** dieser Umstellung. Der DelegationsAgent ist **kein** PendingWrite-Ziel — er wird separat über die Trigger-Prüfung (§3.4) ausgelöst und bekommt den gesamten State, nicht einzelne Writes. Neue PendingWrite-Ziele werden durch neue Manager-Plugins automatisch bedient — der Dispatcher selbst braucht keine Änderung. --- ## 6. Designprinzip > **Entscheider/Arbeiter-Trennung (A1):** Die Salienz entscheidet *was* gespeichert wird. Der Planner entscheidet *welche Management-Aktion* ausgeführt wird. Der Dispatcher *führt aus* — blind, nach Anweisung, ohne eigene Logik. > **Yin-Yang-Prinzip (VENT1, Chat 32):** Der DelegationsAgent ist die Ausnahme — er wird nicht durch pending_writes ausgelöst, sondern durch den emotionalen State. Das ist bewusst: Emotionale Begleitung braucht einen eigenen Trigger-Pfad, nicht den gleichen wie Gedächtnis-Operationen. --- ## 7. Session-Turn-Schreiber (Chat 60) Seit Chat 60 schreibt der Dispatcher den Session-Turn als letzte Aktion — vollständig, mit allen Metadaten. Kein nachträgliches Annotieren. **Automatische Rollen-Erkennung:** Wenn `state["response"]` vorhanden → Assistant-Turn. Sonst → User-Turn. **Geschriebene Felder:** inhalt, rolle, emotion, arousal, modus, intentionen, emotions_vektor, sprach_stil, beziehungs_dynamik, tone, kern (aus `session_turn_kern`, vom KZG-Agent). **Session-Zusammenfassung:** Nach dem Turn-Store wird `session_summarize_if_needed()` aufgerufen — älteste Turns werden komprimiert wenn der Stack > 25 Turns hat. Funktion: `_session_turn_schreiben()` in `dispatcher.py`. --- ## 8. Turn-Rohdaten-Schreiber (Chat 104) Der Dispatcher schreibt pro Turn das vollständige **Reiz-Reaktions-Paar** roh ins `pipeline_log` (`art='turn_roh'`) — die dauerhafte, nicht-wiederherstellbare Quelle für Novas Charakter-Destillation. Funktion: `_turn_roh_schreiben()`, aufgerufen nach `_session_turn_schreiben()`. **Warum hier und nicht früher:** Erst im Dispatcher liegen alle vier Größen gleichzeitig vor — User-Input (a) und User-Emotion (b) ab `ei_calc`, Nova-Antwort (c) erst ab dem Responder, Nova-Emotion (d) final konsolidiert erst nach `ei_calc_persist`. Der ursprüngliche Konzept-Entwurf sah den Reducer vor; der läuft aber **vor** dem Responder und sieht `state["response"]` nie. **Inhalt (JSONB):** `user_prompt`, `response` (ungekürzt), `user_emotion`, `nova_emotion` — beide Emotionen mit allen neun EI-Dimensionen via `Emotion.to_dict()`. Der Wortlaut allein ist mehrdeutig („Na super." = Freude oder Sarkasmus); erst der Emotionszustand *neben* der Äußerung gibt ihr eine Stimme. **Dazu `herkunft`** — `nutzer_turn` oder `eigener_impuls`. Der Session-Turn trägt das Feld seit Chat 119, aber er verfällt; der Rohturn ist die Zeile, die eine Auswertung Tage später liest. Ohne das Feld zählt ein Turn, den Nova von sich aus begonnen hat, dort als Turn wie jeder andere — und verschiebt in einer Messreihe mit festem Gesprächsbogen jede turn-indizierte Sonde dahinter. Das Feld steht **immer**, auch bei `nutzer_turn`. Ein Feld, das nur im Sonderfall erscheint, macht sein Fehlen zweideutig: nicht geschrieben oder nicht zutreffend. Das ist die Gegenrichtung zu `antwort_inhalt`, das genau deshalb weggelassen wird — dort ist die Abwesenheit selbst die Aussage. Die Ableitung liegt in `graph/state.py` (`reiz_herkunft()`), nicht im Dispatcher: Session-Turn und Rohturn brauchen dieselbe Abbildung, und eine zweite Kopie liefe auseinander. **Über `source` ist die Frage nicht entscheidbar** — der Thinker-Retry läuft mit derselben `source="character"` und ist trotzdem die Wiederholung einer Nutzeräußerung. **Drei Guards, alle laut (kein Silent-Skip):** | Bedingung | Verhalten | |---|---| | `user_id` oder `character_id` fehlt | `warning`, kein Write | | `external` oder `internal` fehlt | `warning`, kein Write (kein Pseudo-Turn ohne echtes Emotionspaar) | | `response` leer | `warning`, kein Write — markiert den HumanGraph-Durchlauf (kein Reiz-Reaktions-**Paar** ohne Reaktion) | **Fehlerverhalten:** Ein Serialisierungsfehler kracht sichtbar ins Log (`logger.error` + Forensik-`log_fehler`), reißt aber weder den Turn-Abschluss noch die folgenden Persistenz-Schritte — der Rohturn ist wichtig, aber kein Grund, die Hauptsache mitzunehmen. **Retention:** `art='turn_roh'` ist von `delete_expired_entries` ausgenommen (`AND art <> 'turn_roh'`) und bleibt dauerhaft. Forensik-Arten verfallen weiter nach `LZG_PIPELINE_LOG_VORHALTUNG_TAGE`. → Konzept: `novaberg-charakter-resonanz_k.md` --- → Plugin-System: novaberg-architecture.md → PendingWrite-Format: novaberg-graph.md — Graph-Architektur`, Abschnitt 4 → Salienz (Entscheider): novaberg-node-salience.md → KZG-Agent: novaberg-mem-kzg.md / novaberg-pixie-kzg.md → DelegationsAgent: novaberg-pixie-delegation.md (Yin-Yang-Prinzip / VENT1 — Lesson-Kontext in novaberg-ei-character-profiles_l.md) ## Seit dem 18.09.2026 — Angebot, Ablehnung, Ausgang - **`_angebot_merken`** (Scheibe 12 E1/E3): Enthält die fertige Antwort ein Angebot (`find_offers`), liegt es ~~mit den akuten Objekten des Turns~~ → **seit 18.09.2026 mit der Sache, der es gilt** (`utils/offers.py::offer_binding`: die Sache, die der Verfasser anbieten ließ — `state["angebot_kandidat"]`; sonst die einzige akute Sache am Zettel von Timeline oder Notizen; sonst ohne Gegenstand, dann wird ein *»Gerne«* nicht von selbst zum Auftrag; die Dienste einer Sache in der Reihenfolge ihrer Nähe, `_writing_services`, weil der Router dem ersten zustellt), ihren Eigenschaften und Diensten als offener Punkt in Redis (`angebot:{user}:{figur}`, 15 min); sonst wird ein alter Punkt entfernt. Lehnt der Mensch ein offenes Angebot blank ab (*»Nein danke«*), bekommt jede Sache `Speichern = abgelehnt` im Eigenschaftsgedächtnis (`record_declined`). - **`_ausgang_vermerken`** (Scheibe 12 A, Punkt 2): In beiden Pfaden des Dispatchers wird der Turn des Menschen mit dem Ausgang des Dienstes markiert (`session_turn_mark_action`).