docs/novaberg-mem-session.md
Rama master · réplica de lectura del repositorio original.
# Novaberg — Gedächtnis: Session
**Projekt:** Novaberg — The Nova Anima Resonance System
**Dokument:** Modul Session-Gedächtnis
**Stand:** 18. September 2026, 21:14 UTC (der Aufrufer von `session_turn_mark_action` ist zurück — der Verlauf trägt wieder `[ERLEDIGT]`/`[FEHLGESCHLAGEN]`). Davor 16. September 2026, 15:10 UTC (`date -u`; **der Befund an §3.7 ist behoben** — kein Beitrag wird mehr gekürzt, die Grenze greift über das ganze Fenster und wirft älteste Gruppen weg; gemessen 7213 → 0 abgeschnittene Beiträge über 1439 Fenster. Dazu ein **achter Renderer** in der Tabelle, `sachlage.py::_render_history`, der als einziger weiter je Beitrag schneidet — sein Docstring sagt das Gegenteil). Davor 14. September 2026, 19:52 UTC (Befund an §3.7: jeder Beitrag wird für neun Leser nach 100 Zeichen gekappt). Davor 24. August 2026 (der Sprecher kommt aus dem Feld `herkunft`, nicht aus der Position — §3.8; die Filterzeile des Enrichers war tot und ist entfernt — §5); davor 16. August 2026 (gegen den Code geprüft: `session_turn_annotate` ist entfallen, der Turn entsteht vollständig — §1, §2.2 und §3.2 nachgezogen); davor 15. August 2026 (Schlüssel `haltung:{user_id}:{character_id}`, dazu die beiden Uhren der Eigenzeit in `nova_state`); davor 17. Mai 2026, Chat 90 (PFAD2-PERZEPTION-FIX abgeschlossen, HumanGraph-Slimming Phase 4)
**Pfad:** novaberg/docs/novaberg-mem-session.md
**Quellen:** nova-02-m-a.md
**Datei:** `memory/session.py`
---
## 1. Aufgabe
Das Session-Gedächtnis ist Novas Arbeitsgedächtnis für das laufende Gespräch. Es speichert jeden Turn (User + Assistent) **vollständig in einem Zug** — samt der Salienz-Metadaten, die der Dispatcher aus dem State mitgibt — und fasst ältere Turns zusammen, wenn der Stapel zu groß wird. (Bis zum 16.08.2026 stand hier *reichert User-Turns nachträglich an*; das beschrieb den zweistufigen Weg, den Chat 60 abgelöst hat — §3.2.) Es lebt in Redis mit TTL — nach 4 Stunden Inaktivität verschwindet es.
> **Kognitionswissenschaftliche Analogie:** Das Arbeitsgedächtnis nach Baddeley (1974) hat eine begrenzte Kapazität und hält Informationen nur aktiv, solange sie gebraucht werden. Novas Session ist das Äquivalent: Begrenzt auf die letzten ~20 Turns, mit einer „phonologischen Schleife" (Summary) für ältere Inhalte.
---
## 2. Speicherung in Redis
### 2.1 Schlüssel
| Key | Typ | TTL | Beschreibung |
|-----|-----|-----|-------------|
| `session:{user_id}:{character_id}:turns` | List | 14400s (4h) | Geordnete Liste aller Turns |
| `session:{user_id}:{character_id}:summary` | String | 14400s (4h) | Zusammenfassung älterer Turns |
| `nova_state:{user_id}:{character_id}` | Hash | kein TTL | Persistierter Nova-Zustand: neun EI-Dimensionen (Chat 89) + `raum_tiefe`/`raum_naehe` (Chat 114) + die beiden Uhren der Eigenzeit `turn_zeit`/`nutzer_zeit` (15.08.2026) |
| `haltung:{user_id}:{character_id}` | Hash | kein TTL | Der zuletzt gerechnete **Haltungsstand** (15.08.2026): die fünf Verhaltensgrößen, `cluster`, `turn_id`, `zeit`, dazu die Marke `gerechnet` und ein `grund`. Geschrieben vom `haltungsraum`-Knoten, **auch wenn er nichts rechnen konnte** — sonst bliebe der Vorstand ohne Kennzeichnung stehen. Verwaltet in `memory/haltung.py` |
Seit Chat 60: Session-Key enthält `character_id`. Die Session repräsentiert das Gespräch zwischen einem bestimmten User und einem bestimmten Charakter (z.B. `session:meister:nova:turns`). Helfer: `_session_key(user_id, character_id, suffix)`.
### 2.2 Turn-Format
Jeder Turn ist ein JSON-Objekt in der Redis-Liste:
```json
{
"turn_id": "97b0d23ba3b1495ea6f44c0ce000a86b",
"rolle": "user|assistant",
"inhalt": "Der Wortlaut des Turns",
"zeit": 1711234567.89,
"intentionen": ["information_teilen", "emotionaler_ausdruck"],
"emotion": "begeisterung",
"modus": "alltag",
"kern": "Destillierte Zusammenfassung des Inhalts",
"arousal": 0.7,
"emotions_vektor": "plateau",
"sprach_stil": "locker",
"beziehungs_dynamik": "vertrauen",
"tone": "empathisch",
"aktion_erledigt": true,
"aktion_erfolgreich": true
}
```
**`turn_id`-Korrelation (Chat 88/90):** Jeder Turn trägt eine UUID-Hex,
die im `/chat`- bzw. `/chat/stream`-Handler erzeugt und durch HumanGraph
und CharacterGraph als Korrelations-Marker durchgereicht wird. Damit
korrelieren Pipeline-Log-Spans aller Nodes eines Turns über dieselbe
`turn_id`. Ausführlich: `novaberg-memory-synapsen_k.md` §10.
**Personality-Quellen (Chat 89):** Die emotion-/modus-/stil-/dynamik-/
tone-Felder werden je nach Turn-Rolle aus den Personality-Klassen
befüllt:
| Turn-Rolle | Quelle |
|-----------|--------|
| `user` | `state["external"].emotion.*` (Perzeption im HumanGraph) |
| `assistant` | `state["internal"].emotion.*` (Perzeption-Assistant + EI-Calc-Persist im CharacterGraph) |
Der Turn ist also nach Speicherung der konsolidierte Schnappschuss der
jeweiligen Personality-Klasse zum Zeitpunkt des Dispatcher-Laufs.
~~**Drei Zustände eines User-Turns:**~~
1. ~~**Frisch gespeichert:** `intentionen`, `emotion`, `modus`, `kern` sind leer — die Salienz hat noch nicht annotiert.~~
2. ~~**Annotiert:** Die Salienz hat den Turn nachträglich angereichert (`session_turn_annotate`). Der Enricher sieht dann den destillierten `kern` statt des rohen `inhalt`.~~
3. **Aktions-markiert (Chat 43):** Nach Agent-Dispatch via `session_turn_mark_action`. Felder `aktion_erledigt` + `aktion_erfolgreich` gesetzt.
→ **Am 16.08.2026 gegen den Code geprüft: Es sind zwei Zustände, nicht drei.** Die für Chat 60 angekündigte Deprecation von `session_turn_annotate` ist vollzogen; die Funktion existiert nicht mehr, und kein Knoten annotiert einen Turn nachträglich.
**Der Turn entsteht vollständig.** `graph/nodes/dispatcher.py` ruft `session_turn_store` mit allen Feldern zugleich — `intentionen`, `emotion`, `arousal`, `modus`, `kern`, `emotions_vektor`, `sprach_stil`. Damit gibt es den Zwischenzustand „gespeichert, aber noch leer" nicht: Was der Enricher liest, ist von der ersten Sekunde an der destillierte `kern`.
**Zwei Zustände:**
1. **Gespeichert:** vollständig, mit allen Feldern (`session_turn_store`).
2. **Aktions-markiert (Chat 43):** nach Agent-Dispatch via `session_turn_mark_action`; `aktion_erledigt` und `aktion_erfolgreich` gesetzt.
Die Zusammenlegung ist keine Vereinfachung der Beschreibung, sondern eine des Mechanismus: Ein zweistufiger Schreibvorgang hat einen Zeitraum, in dem der Turn unvollständig gelesen werden kann — dieser Zeitraum ist entfallen.
### 2.3 nova_state-Persistierung (Default Mode Network, Chat 89)
Am Ende jedes CharacterGraph-Laufs schreibt der `ei_calc_persist`-Node
Novas konsolidierten Emotions-Zustand in einen Redis-Hash:
| Feld | Quelle |
|------|--------|
| `emotion` | `state["internal"].emotion.emotion` |
| `arousal` | `state["internal"].emotion.arousal` |
| `emotions_vector` | `state["internal"].emotion.emotions_vector` |
| `mode` | `state["internal"].emotion.mode` |
| `language_style` | `state["internal"].emotion.language_style` |
| `relationship_dynamic` | `state["internal"].emotion.relationship_dynamic` |
| `tone` | `state["internal"].emotion.tone` |
| `intent` | `state["internal"].emotion.intent` |
| `prompt_topic` | `state["internal"].emotion.prompt_topic` |
**Kein TTL.** Der Hash überlebt zwischen Turns und Server-Restarts. Das
ist die strukturelle Grundlage des Default Mode Network: Pixie-Pfade
(Träumen, Recherche, Reflexion) laufen ebenfalls durch den CharacterGraph
und schreiben in denselben Hash — Novas Innenleben moduliert ihre
Stimmung zwischen User-Turns.
Beim Start des nächsten CharacterGraph-Laufs lädt der `db_zugriff`-Node
diesen Hash und befüllt damit `state["internal"].emotion`. Beim
allerersten Turn pro User-Charakter-Paar greifen Defaults
(`emotion="neutral"`, `arousal=0.5`, `mode="alltag"`, etc.).
→ Schreib-Pfad: `novaberg-node-ei-calc-persist.md`
→ Lese-Pfad: `novaberg-node-db-zugriff.md`
→ Architektonischer Kontext: `novaberg-path2-perzeption_k.md` §2.3
---
## 3. Kernfunktionen
### 3.1 session_turn_store
Signatur: `session_turn_store(redis_client, user_id, character_id, rolle, inhalt, ...)`.
Speichert einen Turn (User oder Assistent) am Ende der Liste. Setzt die TTL bei jedem Schreibvorgang zurück — solange gesprochen wird, lebt die Session.
Seit Chat 60: Erweitert um `arousal`, `emotions_vektor`, `sprach_stil`, `beziehungs_dynamik`, `tone`, `themen`. Damit kann ein Turn vollständig in einem Aufruf gespeichert werden — kein nachträgliches Annotieren nötig.
### 3.2 session_turn_annotate — entfallen
**Am 16.08.2026 gegen den Code geprüft: Die Funktion existiert nicht mehr**, ebenso wenig `session_assistant_turn_annotate`. Die für Chat 60 als „perspektivisch" angekündigte Deprecation ist vollzogen, und der Legacy-Code, auf den dieser Abschnitt verwies, ist weg.
Der Abschnitt bleibt als Marke stehen, weil andere Dokumente auf den Namen zeigen — die Beschreibung darunter ist **widerlegt** und nicht mehr gültig:
> ~~Sucht den letzten User-Turn ohne `kern`-Annotation (von hinten nach vorne) und reichert ihn an. Wird von der Salienz nach der Analyse aufgerufen. **Warum nachträglich?** Die Salienz läuft nach dem Responder — erst dann ist die Analyse des User-Turns abgeschlossen.~~
**Was an ihre Stelle getreten ist:** `graph/nodes/dispatcher.py` ruft `session_turn_store` mit dem vollständigen Feldsatz. Die Begründung „warum nachträglich" ist damit gegenstandslos — die Felder sind zum Speicherzeitpunkt da, weil der Dispatcher sie aus dem State nimmt, statt auf die Salienz zu warten.
### 3.3 session_summarize_if_needed
Signatur: `session_summarize_if_needed(redis_client, user_id, character_id)`.
Wenn die Turn-Liste über `SESSION_SUMMARIZE_AT` (25) Turns wächst, werden die ältesten 10 Turns zusammengefasst:
1. Älteste 10 Turns aus der Liste lesen
2. Bisherige Summary laden (falls vorhanden)
3. LLM-Call: „Fasse zusammen, behalte Namen, Fakten, Orte, Zahlen bei"
4. Neue Summary speichern, alte 10 Turns entfernen (`ltrim`)
**Fallback:** Bei LLM-Fehler wird auf `SESSION_MAX_TURNS` (20) getrimmt — lieber Turns verlieren als die Session aufblähen.
### 3.4 session_turns_retrieve
Signatur: `session_turns_retrieve(redis_client, user_id, character_id)`.
Gibt alle Turns der aktuellen Session als Liste von Dicts zurück. Wird vom Enricher verwendet, um den Gesprächskontext zu laden.
### 3.5 session_reset
Signatur: `session_reset(redis_client, user_id, character_id)`.
Löscht alle Session-Daten eines Users: Turns, Summary, Stack, Pending.
### 3.6 session_turn_mark_action (Chat 43)
Signatur: `session_turn_mark_action(redis_client, user_id, character_id, ...)`.
Markiert den letzten User-Turn mit dem Ergebnis einer Agent-Aktion. ~~Wird nach dem Agent-Dispatch aufgerufen.~~ → **Widerlegt am 16.09.2026: Die Funktion hat im ganzen `server/` keinen Aufrufer** → **seit dem 18.09.2026 wieder aufgerufen:** `graph/nodes/dispatcher.py::_ausgang_vermerken`, in beiden Pfaden des Dispatchers — `abgeschlossen` setzt ERLEDIGT, ein `fehler` ohne Erfolg FEHLGESCHLAGEN, Rückfrage und Ablehnung vermerken nichts (Scheibe 12 A, Punkt 2). — nur `memory/__init__.py` exportiert sie weiter. Der Leser steht (`format_session_turns_numbered` rendert `[ERLEDIGT]`/`[FEHLGESCHLAGEN]`), die Marke wird seit dem 23.04.2026 nie gesetzt. **Der Aufrufer ist Teil der Entscheidung vom 16.09.2026** — eine Speicherbehauptung wird kommentiert statt entfernt, und der Ausgang des Turns gehört an den Turn (`novaberg-thinking-lage_k.md`, Scheibe 12). (Der Vergleich mit `session_turn_annotate` stand hier bis zum 16.08.2026 — die Funktion gibt es nicht mehr, siehe §3.2.)
Zwei Flags:
- `aktion_erledigt`: Agent hat Verarbeitung abgeschlossen (true bei `abgeschlossen` und `fehler`)
- `aktion_erfolgreich`: Aktion wurde umgesetzt (true nur bei `abgeschlossen`)
Nicht aufgerufen bei Rückfragen (`status=rueckfrage`) — der Turn ist noch offen.
### 3.7 Turn-Marker in format_session_turns_numbered (Chat 43)
> ~~⚠ **Befund 14.09.2026 — die Kappung je Beitrag.** `format_session_turns_numbered` schneidet jeden Beitrag nach `max_chars = 100` Zeichen ab, und **keiner der neun Aufrufer** übergibt einen anderen Wert: Router, Perzeption, Gesprächsvektor und die Klassifikation von sechs Agenten. `[gemessen 14.09.2026]` Vier Antworten Novas begannen mit Regieanweisungen von 157 bis 208 Zeichen — diese Leser sahen von ihnen nur den Anfang der Regieanweisung. Ein Angebot am Ende einer Antwort erreicht sie nicht.~~ → **Behoben am 16.09.2026** (Scheibe 12 B, `novaberg-thinking-lage_k.md` §4).
**Seit dem 16.09.2026 steht kein Beitrag mehr gekürzt im Verlauf.** `max_chars` ist von 100 auf **keine Vorgabe** (`None`) gesetzt; wo eine Grenze nötig ist, greift sie über das **ganze Fenster** und wirft die ältesten Gruppen ganz weg — `budget_wahren`, `SESSION_HISTORY_BUDGET_CHARS = 8000`. Die jüngste Gruppe bleibt immer, auch allein über dem Budget: Ein Fenster ohne den aktuellen Wortwechsel beantwortet keine Frage, die ein Leser stellt.
**Warum die Grenze das Fenster trifft und nicht den Beitrag:** Ein halber Beitrag sieht aus wie ein ganzer — der Leser bekam eine Regieanweisung und hielt sie für die Antwort. Eine fehlende Gruppe ist dagegen sichtbar, weil die Nummerierung bei 1 beginnt und der Verlauf kürzer ist.
`[gemessen 16.09.2026]` Vor dem Bau, über 1494 Rohturns: **94,2 % der Antworten Novas** liegen über 100 Zeichen (Median 440, Max 5012), 36,8 % der Nutzer-Äußerungen (Median 83, Max 3005). Nach dem Bau, derselbe Code über 1439 Fenster aus fünf Turns, alte Fassung gegen neue: **mitten abgeschnittene Beiträge 7213 → 0**; **1249 Fenster (86,8 %)** gehen vollständig durch, 190 (13,2 %) verlieren Gruppen; die Verlaufslänge steigt von 904 auf 3970 Zeichen im Mittel, das Maximum von 1234 auf 8160.
**Das Budget rechnet auf der ungekürzten Länge**, auch wenn ein Aufrufer zusätzlich `max_chars` setzt — dann wirft es Gruppen weg, die nach der Kürzung gepasst hätten. Gewollt: Die beiden Grenzen zu verrechnen hieße, die Kürzung zur Regel zu machen, deren Abschaffung die Änderung ist.
> **Der Verlauf sprengt dabei kein Kontextfenster.** `[gemessen 16.09.2026, zweite Kontrolle, Betriebs-Log]` Der Router-System-Prompt misst real im Mittel 14.804 Zeichen (Max 15.869, n = 97 echte Aufrufe); der Verlaufsblock machte davon **1,5 %** aus (Mittel 221). Schlimmstenfalls kommt er neu auf 15.768 + 8.160 Zeichen — bei 32.768 Token Kontextfenster und einer Warnschwelle bei 80 % rund 22 bis 29 % der Kapazität. Kein Aufrufer setzt ein eigenes `num_ctx`, und eine Plausibilitätsprüfung auf Promptlänge gibt es im Pfad des Verlaufs nicht.
Turns mit `aktion_erledigt=true` bekommen einen Marker im Header:
- `aktion_erfolgreich=true`: `[ERLEDIGT]`
- `aktion_erfolgreich=false`: `[FEHLGESCHLAGEN]`
Format: `[3] USER (neutral, a=0.3) [ERLEDIGT]: Lösch die Einkaufsliste`
Der Marker verhindert, dass Classify-Nodes erledigte Anweisungen als aktive Aufträge interpretieren (KONTEXT1).
**Seit dem 24.08.2026 steht der Marker in derselben Klammer wie Emotion und Anlass** — `[3] USER (neutral, ERLEDIGT): …`. Zwei Klammern hintereinander lesen sich wie Angaben über verschiedene Dinge; hier sind es Angaben über dieselbe Äußerung.
---
### 3.8 verlauf_gruppieren und sprecher_bezeichnen — der Sprecher steht im Feld (24.08.2026)
**Wer gesprochen hat, entscheidet ausschließlich der Turn selbst** — `rolle` sagt die Person, `herkunft` den Anlass. Die Position in der Liste entscheidet nichts.
`verlauf_gruppieren(turns) -> list[list[Verlaufsbeitrag]]` fasst Turns zu nummerierbaren Anzeigeeinheiten zusammen. **Die Gruppe ist eine Anzeigeeinheit, keine Zuordnung.** Ein Eigen-Impuls öffnet immer seine eigene Gruppe, entschieden am Feld `herkunft`.
`sprecher_bezeichnen(beitrag, nova_name, zusatz) -> str` liefert den Namen für den Prompt:
| Lage | Bezeichnung |
|---|---|
| `rolle = user` | `USER` |
| Antwort auf eine Äußerung | `NOVA` |
| `herkunft = eigener_impuls` | `NOVA (von sich aus)` |
| `herkunft` leer | `NOVA (Anlass unbekannt)` |
Die letzte Zeile folgt der Regel aus §3.1: **leer heißt unbekannt und nicht „auf Zuruf"**. Turns von vor dem 30.07.2026 tragen das Feld nicht, und ein Default hätte ihnen rückwirkend einen Anlass angedichtet.
`fenster_waehlen(gruppen, max_turns)` schneidet den Verlauf auf die letzten `max_turns` **Wortwechsel** — Gruppen mit einer Nutzeräußerung —, und nimmt jede Impulsgruppe mit, die dazwischen liegt.
> **Die Zahl zählt Wortwechsel und nicht Gruppen, und der Grund ist eine Regression, die einen Zug lang gebaut war.** Zählte jede Gruppe, kehrte sich der behobene Defekt um: Der Impuls fällt nicht mehr aus dem Verlauf, aber er **verdrängt** den Nutzer aus dem Fenster derer, die nur fünf Einheiten sehen. Acht der neun Aufrufer übergeben unverändert `5` und wurden nicht angefasst.
>
> `[gemessen]` — 24.08.2026, dieselbe Session Zustand für Zustand nachgefahren: Bei **16 von 24** Zuständen lagen weniger Nutzer-Turns im Fenster als vorher, und bei einem **keiner mehr** — Perzeption, Router und sechs Klassifikations-Knoten hätten dort einen Verlauf aus fünf aufeinanderfolgenden Eigen-Impulsen und keinem Wort des Nutzers bekommen. Nach der Berichtigung: **kein Zustand ohne Nutzer-Turn**, und jeder trägt mindestens so viele wie vorher.
**Die Funktion trägt eine Zählung: so viele Beiträge heraus, wie Turns mit Inhalt hinein** — sonst eine `error`-Zeile.
> **Sie ist ein Regressionsriegel und keine Datenprüfung.** Solange die Schleife jeden Beitrag in genau eine Gruppe legt, prüfen beide Seiten dasselbe Prädikat über dieselbe Menge; sie **kann** heute nicht anschlagen, und in 40.439 konstruierten Fällen tat sie es null Mal. Ihr Wert liegt in der Zukunft: Sie feuert, sobald jemand ein `continue` in diese Schleife setzt — und genau so ist der Defekt entstanden, den sie bewacht.
#### Was sie ersetzt, und warum es teuer war
Bis zum 24.08.2026 bildeten **zwei** Stellen Paare `user` → `assistant` und übersprangen alles, was nicht hineinpasste: `format_session_turns_numbered` und eine wörtliche Kopie derselben Logik im Responder. Ein Eigen-Impuls ist ein **alleinstehender** assistant-Turn und traf genau diesen Zweig — er erreichte **keinen** der beiden Verläufe.
**Gemessen am 24.08.2026:** Von 24 Turns eines laufenden Gesprächs fielen **8** aus. Im Betrieb belegt: Nova schlug um 18:37:40 UTC aus eigenem Antrieb ein Vorhaben vor, der Nutzer fragte sieben Sekunden später nach, worum es gehe — und Nova fragte zurück, worauf **er** damit hinauswolle. Ihr eigener Vorschlag stand in dem Verlauf, den sie las, nicht mehr drin.
> **Der Verlust war unsichtbar, weil das Übrige lesbar blieb.** Fällt der Impuls heraus, stehen im Verlauf noch eine Nova-Zeile und darunter die Nachfrage des Nutzers — ein sauberer Wortwechsel. Eine Lücke ist nur daran zu erkennen, dass eine Antwort auf eine Frage antwortet, die nicht dasteht, und daran zweifelt niemand, der den Verlauf zum ersten Mal liest.
Die Daten waren vollständig: **alle 24 Turns trugen `herkunft`**, acht davon `eigener_impuls`. Das Feld existierte seit dem 30.07.2026 und wurde von keinem Renderer gelesen.
#### Alle sieben Renderer
**Den Verlauf lesen sieben Stellen, nicht zwei.** Die zweite Kontrolle hat sie gesucht statt erinnert; vier waren nachgezogen, drei nicht:
| Renderer | Turns | Anlass genannt |
|---|---|---|
| `format_session_turns_numbered` (§3.7) | 24/24 | 8/8 |
| Responder-Verlauf | 24/24 | 8/8 |
| Zusammenfasser (§3.3) | — | ✅ |
| `session_context_build` (§3.9) | — | ✅ |
| `memory/kontext.py::_turns_formatieren` | 24/24 | 8/8 |
| `enricher.py::_suchtext_bauen` (Query-Rewrite) | 24/24 | 8/8 |
| `verfasser.py`, Verlaufsblock | 24/24 | 8/8 |
| `sachlage.py::_render_history` | — | — |
**Beim Zusammenfasser wiegt es am schwersten** — seine Ausgabe überdauert den Verlauf und wird später als Tatsache gelesen.
> **Es sind acht, nicht sieben — die achte Zeile kam am 16.09.2026 dazu und ist die einzige, die noch je Beitrag kürzt.** `graph/nodes/sachlage.py::_render_history` schneidet bei `_BEITRAG_MAX_ZEICHEN = 1600`. Gefunden hat sie kein Durchgang durch diese Tabelle, sondern ein **Kriterium**: ein Lauf über den Syntaxbaum aller Produktivdateien nach jedem `x[:N]` — 351 Kappstellen in 292 Dateien, davon 3 auf einem Beitragstext.
>
> **Ihr Docstring behauptet das Gegenteil dessen, was der Bestand hergibt:** *„Novas Antworten kommen damit ganz an"* (dort Zeile 704), der Konstantenkommentar *„Die Grenze liegt jetzt über der längsten gemessenen Antwort"*. `[gemessen 16.09.2026]` Mit demselben Code nachgerechnet: **155 von 1494** Antworten liegen darüber (10,4 %, Max 3970); und auch nur für die Zeit seit dem 28.08.2026, an dem der Wert von 400 auf 1600 gesetzt wurde, sind es **33 von 651** (5,1 %, Max 2040). Verloren gehen je Treffer im Mittel 691 Zeichen. Fundliste 16.09.2026 — Bestand, nicht im Zug von Scheibe 12 B behoben.
**Der Verfasser hat dafür seine Prompt-Form gewechselt, und das ist eine Entscheidung.** Er reichte den Verlauf bis zum 24.08.2026 als **Nachrichtenfolge** durch: je Turn eine Chat-Nachricht mit `role: user` oder `role: assistant`. Darin ist die **Person** eindeutig und der **Anlass** nicht — ein Eigen-Impuls und eine Antwort sind beide `assistant`.
> **In einer Chat-Nachricht gibt es für den Anlass nur einen Platz: den Inhalt. Genau dort darf er nicht stehen.** Ein Präfix im Text der eigenen Äußerung ist Iteration 1 aus `novaberg-pixie_l_kontamination.md` — das Modell hat den Marker damals mitgeschrieben. Im Textblock steht der Anlass in der **Sprecherzeile**, also im Rahmen und nicht in Novas Mund; dieselbe Form fährt der Responder seit jeher.
>
> **Der Preis ist benannt:** Das Modell sieht den Verlauf nicht mehr in seinem nativen Format. Woran ein Rückschlag zu erkennen wäre — `(von sich aus)` taucht in Novas Antworten auf, oder der Bezug auf früher Gesagtes wird schlechter. Dabei bekam der Verfasser zugleich eine Obergrenze, die es vorher nicht gab (`VERFASSER_WORTWECHSEL = 8`); er sah bis dahin den ganzen Bestand.
---
## 4. Konstanten
| Konstante | Wert | Beschreibung |
|-----------|------|-------------|
| `SESSION_MAX_TURNS` | 20 | Maximale Turns bei Fallback-Trimming |
| `SESSION_TTL` | 14400 (4h) | Inaktivitäts-Timeout |
| `SESSION_SUMMARIZE_AT` | 25 | Ab diesem Füllstand wird zusammengefasst |
---
## 5. Zusammenspiel mit anderen Nodes
Seit Chat 60: Der Dispatcher (`graph/nodes/dispatcher.py`) schreibt alle Session-Turns — User-Turns (Pfad 1) und Assistant-Turns (Pfad 2). `chat.py` schreibt keine Turns mehr direkt.
| Node | Interaktion |
|------|-------------|
| **Dispatcher** | Schreibt User- und Assistant-Turns vollständig via `session_turn_store` (seit Chat 60) |
| **Enricher** | Liest Turns via `session_turns_retrieve` und reicht sie **vollstaendig** durch. ~~blendet Shadow-Impulse aus~~ — **das hat er nie getan** (24.08.2026): Der Filter prueste auf einen Marker, den niemand setzt (`KONTAMINATIONSFILTER-TOT`), und ist entfernt. Der Impuls gehoert in den Verlauf, kenntlich am Sprecher |
| **db_zugriff** | Liest `nova_state:{user_id}:{character_id}` am CG-Eingang, befüllt `state["internal"].emotion` (Chat 89) und `.raum` (Chat 114; fehlen die Achsen, werden sie aus den Register-Labels abgeleitet) |
| **ei_calc_persist** | Schreibt `nova_state:{user_id}:{character_id}` am CG-Ausgang (Chat 89) |
| **Salienz** | ~~Legacy-Annotation via `session_turn_annotate`~~ → **entfallen** (16.08.2026 geprüft). Die Salienz schreibt nicht mehr in die Session; der Dispatcher speichert den Turn vollständig |
| **API-Layer** (`api/chat.py`) | ~~Markiert User-Turns nach Agent-Dispatch via `session_turn_mark_action`~~ → **widerlegt am 16.09.2026: Die Funktion hat im ganzen `server/` keinen Aufrufer** (Fundliste). Der Leser steht, die Marke wird nie gesetzt |
| **Responder** | Sieht die destillierten Turns (über den Enricher, nicht direkt) |
---
## 6. Designentscheidungen
**Kein LLM-Call im Normalbetrieb:** Die Session speichert und liest — reine Redis-Operationen. Nur die Zusammenfassung (`session_summarize_if_needed`) braucht einen LLM-Call, und die wird nur bei langen Gesprächen (> 25 Turns) getriggert.
**TTL statt explizites Löschen:** Die Session verfällt automatisch nach 4 Stunden Inaktivität. Das ist bewusst: Ein neues Gespräch am nächsten Tag soll nicht von den Turns des Vortags kontaminiert werden. Das KZG und LZG halten die wichtigen Inhalte — die Session ist nur für den aktuellen Dialog.
**Die Untergrenze der Frist steht fest, seit es die Eigenzeit gibt** (15.08.2026, vorher 2 Stunden): Sie muss den Nullpunkt der Verfallskurve überdauern (`EIGENZEIT_NULLPUNKT_SEKUNDEN`, 3 h). Läge sie darunter, entstünde ein Fenster, in dem der **Verlauf vor dem Zustand** verschwindet — Nova wäre noch nicht zur Ruhe gekommen und hätte schon vergessen, worüber gesprochen wurde. Ein Zeuge in `test_eigenzeit_verfall.py` hält die beiden Zahlen aneinander.
**Annotation nachträglich, nicht beim Speichern:** Der Turn muss gespeichert werden bevor die Salienz analysiert, weil der Responder den Turn als Kontext braucht. Die Salienz läuft aber erst nach dem Responder. Deshalb: Speichern → Responder sieht den Turn → Salienz analysiert → `annotate` reichert nach → beim nächsten Turn sieht der Enricher die Annotation.
---
→ Enricher (nutzt Session): novaberg-node-enricher.md
→ Salienz (annotiert Turns): novaberg-node-salience.md
→ db_zugriff (lädt nova_state): novaberg-node-db-zugriff.md
→ ei_calc_persist (schreibt nova_state): novaberg-node-ei-calc-persist.md
→ Personality-Klassen: novaberg-personality.md
→ Gedächtnis-Konzept: novaberg-memory.md