# Novaberg — Systemarchitektur **Projekt:** Novaberg — The Nova Anima Resonance System **Dokument:** Systemarchitektur, Tech-Stack, Plugin-System **Stand:** 8. November 2026, 14:15 UTC (`date -u`; §2.2: `submit_sync` scheitert auf der Schleife des Workers sofort und laut — Code-Tree `8dd9ac9`, Kennung wird nachgetragen). Davor 8. November 2026, 14:10 UTC (`date -u`; §2.1, §2.1.1, §2.5 und §9: der Connector `qwen38` — seit dem 07.11.2026 antwortet `qwen3.8:27b` über die Umgebung, die Vorgabe im Code bleibt `qwen36`; die Liste der Connectoren nennt alle fünf, die Vorgabe in §2.5 berichtigt). Davor 22. Oktober 2026, 20:29 UTC (`date -u`; Verzeichnisbaum: `erinnerungsanker.py` neu, der Knoten zwischen Sachlage und Router; `sachlage.py` schreibt den Kanal der gebundenen Entitäten, `sachlage_resolver.py` bietet die Fakten gebundener Objekte an — gebaut und committet, Code-Trees `d810399`, `0d5f633`, `04f823e`). Davor 22. Oktober 2026, 06:06 UTC (`date -u`; Verzeichnisbaum: `timeline_reading.py` liest Ereignisse anderer Personen mit, ein Monat gilt bis zu seinem Ende; `timeline_repository.py` liefert einen Monat für jedes Fenster, das ihn überlappt). Davor 22. Oktober 2026, 05:27 UTC (`date -u`; Verzeichnisbaum: `agents/timeline/ereignis.py`, der passive Dienst für das Ereignis einer anderen Person). Davor 21. Oktober 2026, 20:11 UTC (`date -u`; Verzeichnisbaum: `timeline_types.py`, die eine Quelle der Typen der Timeline, und `timeline_repository.py` mit dem Verschieben in einer Transaktion, nicht committet). Davor 21. Oktober 2026, 18:51 UTC (`date -u`; Verzeichnisbaum: `sachlage_zeitbezug.py`, das Kalenderfenster der Sachlage aus dem Zeitbezug am Objekt, nicht committet). Davor 21. Oktober 2026, 00:40 UTC (`date -u`; das Fakten-Plugin gelöscht). Davor 18. Oktober 2026, 14:58 UTC (`date -u`; Verzeichnisbaum: der Tageslauf mit dem Verfall der Fakten-Kanten, `fakten_repository.py` im Paar). Davor 18. Oktober 2026, 11:16 UTC (`date -u`; §10 und Verzeichnisbaum: der laute Einstieg `python -m schema_bootstrap` und der Kern in `tools/core_schema.py`, den der Start mitbenutzt — Tree `6f23a80`). Davor 17. Oktober 2026, 23:45 UTC (`date -u`; §10: die Reihenfolge der Agenten-Schemata beim Start und die Vorbedingung von `dateien_index` auf `dateien_wurzeln` — Tree `dbf9063`). Davor 17. Oktober 2026, 20:29 UTC (`date -u`; Verzeichnisbaum: `graph/appointments.py` trägt `time_window`, `memory/timeline_reading.py` ist auch der Leseweg der Sachlage). Davor 15. Oktober 2026, 21:37 UTC (`date -u`; die Termine liest der Turn hinter dem Router — `graph/appointments.py`, `memory/timeline_reading.py`; der TimelineManager liefert nur noch das proaktive Fenster; die Sachlage-Recherche sucht nicht nach dem eigenen Bestand — gebaut). Davor 15. Oktober 2026, 01:24 UTC (`date -u`; Konfiguration: `PIXIE_PAUSE_WARNUNG_SEKUNDEN`, ein stehender Pausenschalter meldet sich gedrosselt). Davor 2. Oktober 2026 (§7 Dokumentenverzeichnis: das Konzept des Emotions-Avatars in fünf Teilen, `novaberg-avatar_k.md` bis `_m.md`). Davor 19. September 2026, 21:27 UTC (Verweise auf das geteilte Bugregister umgestellt). Davor 16. September 2026, 22:31 UTC (JSON-Bereinigung: **gültiges JSON wird nicht repariert** — die Wiederholungskappung schnitt ausgerichtete Einrückungen ab). Davor 15. September 2026 (`utils/storage_claims.py` im Verzeichnisbaum — die Speicherbehauptung der Antwort, gerechnet in der Auswertung des Tribunals). Davor 14. September 2026, 19:52 UTC (was der Feldtest mit `gemma4-a4b-gpu` am Empfang und im Thinker zeigte — gemessen, §2 bei den Worker-Backends). Davor 13. September 2026, 15:11 UTC (**das Eigenschaftsgedaechtnis der Sachlage** — drei Tabellen `sachlage_objekt`, `sachlage_objekt_turn`, `sachlage_eigenschaft`, die Module `graph/nodes/sachlage_form.py`, `graph/nodes/sachlage_memory.py` und `memory/sachlage_properties.py`). Davor 12. September 2026, 16:20 UTC (**ein zweiter Redis-Client** — `redis_client_bytes` ohne Dekodierung, fuer Abfragen, die einen Vektor zurueckholen; mit dem dekodierenden bricht die Suche ab und liefert null Treffer bei vollem Speicher). Davor 11. September 2026, 16:00 UTC (**vier Konstanten ergänzt** — die beiden Teilschalter für Agenten und Zustellung, `QUALITAET_FRISCHE_TAGE` und `VERFASSER_IMPULS_NAHE`; dazu die berichtigte Zahl offener Profil-Kandidaten: 1511 statt 350, also 75 Tage statt 18). Davor 6. September 2026, 11:31 UTC (`agents/qualitaet_profil/` im Baum — die Qualitaetsprofile sind aus dem Tageslauf ausgezogen, weil ein Modellaufruf nicht in die `cpu`-Spur gehoert; der Tageslauf traegt seither **neun** Schritte). Davor 6. September 2026, 10:20 UTC (der Tageslauf `synapsen_decay` traegt **zehn** Schritte — der Preiswaechter kam heute dazu, er hatte seit seinem Bau keinen Aufrufer; dazu `services/model_costs.py` im Baum: die Kostenrechnung sitzt im Provider, weil die Trennung von Ein- und Ausgabe auf dem Rueckweg verlorengeht). Davor 5. September 2026, 20:33 UTC (die Parallelitaets-Auskunft in §2.2 — der Riegel ist fuer den Fernpfad gegenstandslos, die Queue-Tiefe liegt in 231 von 256 Einreihungen bei 1; dazu die Modellgroesse. **Beides stand nur im Gespraech.**) Davor 5. September 2026, 20:31 UTC (die **zwölf OpenRouter-Konstanten** in der Konstantentabelle und der Env-Uebersicht — gefunden vom Bezeichner-Diff, nachdem der dateibasierte Nachzug vollstaendig war; die Env-Zeile nannte ausserdem noch `ollama_gpu` als Zustand). Davor 5. September 2026, 20:16 UTC (die Prompt-Modellebene macht einen Modellnamen mit Schraegstrich **flach** — ein Fernmodell nennt den Anbieter mit, und ein verschachteltes Verzeichnis waere eine Ebene, die niemand nachschlaegt; dazu `prompts/deepseek_deepseek-v4-flash-0731/` im Baum). Davor 5. September 2026, 18:51 UTC (`utils/canon.py` im Baum — der Kanon-Zug an der Naht der Perzeption; `graph/einwand.py` holt seine Umlaut-Abbildung seither dort). Davor 5. September 2026, 18:12 UTC (§2.2 um das Abschalten des Reasonings ergaenzt — der Trace zaehlt gegen `max_tokens`; **im Betrieb**: alle drei Worker laufen ueber den Fernzugang). Davor 5. September 2026, 17:41 UTC (§2.2 um die Anbieterwahl ergaenzt — eine Modell-ID ist eine Ausschreibung unter 29, und der `provider`-Block benennt den Gewinner; dazu `tools/openrouter_price_watch.py` im Baum). Davor 5. September 2026, 17:06 UTC (`OpenRouterProvider` als vierter Rueckhalt in §2.2 und §2.3 — ein Zugang, viele Modelle; dazu die Aussage, dass **alle drei Graphen** ueber die Worker laufen (35 `chat.submit`, 16 `background.submit` ueber 63 Dateien gezaehlt), und die **Prompt-Modellebene folgt jetzt dem Backend** statt unbedingt `OLLAMA_MODEL`). Davor 5. September 2026, 10:20 UTC (der Tageslauf `synapsen_decay` traegt **neun** Schritte — die Traegerseite der Faszination kam heute dazu; `memory/fascination_store.py` im Baum, und `ei/fascination.py` traegt nicht mehr nur den Merkmalszug, sondern die ganze Rechnung aus §10). Davor 3. September 2026, 21:30 UTC (der Tageslauf `synapsen_decay` traegt **acht** Schritte — die Qualitaetsprofile kamen heute dazu; `memory/quality_profile.py`, `ei/fascination.py` und `memory/repositories/quality_profile_repository.py` im Baum, `abstrakt_knoten` und `traeger_qualitaet` in der Tabellenliste — die abstrakte Schicht). Davor 3. September 2026, 19:30 UTC (der Tageslauf `synapsen_decay` traegt **sieben** Schritte — die Einfaerbungen kamen heute dazu). Davor 3. September 2026, 18:57 UTC (`praegung_faden` **5** und `praegung_strang` **2** Zeilen nachgezählt; die Aussage *„Richtung und Staerke nicht gebaut"* ist widerlegt — beide sind gebaut und werden seit heute vom Praegungszug gelesen). Davor 1. September 2026, 19:45 UTC (`praegung_strang` in der Tabellenliste und der Tageslauf `synapsen_decay` traegt **sechs** Schritte — der Strang-Nachzug kam heute dazu; `praegung_beruehrung` stand seit dem Vormittag falsch als leer). Davor 1. September 2026, 15:30 UTC (der Tageslauf `synapsen_decay` traegt vier Schritte, nicht zwei — Queue-Verfall stand seit dem 15.08.2026 nicht im Baum, die Faltung kam heute dazu). Davor 1. September 2026, 14:30 UTC (`graph/nodes/verfasser.py` und `thinker_cache.py` im Baum, Zahl der Knoten-Dateien von 20 auf **25** nachgezaehlt — beide fehlten, gefunden von `C18`). Davor 1. September 2026 (`memory/praegung.py` im Baum, `praegung_faden` und `praegung_beruehrung` in der Tabellenliste — die Praegungsschicht; zwei Lessons im Register). Davor 30. August 2026 (`memory/repositories/node_stance_repository.py` und die Tabelle `lzg_knoten_haltung` im Baum — die Haltungsschicht). Davor 29. August 2026 (`graph/nodes/sachlage_plausibility.py` und `sachlage_research.py` im Baum — Scheiben 7 und 8). Davor 28. August 2026 (`memory/kurzziel.py` im Baum — Scheibe 2; `memory/sachlage_history.py` im Baum, `sachlage_verlauf` und `shadow_auftrag.ausloeser_turn_id` in der Tabellenliste — Scheibe 4 des Lage-Konzepts). Davor: 25. August 2026, 20:10 UTC (das Backlog ist nach Gegenstand geteilt — sechs Kategoriedateien, die Rangordnung eigens, das Findemittel darueber; alle im Dokumenten-Index §7). Davor, 12:00 UTC: die Chronik ist nach Monaten geteilt — `novaberg-roadmap.md` traegt den laufenden Zeitraum, vier `novaberg-roadmap-2026-*.md` die abgeschlossenen, und `novaberg-roadmap-index.md` spannt als erzeugtes Findemittel darueber; alle im Dokumenten-Index §7). Davor: das Bugregister ist geteilt — `novaberg-bugs-archiv.md` neu im Dokumenten-Index §7, `novaberg-bugs.md` traegt nur noch die nicht abgeschlossenen Eintraege). Davor: 24. August 2026 (Telegram-Kanal abgeschaltet — Dienstetabelle §1.2, Compose-Auszug §8.1, Startreihenfolge §8.3). Davor: 23. August 2026 (`shadow_auftrag.grund` in der Tabellenliste; davor am selben Tag: Prompt-Segregation dreistufig — `default` → `{modell}` → `{connector}`; `utils/etikett.py` und der `tools/dateien/`-Baum berichtigt; Connector-Tabelle um `qwen36` ergaenzt; *0 hardcoded Prompts* gegen 33 gemessene Literale widerlegt). Davor: 19. August 2026 (`agents/wissen/` — die Bibliothek als **bestellbarer** Dienst: Aushang in `plugins/wissen_manager/`, vier Ausgaenge, und die Abfrage liegt geteilt im Repository, weil Quelle und Zettel denselben Bestand lesen. **Der Manager fehlt weiterhin in der Plugin-Tabelle unten** — der Bestandsbefund von §5 ist damit aelter geworden, nicht erledigt; davor 18. August 2026 (`agents/wissen_rueckweg/` — der Rueckweg vom Gespraech in die Wissensdatei, ausgeloest von der Promotion; dazu `agents/dateien/` **vollstaendig** — Suche, Zoom und der Aufrufer (Aushang in `plugins/dateien_manager/`, Klassifikation, Dispatch, Auskunft); davor am selben Tag ohne Aufrufer; dazu `agents/dateien_index/aufzeichnungen.py`; die Zaehlaussage „sechste Kontextquelle“ bei `autonomous_wissen` um den Dateien-Index ergaenzt — eine weitere Quelle des Enrichers, die ausdruecklich **kein** Plugin ist); davor 16. August 2026 (gegen den Code geprüft: die Modelle kommen aus der **Connector-Tabelle**, nicht aus `OLLAMA_GPU_MODEL`/`OLLAMA_CPU_MODEL`; der Baum unter `memory/` nannte `embedding.py` und `lzg.py`, die es beide nicht gibt — §2.5 und §4 nachgezogen); davor 15. August 2026 (`services/pixie/riegel.py` und `memory/haltung.py` im Baum, `shadow_auftrag.arousal` in der Tabellenliste); davor 8. August 2026 (`novaberg-graph-rechenkette.md` im Dokumenten-Index Tiefe 1 ergänzt — das Register der 34 Rechensysteme des Charakter-Pfads, zerlegt entlang der Rechnungen statt entlang der Knoten. Zuvor: 31. Juli 2026, abends — `graph/nodes/haltung.py` im Verzeichnisbaum ergänzt, Knotenzahlen des CharacterGraph und des Node-Verzeichnisses neu gezählt — beide standen älter als der Verfasser da. Zuvor: `ei/haltung.py` im Verzeichnisbaum ergänzt und der Eintrag zum Haltungsraum im Dokumenten-Index auf das Beitragsmodell umgestellt — die Rechnung ist gebaut, der Knoten fehlt. Zuvor: `novaberg-node-verfasser_k.md` im Dokumenten-Index ergänzt — Konzept, nicht gebaut. Zuvor: `ruff-hart.toml` im Verzeichnisbaum ergänzt, §3 — die harte Teilmenge ohne geduldeten Bestand. Zuvor: `ruff.toml` im Verzeichnisbaum ergänzt, §3. Zuvor: Verzeichnisbaum nachgezogen — `ei/` vollständig, vier fehlende Agenten ergänzt. Kern: Chat 94, Microservice-Welle Block 2+3; Embedding-Modellwechsel Chat 107, §2.4) **Pfad:** novaberg/docs/novaberg-architecture.md **Quellen:** nova-00-a.md (Architektur-Übersicht), nova-07-a.md (Tech-Stack), nova-07-m-a.md (Plugin-System) --- ## 1. Technologie-Stack ### 1.1 Hardware | Komponente | Spezifikation | Rolle | |------------|---------------|-------| | CPU | AMD Ryzen 9 7900X3D (12 Kerne) | Pixie-LLM (CPU-Modell), allgemeine Verarbeitung | | RAM | 64 GB DDR5 6000MT/s | CPU-Modell (~36 GB), OS, Docker-Services | | GPU | AMD Radeon RX 7900 XTX (24 GB VRAM) | Chat-LLM (~14 GB), Embeddings (~0.6 GB) | | OS | Nobara Linux (Fedora-basiert) | ROCm-Kompatibilität für AMD-GPU | **VRAM-Budget:** 14 GB (Chat Q4) + 0.6 GB (Embeddings) = ~14.6 GB von 24 GB. ### 1.2 Docker-Services | Service | Technologie | Port | Rolle | |---------|------------|------|-------| | **server** | Python 3.12, FastAPI + LangGraph | 8000 | API-Server, Graph-basierte Agenten, Pixie | | **postgres** | PostgreSQL 16 + pgvector | 5432 | LZG, Entitäten, Fakten, Timeline, Notizen | | **redis** | Redis 7 Stack (mit RediSearch) | 6379 | KZG, Session, Queues, Stacks, Vektorsuche | | **searxng** | SearXNG | 8080 | Lokale Metasuchmaschine (JSON-API) — seit 30.08.2026 der **Rueckfall** hinter Serper | | ~~**telegram-bot**~~ | Python, Long Polling | — | **abgeschaltet am 24.08.2026** — von Matrix abgeloest; Behaelter entfernt, Compose-Block genommen. `telegram_bot/` liegt weiter im Repositorium. **Die Zeilen fuer `synapse` und `matrix-bot` fehlen in dieser Tabelle** (Fundliste 24.08.2026) | ### 1.3 Host-native Services (Ollama) | Instanz | Port | Hardware | Warum nicht Docker? | |---------|------|----------|---------------------| | **Ollama GPU** | 11434 | VRAM (ROCm) | ROCm-Versionsinkompatibilität zwischen Docker-Image und Host | | **Ollama CPU** | 11435 | RAM | Separate Instanz fuer Pixie | ROCm-Versionen im Docker-Image und auf dem Host sind inkompatibel. Ollama muss daher direkt auf dem Host laufen, alle anderen Services (FastAPI, PostgreSQL, Redis, SearXNG) laufen in Docker. ### 1.4 Client | Komponente | Technologie | Rolle | |------------|------------|-------| | Desktop-Client | GTK4 (PyGObject) + WebKitGTK | Chat-UI (WebView), Panel-System (System, Emotionen, KZG, LZG, Session, Charakter), WebSocket (Charakter-Antworten + Shadow Delivery) | --- ## 2. LLM-Architektur ### 2.1 Tri-LLM + Claude API | Modell | Ollama-Name | Hardware | Context | Verbrauch | Zweck | |--------|-------------|----------|---------|-----------|-------| | Gemma4 26B-A4B (MoE, Q4) | `gemma4-gpu` | VRAM | 32768 | ~17 GB (Q4) | Chat (HumanGraph) — Standard | | Gemma4 26B-A4B (MoE, Q4) | `gemma4-cpu` | RAM | 32768 | — | Pixie Sprache (Fliesstext, Charakter-Treue) | | Qwen3-32B | `qwen3-32b-cpu` | RAM | 32768 | ~19 GB (Q4) | Pixie Analyse (Reasoning, JSON, Planung) | | Qwen 3.8 27B (dense) | `qwen3.8:27b` | VRAM | 32768 | — | Chat (HumanGraph) — im Betrieb seit 07.11.2026, Connector `qwen38` | | Mistral Small 3.2 (24B) | `mistral-small3.2-gpu` | VRAM | 16384 | ~14 GB (Q4) | Chat (HumanGraph) — Fallback-Connector | | Mistral Small 3.2 (24B) | `mistral-small3.2-cpu` | RAM | 32768 | ~17 GB (Q5) | Pixie Sprache — Fallback-Connector | | Nomic Embed Text v2 MoE | `nomic-embed-text-v2-moe` | VRAM | 512 (Modell-Limit) | ~1.0 GB (netto +351 MB ggü. v1) | Embedding-Erzeugung (seit 12.07.2026; v1 war casing-blind, siehe §2.4) | | Claude Sonnet | — (API) | extern | — | ~$0.02/Turn | Alternativer Chat-Provider (Profil `claude`) | Modelle werden ueber das **Connector-System** in `config.py` gesteuert (`OLLAMA_CONNECTOR`). ~~Aktuell verfuegbar: `gemma4` (Standard) und `mistral` (Fallback).~~ → **Stand 08.11.2026:** fünf Connectoren — `mistral`, `gemma4`, `gemma4a4b`, `qwen36`, `qwen38` (§2.1.1); die Vorgabe im Code ist `qwen36`, im Betrieb läuft seit dem 07.11.2026 `qwen38`, und `qwen3.8:27b` antwortet. Umschalten ueber Env-Variable, Neustart genuegt. Mistral bleibt als Connector verfuegbar und wird nicht geloescht. GPU-Modell (Chat) und CPU-Modelle (Pixie) laufen auf getrennter Hardware. Kein Lock, kein Warten, echte Parallelitaet. Pixie nutzt Qwen3-32B fuer Analyse/Reasoning und das CPU-Sprachmodell fuer Sprachausgabe — statisches Routing pro Workflow-Schritt. **Modelfiles:** `ollama/modelfiles/` enthaelt die Modelfile-Definitionen mit `num_ctx` und `num_gpu`-Konfiguration. Single Source of Truth fuer Context-Groesse. ### 2.1.1 Connector-System (Chat 46) `OLLAMA_CONNECTORS` in `config.py` definiert Modell-Profile. Jeder Connector legt fest: GPU-Modell, CPU-Modell, Analyse-Modell, Context-Groessen, Think-Default. Die bestehenden Variablen (`OLLAMA_MODEL`, `SHADOW_MODEL` etc.) werden beim Start aus dem aktiven Connector aufgeloest. Umschalten: ~~`OLLAMA_CONNECTOR=gemma4|mistral`~~ → `OLLAMA_CONNECTOR=` in der Umgebung des Servers, Neustart. | Connector | GPU-Modell | CPU-Modell | Analyse-Modell | GPU ctx | |-----------|------------|------------|----------------|---------| | `gemma4` | `gemma4-gpu` | `gemma4-cpu` | `qwen3-32b-cpu` | 32768 | | `mistral` | `mistral-small3.2-gpu` | `mistral-small3.2-cpu` | `qwen3-32b-cpu` | 16384 | | `qwen36` | `gemma4-gpu` | `qwen36-cpu` | `qwen36-cpu` | 32768 | | `gemma4a4b` | `gemma4-a4b-gpu` | `qwen36-cpu` | `qwen36-cpu` | 32768 | | `qwen38` | `qwen3.8:27b` | `qwen36-cpu` | `qwen36-cpu` | 32768 | > **`qwen36` teilt sich sein GPU-Modell mit `gemma4`** — die Zeile fehlte hier bis zum 23.08.2026, und genau an ihr wäre die Modellteilung ablesbar gewesen, die `OVERRIDE-NACH-CONNECTOR-STATT-MODELL` verursacht hat. ~~Er ist seit dem 24.05.2026 der aktive Connector.~~ → Er war es bis zum 07.09.2026 und ist die Vorgabe im Code. Im Betrieb lief danach `gemma4a4b`, seit dem 07.11.2026 läuft `qwen38` — beide über die Umgebung. Die Zeilen `gemma4a4b` und `qwen38` sind am 08.11.2026 aus `config.py` nachgetragen; CPU- und Analysemodell der drei Connectoren mit `qwen36-cpu` tragen das Fenster 262144. ### 2.1.2 Prompt-Segregation (Chat 46-47) Statische Prompt-Bloecke sind aus den Node-Dateien in Textdateien extrahiert. Verzeichnisstruktur: ``` server/prompts/ default/ — 91 Bloecke (alle Nodes) gemma4-gpu/ — 7 Overrides (JSON-Regeln, Tribunal-Prompts) ``` > **Beim Start heißt: nur beim Start.** Eine geänderte `.txt`-Vorlage wirkt erst nach einem Neuladen des Servers. Der Entwicklungsserver läuft mit `uvicorn --reload`, und dessen Beobachter reagiert in der Vorgabe auf Python-Dateien — **nicht einzeln gemessen**: Am 14.09.2026 fiel jede Vorlagenänderung mit einer Python-Änderung zusammen, und das Neuladen im Log 1,6 s danach ist beiden zuzurechnen. Eine Messung, die eine geänderte Vorlage im Betrieb sehen will, beginnt deshalb mit einem Neustart; ein Laborprozess, der `config` frisch importiert, sieht sie sofort — und misst damit etwas anderes als der laufende Server. **Drei Ebenen, seit dem 23.08.2026:** `prompt_loader.py` liest beim Start `default/`, dann `{gpu_model}/`, dann `{connector}/` — jede spaetere ueberschreibt. Dictionary auf `PROMPTS` in `config.py`; Nodes greifen ueber `PROMPTS["node.block"]` zu. **Die mittlere Ebene ist der Gespraechspfad.** Er haengt am GPU-Modell, nicht am Connector, und zwei der drei Connectoren fahren dort dasselbe (`gemma4` und `qwen36` beide `gemma4-gpu`). Bis zum Umbau lagen die sieben Overrides deshalb unter dem aktiven Connector `qwen36` still, waehrend Gemma4 antwortete — `OVERRIDE-NACH-CONNECTOR-STATT-MODELL`. Der Connector bleibt die **letzte** Ebene, weil er der engere Schluessel ist; fuer Hintergrund-Bloecke ist er die richtige, dort unterscheiden sich die Connectoren wirklich. **Ein Verzeichnis `mistral/` hat es nie gegeben** — die Zeile stand hier, weil sie erwartet wurde. Erreichbar ist heute jeder Name aus `default`, den drei Connectoren und ihren Modellen; ein Zeuge haelt das fest (`test_jedes_verzeichnis_ist_erreichbar`). Namenskonvention: `{node}.{block}.txt` — Beispiel: `router.identity.txt`, `salienz.rules.txt`, `tribunal_jurist.system.txt`. Die Prompts der **Graph-Knoten** sind aus dem Python-Code extrahiert (Chat 46: Perzeption, Router, Salienz, Tribunal; Chat 47: Responder, Thinker, Corrector, GV, KZG-Verdichtung, 4x Classify-Nodes). ~~0 hardcoded Prompts in Python.~~ → **Am 23.08.2026 gemessen und widerlegt: 33 Prompt-Literale ab 100 Zeichen im Produktivcode**, davon 17 mit der `[BLOCKNAME]`-Konvention — Schwerpunkte `agents/charakter/destillation.py` (9), `agents/recherche/` (7), `memory/kontext.py` (einer mit 1714 Zeichen). Die Aussage galt fuer die Graph-Knoten und ist mit den Agenten-Diensten unwahr geworden; die Fundliste traegt es. Konvention: Alle PROMPTS[]-Zugriffe durch `.format()`, literale Klammern in LLM-Beispielen als `{{ }}` escaped. ### 2.2 Provider-Architektur Seit der Microservice-Welle (Chat 92-94) laufen alle Modell-Aufrufe über eine Worker-Schicht (`services/model_services/`). Konsumenten rufen nicht mehr Provider direkt, sondern reichen typisierte Requests an einen der drei Worker — `submit(...)` async aus dem Haupt-Loop, `submit_sync(...)` aus Worker-Threads (CharacterGraph via `asyncio.to_thread`). Jeder Worker besitzt eine FIFO-Queue (`worker_base.py`). ``` Producers ──submit / submit_sync──▶ model_service.{ embed | chat | background } │ │ │ ▼ ▼ ▼ EmbedWorker ChatWorker BackgroundWorker (GPU fix) (1 Backend) (analyse + sprache) │ │ │ └───────────┴────────────┘ │ _build_backend(kind) kind ← MODEL_WORKER_BACKENDS (Config) │ ┌──────────────────────┬─────────────────┴──────────┬────────────────────┐ ▼ ▼ ▼ ▼ ollama_gpu_client ollama_cpu_client AnthropicProvider OpenRouterProvider (OLLAMA_MODEL) (PIXIE_ANALYSE_MODEL / SHADOW_MODEL) (ANTHROPIC_MODEL) (OPENROUTER_MODEL) ``` Backend-Wahl pro Worker ist config-gesteuert (`MODEL_WORKER_BACKENDS`), nicht hartverdrahtet: `ChatWorker` ist single-backend, `BackgroundWorker` dual-backend > **Der `ChatWorker` bedient nicht nur das Gespräch, und sein Name sagt das nicht.** > `[gemessen 07.09.2026]`: `agent/wissen_rueckweg/zuordnung` — ein **Hintergrund**agent — > läuft über ihn und wechselte deshalb das Backend mit, als `WORKER_BACKEND_CHAT` für einen > Feldtest auf lokal gestellt wurde. Niemand hat das entschieden; es folgte aus der > Zuordnung. Der Agent ist kein kleiner Posten: **204 Aufrufe in 33 Stunden**, nach > `pixie/hash` der zweitgrößte Kostenblock. > > **Wer ein Backend je Worker umstellt, stellt damit alle Aufrufer dieses Workers um** — und > die Liste der Aufrufer steht nirgends. Vor einer Umstellung ist sie zu erheben > (`caller=` in der Zeile `ChatWorker 'chat': …` des Server-Logs), sonst umfasst eine > Messung mehr, als ihr Aufsteller glaubt. > > Zweiter Fall derselben Klasse: Der Connector `qwen36` fährt im Gespräch **gemma4-gpu** und > unterscheidet sich von `gemma4` nur im Hintergrund. Dort steht die Warnung im Code — hier > fehlte sie. (`analyse` für Reasoning/JSON, `sprache` für Fliesstext), `EmbedWorker` fest auf GPU. **Alle drei Graphen laufen über diese Worker** — Mensch, Charakter und Agent kennen kein Modell, sondern nur die Rolle. Gemessen am 05.09.2026: 35 Aufrufe von `model_service.chat.submit` und 16 von `background.submit` über 63 Dateien; die einzigen verbleibenden Direktzugriffe auf einen Modell-Client sind die Statusabfrage (`api/health.py`, `list()`/`pull()`) und die Embedding-Pfade. **Ein Wechsel des Backends ist damit ein Wechsel des Modells für das ganze System, und er ist eine Umgebungsvariable.** **Was der Wechsel an der Nebenläufigkeit ändert — gemessen am 05.09.2026, auf die Frage, ob der Riegel entfallen kann.** Drei Ebenen, und nur eine ist der Riegel: - **Der Ollama-Riegel ist für den Fernpfad gegenstandslos.** Er schützt die GPU als *Ressource* (`F-RIEGEL-1`); der `OpenRouterProvider` fasst ihn nicht an, er hält seinen eigenen HTTP-Client. Es ist nichts abzuschalten. Für das Embedding, das lokal bleibt, ist er weiterhin nötig. - **Die Worker-Queue serialisiert weiter** — eine `asyncio.Queue`, ein Task, FIFO. Sie ist die Fahrgemeinschaft für *ein* Auto; beim Fernmodell gibt es kein Auto. - **Seriell bleiben muss dreierlei, und keines davon hängt am Modell:** `graph_run_lock` (er schützt den Gesprächszustand, nicht die Ressource), der Pixie-Lock, und die Knotenfolge im Graphen — die ist Datenfluss, kein Riegel. **Was ein Umbau brächte:** Queue-Tiefe **1 in 231 von 256** Einreihungen — in 90 % wartet niemand; Tiefe 2 in 13, Tiefe 3 in 12 Fällen. Der Gewinn läge bei gleichzeitigen Turns mehrerer Paare, nicht innerhalb eines Turns, dessen elf Aufrufe datenabhängig sind. **Eine Parallelisierung gäbe die Reihenfolgegarantie der Queue auf, die heute niemand ausdrücklich prüft** — deshalb ist sie ein eigener Auftrag mit eigener Messung und nicht nebenbei zu haben. **Eine Modell-ID ist keine Modellwahl.** Am 05.09.2026 gemessen (`GET /models/{id}/endpoints`): 29 Anbieter liefern dasselbe Modell, mit Faktor 8,8 im Eingangspreis, Faktor 13,2 im Ausgangspreis, vier Quantisierungen (fp4/fp8/bf16/unbekannt), Kontextfenstern von 262.144 bis 1.310.720 — und **vier davon führen `response_format` nicht**, womit `expect_json` dort wieder eine Bitte wäre. Ohne Festlegung wählt der Zugang pro Aufruf; eine Messreihe würde den Anbieterwechsel mitmessen. Der `provider`-Block der Nutzlast nennt deshalb `only` (nicht `order` — das ist eine Reihenfolge und erlaubt Weitergehen), `quantizations` und `allow_fallbacks: false`. **Ein Reasoning-Modell denkt ohne Aufforderung, und der Trace zählt gegen `max_tokens`.** `think=False` schaltet es deshalb ausdrücklich ab (`reasoning_effort="none"`); ohne diese Zeile bekäme `query_rewrite` bei 64 Token eine **leere** Antwort, gemessen am 05.09.2026 (0 Zeichen Inhalt gegen 238 Zeichen Denken). Kein Schalter ist bei einem solchen Modell kein neutraler Zustand. Was der gewählte Anbieter an Sampling-Schrauben führt, steht als Kanon in `OPENROUTER_GEFUEHRTE_PARAMETER`; ein Aufrufer, der etwas anderes setzt, bekommt eine Warnung — der Wert geht trotzdem mit, entfernt wird er nicht. `services/price_watch.py` hält Preis, Rabatt, Fenster, Quantisierung und diesen Kanon gegen den lebenden Endpunkt und braucht dafür keinen Schlüssel. **Die Prompt-Modellebene folgt dem Backend, nicht dem Connector** (seit 05.09.2026). **Ein Modellname mit Schrägstrich wird dabei flach gemacht** — `deepseek/deepseek-v4-flash-0731` sucht unter `prompts/deepseek_deepseek-v4-flash-0731/`. Unverändert eingesetzt ergäbe der Name ein verschachteltes Verzeichnis, dessen Zwischenglied eine Ebene wäre, die niemand nachschlägt; `test_prompt_loader_ebenen` meldet so etwas als totes Material. `prompts/{modell}/` wird nach dem Modell geschlüsselt, das tatsächlich antwortet — `config.antwortendes_chat_modell()` löst es aus `MODEL_WORKER_BACKENDS["chat"]` über `MODELL_NACH_BACKEND` auf. Vorher stand dort unbedingt `OLLAMA_MODEL`; solange der Chat-Worker auf `ollama_gpu` lief, war das deckungsgleich. Bei einem fremden Backend hätte es die sieben Blöcke unter `prompts/gemma4-gpu/` (Perzeption, Router, Salienz, Tribunal) einem Modell untergeschoben, das nie Gemma war. Der JSON-Workaround (Ollama #15260) ist im Worker konzentriert: `expect_json` → `parse_json_strict` (`services/postprocess.py`), kein `format="json"` an Ollama. ### 2.3 Klassen | Klasse | Beschreibung | |--------|-------------| | `LLMProvider` (ABC) | Abstrakte Basisklasse mit `chat()` (die `generate()`-Methode wurde in der MS-Welle entfernt — alle Pfade nutzen `chat`) | | `OllamaProvider` | Wrapper um `ollama.Client` — kapselt model, num_ctx, options. `think=False` nativ. | | `AnthropicProvider` | Wrapper um `anthropic.Anthropic` — Token-Logging mit Kosten, `[AUSGABEFORMAT]`-Block provider-intern, `top_p`-Ignorierung (Anthropic erlaubt nicht `temperature` + `top_p` gleichzeitig). | | `OpenRouterProvider` | Spricht das OpenAI-Chat-Protokoll über `httpx` — kein Anbieter-SDK, ein Zugang für viele Modelle. **Die Modell-ID wählt den Anbieter nicht** — sie ist eine Ausschreibung unter vielen (29 am 05.09.2026); der `provider`-Block der Nutzlast benennt ihn, mit abgeschaltetem Rückfall. Das seit 05.09.2026 eingesetzte Modell ist ein spärlich besetztes Mixture-of-Experts: **284 Mrd. Parameter gesamt, 13 Mrd. aktiv** — daher **70,7 Ausgabe-Token je Sekunde** gegen 49,4 lokal, plus 43 % `[gemessen]`. Setzt `expect_json` als `response_format` **durch** statt es nur zu melden, bildet `think` auf den `reasoning`-Schalter ab und liest den Trace aus `message.reasoning`. Token-Logging mit Kosten und Fenster-Auslastung. | ### 2.4 Embedding Embedding (`nomic-embed-text-v2-moe` seit 12.07.2026) ist bewusst **nicht** Teil der Backend-Wahl. Es läuft fest auf GPU über den `EmbedWorker` (Rolle `embed`, siehe §2.7). Grund: Vektorkonsistenz — ein Wechsel des Embedding-Modells invalidiert alle gespeicherten Vektoren, daher kein Config-Hook. **Modellwechsel Chat 107 (EMBEDDING-CASING-BLIND):** `nomic-embed-text` v1 war durch einen GGUF-Konvertierungsfehler casing-blind (Großbuchstaben-Wörter kollabierten zu `[UNK]`-Skeletten; `embed("Hund") == embed("Katze")` bit-identisch). Der Wechsel auf v2-moe erforderte genau das, wovor dieser Absatz immer gewarnt hat: Re-Embedding des gesamten Bestands bei gestopptem Server, plus Gewichts-Reset und Kanten-Rebuild (Befund: `novaberg-embedding-casing-blind_k.md`). ⚠ **`EMBED_MODEL` steht an DREI Orten** — wer nur einen ändert, tauscht das Modell nicht: | Ort | Rolle | |---|---| | `~/ki-assistent/docker-compose.yml` | **WIRKSAM** — Env schlägt Config-Default. Liegt **außerhalb** dieses Repos! | | `novaberg/docker-compose.template.yml` | Vorlage, muss mitgezogen werden | | `novaberg/server/config.py` | nur der Default (`os.getenv`-Fallback) | ### 2.5 Konfigurationsvariablen (LLM) | Variable | Default | Beschreibung | |----------|---------|-------------| | `OLLAMA_CONNECTOR` | ~~`"gemma4"`~~ → `"qwen36"` | Aktiver Modell-Connector (~~`gemma4` oder `mistral`~~ → einer der fünf aus §2.1.1) — bestimmt die geladenen Ollama-Modelle. Im Betrieb seit dem 07.11.2026 `qwen38`, gesetzt in der Umgebung | | `PIXIE_ANALYSE_MODEL` | aus Connector (`qwen3-32b-cpu`) | Pixie Analyse-Modell (Backend `ollama_cpu_analyse`) | | `SHADOW_MODEL` | aus Connector (`gemma4-cpu`) | Pixie Sprach-Modell (Backend `ollama_cpu_sprache`) | | `WORKER_BACKEND_CHAT` | `"ollama_gpu"` | Backend des ChatWorker (siehe §2.7) | > **Seit dem 09.09.2026, 17:55 UTC laufen alle drei Worker lokal.** `chat` auf `gemma4-a4b-gpu`, `analyse` und `sprache` auf `qwen36-cpu`. **Entscheidung des Eigentuemers:** Der Fernanbieter wird nicht mehr verwendet — er hob den Preis in vier Tagen von $0,04998 auf **$0,44000** je Million und liess den Rabatt ohne Ankuendigung fallen. Der `OpenRouterProvider` bleibt gebaut und bezeugt; er wird nur nicht mehr konfiguriert. **Damit kostet der Betrieb keinen Anbieter mehr Geld**, und `kosten_usd` bucht ueberall null. > > **Der Feldtest läuft weiter — Entscheidung des Eigentümers am 13.09.2026**, auf das Angebot, zum Vorgängermodell zurückzukehren: *„Ja, machen wir mit dem Feldtest weiter"* und *„Wir müssen aber den TimelineAgenten unter Gemma 4 korrekt betreiben"*. Der Router wird an das Modell angepasst, nicht das Modell an den Router. > > **Was der Feldtest mit `gemma4-a4b-gpu` am Empfang zeigt (13./14.09.2026, gemessen):** Dieselben vier Router-Prompts mit Terminaussagen ohne Kommando-Verb stellte `gemma4-a4b-gpu` in **0 von 40** Aufrufen zu, `gemma4-gpu` in **9 von 9** — die Modelle lösen den Widerspruch zwischen Dispatch-Guard und Aushang verschieden auf. Unter der seither geltenden Absicht (eingetragen wird nur auf Auftrag) schreibt die Kette unter `gemma4-a4b-gpu` Aufträge 12/12 und Aussagen 0/21 (`novaberg-node-router.md`, letzter Abschnitt). Im selben Feldtest lieferte der Thinker am 14.09.2026 in vier Nachfass-Iterationen hintereinander leeren `content` bei gefülltem `thinking` (`novaberg-node-thinker.md` §3.5). > > **Seit dem 07.11.2026, 11:06 UTC antwortet `qwen3.8:27b`** (Connector `qwen38`, `OLLAMA_CONNECTOR=qwen38` in der Umgebung des Servers); `analyse` und `sprache` bleiben auf `qwen36-cpu`. **Entschieden am 08.11.2026, vom Meister:** *„Ich denke, Qwen bleibt, Gemma ist von der Leistung einfach viel zu veraltet“*. Der Rückweg ist `gemma4a4b`; die Vorgabe im Code bleibt `qwen36` (`novaberg-architecture_l_connector.md` §12). | `WORKER_BACKEND_BG_ANALYSE` | `"ollama_cpu_analyse"` | Analyse-Backend des BackgroundWorker | | `WORKER_BACKEND_BG_SPRACHE` | `"ollama_cpu_sprache"` | Sprach-Backend des BackgroundWorker | | `LLM_PROFILE` | `"lokal"` | Nur noch Schalter für den ThinkingNormalizer: bei `!= "lokal"` läuft dieser als No-Op (kein Ollama-``-Split nötig). Die alte globale Profil-Umschaltung ist durch `WORKER_BACKEND_*` ersetzt. | | `ANTHROPIC_API_KEY` | `""` | API-Key für Claude (aus `.env`) | | `ANTHROPIC_MODEL` | `"claude-sonnet-4-6"` | Claude-Modell (Backend `anthropic`) | | `OPENROUTER_API_KEY` | `""` | Schlüssel für den Fernzugang (aus `.env`) | | `OPENROUTER_MODEL` | `"deepseek/deepseek-v4-flash-0731"` | Modell-ID beim Zugang (Backend `openrouter`) | | `OPENROUTER_URL` | `…/api/v1/chat/completions` | Ziel des Aufrufs | | `OPENROUTER_PROVIDER` | `"baidu"` | **Welcher Anbieter die ID bedienen darf.** Hinter ihr standen am 05.09.2026 **29**; ohne diese Zeile wählt der Zugang pro Aufruf, und Preis, Quantisierung und Kontextfenster sind nicht die konfigurierten · **Am 06.09.2026 gegen `Relace/fp4` geprüft und beim Bestand geblieben:** Relace ist ~10 % billiger **und dauerhaft** (kein Rabatt, der enden kann), führt alle drei Penalties und bietet das siebenfache Ausgabefenster — **aber kein `response_format` und keine `structured_outputs`**. Neun Produktivaufrufe setzen `expect_json=True`; sie brächen laut (die Worker propagieren `JSONDecodeError`), nicht still. Dazu FP4 statt FP8. **Der Tausch wäre Wiederholungsbremse gegen JSON-Fessel** — vor einem Wechsel sind die neun Aufrufe zu messen | | `OPENROUTER_QUANTISIERUNG` | `"fp8"` | Zweiter Riegel — bei einem Anbieter mit mehreren Endpunkten der Unterschied zwischen zwei Modellen | | `OPENROUTER_FALLBACKS` | `false` | Rückfall aus: lieber ein Fehler als eine unbemerkt andere Rechnung | | `OPENROUTER_DENKEN_AUS` | `"none"` | `reasoning_effort` bei `think=False`. **Kein Schalter ist kein neutraler Zustand:** Das Modell denkt von sich aus, und der Trace zählt gegen `max_tokens` — mit 64 Token Grenze kam **null Zeichen Inhalt** gegen 238 Zeichen Denken `[gemessen 05.09.2026]` | | `OPENROUTER_NUM_CTX` | `1048576` | Kontextfenster **beim gewählten Anbieter** — geht in keine Nutzlast, nur in die Warnschwellen der Token-Buchführung | | `OPENROUTER_TIMEOUT_S` | `180.0` | Frist eines einzelnen Aufrufs | | `OPENROUTER_PRICE_INPUT_PER_M` | `0.04998` | USD je Million Eingangs-Token, vom **Endpunkt** gelesen (nicht aus der aggregierten Modellliste — die nannte einen anderen Anbieter). Trägt 64,3 % Rabatt **ohne genannte Frist**; `tools/openrouter_price_watch.py` hält beides gegen den lebenden Endpunkt | | `OPENROUTER_PRICE_OUTPUT_PER_M` | `0.09996` | dasselbe für Ausgangs-Token | | `PREIS_BEFUND_KEY` | `preis_befund` | Wo ein Befund des Preiswächters liegt, bis ihn eine Antwort mitnimmt. **Redis und nicht der direkte Zuruf:** Der Tageslauf hat keinen Event-Loop, und `broadcast_threadsafe` braucht einen | | `PREIS_BEFUND_TTL_S` | `172800` | Zwei Tage — länger als der Abstand zweier Tagesläufe, damit ein Befund nicht verfällt, bevor ihn jemand gesehen hat; kurz genug, dass ein behobener nicht nachhallt | | `PIXIE_QUALITAET_INTERVALL_SEKUNDEN` | `86400` | Takt des Profil-Agenten. **Setzung des Eigentümers, 06.09.2026: einmal am Tag.** ~~Gemessen: 350 Kandidaten offen, 20 je Lauf — rund 18 Tage bis zum aufgeholten Bestand~~ → **am 11.09.2026 nachgemessen: 1511 offene Kandidaten, also rund 75 Tage.** Der Bestand wächst schneller, als der Lauf ihn abarbeitet. Der Takt ist deshalb unverändert geblieben und stattdessen die **Auswahl** geändert: Die Kandidatensuche sortiert seit dem 11.09.2026 zuerst nach `QUALITAET_FRISCHE_TAGE` (7) — die zuletzt gelesenen Träger kommen zuerst, statt auf ihre Stelle in einer 75 Tage langen Schlange zu warten | | `PIXIE_QUALITAET_PRIORITAET` | wie `PIXIE_DECAY_PRIORITAET` (0.2) | Ein Wartungslauf drängt nicht, er soll nur zuverlässig drankommen | | `QUALITAET_FRISCHE_TAGE` | `7` | **Neu am 11.09.2026.** Das Fenster, in dem ein Lesevorgang als *frisch* zählt; die Kandidatensuche sortiert zuerst danach. Die Gesamtlesezahl wählte die historisch häufigen, und ein heute erstmals gelesener Knoten stand hinter allen mit zwanzig — obwohl die Faszination genau ihn jetzt braucht. Sieben Tage decken den Gesprächsverlauf ab, nicht den Bestand | | `PIXIE_PAUSE_SCHLUESSEL` | `pixie:paused` | **Als Konstante benannt am 11.09.2026.** Der Schlüssel stand viermal als Zeichenkette im Code, und ein Leser fehlte: Die Pause hielt den Scheduler an, während die Zustellschleife weiter auslieferte | | `PIXIE_AGENTEN_PAUSE_SCHLUESSEL` | `pixie:agenten_paused` | **Neu am 11.09.2026.** Hält nur die Hintergrundagenten an; die Zustellung läuft weiter. Für eine Reihe, die zugestellte Impulse prüft, ohne dass ein frischer Fund dazwischenfällt | | `PIXIE_DELIVERY_PAUSE_SCHLUESSEL` | `pixie:delivery_paused` | **Neu am 11.09.2026.** Hält nur die Zustellung an; die Agenten arbeiten. Für eine Messung am Hintergrund, deren Ergebnisse nicht mitten in die Reihe fallen sollen | | `PIXIE_PAUSE_WARNUNG_SEKUNDEN` | `900` | **Neu am 15.10.2026.** Ein stehender Pausenschalter meldet sich in Heartbeat und Zustellschleife auf `warning` — höchstens einmal je Schalter und Intervall, nicht je Zyklus (der Heartbeat läuft alle 120 s). Dicht genug, dass ein vergessener Schalter bald auffällt; selten genug, dass die Zeile nicht zum Rauschen wird. Die Drossel liegt in Redis (`:warnung`), der Zeitpunkt des Setzens in `:seit` (`services/pixie/pause.py`, `novaberg-pixie.md`) | | `VERFASSER_IMPULS_NAHE` | `0.55` | **Neu am 11.09.2026.** Ab dieser thematischen Nähe kommt ein zugestellter Fund ohne Übergangsblock aus. Die Zustellschwelle liegt bei 0,30 — ein Eintrag mit 0,37 sprang gemessen ohne ein Wort des Übergangs vom Gesprächsthema weg. Gesetzt, nicht abgeleitet: Eine Spanne gemessener Nähen mit dem Urteil *brauchte eine Brücke* daneben gibt es noch nicht | | `OPENROUTER_GEFUEHRTE_PARAMETER` | elf Werte | Was der gewählte Anbieter an Sampling-Schrauben führt. **`presence_penalty` und `repetition_penalty` sind nicht dabei**, obwohl Responder und Verfasser sie setzen — der Provider meldet das und entfernt sie nicht | ### 2.6 Bekannter Bug: Ollama think+format (Chat 46) Ollama Issue #15260: Bei Gemma4 (und Qwen 3.6) bricht `think=false` den `format="json"`-Constraint — JSON wird stillschweigend ignoriert. Workaround: `think=False` senden, `format="json"` NICHT senden. JSON-Einhaltung erfolgt über Prompt-Overrides (Gemma4-spezifische `[REGELN]`) + die Cleanup-Pipeline in `services/postprocess.py` (`clean_json_response` → `deduplicate_repetition` → `repair_truncated_json`, gebündelt in `parse_json_strict`). **Seit dem 16.09.2026 in dieser Reihenfolge nur noch für ungültigen Text:** Nach `clean_json_response` wird zuerst geparst; nur wenn das scheitert, greifen Kappung und Klammerreparatur, und das steht als `JSON repariert` im Log. Gemessen: Die Kappung hielt eine Einrückung von 25 Leerzeichen für eine Endlosschleife und schnitt eine gültige Sachlage ab. Die Pipeline läuft seit der MS-Welle im Worker (`expect_json`-Pfad), nicht mehr im Provider. Status: Ollama-Bug offen (Stand 15.04.2026). ### 2.7 Model-Service-Schicht (seit Chat 92, erweitert Chat 94) Alle Modell-Aufrufe laufen über eine In-Process-Microservice-Architektur in `server/services/model_services/`. Konsumenten kennen keine Modelle, nur drei abstrakte Rollen; ein Worker pro Rolle vermittelt zwischen Konsument-Absicht und Modell-Aufruf über eine FIFO-Queue. Die Provider-Klassen (`OllamaProvider`/`AnthropicProvider`, §2.3) werden nur noch von der Registry instanziiert — kein Konsument ruft sie direkt. **Drei Rollen:** | Rolle | Worker | Backend(s) | Modell (Connector `gemma4`) | |-------|--------|-----------|------------------------------| | `embed` | `EmbedWorker` | fest GPU | `nomic-embed-text-v2-moe` | | `chat` | `ChatWorker` | single, `WORKER_BACKEND_CHAT` | `gemma4-gpu` | | `background` | `BackgroundWorker` | dual: `analyse` + `sprache` | `qwen3-32b-cpu` / `gemma4-cpu` | **Backend-Wahl** ist config-gesteuert über `MODEL_WORKER_BACKENDS` (`config.py`), nicht hartverdrahtet. `_build_backend(kind)` in `registry.py` baut aus dem Schlüssel die Provider-Instanz; gültige Schlüssel: `ollama_gpu`, `ollama_cpu_analyse`, `ollama_cpu_sprache`, `anthropic` (fail-loud bei unbekanntem Schlüssel). Das ersetzt das alte globale `LLM_PROFILE`-Schema feinkörnig — eine Rolle kann auf Claude laufen, während die anderen lokal bleiben (z. B. Chat-Eval auf `anthropic`, Background weiter lokal). **Komponenten:** - `types.py` — typisierte Requests/Responses: `EmbedRequest`/`EmbedResponse`, `EmbedBatchRequest`/`EmbedBatchResponse` (mehrere Texte in einem Aufruf, seit 12.09.2026), `ChatRequest`/`ChatResponse`, `BackgroundRequest`/`BackgroundResponse` - `worker_base.py` — `ModelWorker`-Basisklasse mit FIFO-Queue, `submit` (async, aus dem Haupt-Loop) und `submit_sync` (Brücke für Worker-Thread-Konsumenten) - `embed_worker.py` — `EmbedWorker` (Rolle `embed`, GPU-fix), geteilt von Nova und Pixie - `chat_worker.py` — `ChatWorker` (Rolle `chat`, single-backend), `expect_json` → `parse_json_strict` - `background_worker.py` — `BackgroundWorker` (Rolle `background`, dual-backend analyse/sprache), CJK-Guard für Qwen-Output, `expect_json`-Pfad - `registry.py` — `ModelServiceRegistry`, Lifecycle (`startup`/`shutdown` im FastAPI-Lifespan), Singleton `model_service`, `_build_backend` JSON-Post-Processing liegt in `services/postprocess.py` (zustandsloser Util, bewusst außerhalb des `model_services`-Pakets — siehe Lesson `paket-init-zyklus`). **Aufruf-Konvention:** Konsumenten im Worker-Thread (LangGraph-Nodes, die meisten Agenten): `model_service.chat.submit_sync(ChatRequest(...))` Konsumenten im Haupt-Event-Loop (Lifespan-Repair, `shadow_delivery_loop`): `await model_service.chat.submit(ChatRequest(...))` **Seit dem Code-Tree `8dd9ac9` (Kennung wird nachgetragen) prüft `submit_sync` die Konvention am Eingang.** Läuft der Aufruf auf der Schleife des Workers — der Schleife, die die Antwort zustellen müsste —, wirft er sofort `RuntimeError` mit Worker, Aufrufstelle (`::`) und Frist und schreibt dieselbe Meldung als `error` ins Log; bis dahin stand er bis zur Frist und endete in einem leeren `TimeoutError`. Ein Arbeits-Thread und eine fremde Schleife sind erlaubt, die Frist ist unverändert. Gemessen vor dem Commit über den echten `main.lifespan` mit Doppeln für die Modelle: **0 Abweisungen bei 95 Aufrufen** aus 25 Aufrufer-Funktionen, die Positivkontrolle 1 (`labor/2026-11-07_submitsync_gegenprobe.txt`, 07.11.2026). **Stand Chat 94:** Block 1 (`embed`), Block 2 (`chat` + `background`) und Block 3 (`think` pro Call) abgeschlossen. Backend-Switch zu Qwen 3.6 (`qwen36-cpu`) folgt in Block 4. Details: `novaberg-microservice-modell-queue_k.md`. --- ## 3. Projektstruktur ``` project/ ├── client/ │ ├── main.py # GTK4 Application Einstieg │ └── ui/ # Chat, Fakten, Gedaechtnis, Timeline, Schatten, │ # Status, System-Panels, Hauptfenster, │ # Emotions-Radar (Cairo) ├── server/ │ ├── main.py # App-Start, Lifespan, Router-Includes │ ├── schema_bootstrap.py # Lauter Aufbau des Schemas von null: Kern, Agenten, Pruefung gegen den Baum (python -m schema_bootstrap) │ ├── config.py # Zentrale Konfiguration, Umgebungsvariablen │ ├── prompt_loader.py # Laedt PROMPTS-Dict (default/ + Modell + Connector) │ │ │ ├── api/ # REST + WebSocket Endpoints │ │ ├── chat.py # /chat, /chat/stream (Pfad 1 + Event-Erzeugung) │ │ ├── health.py # /health (5 Dienste + Pixie), /modelle │ │ ├── gedaechtnis.py # /gedaechtnis/*, /fakten/*, /emotionen/* │ │ ├── session.py # /session/* │ │ ├── websocket.py # WebSocket (Charakter-Antwort + Shadow Delivery) │ │ ├── admin.py # /admin/pixie/{pause,resume,status} │ │ └── models.py # Pydantic-Modelle │ │ │ ├── graph/ # LangGraph Graphen + State (Chat 60: Graph-Split) │ │ ├── base.py # GraphBase (abstrakt, create_state) │ │ ├── human_graph.py # HumanGraph (Pfad 1, 5 Nodes) │ │ ├── character_graph.py # CharacterGraph (Pfad 2, 20 Nodes — gezählt 31.07.2026; die frühere Angabe 17 ist älter als Verfasser und Haltungsraum) │ │ ├── agent_graph.py # AgentGraph (Pixie-Pipeline, 3 Nodes) │ │ ├── builder.py # Fassade, Plugin-Init │ │ ├── personality.py # Personality-Klassen (Character, Emotion, Personality, InternalPersonality) │ │ ├── state.py # State-Definition, PendingWrite │ │ ├── appointments.py # Das Lesefenster der Termine eines Turns aus der Anfrage des Routers (15.10.2026; → novaberg-agent-timeline.md §4a); seit 17.10.2026 auch `time_window`, das Fenster eines Zeitausdrucks für Timeline-Agent ~~und Sachlage~~ (§4.2; die Sachlage liest seit 21.10.2026 aus ihrem Zeitbezug über `range_window`/`search_window`) │ │ └── nodes/ # 25 Node-Dateien, von den drei Graphen geteilt (gezählt 01.09.2026; 20 am 31.07.2026) │ │ ├── haltung.py # Knoten `haltungsraum`: lädt Landschaft + Rad, rechnet, schreibt state["haltung"] (→ novaberg-haltungsraum_k.md §2) │ │ ├── perzeption.py # → novaberg-node-perception.md (rolle user/assistant) │ │ ├── db_zugriff.py # → novaberg-node-db-zugriff.md (CG-Entry, lädt Personality) │ │ ├── enricher.py # → novaberg-node-enricher.md (Methodensplit nach Phase 4) │ │ ├── emotionale_gravitation.py # → novaberg-node-emotionale-gravitation.md (CG, zwischen Enricher und Reducer) │ │ ├── reducer.py # Dedupliziert memory_entries → memory_context (nur CG, seit Chat 75) │ │ ├── sachlage.py # → novaberg-thinking-lage_k.md (CG, reducer→sachlage_node→~~router~~ → erinnerungsanker (22.10.2026, committet, Code-Tree `04f823e`): fortgeschriebenes Verstehen je Turn, seit 28.08.2026; seit 22.10.2026 auch der Kanal `objekt_entitaeten`) │ │ ├── erinnerungsanker.py # → novaberg-memory-fakten_b.md §6 (CG, sachlage_node→erinnerungsanker→router: Fakten und Erinnerungen der gebundenen Personen im Kontext, vor dem Router; seit 22.10.2026, committet, Code-Tree `04f823e`) │ │ ├── sachlage_resolver.py # → novaberg-thinking-lage_k.md §4 Scheibe 6 (der Frame-Auflöser: offene Eigenschaften gegen den Gedächtnis-Pool des Turns, eigener Call; seit 28.08.2026 — seit 22.10.2026 (committet, Code-Tree `d810399`) auch die Fakten der gebundenen Objekte, Scheibe 13 Teil H) │ │ ├── sachlage_plausibility.py # → novaberg-thinking-lage_k.md §4 Scheibe 7 (Behauptungen des Nutzers gegen Weltwissen, vier Stufen, eigener Call bei akutem Objekt; seit 29.08.2026) │ │ ├── sachlage_research.py # → novaberg-thinking-lage_k.md §4 Scheibe 8 (eine Websuche je Turn für die erste »nachschlagen«-Eigenschaft; seit 29.08.2026 — seit 15.10.2026 nicht bei einer Frage nach dem eigenen Bestand) │ │ ├── sachlage_form.py # → novaberg-thinking-lage_k.md §3 Festlegung 1 (die Form jedes Objektfelds beim Parse, beim Laden der vorigen Blase und im Kontext-Tab; seit 13.09.2026) │ │ ├── sachlage_memory.py # → novaberg-thinking-lage_k.md §4 Scheibe 11 (die Objekte eines Turns ablegen, an Entitäten und Zeitanker binden; seit 13.09.2026) │ │ ├── sachlage_zeitbezug.py # → novaberg-thinking-lage_b.md §4 Scheibe 6 (der Zeitbezug am akuten Objekt — Zeitraum oder Titel aus Äußerung und Name, ein Feld des Servers — und daraus das Kalenderfenster des Frame-Auflösers; seit 21.10.2026, nicht committet) │ │ ├── ei_calc.py # → novaberg-node-ei-calc.md (rolle user/character) │ │ ├── ei_calc_persist.py # → novaberg-node-ei-calc-persist.md (CG-Ausgang, nova_state-Persistierung) │ │ ├── router.py # → novaberg-node-router.md │ │ ├── planner.py # → novaberg-node-planner.md │ │ ├── responder.py # → novaberg-node-responder.md │ │ ├── thinker.py # → novaberg-node-thinker.md │ │ ├── salience.py # → novaberg-node-salience.md │ │ ├── praegung.py # → novaberg-node-praegung.md (Faden-Tor, Praegungszug, Faszination je Turn) │ │ ├── dispatcher.py # → novaberg-node-dispatcher.md (zentraler Session-Turn-Schreiber) │ │ ├── tribunal.py # → novaberg-node-tribunal.md │ │ ├── corrector.py # → novaberg-node-corrector.md │ │ ├── agent_dispatch.py # Agent-Dispatch (Epic 11) │ │ ├── verfasser.py # → novaberg-node-verfasser_k.md (zwischen GV und Responder: bestimmt WAS gesagt wird, der Responder gibt die Form) │ │ ├── thinker_cache.py # Per-Turn-Tool-Cache des Thinkers, zwei Stufen (THINK-MEM-LOOP); strikt lokal je think()-Aufruf │ │ └── gespraechsvektor.py # → novaberg-node-gv_k.md │ │ │ ├── agents/ # Agent-System (Epic 11 + Epic 5) — 18 Agenten │ │ ├── __init__.py # AgentRegistry, Auto-Discovery │ │ ├── base.py # BaseAgent, AgentState, AgentResult, PeriodicTask │ │ ├── crud_validation.py # Gemeinsame CRUD-Haertung (Chat 42) │ │ ├── notizen/ # NotizenAgent (User-Agent, Resume + Bestaetigung) │ │ ├── timeline/ # TimelineAgent (User-Agent, Zeitparser + Resume); dazu `ereignis.py` — das Ereignis einer anderen Person, passiv über den Dispatcher (22.10.2026; → novaberg-agent-timeline.md §5.5) │ │ ├── charakter_identitaet/ # CharakterIdentitaetAgent (User-Agent, Resume + init.sql) │ │ ├── direktiven/ # DirektivenAgent (User-Agent, HITL-Gate + init.sql) │ │ ├── dateien_index/ # DateienIndexAgent (Waechter, ohne Takt, init.sql) │ │ │ ├── wandern.py # neu | geaendert | unveraendert | verschwunden | ausserhalb │ │ │ # Hash: neu/geaendert. Blick auf die Platte: fort/ausserhalb │ │ │ ├── indizieren.py # Thema, Stichwoerter, Blockkarte, Einbettung │ │ │ ├── speicher.py # Zeilen ueber Dateien, nie Dateien │ │ │ └── aufzeichnungen.py # der Enricher-Weg: Treffer je Turn (seit 18.08.2026) │ │ ├── dateien/ # DateienAgent (lesend, Empfang, vier Ausgaenge, seit 18.08.2026) │ │ │ ├── klassifikation.py # wonach gesucht wird und wie tief (finden | lesen) │ │ │ ├── suche.py # drei Kanaele, scharf vor unscharf │ │ │ ├── zoom.py # Karte (aus dem Index), Block, Nadel │ │ │ ├── auskunft.py # die Beschriftung: jede Zeile traegt ihre Fundstelle │ │ │ └── dispatch.py # kein Tor, kein Rueckweg — der Dienst aendert nichts │ │ ├── dateien_wurzeln/ # DateienWurzelnAgent (User-Agent, Tor + Aussenrand + init.sql) │ │ │ ├── aussenrand.py # Die Schranke: aufloesen, dann pruefen (→ novaberg-agent-dateien_k.md §7) │ │ │ ├── resume.py # Rueckweg der Rueckfrage — nein und unklar fuehren nie zur Ausfuehrung │ │ │ └── init.sql # Tabelle dateien_wurzeln (seit 18.08.2026) │ │ ├── kzg/ # KZG-Agent (LangGraph-Subgraph, 5 Nodes inkl. magnete_aufloesen) │ │ ├── delegation/ # DelegationsAgent (Halluzinations-Ventil, init.sql) │ │ ├── recherche/ # RechercheAgent (Pixie, Web-Recherche) │ │ ├── promotion/ # PromotionAgent (Pixie, KZG -> LZG) │ │ ├── charakter/ # CharakterAgent (Pixie, Hash-Destillation + zwei Charakter-Raeder) │ │ │ ├── rad_messreihe.py # Messreihe der Raeder: rohe Messungen + gewichtetes Mittel (→ novaberg-charakter-rad-messreihe_k.md) │ │ │ └── init.sql # Tabelle charakter_rad_messung (seit 01.08.2026) │ │ ├── wiedervorlage/ # WiedervorlageAgent (Pixie) │ │ ├── synapsen_promotion/ # Synapsen P4 (Pixie, KZG -> lzg_knoten) │ │ ├── synapsen_decay/ # Der Tageslauf (Pixie), ~~neun~~ → zehn Schritte: Knoten-Decay · pipeline_log-Retention · Queue-Verfall · Faltung der Prägungsfäden · Nachzug der Stränge · Strang-Richtungen · Einfärbungen · Trägerseite der Faszination · Verfall der Fakten-Kanten (seit 18.10.2026, eigener Audit `fakten_verfall`) · Preiswächter — die Qualitätsprofile sind am 06.09.2026 nach agents/qualitaet_profil/ ausgezogen (Modellaufruf gehört nicht in die cpu-Spur) (→ novaberg-memory-qualitaetsprofil.md, novaberg-thinking-faszination_k.md §10.6) │ │ ├── qualitaet_profil/ # Die sechs Qualitäten je Träger, täglich, `llm`-Spur — am 06.09.2026 aus dem Tageslauf ausgezogen: dort scheiterte jeder Träger am Spur-Riegel (0 von 20 über zwei Läufe), weil ein Profil einen Modellaufruf kostet │ │ ├── wissen/ # WissenAgent — die eigene Bibliothek als bestellbarer Dienst (Empfang, CPU-Spur, seit 19.08.2026) │ │ ├── wissen_rueckweg/ # Rueckweg: zwei Auftragsarten — 'wissen_rueckweg' schneidet ein, 'wissen_verweis' verstaerkt nur (Pixie, LLM-Spur, seit 18.08.2026) │ │ │ ├── herkunft.py # roh vor verdichtet — die Marke reist mit │ │ │ ├── zuordnung.py # Pflegbarkeit statt Naehe; keine Datei ist eine Antwort │ │ │ └── einarbeitung.py # Absatz an die Stelle, versioniert, umkehrbar │ │ ├── wissensluecken/ # WissensluckenAgent (Pixie, Chat 112) │ │ └── ziel_decay/ # Motivations-Verfall der Ziele (Pixie, Anker + Uhr seit Chat 113) │ │ │ ├── ei/ # EI-Berechnungsmodul (Chat 58 ausgelagert, Chat 61 Refactor) │ │ ├── berechnung.py # Verlauf, Vektor, Nova-Empathie, sin^0.5-Glaettung │ │ ├── dreischicht.py # 6 Achsen → 64 Sektoren → 13 Cluster → Strategie (→ novaberg-gv-strategie_k.md) │ │ ├── raum.py # Novas Gespraechsraum, Zug zum Register des Sprechers (Chat 114) │ │ ├── initiative.py # Achse I: wer im Turn die Richtung setzt (Chat 116, → novaberg-gv-initiative.md) │ │ ├── neugier.py # GV4: sechs Saeulen × Persoenlichkeit → Aufnahmebereitschaft │ │ ├── wissensluecken.py # GV4: semantisch nahe, aber unbesprochene Konzepte │ │ ├── source_weights.py # GV4: Rang des Gewichts in der eigenen Quelle — die Naht zwischen LZG und KZG │ │ ├── gap_topics.py # GV4: Themen als Lückenkandidaten, Themenbestand des Nutzers, Themenvektoren im Stapel │ │ ├── gravitation.py # Emotionale Gravitation: Erinnerungen als Attraktoren (→ novaberg-node-emotionale-gravitation.md) │ │ ├── fascination.py # Die ganze Rechnung der Faszination: Merkmalszug, Anker, sechs Turn-Modulatoren, Strangzug, Verfall je Dimension, Zusammenfuehrung (→ novaberg-thinking-faszination_k.md §10) │ │ ├── salienz.py # Salienz-Formel (→ novaberg-salienz-berechnung_k.md) │ │ ├── haltung.py # Landschaft + Zuwendungsrad → fuenf Verhaltensgroessen (→ novaberg-haltungsraum_k.md) │ │ ├── farbton.py # Farbmisch-System: 8 Dimensionen → Landschaftsbeschreibung │ │ └── utils.py # Gemeinsam genutzt von Neugier, Wissensluecken, Dreischicht (u.a. modus_pruefen) │ │ │ ├── tools/ # Tool-Manager (Epic 11) │ │ ├── core_schema.py # Kernschema db/init.sql anwenden, laut; Sperrfrist, Bereinigung der Verbindungszeichenkette │ │ ├── db_manager.py # PostgreSQL + Connection Pool │ │ ├── redis_manager.py # Redis (nativ threadsafe) │ │ ├── web/ # Web-Infrastruktur (Chat 35) │ │ │ ├── search.py # WebSearchManager (Serper, SearXNG als Rueckfall) │ │ │ └── fetch.py # PageFetcher (trafilatura + BS4) │ │ └── dateien/ # Datei-Werkzeuge (WIS-3) │ │ └── schreiben.py # Pfadwaechter + Modusbits (F-WISSEN-1) │ │ # die uebrigen Dateien dieses Verzeichnisses │ │ # fehlen im Baum — siehe novaberg-fundliste.md │ │ │ ├── memory/ # Gedaechtnis-Schicht │ │ ├── kzg.py # Kurzzeitgedaechtnis (Redis, RediSearch-Index, Magnet-Felder P3) │ │ ├── lzg_knoten.py # Langzeitgedaechtnis: Knoten (PostgreSQL, Ebbinghaus) │ │ ├── lzg_kanten.py # Langzeitgedaechtnis: Kanten │ │ ├── pipeline_log.py # Forensik-Sink (Synapsen P1, asynchroner Writer-Task); `log_decision` = Entscheidungs-Eintrag einer Weiche (seit 18.09.2026) │ │ ├── background_audit.py # Die eine Senke des `hintergrund_log` (`write_audit`, Status aus `AUDIT_STATUSES`, seit 18.09.2026); Dienste schreiben ueber `BaseAgent._audit` │ │ ├── sachlage_verlauf.py # Sachlage-Gedaechtnis je Turn, ohne Verfall (→ novaberg-thinking-lage_k.md §4) │ │ ├── timeline_reading.py # Leseweg der Termine eines Turns, ~~hinter dem Router~~ → hinter dem Router, für den Timeline-Agenten und seit 17.10.2026 für das Angebot der Sachlage, ohne Erinnerungs-Anker (15.10.2026; → novaberg-agent-timeline.md §4a); seit 22.10.2026 mit den Ereignissen anderer Personen, die der Block gekennzeichnet setzt, ein Eintrag mit `month` bis zum Monatsende kommend │ │ ├── timeline_types.py # Die Typen der Timeline an einer Stelle: Merkmale je Typ, `ereignis`, Wiedervorlage aus `remind`, ein unbekannter Typ laut (21.10.2026; → novaberg-agent-timeline.md §8) │ │ ├── sachlage_properties.py # Eigenschaftsgedaechtnis der Sachlage: Objekte je Paar, ihre Turns, Werte mit Historie (→ novaberg-thinking-lage_k.md §4, Scheibe 11) │ │ ├── charakter.py # Charakter-Hash (Read) │ │ ├── haltung.py # Haltungsstand je Paar (Zustand, nicht Verlauf) │ │ ├── fascination_store.py # Die Traegerseite der Faszination: Anker-Zaehler, verfallenes Profil, Strangnaehe, Bestandslauf (→ novaberg-thinking-faszination_k.md §10.2, §10.3a) │ │ ├── praegung.py # Praegungsfaeden: Tor, Formkurve, Auffrischung, Faltung, Straenge, Zug (→ novaberg-node-praegung.md) │ │ ├── quality_profile.py # Der Erzeuger der Qualitätsprofile: Prompt, Annahme der Modellantwort, gedeckelter Lauf (→ novaberg-memory-qualitaetsprofil.md) │ │ ├── session.py # Session (_session_key mit character_id, Chat 60) │ │ ├── kontext.py # Session-Kontext-Extraktion (LLM-gestuetzt) │ │ ├── ziele.py # Ziele: lang-, mittel- und (seit 28.08.2026) kurzfristig; Verfall — beim Lesen gerechnet (ziele_live_bewerten) │ │ ├── kurzziel.py # Das kurzfristige Ziel aus der Sachlage (→ novaberg-thinking-lage_k.md §4, Scheibe 2) │ │ ├── utils.py # Gemeinsame Helfer der Schicht │ │ ├── repositories/ # Daten-Repositories (CRUD gegen PostgreSQL) │ │ │ ├── entitaeten_repository.py # Knowledge Graph Nodes │ │ │ ├── fakten_repository.py # Knowledge Graph Edges (bi-temporal) — jede Abfrage im Paar, Lesen ohne Schreiben, Ablösen auf dem Cursor des Aufrufers, Verfall der Kanten (18.10.2026) │ │ │ ├── timeline_repository.py # Termine und Ereignisse — im Paar; Verschieben in einer Transaktion mit `nachfolger_id`, `aktive_zeile` folgt der Kette (21.10.2026; → novaberg-agent-timeline.md §5.2); `MONTH_PRECISION`, und `find_by_date_range` liefert eine Zeile mit `month` für jedes Fenster, das ihren Monat überlappt (22.10.2026) │ │ │ ├── notizen_repository.py # Merkzettel und Listen │ │ │ ├── autonomous_wissen_repository.py # Metadaten der Wissens-Bibliothek (WIS-2/3) │ │ │ ├── verbindung_repository.py # KZG-Verbindungen │ │ │ ├── shadow_auftrag_repository.py # Shadow-Queue samt Verfall (→ novaberg-queue-verfall_k.md) │ │ │ ├── node_stance_repository.py # Die Ladung eines Knotens (→ novaberg-thinking-opinion_k.md §5a) │ │ │ └── quality_profile_repository.py # Qualitätsprofil eines Trägers, vorzeichenlos (→ novaberg-memory-qualitaetsprofil.md) │ │ └── services/ │ │ └── entity_resolution.py # Entity Resolution (Name + Fuzzy + Embedding) │ │ │ ├── utils/ # Hilfsfunktionen — reine Rechnung, kein Zugriff │ │ ├── zeitparser.py # Zeitaufloesung (Fuzzy + Normalisierung + Vektor) │ │ ├── datum_pruefung.py # Wochentag gegen Datum │ │ ├── storage_claims.py # Speicherbehauptung der Antwort gegen die Dienste (15.09.2026) │ │ ├── etikett.py # was ein Pfad ueber die Geltung sagt (23.08.2026) │ │ └── canon.py # Modellwert auf kanonische Form ziehen (05.09.2026) │ │ │ ├── plugins/ # Manager-Plugins │ │ ├── base.py # BaseManager Interface │ │ ├── kzg_manager/ # KZG-Schreiboperationen │ │ ├── ~~fakten_manager/~~ # ~~Fakten + Entity Resolution~~ → am 21.10.2026 gelöscht (nicht committet); Fakten schreibt der Fakten-Agent, gelesen wird über die Mappe (`memory/fakten_mappe.py`) │ │ ├── timeline_manager/ # Termine + Zeitparser │ │ ├── notizen_manager/ # Merkzettel, Listen │ │ ├── charakter_identitaet_manager/# Charakter-Identitaet (Router-Prompt) │ │ ├── dateien_manager/ # Der Aushang des lesenden Dienstes (seit 18.08.2026, kein Schreibpfad) │ │ └── direktiven_manager/ # Direktiven (Router-Prompt) │ │ │ └── services/ # Dienste │ ├── llm_provider.py # LLM-Provider-Klassen (LLMProvider-ABC, OllamaProvider, AnthropicProvider) — nur Instanziierung durch Registry │ ├── model_costs.py # Kosten je Aufruf + Turn-Kennung — die Rechnung sitzt im Provider, weil ChatResponse nur die Summe traegt │ ├── postprocess.py # JSON/CJK-Postprocess-Util (zustandslos, aus model_services gelöst — Lesson paket-init-zyklus) │ ├── model_services/ # Model-Service-Schicht (In-Process-Microservice, §2.7) │ │ ├── types.py # Typisierte Requests/Responses (Embed/Chat/Background) │ │ ├── worker_base.py # ModelWorker-Basis (FIFO-Queue, submit / submit_sync) │ │ ├── embed_worker.py # EmbedWorker (Rolle embed, GPU-fix) │ │ ├── chat_worker.py # ChatWorker (Rolle chat, single-backend) │ │ ├── background_worker.py # BackgroundWorker (Rolle background, dual analyse/sprache, CJK-Guard) │ │ └── registry.py # ModelServiceRegistry, _build_backend, Singleton model_service │ ├── events.py # Event-Queue (Redis FIFO, Self-Trigger-Schutz, Chat 60) │ ├── event_consumer.py # Event-Consumer (async-Loop, WebSocket-Delivery, Chat 60) │ ├── prompt_eingang.py # Eingangs-Queue vor Pfad 1, Blockbildung, Turn-Marker (Chat 124) │ ├── prompt_consumer.py # Prompt-Consumer (async-Loop, faehrt Pfad 1 hinter der Queue, Chat 124) │ ├── shadow_delivery.py # Pixie -> Chat-Einspeisung │ ├── shadow_agent/ # Alter Pixie-Runner (extern aufruflos, PIX-CLEAN) │ └── pixie/ # Neues Pixie-System (Chat 33+) │ ├── scheduler.py # APScheduler-Heartbeat │ ├── kandidaten.py # Queue-Peek + periodische Aufgaben │ ├── router.py # Aufgabe -> Agent-Name │ ├── dispatch.py # Agent-Ausfuehrung; `fehler` ins Audit nur, wenn der Dienst schweigt (entkommene Ausnahme, fehlender Agent) — jeder Dienst schreibt sein Audit selbst (NMCP §8.4, seit 18.09.2026) │ ├── riegel.py # Riegelkette der Zustellung (Riegel 1 gebaut) │ └── stack.py # Shadow-Stack Push │ ├── db/ │ └── init.sql # Kern-Schema (LZG, Entitaeten) │ ├── ollama/ │ └── modelfiles/ # GPU + CPU Modelfiles │ ├── ruff.toml # Linter-Konfiguration, gilt fuer allen Python-Code ├── ruff-hart.toml # engere Auswahl: Familien ohne geduldeten Bestand └── docker-compose.yml ``` **Zur `ruff.toml`:** Sie steht in der Wurzel, weil es keine `pyproject.toml` gibt — der Server wird nicht als Paket installiert, sondern als Quellbaum in den Behaelter gemountet. Zielversion ist `py312` aus der Bildbasis des Servers, nicht die Python-Version des Hosts. Jeder gesetzte Grenzwert traegt in der Datei seine Herleitung und seine Messzahlen; jede abgeschaltete Regel ihre Begruendung. Vier Regeln sind abgeschaltet, weil sie einer im Projekt vorgeschriebenen Bauart widersprechen — darunter das f-String-Verbot im Logging und die Zaehlung der Rueckkehrpunkte, die mit der EVA-Disziplin unvereinbar ist. **Zur `ruff-hart.toml`:** Die `ruff.toml` duldet den Bestand — ihr Lauf endet mit Treffern, die gezaehlt und nicht behoben werden. Die `ruff-hart.toml` fuehrt die Familien, fuer die das nicht mehr gilt: Sie erbt die erste ueber `extend` vollstaendig und ersetzt davon nur die Regelauswahl, und **ihr Lauf muss sauber sein**. Aufruf aus der Repo-Wurzel: `ruff check --config ruff-hart.toml server/`, Rueckgabewert 0 ist die Bedingung. Die Trennung ist noetig, weil Ruff keine Schweregrade kennt — in einer gemeinsamen Datei waere ein neuer Treffer der harten Familie von den geduldeten nicht zu unterscheiden. Aufgenommen wird eine Familie, wenn sie ueber `server/` null Treffer hat, keine ihrer Regeln im Preview-Status steht und geprueft ist, dass das Werkzeug jedes Exemplar im Bestand sehen kann. Stand 30.07.2026: `LOG`. ### Event-Modell (Chat 60) Seit Chat 60 sind User und Charakter zwei unabhängige Akteure: | Pfad | Graph | Nodes | Aufgabe | |------|-------|-------|---------| | Pfad 1 | HumanGraph | 5 | User schreibt: Wahrnehmung + Speicherung | | Pfad 2 | CharacterGraph | 17 | Charakter reagiert: Identität laden + Lesen + Entscheiden + Antworten + Perzeption(Nova) + Konsolidieren + Speichern | Verbunden durch eine Redis-Event-Queue (`event_queue:{user_id}:{character_id}`). Ein Event-Consumer (`services/event_consumer.py`) pollt die Queue und startet CharacterGraph-Durchlaeufe. Antworten erreichen den Client per WebSocket. **Neue Services:** | Service | Datei | Aufgabe | |---------|-------|---------| | Event-Queue | `services/events.py` | Event erzeugen, lesen, Self-Trigger-Schutz | | Event-Consumer | `services/event_consumer.py` | Queue-Polling, Debouncing, Graph-Aufruf, WebSocket-Delivery, Turn-Marker loeschen | | Prompt-Consumer | `services/prompt_consumer.py` | Eingangs-Queue-Polling, Blockbildung, Pfad-1-Lauf, Turn-Marker setzen | **Ersetzte Services:** | Service | Status | Ersetzt durch | |---------|--------|---------------| | Nachbearbeitung | Deprecated | Event-Consumer | | SSE-Streaming (Antwort) | Deprecated | WebSocket-Delivery | Session-Key seit Chat 60: `session:{user_id}:{character_id}:turns` (vorher: `session:{user_id}:turns`). → Details: `novaberg-graph.md` §3, `novaberg-convention-event-model.md` --- ## 4. Plugin-System ### 4.1 Ueberblick Das Plugin-System ist Novas Erweiterungsmechanismus fuer strukturiertes Wissen. Jeder Manager ist ein eigenstaendiges Plugin, das sich beim System registriert und dem Router, Enricher, der Salienz und dem Planner mitteilt, was es kann. Neue Faehigkeiten entstehen durch neue Ordner in `plugins/`, nicht durch Aenderungen am Graph. ### 4.2 BaseManager — Das Interface `plugins/base.py` definiert die abstrakte Basisklasse. Alle Methoden sind optional — ein Manager implementiert nur, was er braucht: | Methode / Property | Rueckgabe | Aufgabe | Aufrufer | |-------------------|----------|---------|----------| | `name` | `str` | Eindeutiger Name (z.B. ~~`"fakten"`~~ → `"notizen"`; das Fakten-Plugin ist am 21.10.2026 gelöscht) | Logging, Registry | | `router_intents` | `set[str]` | Intents, die dieser Manager bedient | Planner (Prioritaet 2) | | `router_prompt` | `str` | Ergaenzung zum Router-Prompt | Router | | `salienz_prompt` | `str` | Ergaenzung zum Salienz-Prompt | Salienz | | `enricher_prompt` | `str` | Kontext-Beschreibung | Enricher | | `enrich(state, postgres_url)` | `str` | Kontext liefern (Fakten, Termine, Notizen) | Enricher | | `plan(state, postgres_url)` | `dict` | Management-Aktion planen (LLM via `model_service.chat`) | Planner | | `execute(writes, user_id, redis_client, postgres_url, embed_client, embed_model)` | `int` | DB-Writes ausfuehren | Dispatcher | | `setup(postgres_url, redis_client=None)` | — | Schema anlegen (`init.sql` ausfuehren), optional Redis-Init | GraphBase beim Start | ### 4.3 Selbstbeschreibende Prompts Die drei Prompt-Properties (`router_prompt`, `salienz_prompt`, `enricher_prompt`) sind der Kern des Plugin-Prinzips: Der Manager beschreibt sich selbst, statt dass der Router oder Enricher ihn kennen muss. Wenn ein neuer Manager hinzukommt, lernt der Router automatisch seine Trigger-Phrasen — weil der Router-Prompt dynamisch aus allen registrierten Managern zusammengesetzt wird. ### 4.4 Auto-Discovery `plugins/__init__.py` enthaelt `discover_managers()`: 1. Scannt alle Unterordner von `plugins/` mit `_manager` im Namen 2. Importiert das Package (erwartet `__init__.py`) 3. Findet alle `BaseManager`-Subklassen 4. Instanziiert und registriert sie in der Plugin-Registry (ein Dict: `name -> Manager-Instanz`) Kein manuelles Registrieren, keine Import-Liste. Neuer Ordner = neuer Manager. ### 4.5 Die acht Manager > **Acht sind registriert, sieben stehen in der Tabelle.** `ls -d server/plugins/*_manager | wc -l` → **8**; die Tabelle unten trägt **7** Zeilen. Es fehlt `WissenManager` (`plugins/wissen_manager/`) — er ist seit dem 04.08.2026 im Bestand und hat nie eine Zeile bekommen. **Bestandsbefund, hier benannt und nicht im Vorbeigehen aufgefüllt** (Fundliste, 18.08.2026). | Manager | Ordner | Verantwortung | |---------|--------|---------------| | **KzgManager** | `plugins/kzg_manager/` | KZG-Schreiboperationen (Store, Verstaerkung) | | ~~**FaktenManager**~~ | ~~`plugins/fakten_manager/`~~ | ~~Fakten + Entity Resolution (Typ 1 + 2, bi-temporal)~~ → **am 21.10.2026 gelöscht** (nicht committet); Fakten schreibt der Fakten-Agent (`agents/fakten/`) auf den Schreibauftrag der Sachlage, gelesen wird über die Mappe (`memory/fakten_mappe.py::load_mappe`) | | **TimelineManager** | `plugins/timeline_manager/` | Termine + Zeitparser (CRUD) | | **NotizenManager** | `plugins/notizen_manager/` | Merkzettel, Listen, Snippets (CRUD + Append) | | **CharakterIdentitaetManager** | `plugins/charakter_identitaet_manager/` | Router-Prompt für Identitätszuweisungen (CRUD via CharakterIdentitaetAgent) | | **DirektivenManager** | `plugins/direktiven_manager/` | Router-Prompt für Verhaltensdirektiven (CRUD via DirektivenAgent) | | **DateienWurzelnManager** | `plugins/dateien_wurzeln_manager/` | Router-Prompt für Verzeichnis-Freigaben (CRUD via DateienWurzelnAgent) | | Manager | `enrich()` | `plan()` | `execute()` | Besonderheit | |---------|-----------|---------|-------------|-------------| | **KzgManager** | — | — | KZG Store/Verstaerkung | Kein LLM, kein Plan. Nur Write. | | ~~**FaktenManager**~~ | ~~Relevante Fakten laden~~ | — | ~~Fakten CRUD + Entity Resolution~~ | ~~Bi-temporal, Typ 1 + 2~~ → **Plugin am 21.10.2026 gelöscht** (nicht committet) | | **TimelineManager** | ~~Termine im Zeitraum laden~~ → **seit 15.10.2026 nur das proaktive Fenster** (drei Tage zurück, vierzehn voraus) für die Analyse-Knoten; den gefragten Zeitraum liest der Turn hinter dem Router (`memory/timeline_reading.py`) — der Zeitraum-Zweig hier war vor dem Router nie erreichbar | LLM plant CRUD | Zeitparser + DB-Write | Datum-Inferenz via `utils/zeitparser.py` | | **NotizenManager** | Aktive Notizen als Uebersicht | LLM plant CRUD + Append | Text-Updates via LLM | Rueckfrage-Logik | ### 4.6 PendingWrite — Das Austauschformat ```python PendingWrite = TypedDict("PendingWrite", { "ziel": str, # "kzg", "fakten", "timeline", "notizen" "aktion": str, # "create", "update", "delete" "daten": dict, # Manager-spezifische Payload "beschreibung": str, # Menschenlesbar fuer Logging }) ``` **Salienz-Guard (P5/P6):** Wenn der Planner in diesem Turn aktiv war, unterdrueckt die Salienz ihre eigenen Writes fuer `fakten` und `timeline`. Sonst entstehen Doppeleintraege — zwei Zahnarzttermine, einmal vom Planner, einmal von der Salienz. KZG-Writes bleiben aktiv. ### 4.7 Koexistenz mit Agent-System (seit Chat 22, Epic 11) Seit Epic 11 ergaenzen Agenten die Manager schrittweise — sie ersetzen sie nicht vollstaendig: | Manager | Agent | Status | |---------|-------|--------| | NotizenManager | NotizenAgent | Agent bedient `/notizen/*`-Dispatches; Manager liefert `router_prompt`, `enrich()`, und traegt weiterhin seine eigenen `plan()`/`execute()`-Pfade | | TimelineManager | TimelineAgent | Agent bedient `/timeline/*`-Dispatches; Manager liefert `router_prompt`, `enrich()`, `plan()` (Zeitparser + Entity Resolution) und `execute()` | | ~~FaktenManager~~ | ~~—~~ → Fakten-Agent | ~~Kein Agent. Manager traegt vollstaendiges `execute()` (CRUD + Entity Resolution, bi-temporal)~~ → Das Plugin ist am 21.10.2026 gelöscht (nicht committet). Der Fakten-Agent (`agents/fakten/`) ist als Dienst mit `zustellart = "queue"` registriert und bekommt seine Aufträge vom Dispatcher (Schreibauftrag aus der Sachlage); der Planner fragt ihn nicht | | KzgManager | — | Kein Agent (KZG-Schreiben geht ueber den KZG-*Subgraph-Agent*, nicht den Manager). Manager traegt nur `execute()` als Legacy-Fallback | Die Manager bleiben **nicht** leere Huellen. Sie halten drei Aufgaben fuer Router/Enricher/Salienz und zusaetzlich ihre eigentliche Domain-Logik (LLM-Extraction, Zeitparser, Entity Resolution) in `plan()` und `execute()`. Diese Methoden werden weiter aktiv benutzt: - `router_prompt` — Domaenen-Erkennung fuer den Router - `enrich()` — Kontext-Hook fuer den Enricher - `salienz_prompt` — Salienz-Erweiterung - `plan()` — vom Planner aufgerufen, wenn kein Agent die Domaene uebernimmt (aktueller Stand: ~~bei FaktenManager und KzgManager~~ → bei KzgManager; das Fakten-Plugin ist am 21.10.2026 gelöscht, nicht committet, und ein Router-Ziel `fakten` führt den Planner zu den Notizen). Fuer NotizenManager und TimelineManager ist der Code vorhanden, wird aber vom Planner uebersprungen, sobald ein passender Agent gefunden ist. - `execute()` — vom Dispatcher aufgerufen, wenn `pending_writes` ein Manager-Ziel (~~`fakten`,~~ `kzg`, `timeline`, `notizen`) treffen. Der Agent-Pfad erzeugt `pending_writes` nur fuer `kzg`; alle anderen Ziele gehen weiter ueber den Manager-`execute()`. → **Ausgenommen ist `fakten`:** Diese Writes reicht der Dispatcher seit dem 20.10.2026 an den Fakten-Agenten (`_run_fact_orders`); das Fakten-Plugin ist am 21.10.2026 gelöscht (nicht committet). **Ziel langfristig:** Wenn alle Domains einen Agenten haben, koennen Agenten die Selbstbeschreibung (`BaseAgent.router_prompt` etc.) selbst tragen, und das Plugin-System wird optional. Aktuell sind Plugin-Manager und Agent-System parallele Saeulen, die sich ergaenzen, nicht zwei Schichten, die sich ersetzen. --- ## 5. Zentrale Architekturprinzipien ### Entscheider/Arbeiter-Trennung Die Salienz entscheidet *was* gespeichert wird, fuehrt aber *nichts* aus. Sie schreibt `pending_writes`. Der Dispatcher verteilt an die zustaendigen Manager-Plugins. Kein Node hat gleichzeitig Bewertungs- und Schreibverantwortung. ### Trust Boundary Public-Methoden (Graph-Entry-Points, API-Endpoints) validieren Eingaben. Die Business-Logik in Nodes und Managern arbeitet mit bereits validierten Daten. Gleicher Grundsatz wie im Entwicklerhandbuch — auf Architekturebene angewendet. ### Tri-LLM + Claude API — Spezialisierung statt Generalismus GPU-Modell (Chat) und zwei CPU-Modelle (Pixie) laufen auf getrennter Hardware. Kein Lock, kein Warten, echte Parallelitaet. Pixie nutzt Qwen3-32B fuer Analyse/Reasoning und Mistral fuer Sprachausgabe — statisches Routing pro Workflow-Schritt. Anthropic Claude API als alternativer Provider fuer den Chat-Graph (Profil `claude` in config.py, Token/Kosten-Logging). ### Plugin-System mit Auto-Discovery Manager registrieren sich selbst. Jeder Manager beschreibt dem Router, Enricher und der Salienz per Property, was er kann. Neue Faehigkeiten entstehen durch neue Ordner in `plugins/`, nicht durch Aenderungen am Graph. ### Bi-temporales Modell Nichts wird ueberschrieben. Update = Invalidieren + Neu Anlegen. Die Historie bleibt erhalten. Gilt fuer Fakten und Timeline gleichermassen. ### Soft-Delete ueberall Nichts wird geloescht. Inaktive Eintraege bleiben erhalten (`aktiv = FALSE`). Partial Indexes sorgen fuer Performance. Speicherplatz ist vernachlaessigbar, Reaktivierung moeglicherweise wertvoll. ### Prompt-Hierarchie: Persoenlichkeit entsteht, sie wird nicht programmiert Der System-Prompt ist eine Stellenbeschreibung, keine Persoenlichkeit. Er definiert nur die technische Rahmenbedingung: Rolle, Sprache, Grenzen. Alles andere kommt aus dynamischen, gewachsenen Quellen. Die Verhaltensregeln kommen nicht aus einem statischen Prompt — der Nutzer gibt sie. Der Nutzer praegt den Assistenten durch Interaktion, nicht durch Programmierung. ### Berechnung in Python, nicht im LLM Deterministische Operationen (Decay-Kurven, Emotions-Vektoren, Stilanalyse, Zeitparser) sind Python-Funktionen. Das LLM bekommt nur die Ergebnisse als Klartext. Schneller, exakter, reproduzierbar. ### Session-Kontext in fruehen Nodes Perzeption und Router bekommen die letzten 5 Session-Turns als Hintergrund-Kontext (Redis-Read, <5ms). Ermoeglicht Pronomen-Aufloesung ("die Liste"), Emotions-Kontinuitaet und Kontext-Routing. Klar abgegrenzt im Prompt: "Analysiere NUR den aktuellen Prompt." Validiert: keine Kontamination bei Themenwechsel. Perzeption ist kontext-*informiert*, aber prompt-*fokussiert*. --- ## 6. Feature-Matrix | Feature | Status | Referenz | |---------|--------|----------| | Graph-Pipeline (HumanGraph: 5 Nodes Pfad 1 | CharacterGraph: 17 Nodes Pfad 2 | AgentGraph: 3 Nodes) | Implementiert & getestet | novaberg-graph.md, novaberg-node-*.md | | PFAD2-PERZEPTION-FIX Phase 2 (db_zugriff + ei_calc_persist als CG-Klammer) | Implementiert & validiert (Chat 89) | novaberg-node-db-zugriff.md, novaberg-node-ei-calc-persist.md, novaberg-path2-perzeption_k.md | | PFAD2-PERZEPTION-FIX Phase 3 (Personality-Klassen-Schicht: external/internal mit emotion/character/identities/directives) | Implementiert & validiert (Chat 89) | novaberg-personality.md, DEVELOPER_HANDBOOK.md §6, novaberg-lesson_l_klassen-statt-flache-keys.md | | HumanGraph-Slimming Phase 4 (Enricher-Methodensplit, Reducer aus HG raus, kein KZG/LZG-Lauf im HG) | Implementiert & validiert (Chat 90) | novaberg-graph.md §3.1, novaberg-node-enricher.md | | EI-Calc-Node (reine Python-Berechnung, Dual-Modus User + Nova) | Implementiert & validiert | novaberg-node-ei-calc.md | | Dual-Emotion Phase 2 (Nova-Empathie, Konflikt-Erkennung) | Implementiert (AP1–3, AP7, AP4 teilw., AP8 teilw.) | novaberg-ei-dual-emotion_k.md | | Graph-Pipeline (AgentGraph, 3 Nodes) | Implementiert & getestet | novaberg-graph.md, novaberg-pixie.md | | Kurzzeitgedaechtnis (Redis + Vektor) | Implementiert & getestet | novaberg-mem-kzg.md | | Kurzzeitgedaechtnis Magnet-Felder (entitaet_ids, timeline_id, Synapsen P3) | Implementiert | novaberg-mem-kzg.md, novaberg-pixie-kzg.md | | Langzeitgedaechtnis (PostgreSQL) | **Abgeloest durch das Synapsen-Modell (P9, 02.08.2026)** | novaberg-memory-synapsen_k.md · archive/novaberg-mem-lzg.md | | Pipeline-Log-Forensik (asynchroner Writer, JSONB-Inhalt, Span-Korrelation, turn_id über /chat + /chat/stream + fail-loud bei leerem turn_id) | Implementiert (Chat 88 P1.1, vervollständigt Chat 90 TURN-ID-FIX) | novaberg-memory-synapsen_k.md §10 | | Synapsen-Tabellen (lzg_knoten, lzg_kanten, parallel zum LZG) | Schema angelegt, leer | novaberg-memory-synapsen_k.md §4 | | `praegung_faden` | im Betrieb belegt, **5 Zeilen** (03.09.2026 gezählt), alle einem Strang zugeordnet | novaberg-node-praegung.md | | `abstrakt_knoten` | die geschlossenen Sätze der abstrakten Schicht; **6 Zeilen** `art = 'qualitaet'` (gesetzt im Schema), **0** `art = 'wert'` (03.09.2026) | novaberg-memory-qualitaetsprofil.md | | `traeger_qualitaet` | die **vorzeichenlose** Kante Träger → Qualität; **168 Zeilen an 28 Trägern** (03.09.2026, 21:38 UTC gemessen) | novaberg-memory-qualitaetsprofil.md | | `praegung_beruehrung` | im Betrieb belegt 01.09.2026 — 7 Zeilen | novaberg-node-praegung.md | | `praegung_strang` | im Betrieb belegt, **2 Zeilen** (03.09.2026 gezählt) ueber 4 und 1 Faden; ~~**Richtung und Staerke nicht gebaut**~~ → **beide gebaut** (02.09.2026) und seit dem 03.09.2026 vom Praegungszug gelesen, keine davon gespeichert | novaberg-node-praegung.md §6b | | Ebbinghaus-Decay + Soft-Delete | **Abgeloest: `SynapsenDecayAgent` materialisiert `gewicht_decay`** (P9) | novaberg-memory-synapsen_k.md §9 · archive/novaberg-pixie-decay.md | | Salienz als Entscheider | Implementiert & getestet | novaberg-node-salience.md | | Fakten-Pipeline (Typ 1 + 2, bi-temporal) | Implementiert & getestet | novaberg-mem-knowledge-graph.md | | Timeline (CRUD + Vektor-Modus) | Implementiert & getestet | novaberg-agent-timeline.md | | Zeitparser Vektor-Modus (P8) | Implementiert & getestet | novaberg-tool-timeparser.md | | Notizen (Create + Read) | Implementiert & getestet | novaberg-agent-notes.md | | Zeitparser (47/47 Tests) | Implementiert & getestet | novaberg-tool-timeparser.md | | Knowledge Graph (Entitaeten + Edges) | Implementiert & getestet | novaberg-mem-knowledge-graph.md | | Entity Resolution | Implementiert & getestet | novaberg-pattern-entity-resolution.md | | Zwei-Call-Promotion | Implementiert & getestet | novaberg-pixie-promotion.md | | Emotionale Intelligenz (9 Vektoren, Arousal-Decay, Normalisierung, EI-MIKRO) | Implementiert & validiert | novaberg-ei.md, novaberg-node-perception.md | | Charakter-Profile (5 destilliert, alle im Prompt genutzt) | Implementiert & getestet | novaberg-ei-character-profiles.md | | Perzeption-Node | Implementiert & getestet | novaberg-node-perception.md | | Pixie (9 Tasks, CPU-Runner) | Implementiert & getestet | novaberg-pixie.md, novaberg-pixie.md | | Shadow Delivery Service | Implementiert & getestet | novaberg-pixie.md | | Plugin-System (8 Manager) | Implementiert & getestet | novaberg-architecture.md | | LLM-Abstraktionsschicht (Provider, Profile, Anthropic Claude API) | Implementiert | novaberg-architecture.md | | Tri-LLM-Architektur (GPU Chat + CPU Analyse + CPU Sprache) | Implementiert | novaberg-pixie_l_spezialisierung.md | | Gespraechsvektor-Node (GV1+GV2) | Implementiert | novaberg-node-gv_k.md | | CharakterIdentitaetAgent + DirektivenAgent | Implementiert | novaberg-agent-directives.md | | Tribunal Score-System (T1, Dual-Score Jurist) | Implementiert | novaberg-node-tribunal.md | | Health-Check (5 Dienste + SearXNG) | Implementiert | novaberg-architecture.md | | Client: Charakter-Visualisierung (5 Profile, Meister/Nova) | Implementiert | — | | Client: Emotions-Radar (QPainter, Session + KZG) | Implementiert | — | | Client: Kompakte KZG/LZG-Listen | Implementiert | — | | EI-Plausibilitaets-Gate (8 Plutchik-Sektoren) | Implementiert | novaberg-node-perception.md | | EI-MIKRO (situative Mikro-Anweisungen) | Implementiert | novaberg-node-responder.md | | Arousal-basierter Decay (Emotions-Persistenz) | Implementiert | archive/novaberg-pixie-decay.md | | Admin-API (Pixie Pause/Resume/Flush) | Implementiert | novaberg-architecture.md | | Sprachadaption (CAT): Feature-Scoring + Profil-Pipeline | Implementiert & validiert | novaberg-ei-character-profiles.md, novaberg-ei-language-adaptation.md | | Novas eigener Charakter-Hash im Responder | Implementiert & validiert | novaberg-ei-character-profiles.md | | System-Prompt-Hierarchie (5-Schichten-Modell) | Konzipiert & teilweise implementiert | novaberg-ei-character-profiles.md | | Agent-System (Epic 11, 11 Agenten) | Implementiert & validiert | novaberg-graph.md | | pg_trgm Fuzzy-Suche (Notizen) | Implementiert | novaberg-agent-notes_l.md | | Session-Kontext in Perzeption + Router | Implementiert & validiert | novaberg-node-perception.md, novaberg-node-router.md | | Rueckfrage-Kette (Redis-Pending Resume) | Implementiert & validiert | novaberg-graph.md | | Web-Integration (Serper + SearXNG + PageFetcher) | Implementiert | Chat 35; Serper als erster Anbieter seit 30.08.2026 | | TimelineAgent (CRUD + bi-temporal + ZeitVektor) | Implementiert & validiert | novaberg-agent-timeline.md | | KZG-Agent (LangGraph-Subgraph, 5 Nodes) | Implementiert & validiert | novaberg-mem-kzg.md | | DelegationsAgent (Halluzinations-Ventil) | Implementiert & validiert | novaberg-pixie-delegation.md | | RechercheAgent (Pixie Web-Recherche) | Implementiert | novaberg-pixie-research.md | | Gespraechsvektor (GV1+GV2: Node, Farbmisch, ~~Entity-Hop~~ Resonanz-Kontext) | Implementiert & validiert | novaberg-node-gv_k.md | | Session-Kontext-Extraktion (memory/kontext.py) | Implementiert | Chat 35 | | Auto-Fetch (web_search -> page_fetch automatisch) | Implementiert | Chat 35 | | Prompt-Schema [BLOCKNAME] auf allen Nodes | Implementiert | novaberg-pattern-prompt-schema.md | | Test-Runner (Prompt-Ketten) | Implementiert & getestet | TEST0 | | EI/Routing in API-Response (12 Felder) | Implementiert & getestet | novaberg-node-responder.md | | Kognitive Anreicherung (Epic 8) | Konzipiert | Roadmap Epic 8 | | Erinnerungsfunktion | Geplant | Roadmap ERN1 | | Telemetrie | Geplant | Roadmap TEL1 | | Voice (TTS/STT) | Vision | Roadmap | --- ## 7. Dokumentenverzeichnis Das Handbuch ist nach Betrachtungstiefen organisiert. Tiefe 0 ist der Einstiegspunkt, jede weitere Tiefe geht ins Detail. **131 Dateien** im Verzeichnis `novaberg/docs/` (gezaehlt 28.07.2026 — eine Zaehlung ohne Messdatum veraltet zwischen zwei Sitzungen). ### Tiefe 0 — Projekt | Dokument | Beschreibung | |----------|-------------| | novaberg-project_k.md | Konzept, Vision, Leitprinzipien, Persoenlichkeit | ### Tiefe 1 — Architektur & Subsysteme | Dokument | Beschreibung | |----------|-------------| | novaberg-architecture.md | Systemarchitektur, Tech-Stack, Plugin-System (dieses Dokument) | | novaberg-graph.md | Graph-Architektur, HumanGraph, AgentGraph, Agent-System, State | | novaberg-graph-rechenkette.md | **Rechenkette des CharacterGraph** — Register der 34 Rechensysteme in sechs Stufen: was jedes aus welchen Eingängen berechnet und auf welchem Weg es die Antwort erreicht. Zerlegt entlang der Rechnungen statt entlang der Knoten (ein Knoten trägt bis zu sechs Systeme, ein System läuft über zwei Knoten) und weist je System Reinheit und Prüfstand aus; §11 nennt die drei Größen, die erst im Folgeturn wirken | | novaberg-memory.md | Gedaechtnis-Ueberblick (Session → KZG → LZG → KG) | | novaberg-ei.md | Emotionale Intelligenz Ueberblick | | novaberg-pixie.md | Pixie-System, Scheduling, Queue/Stack/Delivery | | novaberg-queue-verfall_k.md | **Verfall der Shadow-Queue** — Konzept: Ein Auftrag ist ein Vorsatz und verliert seinen Anlass; die Queue kannte bis dahin keinen Weg hinaus außer Ausführung. Übernimmt die Bauart von `lzg_knoten` (Sinus-Sättigung im Aufbau, exponentieller Verfall, **Soft-Delete statt Löschen**, Halbreaktivierung auf 50 % des Bandes über der Schwelle) mit eigener Rate: 30 Tage bis Schwelle 0,3. **Die Queue zieht dafür nach PostgreSQL um, der Stapel nicht** — gemessen an der Lesefrequenz, nicht an der Datenmenge (⬜ nicht gebaut, Bestand 1036 am 15.08.2026 gemessen) | ### Tiefe 2 — Pipeline-Nodes (15) | Dokument | Beschreibung | |----------|-------------| | novaberg-node-perception.md | Perzeption (Emotion, Arousal, Intent, Plutchik-Oktagon, Dual-Modus User/Nova) | | novaberg-node-db-zugriff.md | db_zugriff (CG-Entry, lädt Personality-Klassen aus PostgreSQL/Redis) | | novaberg-node-enricher.md | Enricher (Methodensplit nach Phase 4: _enrich_human schlank, _enrich_character voll, Plugin-Hooks im CG) | | novaberg-node-ei-calc.md | EI-Calc (Python-Berechnung: Verlauf, Vektor, Nova-Empathie, kein LLM) | | novaberg-node-emotionale-gravitation.md | Emotionale Gravitation (CG, injiziert reaktivierte Erinnerungen in Novas Emotions-Verlauf) | | novaberg-node-ei-calc-persist.md | ei_calc_persist (CG-Ausgang, konsolidiert internal.emotion, persistiert nova_state) | | novaberg-node-router.md | Router (Routing, Agenten-Delegation) | | novaberg-node-planner.md | Planner (Agent-Loop, Resume-Flow) | | novaberg-node-agent-dispatch.md | Agent-Dispatch (Zentraler Entry-Point) | | novaberg-node-gv_k.md | Gespraechsvektor (Farbmisch-System, zweite Wissensquelle: ~~Entity-Hop~~ Resonanz-Kontext seit Chat 115) | | novaberg-gv-initiative_k.md | Initiative-Achse — Konzept: Herleitung, verworfene Wege, Kalibrier-Agent (Entwurf) | | novaberg-gv-initiative.md | Initiative-Achse — Modul: Datenfluss, Konstanten mit Kalibrierungsstand, Messungen | | novaberg-charakter-rad-messreihe_k.md | Die Charakter-Räder als Messreihe — Konzept: Das Rad misst einen akuten Zustand und wird durch die Messungen der letzten Tage stabilisiert; rohe Messungen in `charakter_rad_messung`, gelesener Wert als gewichtetes Mittel (🔶 gebaut für das Zuwendungs-Rad) | | novaberg-erreichbarkeit_k.md | **Erreichbarkeit** — Konzept: Eine Landschaft, die nie betreten wird, und eine, die immer betreten wird, sind derselbe Defekt. Überträgt das für die GV-Sektoren entschiedene Kriterium *Erreichbarkeit statt Häufigkeit* auf die vierzehn Gesprächslandschaften; der Charakter verschiebt die Verteilung, schließt aber keine Landschaft, und nachgeregelt wird nicht zur Laufzeit (⬜ nicht gebaut, alle vierzehn erreichbar gemessen, Ablesung fällt in 14 % aus) | | novaberg-haltungsraum_k.md | Haltungsraum — Konzept: Landschaft setzt Grundwerte für fünf Verhaltensgrößen, das Zuwendungsrad modifiziert sie; Grenzen multiplizieren, Neigungen addieren, und ein **Zug** nach der Rechnung lässt eine extrem ausgeprägte Speiche die Lage überstimmen (🔶 Rechnung, Knoten und Protokoll gebaut und im Betrieb, **Prompt offen — kein Leser der Haltung**) | | novaberg-kalibrierung_k.md | **Kalibrierung** — Konzept über alle Stellschrauben: sechs Klassen (Naben, Beiträge, Schwellen, Verfall, Glättung, Kennlinien), der Erwartungskorridor als Pflicht vor jedem Dreh, die Trennung von Ablesung und Wirkgröße — eine gestreckte Skala ist kein Befund —, und **die Trennung von Kalibrier- und Validierungsmenge** (§5): Eine Zahl, gegen die eingestellt wurde, ist als Beleg verbraucht (⬜ Verfahren steht, kein Korridor geschrieben, keine Validierungsmenge erhoben) | | novaberg-node-verfasser_k.md | Verfasser — Konzept: ein Node vor dem Responder, der den fachlichen Inhalt bestimmt; Inhalt und Wesen werden getrennt (⬜ nicht gebaut) | | novaberg-node-responder.md | Responder (Antwortgenerierung, EI-MIKRO) | | novaberg-node-thinker.md | Thinker (Faktenpruefung, Web-Suche) | | novaberg-node-tribunal.md | Tribunal (Drei-Perspektiven-Bewertung, Score-System) | | novaberg-node-corrector.md | Corrector (Korrekturschleife) | | novaberg-node-salience.md | Salienz (Bewertung, pending_writes, Input-Switch nach graph_rolle) | | novaberg-node-dispatcher.md | Dispatcher (Session-Turn-Schreiben, KZG-Dispatch, Schreiboperationen verteilen) | ### Tiefe 2 — User-Agenten (4) | Dokument | Beschreibung | |----------|-------------| | novaberg-agent-notes.md | NotizenAgent (CRUD, pg_trgm-Suche, Domain Language) | | novaberg-agent-timeline.md | TimelineAgent (Zeitparser, bi-temporal, ZeitVektor) | | novaberg-agent-directives.md | DirektivenAgent (Verhaltensanweisungen, HITL-Gate) | | novaberg-agent-character.md | CharakterIdentitaetAgent (Persoenlichkeits-Saatgut) | ### Tiefe 2 — Pixie-Agenten (8) | Dokument | Beschreibung | |----------|-------------| | novaberg-pixie-kzg.md | KZG-Agent (5-Node-Subgraph, Verdichtung, Aehnlichkeit) | | novaberg-pixie-promotion.md | PromotionAgent (Zwei-Call-Promotion, KZG → LZG) | | archive/novaberg-pixie-decay.md | DecayAgent — **archiviert (P9)**, Agent geloescht | | novaberg-pixie-character-hash.md | CharakterAgent (5-Profil-Destillation, alle im Prompt genutzt) | | novaberg-pixie-reminder.md | WiedervorlageAgent (4-Tabellen-Scan, Snooze) | | novaberg-pixie-research.md | RechercheAgent (Web-Recherche, Dual-Modell) | | novaberg-pixie-delegation.md | DelegationsAgent (Halluzinations-Ventil, Yin-Yang) | | novaberg-pixie-deepdive_k.md | VertiefungsAgent (Konzept, nicht implementiert) | ### Tiefe 2 — Gedaechtnis-Module (5) | Dokument | Beschreibung | |----------|-------------| | novaberg-memory-qualitaetsprofil.md | **Die abstrakte Schicht: Qualitätsprofile** — `abstrakt_knoten` (Typ-Diskriminator, sechs gesetzte Dimensionen) und `traeger_qualitaet` (**vorzeichenlose** Kante). Der Träger einer Faszination ist nicht die Entität, sondern ein Merkmalsprofil; der `merkmalszug` verrechnet die sechs zu einem weichen ODER. Erzeuger ist der achte Schritt des Tageslaufs, ein Modellaufruf je Träger (🟠 gebaut und im Bestand — 25 Träger, 150 Kanten —, **aber die Dominanz kollabiert auf `komplexitaet`, 23 von 25**) | | novaberg-mem-session.md | Session-Gedaechtnis (Redis, Turn-Formatierung) | | novaberg-mem-kzg.md | Kurzzeitgedaechtnis (Redis, TTL, Vektorsuche) | | archive/novaberg-mem-lzg.md | Langzeitgedaechtnis — **archiviert (P9)**, Tabelle und Modul geloescht | | novaberg-mem-knowledge-graph.md | Knowledge Graph (Entitaeten, Fakten, Entity Resolution) | ### Tiefe 2 — EI-Module (4) | Dokument | Beschreibung | |----------|-------------| | novaberg-ei-plutchik.md | Plutchik-Emotionsmodell (Oktagon, Normalisierung) | | novaberg-ei-character-profiles.md | Charakter-Profile & Hash (5 Dimensionen, alle im Prompt, Pipeline) | | novaberg-ei-language-adaptation.md | Sprachadaption (CAT, Feature-Scoring) | | novaberg-avatar_k.md | **Emotions-Avatar** — Konzept: Ein 2D-Avatar im GTK4-Client zeigt Novas eigenen Emotionszustand als lebende Bleistiftskizze und entscheidet selbst nichts — Absicht, mit Kopfblock und Tabelle „§ → Datei“ (⬜ im Client nicht gebaut, HTML-Prototyp unter `labor/avatar/`) | | novaberg-avatar_t.md | Emotions-Avatar — Ausarbeitung: Parameterraum `FaceState`, 8 Sektor-Gesichter, Arousal und Mischung, Federübergang, Schichten, Figur aus Vorlage, Sprechschicht, Muskelkanäle nach FACS, Datenvertrag mit dem Server, Portierung nach GTK4 und Cairo | | novaberg-avatar_b.md | Emotions-Avatar — Bauplan: Tests der Bausteine, Feinabstimmung des Prototyps, Bauteile B1–B8 für den Client | | novaberg-avatar_e.md | Emotions-Avatar — Diskussion und Ergänzungen: Entscheidungen, offene Punkte O1–O14, Annahmen, Befunde der Aufteilung, Abweichungen Prototyp ↔ Konzept | | novaberg-avatar_m.md | Emotions-Avatar — Messungen: Mundgeometrie je Emotion an Fotos dreier Personen, Lippenbewegung beim Sprechen an zwei Sprecherinnen | ### Tiefe 2 — Kognition (2) | Dokument | Beschreibung | |----------|-------------| | novaberg-thinking-curiosity_k.md | Neugier (Charakter-Resonanz, intrinsische Motivation, Reflexion) | | novaberg-thinking-drive_k.md | Antrieb (Ziele, Motivation, Gravitation, Dual-Emotion) — Absicht, mit Kopfblock und Tabelle „§ → Datei“ | | novaberg-thinking-drive_t.md | Antrieb — Ausarbeitung: Gravitations-Rechnung, Speicher der Dual-Emotion, Zieltabelle, Konfiguration | | novaberg-thinking-drive_b.md | Antrieb — Bauplan und Umstellung: Eingriffspunkte, Bauberichte | | novaberg-thinking-drive_e.md | Antrieb — Diskussion und Ergänzungen: Entscheidungen, offene Fragen, Befunde | | novaberg-thinking-drive_m.md | Antrieb — Messungen | | novaberg-thinking-lage_k.md | Lage (Sachlage, Scheiben 1–12) — Absicht, mit Kopfblock und Tabelle „§ → Datei“ | | novaberg-thinking-lage_t.md | Lage — Ausarbeitung: Tabellen, Schreib- und Rückweg | | novaberg-thinking-lage_b.md | Lage — Bauplan und Umstellung: die Scheiben mit `ZIEL`/`TEST`/`MESSUNG` und ihren Bauberichten | | novaberg-thinking-lage_e.md | Lage — Diskussion und Ergänzungen: Entscheidungen, offene Fragen, Befunde | | novaberg-thinking-lage_m.md | Lage — Messungen je Scheibe, Eichung der Nähe | | novaberg-thinking-faszination_k.md | Faszination — Absicht | | novaberg-thinking-faszination_t.md | Faszination — Ausarbeitung: Rechnung, verworfene technische Varianten | | novaberg-thinking-faszination_b.md | Faszination — Bauplan und Umstellung: Reihenfolge, Bauberichte | | novaberg-thinking-faszination_e.md | Faszination — Diskussion und Ergänzungen: Entscheidungen, offene Fragen, Befunde | | novaberg-thinking-faszination_m.md | Faszination — Messungen | | novaberg-thinking-opinion_k.md | Meinung / Willensstrang — Absicht | | novaberg-thinking-opinion_t.md | Meinung / Willensstrang — Ausarbeitung (was gebaut ist) | | novaberg-thinking-opinion_e.md | Meinung / Willensstrang — Diskussion und Ergänzungen: Entscheidungen, offene Fragen, Befunde | | novaberg-thinking-opinion_m.md | Meinung / Willensstrang — Messungen | | novaberg-thinking-erkenntniszyklus_k.md | **Der Erkenntniszyklus** — die Folge, in der Nova ein Thema durchdringt: Nachdenken vor Nachschlagen. Übergeordnet über Meinung, Neugier, Wissenslücken und Wissensspeicher | ### Tiefe 2 — Technik & Tools (3) | Dokument | Beschreibung | |----------|-------------| | novaberg-tool-timeparser.md | Zeitparser (Fuzzy, Normalisierung, ZeitVektor) | | novaberg-tool-web.md | Web-Infrastruktur (Suchanbieter, PageFetcher) | | novaberg-tool-multi-channel.md | Multi-Channel (Telegram Bot, Formatierung) | ### Tiefe 3 — Querschnittsmuster (4) | Dokument | Beschreibung | |----------|-------------| | novaberg-pattern-crud-hardening.md | CRUD-Haertungs-Pattern (4-Phasen-Transaktion) | | novaberg-pattern-domain-language.md | Domain-Language-Normalisierung ([FACHSPRACHE]) | | novaberg-pattern-prompt-schema.md | Prompt-Schema [BLOCKNAME] (Konvention) | | novaberg-pattern-entity-resolution.md | Entity Resolution (Name-Match, Embedding) | ### Lessons Lessons leben als `{modul}_l.md` oder `{modul}_l_{thema}.md` neben ihrem Modul-Dokument: | Dokument | Thema | |----------|-------| | novaberg-graph_l.md | Kontextuelle Kontamination | | novaberg-graph_l_kontextualisierung.md | Strukturierte Kontextualisierung | | novaberg-graph_l_datentransport.md | Daten vollstaendig transportieren | | novaberg-node-thinker_l.md | Suchbegriff-Verzerrung | | novaberg-node-salience_l.md | Salienz-Mittlung | | novaberg-node-dispatcher_l.md | Doppelspeicherung Salienz + Planner | | novaberg-node-responder_l.md | Unsichtbarer Default (System-Prompt) | | novaberg-node-router_l.md | "NIEMALS" ist kein Proxy | | novaberg-agent-notes_l.md | Namens-Identitaet | | novaberg-ei_l.md | Blindflug (EI ohne Sichtbarkeit) | | novaberg-ei-plutchik_l.md | Arousal-basierter Decay | | novaberg-ei-character-profiles_l.md | Persona-Isolation (Yin-Yang) | | novaberg-architecture_l.md | ROCm-Versionsinkompatibilitaet | | novaberg-pixie_l_feedback.md | Feedback-Loop Pixie | | novaberg-pixie_l_kontamination.md | Session-Kontamination durch Delivery | | novaberg-pixie_l_spezialisierung.md | Spezialisierung schlaegt Generalisierung | | novaberg-pixie_l_arbeitspaket.md | Enges Arbeitspaket (OpenClaw) | | novaberg-tool-timeparser_l_timezone.md | Timezone UTC vs. Lokal | | novaberg-tool-timeparser_l_evolution.md | Zeitparser-Evolution | | novaberg-tool-timeparser_l_vektor.md | Vektor-Modus Zeitparser | | novaberg-lesson_l_miss-als-sicherung.md | Der Miss ist manchmal die Sicherung (Router-Miss als Schutz vor defektem Pfad) | | novaberg-lesson_l_default-wie-fehlschlag.md | Ein Default darf nie wie ein Fehlschlag aussehen (nie geladen vs. leer geladen) | | novaberg-lesson_l_log-behauptet-was-es-weiss.md | Ein Log darf nur behaupten, was es weiß (lügende Logs, broadcast-Wurzel) | | novaberg-lesson_l_stichprobe-trifft-den-pfad.md | Die Stichprobe ist repräsentativ für den Pfad, den sie trifft | | novaberg-lesson_l_fehlschlag-als-absicht.md | Ein Fehlschlag darf nie wie eine Absicht aussehen (der getarnte Miss) | | novaberg-lesson_l_analyse-ersetzt-keine-messung.md | Ein Analyse-Upgrade ersetzt keine Messung | | novaberg-lesson_l_zweite-fundstelle-nach-behebung.md | Eine behobene Fehlerklasse steht an ihrer zweiten Stelle weiter (Grep auf die Quelle des falschen Werts) | | novaberg-lesson_l_konzept-spricht-code.md | Ein Konzept, das in Code-Sprache spricht, wird für einen Befund gehalten | | novaberg-lesson_l_ableitung-als-messung.md | Eine Ableitung ist keine Messung — auch wenn sie stimmt | | novaberg-lesson_l_gelesen-ist-nicht-wirksam.md | Die gelesene Quelle ist nicht die wirksame (zwei Speicher, einer gilt) | | novaberg-lesson_l_gegenprobe-misst-den-cache.md | Eine Gegenprobe, die den Bytecode-Cache erwischt, misst gar nichts | | novaberg-lesson_l_weg-vor-wortschatz.md | Erst messen, ob der Weg befahren wird — sonst erweitert man einen toten Pfad | | novaberg-lesson_l_parst-nicht-schlaegt-parst-falsch.md | Eine zu breite Regel macht aus dem harmlosen Ausfall den schädlichen | | novaberg-lesson_l_meldung-im-bestand-ertrinkt.md | Eine Regel, deren Verletzung ein Absturz ist, gehört nicht in eine geduldete Trefferzahl | | novaberg-lesson_l_zwei-grundgesamtheiten.md | Zwei Zahlen, die verschiedene Mengen messen, sehen wie ein Widerspruch aus | | novaberg-lesson_l_die-sonde-misst-sich-selbst.md | Ein Reizsatz aus einem Register misst die Monotonie des Reizsatzes | | novaberg-node-synapsen-promotion.md | SynapsenPromotionAgent — der Weg vom Kurzzeit- ins Langzeitgedaechtnis (loest novaberg-pixie-promotion.md ab) | ### Uebergreifend | Dokument | Beschreibung | |----------|-------------| | novaberg-roadmap.md | Projektchronik, **laufender Zeitraum** (nur abgeschlossene Arbeit) | | novaberg-roadmap-2026-*.md | Chronik abgeschlossener Zeitraeume — je Monat eine Datei | | novaberg-roadmap-index.md | Findemittel ueber alle Chronikteile — eine Zeile je Abschnitt, **erzeugt** | | novaberg-backlog.md | Backlog: Kopf, Wegweiser und der Verlauf des Standes | | novaberg-backlog-{gegenstand}.md | Backlog nach Gegenstand — sechs Dateien: Gedaechtnis, Hintergrund, Charakter, Antwortpfad, Wissen, Bauart | | novaberg-backlog-rangordnung.md | Baender und laufende Reihen — quer zu allen Gegenstaenden | | novaberg-backlog-index.md | Findemittel ueber alle Backlog-Teile, mit der Rangordnung, **erzeugt** | | novaberg-bugs.md | Bekannte Probleme & Limitationen, offen — Wegweiser und Formregel, **traegt keinen Eintrag** | | novaberg-bugs-{gegenstand}.md | Offene Defekte nach Gegenstand — sechs Dateien: Gedaechtnis, Hintergrund, Charakter, Antwortpfad, Wissen, Bauart | | novaberg-bugs-archiv.md | Abgeschlossene Defekte, Wegweiser — dazu die Kurzeintraege der alten Tabelle | | novaberg-bugs-archiv-{gegenstand}.md | Abgeschlossene Defekte nach Gegenstand: behoben, geschlossen, gegenstandslos, verworfen — sechs Dateien | | novaberg-bugs-index.md | Findemittel ueber alle Bug-Teile, offen und abgeschlossen — eine Zeile je Kennung, **erzeugt** | | novaberg-fundliste.md | Rohe, noch unklassifizierte Funde aus laufender Arbeit | --- ## 8. Deployment ### 8.1 Docker Compose ```yaml services: server: build: ./server ports: ["8000:8000"] depends_on: [postgres, redis] environment: - OLLAMA_GPU_URL=http://host.docker.internal:11434 - OLLAMA_CPU_URL=http://host.docker.internal:11435 - POSTGRES_URL=postgresql://ki:***@postgres:5432/gedaechtnis - REDIS_URL=redis://redis:6379 - SEARXNG_URL=http://searxng:8080 postgres: image: pgvector/pgvector:pg16 volumes: - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql - pgdata:/var/lib/postgresql/data redis: image: redis/redis-stack-server:latest command: redis-stack-server --appendonly yes --dir /data volumes: - redisdata:/data searxng: image: searxng/searxng:latest volumes: - ./searxng:/etc/searxng:rw ports: ["8080:8080"] # telegram-bot: abgeschaltet am 24.08.2026, Block entfernt ``` > **Der Auszug ist unvollstaendig:** `synapse` und `matrix-bot` laufen seit dem 23.08.2026 > und stehen hier nicht. Die Betriebsdatei ist `docker-compose.yml` im Arbeitsverzeichnis, > das Muster im Repositorium `docker-compose.template.yml`. ### 8.2 Ollama (Host-native) Zwei systemd-Services, getrennte Instanzen: ```bash # GPU-Instanz (Port 11434) OLLAMA_HOST=0.0.0.0:11434 ollama serve # CPU-Instanz (Port 11435) OLLAMA_HOST=0.0.0.0:11435 ollama serve ``` Docker-Container erreichen den Host via `host.docker.internal`. Ollama muss explizit auf `0.0.0.0` gebunden werden (systemd-Override), da der Default `127.0.0.1` nur lokalen Zugriff erlaubt. ### 8.3 Startup-Reihenfolge ``` 1. systemd -> Ollama GPU (Port 11434) 2. systemd -> Ollama CPU (Port 11435) 3. docker compose up -> postgres, redis, searxng 4. docker compose up -> server ├── Lifespan: model_service.startup() -> Worker-Schicht (embed, chat, background) ├── Lifespan: Plugin Discovery + Manager Setup ├── init.sql -> Schema-Migrationen (idempotent) ├── build_human_graph() + build_agent_graph() ├── APScheduler -> SchattenArbeit (alle 10 Min) └── API bereit auf :8000 5. docker compose up -> matrix-bot (Application Service, wartet auf server + synapse) (bis 24.08.2026 stand hier telegram-bot, Long Polling — der Kanal ist abgeschaltet) 6. Client -> GTK4 App verbindet sich via SSE + WebSocket ``` ### 8.4 Admin-API Vier Endpoints zur Steuerung des Pixie-Schedulers: | Endpoint | Methode | Beschreibung | |----------|---------|-------------| | `/admin/pixie/pause` | POST | Setzt Redis-Key `pixie:paused` — Runner ueberspringt | | `/admin/pixie/resume` | POST | Loescht `pixie:paused` — Runner arbeitet wieder | | `/admin/pixie/status` | GET | Gibt `paused: true/false` zurueck | | `/admin/pixie/flush` | POST | Fuehrt alle Pixie-Tasks synchron aus bis nichts mehr uebrig ist | ### 8.5 Health-Check ```bash curl http://localhost:8000/health ``` Prueft 5 Dienste + Pixie-Status: | Feld | Prueft | Methode | |------|-------|---------| | `server` | FastAPI erreichbar | Implizit (Endpoint antwortet) | | `redis` | Redis-Verbindung | `redis_client.ping()` | | `postgres` | PostgreSQL + pgvector-Extension + Schema | SQL-Query | | `ollama` | GPU-Ollama + Modellverfuegbarkeit | `ollama_gpu_client.list()` | | `searxng` | SearXNG erreichbar | HTTP GET auf `SEARXNG_URL` (Timeout 3s) — **prueft den Rueckfall, nicht den ersten Anbieter** (Fundliste 30.08.2026) | | `shadow` | Pixie-Status (idle/aktiv + Thema) | Redis-Key `shadow_status` | > **Zwei Redis-Clients, ein Dienst** (12.09.2026). `config.py` hält neben > `redis_client` (`decode_responses=True`) einen zweiten: `redis_client_bytes` > (`decode_responses=False`). Er ist für Abfragen da, die einen **Vektor** > zurückholen — der Lückenpfad holt seit dem 12.09.2026 das `embedding` der > KZG-Treffer mit, um die Kern-Nähe je Kandidat zu rechnen. > > **Der Grund ist kein Stil, sondern ein gemessener Ausfall:** Mit > `decode_responses=True` liest redis-py die ganze Antwort als UTF-8, ein > float32-Blob bricht das ab, die Ausnahme wird gefangen — und die Suche liefert > **null Treffer bei 3544 Schlüsseln im Speicher**. Ein gebrochener Zugriff sieht > dann aus wie ein leeres Kurzzeitgedächtnis. Die Lebenszeichen-Prüfung oben > bleibt bei einem Client: Sie prüft den **Dienst**, und den gibt es einmal. Client-Statusleiste pollt `/health` alle 5 Sekunden. Zeigt 5 Dienste mit gruen/rot-Indikatoren. --- ## 9. Konfiguration Alles Konfigurierbare lebt in `config.py`, gelesen aus Umgebungsvariablen mit Defaults: | Kategorie | Beispiele | |-----------|-----------| | Verbindungen | `OLLAMA_GPU_URL`, `OLLAMA_CPU_URL`, `POSTGRES_URL`, `REDIS_URL` | | Modelle | `OLLAMA_CONNECTOR` (~~aktiv: `qwen36`~~ → Vorgabe im Code `qwen36`, im Betrieb seit dem 07.11.2026 `qwen38`), `EMBED_MODEL` — ~~`OLLAMA_GPU_MODEL`, `OLLAMA_CPU_MODEL`~~ existieren nicht mehr, siehe unten | | Context | `OLLAMA_GPU_NUM_CTX`, `OLLAMA_CPU_NUM_CTX` — **abgeleitet, nicht gesetzt** | | Pixie | `PIXIE_INTERVALL_MIN` (10), `PIXIE_PROMOTION_PRIORITAET` (0.9), `PIXIE_PROMOTION_INTERVALL_SEKUNDEN` (300), `PIXIE_QUALITAET_INTERVALL_SEKUNDEN` (86400), `PIXIE_QUALITAET_PRIORITAET` (0.2), `PROMOTION_THRESHOLD` (0.8, in `memory/kzg.py` — Salienz-Schwelle, nicht in config.py) | | EI | `EMOTION_DECAY_FACTOR` (0.8), `EMOTION_DECAY_BASE` (10) | | Decay | `EBBINGHAUS_DECAY_RATE` (0.0015), `EBBINGHAUS_MIN_GEWICHT` (0.1) | | Namen | `ASSISTANT_NAME` ("Nova"), `BACKGROUND_NAME` ("Pixie") | > **Am 16.08.2026 gegen den Code geprueft: Die Modelle werden nicht mehr einzeln gesetzt.** > > `config.py` fuehrt eine **Connector-Tabelle** `OLLAMA_CONNECTORS` mit je einem Satz aus `gpu_model`, `gpu_num_ctx`, `cpu_model`, `cpu_num_ctx`, `analyse_model` und `analyse_num_ctx`. Eine einzige Umgebungsvariable waehlt aus: > > ```python > OLLAMA_CONNECTOR = os.getenv("OLLAMA_CONNECTOR", "qwen36") > _connector = OLLAMA_CONNECTORS[OLLAMA_CONNECTOR] > OLLAMA_MODEL = _connector["gpu_model"] > SHADOW_MODEL = _connector["cpu_model"] > PIXIE_ANALYSE_MODEL = _connector["analyse_model"] > ``` > > **Was daraus folgt und in der alten Tabelle nicht stand:** Modell und Kontextgroesse sind **zusammen** gewaehlt und koennen nicht einzeln verstellt werden — genau der Fehler, den getrennte Variablen erlauben (ein Modell mit dem Fenster eines anderen). Die Klammerwerte der Zeile `Context` waren deshalb ebenfalls falsch: `(16384)` ist der Wert des Connectors `mistral`, waehrend `qwen36` aktiv ist und 32768 fuehrt. > > Die drei Verbraucher heissen `OLLAMA_MODEL` (GPU), `SHADOW_MODEL` (CPU) und `PIXIE_ANALYSE_MODEL` (Analyse). | Zeitzonen | `TIMEZONE` ("Europe/Berlin") | | LLM-Backends | `WORKER_BACKEND_CHAT`, `WORKER_BACKEND_BG_ANALYSE`, `WORKER_BACKEND_BG_SPRACHE` — **alle drei seit 05.09.2026 auf `openrouter`**, gesetzt in der `.env` neben der Compose-Datei; die Vorgabewerte im Code bleiben lokal und sind der Rückweg. Dazu `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL`, `OPENROUTER_API_KEY`, `OPENROUTER_MODEL`, `OPENROUTER_PROVIDER`, `OPENROUTER_QUANTISIERUNG`, `OPENROUTER_FALLBACKS`, `LLM_PROFILE` (lokal — nur noch ThinkingNormalizer-Schalter, siehe §2.5/§2.7) | | Web-Suche | `SERPER_API_KEY` (leer = Anbieter uebersprungen), `SERPER_URL` (https://google.serper.dev/search), `SERPER_TIMEOUT` (10.0), `SEARXNG_URL` (http://searxng:8080), `SEARXNG_TIMEOUT` (10.0), `SEARXNG_MAX_RESULTS` (10) | **Keine Magic Numbers im Code.** Alle Schwellwerte, Raten und Gewichtungen sind konfigurierbar. --- ## 10. Datenbankschema Das Kern-Schema lebt in `db/init.sql` als Single Source of Truth (Synapsen P0). Agent-spezifische Tabellen liegen in `server/agents//init.sql` und werden beim Agent-Discovery aufgesetzt. | Tabelle | Quelle | Beschreibung | |---------|--------|-------------| | `lzg_knoten` | `db/init.sql` | Synapsen-Knoten — **das Langzeitgedaechtnis** seit P9 (02.08.2026); 1108 Knoten am 02.08. | | `lzg_kanten` | `db/init.sql` | Synapsen-Kanten-Cache (Synapsen P2, abgeleiteter Cache; drei Trigger zur Neuberechnung) | | `pipeline_log` | `db/init.sql` | Forensik-Tabelle für Node-Entscheidungen pro Turn (Synapsen P1, JSONB-Inhalt) | | `sachlage_verlauf` | `db/init.sql` | **Das Sachlage-Gedaechtnis** (28.08.2026) — je gerechnetem Turn eine Zeile: `turn_id`, Paar, `thema`, `gegenstand`, `nutzerziel`, `ausdrucksweise`, `objekte` (JSONB), `herkunft`, `embedding VECTOR(768)` ueber den Gegenstand-Satz (`F-EMBED-1`), `erstellt_am`. **Verfaellt nicht** — ein Faktum, und Turns verfallen auch nicht (`F-VERFALL-1`). Kein Fremdschluessel. Zweites Ende der Sachlage-Bruecke bei Zustellungen; Konzept `novaberg-thinking-lage_k.md` §4 | | `sachlage_objekt` | `db/init.sql` | **Das Eigenschaftsgedaechtnis der Sachlage, Objekte** (13.09.2026) — eine Zeile je Paar und Objektname (`name_schluessel` eindeutig je Paar), `klasse`, `entitaet_id` (→ `entitaeten`, **ON DELETE SET NULL**, gesetzt erst, wenn die Magnete eines Turns das Objekt nennen) und `entitaet_bindung` (wie gebunden wurde, heute `turn_magnet`). Kein Verfall. Konzept `novaberg-thinking-lage_k.md` §4, Scheibe 11 | | `sachlage_objekt_turn` | `db/init.sql` | **Die Objekte je Turn** (13.09.2026) — `verlauf_id` (→ `sachlage_verlauf`), `objekt_id` (→ `sachlage_objekt`), beide **ON DELETE RESTRICT**, `turn_id`, `akut`; eindeutig je Verlaufszeile und Objekt. Ueber `turn_id` erreicht die Bindung `verbindung`, KZG und LZG | | `sachlage_eigenschaft` | `db/init.sql` | **Die Werte mit Historie** (13.09.2026) — `objekt_id` (**RESTRICT**), `eigenschaft` samt Schluessel, `wert`, `sprecher`, `quelle` (JSONB), `turn_id`, `bestaetigt_turn_id`, `timeline_id` (nackt im Kern; Fremdschluessel **SET NULL** in `server/agents/timeline/init.sql`), `aktiv`, `t_valid`, `t_invalid`, `abgeloest_durch` (selbstbezogen, **SET NULL**). **Ein neuer Wert ueberschreibt nicht** — der alte wird inaktiv; hoechstens ein aktiver Wert je Objekt und Eigenschaft, erzwungen per eindeutigem Teilindex. Kein Verfall (`F-VERFALL-1`) | | `autonomous_wissen` | `db/init.sql` | Metadaten der Wissens-Bibliothek (WIS-2, 04.08.2026) — **nicht der Inhalt**: Der liegt als Datei ausserhalb des Git-Roots. Paar-Schema und `salienz_anfang` ohne Vorgabewert; gelesen vom `WissenManager` als sechste Kontextquelle des Enrichers. **Seit dem 18.08.2026 ist der Dateien-Index eine weitere Quelle des Enrichers — aber kein Plugin und kein Beitrag zu diesem Pool:** Er schreibt in den eigenen Zustandskanal `aufzeichnungen`, weil alles aus dem Plugin-Pool unter `[GEDAECHTNIS]` gerendert wird und Dateiinhalt dort nicht hineindarf | | `shadow_auftrag` | `db/init.sql` | **Die Shadow-Queue** (15.08.2026) — bis dahin eine Redis-Liste. Traegt die Auftraege des Pixie-Heartbeats samt Verfallsmodell nach dem Muster von `lzg_knoten`: drei Salienz-Staende, `haeufigkeit`, `aktiv` als Soft-Delete, zwei Uhren. **Acht Spalten ohne Vorgabewert** — Paar-Tripel, Gegenstand und die drei Salienz-Staende; die Sperre wanderte aus der Signatur hierher. Uebernommen wurden 1036 Auftraege. **Seit dem 15.08.2026 zusaetzlich `arousal`** (NULL-faehig, ohne Vorgabewert): die dritte Groesse der Lage neben `emotion` und `modus`. NULL heisst unbekannt — Bestandszeilen tragen sie nicht, und ein Vorgabewert waere ein Messwert, den nie jemand gemessen hat. **Seit dem 23.08.2026 zusaetzlich `grund`** (`VARCHAR(20) NOT NULL DEFAULT ''`): warum die Zeile ist, wie sie ist. `aktiv` sagt, **ob** sie gesucht wird, `grund` sagt, **warum** sie stillliegt (`F-STILLLEGUNG-1`) — Kanon `''` · `verfall` · `fehlversuch`. Anlass war der Fehlversuchspfad, der bis dahin hart loeschte; die 247 stillgelegten Altzeilen tragen `''` und sind daran als Altbestand erkennbar. **Seit dem 28.08.2026 zusaetzlich `ausloeser_turn_id`** (`TEXT`, NULL-faehig, ohne Vorgabewert): der Turn, aus dem der Auftrag entstand — erstes Glied der Sachlage-Bruecke; NULL heisst unbekannt, der Altbestand traegt es. Konzept: `novaberg-queue-verfall_k.md` | | `charakter_hash` | `db/init.sql` | 5 Persoenlichkeitsprofile (kern_hash, adaptive_hash, beziehungsprofil, intentions_profil, emotions_profil), alle im Prompt injiziert (seit Chat 52) | | `hintergrund_log` | `db/init.sql` | Pixie-Aufgabenprotokoll | | `gespraech_archiv` | `db/init.sql` | Session-Archivierung | | `entitaeten` | `db/init.sql` | Knowledge Graph Nodes | | `fakten` | `db/init.sql` | Knowledge Graph Edges | | `timeline` | `db/init.sql` (CREATE) / `agents/timeline/init.sql` (FK + Indizes) | Termine und Ereignisse | | `notizen` | `db/init.sql` (CREATE) / `agents/timeline/init.sql` (FK auf `timeline_id`) | Merkzettel und Listen | Alle Migrationen sind idempotent (`IF NOT EXISTS`, `ADD COLUMN IF NOT EXISTS`). Wiederholtes Ausfuehren von `init.sql` ist sicher. Konvention seit Synapsen P0: Neue Schema-Änderungen werden als ALTER-Statements im Migrations-Block am Ende von `db/init.sql` ergänzt, NICHT direkt in die ursprüngliche CREATE TABLE-Definition eingearbeitet — damit bleibt nachvollziehbar, was wann hinzugekommen ist. Bei späteren Reviews werden die akkumulierten ALTER-Statements in die CREATE-Definitionen konsolidiert. Foreign-Key-Spalten von Kern-Tabellen auf Agent-Tabellen (z.B. `timeline_id`) sind in `db/init.sql` als nackte INTEGER-Spalten definiert; der FK-Constraint wird in der jeweiligen Agent-`init.sql` gesetzt, damit die Abhängigkeit erst nach Anlage der Agent-Tabelle realisiert wird. **Reihenfolge der Agenten-Schemata und Abhängigkeiten zwischen ihnen** (seit dem 17.10.2026, Tree `dbf9063`): Der Start wendet zuerst `db/init.sql` an (`main.py::schema_migrieren`), dann die `init.sql` der Agenten in der Reihenfolge der Ordnernamen — `agents/__init__.py::discover_agents` registriert in `sorted(...)`-Folge, `main.py::lifespan` ruft `setup` je Agent der Registry. Verweist das Schema eines Agenten auf die Tabelle eines Agenten, der später kommt, steht die Vorbedingung im `setup` des Abhängigen und nicht in der Reihenfolge des Starts. Heute ist das ein Fall: `dateien_index` verweist mit `wurzel_id` auf `dateien_wurzeln(id)`, und `DateienIndexAgent.setup` wendet zuerst das Schema von `dateien_wurzeln` an, dann das eigene, und prüft mit `to_regclass`, dass beide Tabellen bestehen; das Schema der Wurzeln läuft beim Start damit zweimal, idempotent. Der abhängige Agent importiert dafür das Modul des anderen, nicht dessen Klasse — `discover_agents` registriert jede `BaseAgent`-Unterklasse im Namensraum einer `agent.py`. Bis dahin brach ein Aufbau von null an genau dieser Stelle ab (`SCHEMA-VON-NULL-SCHEITERT`, `novaberg-bugs-archiv-bauart.md`); gemessen ist der Aufbau über den Startweg mit `labor/2026-10-17_aufbau_von_null.sh` (alle 21 Agenten, 34 Tabellen). Dass jede Tabelle, auf die eine Schemadatei sich bezieht (`REFERENCES`, `ALTER TABLE`, `CREATE … INDEX … ON`), vor ihr besteht, prüft `server/tests/test_agent_schema_reihenfolge.py` über alle Schemadateien. **Still bleibt** eine fehlende `init.sql`: `BaseAgent.setup` übergeht sie ohne Meldung (`AGENT-SCHEMA-STILL-VERLOREN`, novaberg-bugs-bauart.md). **Der Weg ist seit dem 18.10.2026 derselbe für Start und Aufbau von null** (Tree `6f23a80`): `schema_migrieren` umschließt `tools/core_schema.py::apply_core_schema` und schluckt dessen Fehler wie bisher mit `warning`/`error`; `lifespan` ruft `schema_bootstrap.py::apply_agent_schemas` — `discover_agents`, dann `setup` je Agent in der Reihenfolge der Registry. **Laut ist nur der Einstieg** `python -m schema_bootstrap` (liest `POSTGRES_URL`): Kern, Agenten, dann die Prüfung, dass jede `init.sql` unter `server/` angewandt ist; Ausgang 0, oder 1 mit genau einer Zeile `schema_bootstrap: : `, nie mit der Verbindungszeichenkette. `core_schema.py` liegt als Infrastruktur unter `tools/` und importiert keinen Geschäftsablauf; was die Agenten kennt, liegt neben `main.py`, an der Wurzel von `server/`, die wie der Start die Schichten verbindet. Gemessen mit `labor/2026-10-18_schema_bootstrap.sh` an je eigenen Wegwerf-Behältern. Das Plugin-System lebt weiter parallel (siehe §4) — `plugins/*/init.sql` für Plugin-spezifische Hilfstabellen ist weiterhin möglich, das primäre Datenbankschema ist aber `db/init.sql` plus die Agent-init-SQL. --- *Konsolidiert aus nova-00-a.md, nova-07-a.md, nova-07-m-a.md.* --- ## Befunde aus dem Betrieb — nachgetragen am 20.08.2026 Aus `novaberg-fundliste.md` hierher gezogen: Aussagen ueber den **Zustand** dieses Gegenstands, die dort als rohe Funde standen und in kein Defekt- oder Vorhabenregister gehoeren. Der Wortlaut ist unveraendert, das Datum steht an jedem Befund — geprueft ist keiner von ihnen gegen den heutigen Code. - **19.08.2026** — **Ollama 0.32.14 schaltet Thinking per Default ein; ein Aufrufer, der `think` wegläßt, verliert den Großteil seiner Ausgabe in einen ungelesenen Kanal.** Gemessen an `gemma4-gpu` mit derselben Frage: ohne das Feld `content=214, thinking=1467` bei 425 Token — mit `think=false` `content=248, thinking=0` bei **60** Token. Der Bestand ist nicht betroffen (`services/llm_provider.py` setzt `"think": think` unbedingt), aber jeder neue Aufrufweg, der das Payload selbst baut, ist es. **Der Nebenbefund ist der Preis:** derselbe Inhalt kostet ohne das Feld das Siebenfache an Ausgabe-Token. - **19.08.2026** — **Mehrfachvorhersage (MTP) ist in diesem System weder eingeschaltet noch einschaltbar noch in den Gewichten vorhanden — und ein Update änderte daran nichts.** Geprüft auf drei Ebenen: Der Bestand sendet keine Draft-Option; Ollama 0.20.7 kennt keine (vollständige Variablenliste durchgesehen); und in beiden GGUF-Blobs findet sich über `/api/show verbose` **kein einziger** Metadatenschlüssel mit `nextn`, `mtp`, `draft`, `specul` oder `predict` — 53 bzw. 56 Schlüssel geprüft. **Der Grund, warum ein Update es nicht bringt, steht in den Release Notes:** Jede der sieben Nennungen von MTP oder spekulativem Dekodieren hängt am **MLX-Runner**, also Apple Silicon (v0.23.1 *„supported on Macs"*, v0.31.1 *„on Apple Silicon"*, v0.32.6 *„on Apple GPUs"*). Diese Maschine fährt AMD unter Linux. **Nicht widerlegt ist die Aussage über die Modelle selbst:** MTP steckt in eigenen Varianten (`gemma4:31b-coding-mtp-bf16`), nicht in den normalen Q4-Quantisierungen. - **18.08.2026** — **Der Verzeichnisbaum der Architektur kennt zwei Manager nicht, die seit Tagen laufen.** Beim Nachzug des Aufrufers aufgefallen: `plugins/` listet sieben Einträge; im Baum fehlen `dateien_wurzeln_manager/` (seit 18.08. vormittags) und `wissen_manager/` (seit dem 04.08.). **Beidseitig gezählt:** `ls server/plugins/` → 9 Verzeichnisse plus `base.py`, der Baum nennt 7. Dieselbe Klasse wie die drei Register vom selben Tag — ein Register, das seinen Gegenstand nicht kennt, und es fällt nur auf, wenn jemand daneben etwas einträgt. - **18.08.2026** — **Der Architektur-Bauplan kennt einen Manager nicht und der veröffentlichte Compose-Bauplan vier Mounts nicht.** Beides beidseitig gezählt. **Manager:** `ls -d server/plugins/*_manager | wc -l` → **8**, die Tabelle in `novaberg-architecture.md` §4.5 trägt **7** Zeilen — `WissenManager` fehlt, seit er am 04.08.2026 entstand. **Mounts:** `docker-compose.template.yml` gibt dem `server`-Dienst **1** Volume, die betriebene `docker-compose.yml` hat **5** (`/app`, `/app/db:ro`, `/knowledge`, `/logs`, `/files:ro`). Die READMEs weisen an, das Template zu kopieren — wer das tut, bekommt ein System ohne Wissensspeicher, ohne dauerhaftes Log und mit einem Dateien-Dienst, der geschlossen ausfällt, weil `DATEIEN_AUSSENRAND` per Vorgabe auf `/files` steht und dort nichts liegt. **Drei der vier fehlenden Mounts sind älter als heute**, `files` kam am 18.08.2026 dazu. **Gefunden hat es die zweite Kontrolle über einen Zugriff, den der Bau nicht hatte** — den Diff zwischen veröffentlichtem Bauplan und betriebener Umgebung; kein Werkzeug des Nachzugs zieht ihn, weil das Template außerhalb jeder Bezeichner-Suche liegt.