""" KI-Server — Zentrale Konfiguration. Umgebungsvariablen, Verbindungen, gemeinsame Ressourcen. """ import logging import os import threading import ollama import psycopg2 import redis from dotenv import load_dotenv from prompt_loader import prompt_laden from services.llm_riegel import GesperrterOllamaClient # .env laden (Repo-Root und aufwaerts). Fuer lokale Entwicklung; im Docker- # Container werden die Werte zusaetzlich ueber env_file injiziert. load_dotenv() # ───────────────────────────────────────────── # Logging # ───────────────────────────────────────────── logging.basicConfig( level = logging.DEBUG, format = "%(asctime)s [%(levelname)s] %(name)s: %(message)s" ) logger = logging.getLogger("ki_server") # Token-Tracking auf DEBUG sichtbar machen _llm_logger = logging.getLogger("ki_server.llm") _llm_logger.setLevel(logging.DEBUG) _llm_handler = logging.StreamHandler() _llm_handler.setLevel(logging.DEBUG) _llm_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")) _llm_logger.addHandler(_llm_handler) _llm_logger.propagate = False # ───────────────────────────────────────────── # Dauerhaftes Datei-Log # ───────────────────────────────────────────── # **Das Behaelter-Log stirbt mit dem Behaelter.** `docker compose up -d` mit # geaenderter Umgebung erzeugt ihn neu, und `docker compose logs` zeigt # danach ausschliesslich den neuen — das Log des gerade gefahrenen Laufs ist # fort. Am 09.08.2026 kostete das drei Untersuchungen: Jedesmal hatte die # Antwort im Log gestanden, jedesmal war sie beim Nachsehen weg, und # zweimal wurde die leere Ausgabe als Ergebnis gelesen. # # Die Ablage liegt **ausserhalb des Arbeitsbaums**, eingehaengt unter # `/logs`. Dieselbe Begruendung wie bei `/knowledge` (Register F-WISSEN-1): # Server-Logs tragen Gespraechsinhalte, und unterhalb von `novaberg/` wuerde # sie jeder Push veroeffentlichen. SERVER_LOG_DATEI: str = os.getenv("SERVER_LOG_DATEI", "/logs/server.log") SERVER_LOG_BYTES: int = int(os.getenv("SERVER_LOG_BYTES", str(128 * 1024 * 1024))) SERVER_LOG_GENERATIONEN: int = int(os.getenv("SERVER_LOG_GENERATIONEN", "9")) def _dateilog_einrichten(pfad: str) -> bool: """Haengt einen rotierenden Datei-Handler an Wurzel- und LLM-Logger. Vorbedingung: `pfad` ist nichtleer und sein Verzeichnis beschreibbar. Nachbedingung: True, wenn beide Logger in die Datei schreiben. Sonst False und eine `error`-Zeile — der Dienst laeuft dann weiter, aber ohne dauerhaftes Log. Das ist ein degradierter Zustand, der die Auswertung kostet, und er darf nicht still eintreten (`22_STILLE_FEHLER.md`). **Der LLM-Logger braucht denselben Handler eigens.** Er steht auf `propagate = False`, damit seine Token-Zeilen nicht doppelt auf der Konsole landen; genau deshalb erreicht ihn ein Handler an der Wurzel nicht. Die `LLM-Call`-Zeilen sind der meistgelesene Teil des Logs — ohne diese zweite Zuweisung waere die Datei genau dort leer, wo zuerst hingesehen wird. **Die Modusbits sind gemessen, nicht abgeleitet.** Der Behaelter laeuft als `root`; ohne sie gehoert die Datei auf dem Wirt `root`, und der Nutzer kann sein eigenes Log weder auswerten noch loeschen. Dieselbe Erfahrung steht als F-WISSEN-1 im Register. """ if not pfad: logger.error("Datei-Log: leerer Pfad — kein dauerhaftes Log") return False try: from logging.handlers import RotatingFileHandler verzeichnis = os.path.dirname(pfad) if verzeichnis: os.makedirs(verzeichnis, exist_ok=True) os.chmod(verzeichnis, 0o777) handler = RotatingFileHandler( pfad, maxBytes = SERVER_LOG_BYTES, backupCount = SERVER_LOG_GENERATIONEN, encoding = "utf-8", ) handler.setLevel(logging.DEBUG) handler.setFormatter( logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s") ) logging.getLogger().addHandler(handler) _llm_logger.addHandler(handler) os.chmod(pfad, 0o666) except OSError as fehler: logger.error( f"Datei-Log: '{pfad}' nicht beschreibbar ({fehler}) — " f"der Dienst laeuft ohne dauerhaftes Log weiter" ) return False logger.info( f"Datei-Log: '{pfad}', Rotation bei {SERVER_LOG_BYTES} Bytes, " f"{SERVER_LOG_GENERATIONEN} Generationen" ) return True DATEILOG_AKTIV: bool = _dateilog_einrichten(SERVER_LOG_DATEI) # ───────────────────────────────────────────── # Infrastruktur (profilunabhaengig) # ───────────────────────────────────────────── REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379") POSTGRES_URL: str = os.getenv("POSTGRES_URL", "postgresql://ki:ki@localhost:5432/gedaechtnis") TIMEZONE: str = os.getenv("TIMEZONE", "Europe/Berlin") ASSISTANT_NAME: str = os.getenv("ASSISTANT_NAME", "Nova") # Das grammatische Geschlecht der Figur — "w", "m" oder "n". # Es steht neben dem Namen, weil es dieselbe Sorte Datum ist: Wer die # Figur umbenennt, entscheidet damit auch ueber ihre Pronomen. Ohne den # Wert raet das Modell, und im selben Lauf verschieden # (PROFILPROMPT-OHNE-GESCHLECHT). ASSISTANT_GENUS: str = os.getenv("ASSISTANT_GENUS", "w") BACKGROUND_NAME: str = os.getenv("BACKGROUND_NAME", "Pixie") # ── Identität ────────────────────────────────────────────────── # Welche Zeile in charakter_hash ist die Assistentin (Novas Persönlichkeit) ASSISTANT_USER_ID: str = os.getenv("ASSISTANT_USER_ID", "nova") # Welcher User ist der Standard-Mensch (Fallback wenn kein user_id übergeben wird) DEFAULT_USER_ID: str = os.getenv("DEFAULT_USER_ID", "meister") # Welches Paar bedient Pixie? Der Mensch der EINEN Beziehung, in der die # Hintergrundarbeit dieses Laufs stattfindet — Promotion, Destillation, # Wissensluecken. Gespraeche sind davon unberuehrt: Der Chat-Endpunkt nimmt die # `user_id` je Anfrage, mehrere Menschen koennen gleichzeitig schreiben. # # Der Unterschied zu DEFAULT_USER_ID ist die Richtung: Jener ist ein FALLBACK # fuer eine fehlende Angabe, dieser eine ENTSCHEIDUNG darueber, wessen Arbeit # laeuft. Beide traegt heute derselbe Mensch, und genau deshalb braucht es zwei # Namen — sonst verschiebt eine Aenderung am Fallback still den Arbeitsbereich # der Hintergrundagenten. # # Fuer eine Messreihe wird dieser Wert je Lauf umgestellt und der Server neu # gestartet. Was in den Queues der uebrigen Paare liegt, bleibt liegen und # laeuft ab, sobald sie wieder an der Reihe sind: Die KZG-TTL reicht von sieben # bis dreissig Tagen. AKTIVES_PAAR_USER_ID: str = os.getenv("AKTIVES_PAAR_USER_ID", DEFAULT_USER_ID) # Auftragsarten, die waehrend eines Messreihen-Laufs gar nicht erst in die # Shadow-Queue kommen. Leer im Normalbetrieb. # # **Eine Messreihe muss abschliessen.** Die Charakterbildungs-Reihe misst # Turns, Charakter und Rad; Recherche, Vertiefung und Nachfragen tragen dazu # nichts bei. Sie verbrauchen aber die LLM-Spur: Ein Bogen erzeugt rund # sechzig solcher Auftraege mit Prioritaet 0.94 bis 1.00, jeder Minuten lang, # und die Destillation steht mit Basis 0.30 dahinter. Gemessen am 09.08.2026: # Nach zwei vollstaendigen Boegen hatte Nova fuer die eine Persona **kein** # Profil und fuer die andere ein Drittel — nicht weil die Destillation # scheiterte, sondern weil sie nie an die Reihe kam. # # Warten hilft nicht: Um eine 1.00 zu ueberholen, braeuchte sie bei 0.5/h # Alterung **84 Minuten** Ueberfaelligkeit; ein Bogen dauert 27 bis 33. Und # sieben der Auftraege sind `vertiefen`, deren Agent nicht existiert — die # Queue wuerde selbst nach Stunden nicht leer. # # Deshalb nicht spaeter abraeumen, sondern **gar nicht erst einreihen**. Der # Preis steht dabei: Eine so erhobene Persona traegt kein recherchiertes # Wissen. Das ist eine Verengung der Messung, keine Nebensache — Boegen mit # und ohne Unterdrueckung sind nicht miteinander vergleichbar. MESSREIHE_OHNE_AUFTRAGSARTEN: frozenset[str] = frozenset( art.strip() for art in os.getenv("MESSREIHE_OHNE_AUFTRAGSARTEN", "").split(",") if art.strip() ) # Stellt den **automatischen** Anstoss der Charakter-Destillation still. # # Im Regelbetrieb setzt jeder Turn `hash_dirty`, und Pixie destilliert, sobald # ein Platz frei wird. Fuer eine Messreihe ist genau das untauglich: Der # Zeitpunkt haengt dann an der Auslastung des Modells, und der Charakter # entsteht mal nach dem zwoelften, mal nach dem vierzehnten Turn — jeder Bogen # unter anderen Bedingungen. # # **Gemessen am 11.08.2026, woran das haengt:** In Mehmets 40-Minuten-Bogen # wurde das Rad einmal erhoben, auf einem Profil von 373 Zeichen; alle # spaeteren Laeufe fanden `messung_faellig` = nicht faellig (Sperre 12 # Stunden) und liessen es stehen. Am Ende lag ein Rad von 09:23 neben einem # Profil von 10:00 — zwei Momente, eine Zeile. # # Mit diesem Schalter setzt niemand mehr automatisch; der Bogenlaeufer stoesst # an definierten Punkten an (nach Turn 10 und nach Turn 30) und wartet auf das # Ergebnis. `RAD_MESSUNG_ABSTAND_STUNDEN=0` gehoert im selben Lauf dazu, sonst # greift die Zwoelf-Stunden-Sperre beim zweiten Anstoss. # # **Der Agent raeumt das Flag nach getaner Arbeit selbst weg** (agent.py) — # ein Anstoss ergibt deshalb genau eine Destillation. MESSREIHE_OHNE_AUTOMATISCHE_DESTILLATION: bool = ( os.getenv("MESSREIHE_OHNE_AUTOMATISCHE_DESTILLATION", "false").lower() == "true" ) redis_client: redis.Redis = redis.from_url(REDIS_URL, decode_responses=True) # **Ein zweiter Client, der NICHT dekodiert** — fuer Abfragen, die einen Vektor # zurueckholen (12.09.2026). `decode_responses=True` laesst redis-py die ganze # Antwort als UTF-8 lesen, und ein float32-Blob ist keins: Die Suche bricht mit # `'utf-8' codec can't decode byte 0xd0` ab, **gefangen und als Warnung # protokolliert** — also null Kandidaten, die aussehen wie ein leerer Speicher. # # Gemessen am 12.09.2026 beim Bau des Kandidatenfilters: Der Docstring der # KZG-Suche nannte den Grund ausdruecklich (*„da wir nur Text-/Numeric-Felder # zurueckliefern (kein Embedding-Blob), spielt decode_responses=True hier keine # Rolle"*), und die Aenderung hat ihn ueberfahren. Die Suite blieb gruen, die # Zeugen auch; gefunden hat es ein Lauf gegen den echten Bestand. redis_client_bytes: redis.Redis = redis.from_url(REDIS_URL, decode_responses=False) graph_run_lock: threading.Lock = threading.Lock() shutdown_event: threading.Event = threading.Event() # ───────────────────────────────────────────── # LLM-Modell-Auswahl # ───────────────────────────────────────────── # Backend-Wahl läuft pro Worker über MODEL_WORKER_BACKENDS (siehe unten), # nicht mehr global über ein Profil. LLM_PROFILE ist nur noch ein Schalter # für den ThinkingNormalizer: bei != "lokal" läuft dieser als No-Op (Anthropic # sendet keinen -Block, also kein Ollama-Split nötig). LLM_PROFILE: str = os.getenv("LLM_PROFILE", "lokal") # Connector innerhalb "lokal" — bestimmt welche Modelle geladen werden # "mistral" = Mistral Small 3.2 (bisheriger Standard) # "gemma4" = Google Gemma 4 26B-A4B (MoE, 3.8B aktiv) # "qwen36" = Qwen 3.6 35B-A3B (MoE, 3B aktiv) OLLAMA_CONNECTOR: str = os.getenv("OLLAMA_CONNECTOR", "qwen36") # ───────────────────────────────────────────── # Ollama — Verbindungen + Connector-Modelle (immer aktiv) # ───────────────────────────────────────────── # Verbindungen OLLAMA_GPU_URL: str = os.getenv("OLLAMA_GPU_URL", "http://localhost:11434") OLLAMA_CPU_URL: str = os.getenv("OLLAMA_CPU_URL", "http://localhost:11435") # **Drei Ressourcen, drei Riegel, drei Verbindungspools.** # # Jeder dieser Namen ist eine Ressource, wie ein Aufrufer sie sieht: Wer ihn # hat, spricht das LLM an und haelt dabei den Riegel, ohne davon zu wissen. # Ein Weg daran vorbei ist nicht vorgesehen und wird von # `tests/test_llm_riegel.py` ueber den ganzen Baum ausgeschlossen. # # **Warum jeder seinen EIGENEN `ollama.Client` bekommt und nicht nur sein # eigenes Sperrmittel:** Ein `ollama.Client` haelt einen `httpx.Client` und # damit einen Verbindungspool. Zwei Riegel auf **einem** Client waeren zwei # Schloesser an derselben Tuer — der geteilte Pool bliebe, und mit ihm der # Zustand, den zwei Threads sich teilen. Gemessen am 25.08.2026: Der # ChatWorker sendete auf einer Verbindung, die der EmbedWorker 77 ms zuvor # geoeffnet hatte und noch benutzte. # # **GPU-Chat und GPU-Embedding bleiben damit gleichzeitig moeglich.** Sie # teilen die Hardware, aber keinen Prozesszustand mehr; Ollama serialisiert # auf seiner Seite ueber Slots. Wer sie auch gegeneinander sperren will, gibt # ihnen dasselbe Riegel-Objekt — eine Zeile, und der Preis ist, dass jede # Einbettung hinter dem laufenden Turn wartet. _ollama_gpu_chat_roh: ollama.Client = ollama.Client(host=OLLAMA_GPU_URL) _ollama_gpu_embed_roh: ollama.Client = ollama.Client(host=OLLAMA_GPU_URL) _ollama_cpu_chat_roh: ollama.Client = ollama.Client(host=OLLAMA_CPU_URL) ollama_gpu_chat: GesperrterOllamaClient = GesperrterOllamaClient( _ollama_gpu_chat_roh, "gpu-chat", ) ollama_gpu_embed: GesperrterOllamaClient = GesperrterOllamaClient( _ollama_gpu_embed_roh, "gpu-embed", ) ollama_cpu_chat: GesperrterOllamaClient = GesperrterOllamaClient( _ollama_cpu_chat_roh, "cpu-chat", ) # Connector-Definitionen (Modelle + Context) # # ACHTUNG, die Namen folgen keiner einheitlichen Regel: "mistral" und # "gemma4" sind nach ihrem GPU-Modell benannt, "qwen36" nach seinem # CPU-Modell. "gemma4" und "qwen36" fahren BEIDE gemma4-gpu im Gespraech und # unterscheiden sich ausschliesslich im Hintergrund. Wer qwen36 setzt und # erwartet, dass Qwen antwortet, irrt sich. OLLAMA_CONNECTORS: dict = { "mistral": { "gpu_model": "mistral-small3.2-gpu", "gpu_num_ctx": 16384, "cpu_model": "mistral-small3.2-cpu", "cpu_num_ctx": 32768, "analyse_model": "qwen3-32b-cpu", "analyse_num_ctx": 32768, }, "gemma4": { "gpu_model": "gemma4-gpu", "gpu_num_ctx": 32768, "cpu_model": "gemma4-cpu", "cpu_num_ctx": 32768, "analyse_model": "qwen3-32b-cpu", "analyse_num_ctx": 32768, }, # **Feldtest ab 07.09.2026, auf zwei bis drei Tage angelegt.** Er # unterscheidet sich von `qwen36` in **einer** Zeile: dem GPU-Modell. Der # Hintergrund bleibt auf `qwen36-cpu`, damit Destillation und Analyse # vergleichbar bleiben und der Test nur eine Groesse bewegt. # # `gemma4-a4b-gpu` ist derselbe Bautyp wie `gemma4-gpu` — 26B-A4B, MoE — # und unterscheidet sich in drei Punkten: **QAT** statt nachtraeglicher # Quantisierung, drei Monate juenger, und ein **uncensored**-Finetune. # `[gemessen 07.09.2026]` ueber sechs Reize: Durchsatz 99,26 gegen 96,42 # tps, Antwortlaenge gleich — **der Durchsatz ist kein Argument**, weder # dafuer noch dagegen. Was zu pruefen ist, ist die Guete. # # **Zurueck geht es ohne Code-Aenderung:** `OLLAMA_CONNECTOR=qwen36`. "gemma4a4b": { "gpu_model": "gemma4-a4b-gpu", "gpu_num_ctx": 32768, "cpu_model": "qwen36-cpu", "cpu_num_ctx": 262144, "analyse_model": "qwen36-cpu", "analyse_num_ctx": 262144, }, "qwen36": { "gpu_model": "gemma4-gpu", "gpu_num_ctx": 32768, "cpu_model": "qwen36-cpu", "cpu_num_ctx": 262144, "analyse_model": "qwen36-cpu", "analyse_num_ctx": 262144, }, # **Qwen 3.8 im Gespraech, angelegt am 07.11.2026, um es in echten Turns # zu pruefen.** Anders als unter `qwen36` antwortet hier wirklich Qwen: Der # Name folgt dem GPU-Modell. Gegenueber `gemma4a4b` aendert sich **eine** # Zeile, das GPU-Modell; Fenster und Hintergrund bleiben, damit der # Wechsel nur eine Groesse bewegt. # # `[gemessen 07.11.2026]` gegen `gemma4-a4b-gpu`: Das Fenster 32768 steht # zu 100 % auf der GPU, auch neben dem Embedding-Runner. Gefuellt lief es # nur gestuft, am Stueck brach der Lauf zweimal ab. Freier Text laeuft mit # 35,6 gegen 100,3 tps. Turns liefen dabei keine, ueber das Verhalten im # Gespraech sagt die Messung nichts. # # **Zurueck geht es ohne Code-Aenderung:** `OLLAMA_CONNECTOR=gemma4a4b`. "qwen38": { "gpu_model": "qwen3.8:27b", "gpu_num_ctx": 32768, "cpu_model": "qwen36-cpu", "cpu_num_ctx": 262144, "analyse_model": "qwen36-cpu", "analyse_num_ctx": 262144, }, } # Aktiver Connector → bestehende Variablen auflösen _connector = OLLAMA_CONNECTORS[OLLAMA_CONNECTOR] OLLAMA_MODEL: str = _connector["gpu_model"] OLLAMA_GPU_NUM_CTX: int = _connector["gpu_num_ctx"] SHADOW_MODEL: str = _connector["cpu_model"] OLLAMA_CPU_NUM_CTX: int = _connector["cpu_num_ctx"] PIXIE_ANALYSE_MODEL: str = _connector["analyse_model"] PIXIE_ANALYSE_NUM_CTX: int = _connector["analyse_num_ctx"] # Embedding (immer Ollama, GPU-fix — backend-unabhängig). # v2-moe seit A4 (Chat 107, EMBEDDING-CASING-BLIND): v1 trug ein uncased- # Vokabular ohne do_lower_case — jedes großgeschriebene Wort fiel auf [UNK]. # OHNE Task-Präfixe ("search_document:"/"search_query:"): Das Datenblatt # empfiehlt sie, die Messung (Chat 107) zeigt konsistent Schaden bei allen # Modellen — die Messung schlägt das Datenblatt. # ⚠ Wirksam ist die Env-Variable aus ~/ki-assistent/docker-compose.yml # (außerhalb des Repos) — dieser Default greift nur ohne Env. # Provisionierung (keine Pull-Automatik): ollama pull nomic-embed-text-v2-moe EMBED_MODEL: str = os.getenv("EMBED_MODEL", "nomic-embed-text-v2-moe") # ───────────────────────────────────────────── # Per-Worker-Backend-Wahl (Microservice-Welle Block 2) # ───────────────────────────────────────────── # Jeder LLM-Worker (chat, background_analyse, background_sprache) waehlt sein # Backend per Env-Variable. Erlaubte Werte: "ollama_gpu", "ollama_cpu_analyse", # "ollama_cpu_sprache", "anthropic", "openrouter". # Hinweis: "anthropic" ist im Schema waehlbar, aber Block 2 testet das noch # nicht smoke — vor der ersten Claude-Runde muss ein eigenes Smoke laufen. # Architektur-Doku: docs/novaberg-microservice-modell-queue_k.md. MODEL_WORKER_BACKENDS: dict[str, str] = { "chat": os.getenv("WORKER_BACKEND_CHAT", "ollama_gpu"), "background_analyse": os.getenv("WORKER_BACKEND_BG_ANALYSE", "ollama_cpu_analyse"), "background_sprache": os.getenv("WORKER_BACKEND_BG_SPRACHE", "ollama_cpu_sprache"), } # Submit-Timeout fuer Background-Worker (CPU-Generation, z.B. qwen36-cpu 36B MoE). # Deutlich hoeher als der 60s-Basis-Default, weil CPU-Destillationen ~2min dauern. # Chat/Embed behalten den 60s-Basis-Default (Fruehwarn-Eigenschaft). MODEL_BACKGROUND_TIMEOUT_S: float = float(os.getenv("MODEL_BACKGROUND_TIMEOUT_S", "300")) # ───────────────────────────────────────────── # Anthropic — Backend "anthropic" (per Worker wählbar, siehe MODEL_WORKER_BACKENDS) # ───────────────────────────────────────────── ANTHROPIC_API_KEY: str = os.getenv("ANTHROPIC_API_KEY", "") ANTHROPIC_MODEL: str = os.getenv("ANTHROPIC_MODEL", "claude-sonnet-4-6") ANTHROPIC_PRICE_INPUT_PER_M: float = float(os.getenv("ANTHROPIC_PRICE_INPUT_PER_M", "3.0")) ANTHROPIC_PRICE_OUTPUT_PER_M: float = float(os.getenv("ANTHROPIC_PRICE_OUTPUT_PER_M", "15.0")) # ───────────────────────────────────────────── # OpenRouter — Backend "openrouter" (per Worker wählbar, siehe MODEL_WORKER_BACKENDS) # ───────────────────────────────────────────── # **Ein Zugang, viele Modelle.** OpenRouter spricht das OpenAI-Chat-Protokoll # und leitet an den Anbieter hinter der Modell-ID weiter. Der Wechsel des # Modells ist damit eine Zeichenkette, kein zweiter Provider. # # Die Modell-ID ist am 05.09.2026 gegen die Modellliste geprueft # (`GET https://openrouter.ai/api/v1/models`, 431 Eintraege): Sie existiert, # traegt 1.310.720 Token Kontext und kennt `response_format` sowie # `structured_outputs`. **Eine ID, die dort nicht steht, wird mit HTTP 400 # beantwortet** — deshalb steht sie hier als Vorgabewert und nicht als # Vermutung im Prompt. OPENROUTER_API_KEY: str = os.getenv("OPENROUTER_API_KEY", "") OPENROUTER_MODEL: str = os.getenv("OPENROUTER_MODEL", "deepseek/deepseek-v4-flash-0731") OPENROUTER_URL: str = os.getenv( "OPENROUTER_URL", "https://openrouter.ai/api/v1/chat/completions", ) # ── Der Anbieter hinter der Modell-ID ─────── # # **Eine Modell-ID ist bei diesem Zugang kein Modell, sondern eine # Ausschreibung.** Am 05.09.2026 gemessen: **29 Anbieter** liefern # `deepseek/deepseek-v4-flash-0731`, und sie unterscheiden sich in allem, was # eine Messung traegt: # # Eingangspreis $0,04998 bis $0,44000 je Million — Faktor 8,8 # Ausgangspreis $0,09996 bis $1,32000 je Million — Faktor 13,2 # Quantisierung 13x fp8, 5x fp4, 1x bf16, 10x unbekannt # Kontextfenster 262.144 bis 1.310.720 # `response_format` 4 der 29 fuehren es NICHT — dort faellt die Fessel aus # Verfuegbarkeit von 3,96 % (Makora) bis 100 % # # **Ohne Festlegung waehlt der Zugang selbst, und zwar pro Aufruf.** Ein # Vergleich zweier Modelle, dessen eine Seite mal in fp4 und mal in bf16 # rechnet, misst den Anbieterwechsel mit — er ist dann keine Messung mehr. # Deshalb steht hier ein Anbieter und kein Rueckfall. OPENROUTER_PROVIDER: str = os.getenv("OPENROUTER_PROVIDER", "baidu") # Die Quantisierung als zweiter Riegel. Sie ist bei einem Anbieter mit nur # einem Endpunkt redundant und bei einem mit mehreren der Unterschied # zwischen zwei Modellen. OPENROUTER_QUANTISIERUNG: str = os.getenv("OPENROUTER_QUANTISIERUNG", "fp8") # **Rueckfall aus: lieber ein Fehler als eine unbemerkt andere Rechnung.** # Ein Ausweichen auf den naechsten Anbieter ist im Betrieb bequem und in # einer Messreihe ein stiller Fehler — die Zahlen daneben gaelten dann fuer # ein anderes Modell. OPENROUTER_FALLBACKS: bool = os.getenv("OPENROUTER_FALLBACKS", "false").lower() == "true" # Was der gewaehlte Anbieter an Sampling-Schrauben fuehrt — sein # `supported_parameters` aus `GET /models/{id}/endpoints`, gemessen am # 05.09.2026 fuer `baidu/fp8`. # # **Der Kanon steht hier, damit ein Aufrufer nicht ins Leere schraubt.** Der # Responder und der Verfasser setzen `presence_penalty` (0,3) und # `repeat_penalty` (1,1) gegen Wiederholungen; **Baidu fuehrt beide nicht**. # Ohne diese Liste gingen sie mit und wuerden verworfen, ohne dass irgendwo # etwas stuende — Novas Sprache wuerde repetitiver, und niemand haette einen # Anhaltspunkt. OPENROUTER_GEFUEHRTE_PARAMETER: frozenset[str] = frozenset( os.getenv( "OPENROUTER_GEFUEHRTE_PARAMETER", "temperature,top_p,max_tokens,stop,response_format,structured_outputs," "tools,tool_choice,reasoning,include_reasoning,reasoning_effort", ).split(",") ) # **Wie das Modell dazu gebracht wird, NICHT zu denken.** # # `[gemessen]` — 05.09.2026, dieselbe Frage mit `max_tokens=64`, nur der # Schalter wechselt: # # ohne Feld finish=length out=64 content=0 reasoning=238 # reasoning.exclude=True finish=length out=64 content=0 reasoning=0 # reasoning.enabled=False finish=length out=64 content=224 reasoning=0 # reasoning_effort=none finish=stop out=22 content=69 reasoning=0 # # **Das Modell denkt von sich aus, und der Trace zaehlt gegen `max_tokens`.** # Ohne diesen Schalter kaeme bei `query_rewrite` (64 Token) eine **leere** # Antwort, bei Perzeption, Router und Tribunal (je 512) eine abgeschnittene — # ein Ausfall, der wie ein stummes Modell aussieht und keiner ist. # # `exclude` ist die schlechteste der Varianten: Es rechnet und verbirgt das # Ergebnis, das Budget ist trotzdem fort. # # Leer setzen schaltet die Zeile ab — fuer ein Modell, das `reasoning_effort` # nicht fuehrt. OPENROUTER_DENKEN_AUS: str = os.getenv("OPENROUTER_DENKEN_AUS", "none") # Das Kontextfenster des Modells **beim gewaehlten Anbieter** — Baidu fuehrt # 1.048.576, nicht die 1.310.720 der Spitze (Cloudflare). Es geht in **keine** # Nutzlast — OpenRouter kennt kein `num_ctx` —, sondern allein in die # Warnschwellen der Token-Buchfuehrung. Ohne es waere die Auslastungszeile # eine Zahl ohne Bezug. OPENROUTER_NUM_CTX: int = int(os.getenv("OPENROUTER_NUM_CTX", "1048576")) # Preise in USD je Million Token, am 05.09.2026 vom **Endpunkt** gelesen # (`GET /models/{id}/endpoints`, `baidu/fp8`), nicht aus der aggregierten # Modellliste — die nannte $0,065/$0,18 und meinte damit einen anderen # Anbieter. # # **Sie tragen einen Rabatt von 64,3 %** (`pricing.discount: 0.643`), und die # Schnittstelle nennt **keine Frist dazu**. Der Listenpreis waere $0,14 und # $0,28. Ein Werkzeug haelt die Zahlen hier gegen den Endpunkt: # `services/price_watch.py`. # # Sie sind Buchhaltung und gehen in keine Entscheidung; steht ein falscher # Wert hier, ist die Kostenzeile falsch und sonst nichts. #: Wo ein Befund des Preiswaechters 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_KEY: str = "preis_befund" #: Wie lange ein Befund liegen bleibt. Zwei Tage — laenger als der Abstand #: zweier Tagelaeufe, damit ein Befund nicht verfaellt, bevor ihn jemand #: gesehen hat, und kurz genug, dass ein behobener nicht ewig nachhallt. PREIS_BEFUND_TTL_S: int = 172_800 #: Die Preise je Million Token, gegen die `cost_usd` rechnet. #: #: **Sie sind eine Momentaufnahme, und das ist keine Nachlaessigkeit, sondern #: der Befund.** `[gemessen]` Der Preiswaechter meldete am 07.09.2026 lebend #: $0,14000 / $0,28000 — Faktor 2,80 gegen die damalige Konfiguration und #: *„der Rabatt ist fort"*. Am 08.09.2026 meldete er $0,06496 / $0,12992 bei #: 53,6 % Rabatt: **binnen eines Tages Faktor 2,15 nach unten.** #: #: **Eine Konfigurationskonstante kann diesem Preis nicht folgen.** Jede #: nachgezogene Zahl ist am naechsten Tag wieder falsch, und die Kostenspalte #: rechnet still weiter. Die strukturelle Abhilfe waere der Preis **aus der #: Antwort**; ob der Endpunkt ihn mitliefert, ist ungeprueft (der #: Anbieter-Umschlag fuehrt nur Token). Bis dahin gilt: Der Waechter meldet #: die Abweichung, ein Mensch entscheidet, und der Stand steht hier mit Datum. #: #: Stand: 08.09.2026, uebernommen nach Vorgabe des Eigentuemers. OPENROUTER_PRICE_INPUT_PER_M: float = float(os.getenv("OPENROUTER_PRICE_INPUT_PER_M", "0.06496")) OPENROUTER_PRICE_OUTPUT_PER_M: float = float(os.getenv("OPENROUTER_PRICE_OUTPUT_PER_M", "0.12992")) # Frist eines einzelnen HTTP-Aufrufs. **Sie steht ueber der Frist des # Hintergrund-Workers nicht**: Ein Aufruf, der laenger braucht als der # Aufrufer wartet, erzeugt Kosten fuer eine Antwort, die niemand mehr liest # (`F-FRIST-1`). OPENROUTER_TIMEOUT_S: float = float(os.getenv("OPENROUTER_TIMEOUT_S", "180")) # ───────────────────────────────────────────── # Pixie-Einstellungen (Background Task) # ───────────────────────────────────────────── # Pixie-Master-Switch (Chat 78, PIXIE-OFF). # False = kein Scheduler-Job, keine Queue-Pushes, kein Shadow-Delivery, # keine Dirty-Flags, kein Nova-Schreiben. Komplett still. # True = normaler Betrieb. PIXIE_AKTIV: bool = os.getenv("PIXIE_AKTIV", "false").lower() == "true" PIXIE_INTERVALL_MIN: int = int(os.getenv("PIXIE_INTERVALL_MIN", "2")) # Pixie Heartbeat (kompetitives Scheduling, Chat 33) PIXIE_INTERVALL_SEKUNDEN: int = int(os.getenv("PIXIE_INTERVALL_SEKUNDEN", "120")) # Der Takt der CPU-Spur. Sie faehrt Agenten ohne Sprachmodell — Promotion, # Synapsen-Decay, Ziel-Decay —, die in Sekunden fertig sind und die meiste # Zeit leerlaufen. Ein kurzer Takt kostet dort fast nichts und entscheidet # alles: Gemessen am 09.08.2026 kam die Promotion in einem 28-Minuten-Bogen # EINMAL dran und brachte 1 von 72 Auftraegen durch, weil sie in derselben # Schlange stand wie eine Recherche. # # GESETZT, nicht hergeleitet — entschieden am 09.08.2026. 30 s heisst: Der # Rueckstand eines Bogens laeuft waehrend des Bogens ab, nicht nach ihm. PIXIE_CPU_INTERVALL_SEKUNDEN: int = int(os.getenv("PIXIE_CPU_INTERVALL_SEKUNDEN", "30")) PIXIE_LOCK_TTL_SEKUNDEN: int = int(os.getenv("PIXIE_LOCK_TTL_SEKUNDEN", "600")) # **Der Pausenschalter, an einer Stelle benannt.** Er liegt in Redis und nicht # in der Umgebung, weil eine Umgebungsvariable beim Import gelesen wird und nur # mit einem Neustart zu aendern waere — waehrend einer Messreihe ist genau das # der Fall, den der Schalter vermeiden soll (`F-MESS-2`). # # **Drei Leser, ein Schluessel:** die Admin-Endpunkte, der Pixie-Scheduler und # die Zustellschleife. Bis zum 11.09.2026 stand er viermal als Zeichenkette im # Code, und die Zustellschleife las ihn gar nicht — der Schalter hielt den # Scheduler an und liess fertige Impulse weiter zustellen. PIXIE_PAUSE_SCHLUESSEL: str = "pixie:paused" # **Zwei Haelften, die sich getrennt anhalten lassen** (11.09.2026, Vorgabe des # Eigentuemers). `pixie:paused` oben haelt beide an — das ist der Schalter fuer # eine Messreihe, die ueberhaupt keinen Hintergrund sehen soll. # # **Fuer einen Test reicht das nicht.** Wer pruefen will, ob ein zugestellter # Impuls an das Gespraech anschliesst, braucht die Zustellung **an** und den # Rechercheagenten **aus**: Sonst schiebt sich waehrend der Reihe ein frischer # Fund dazwischen, und die geprueften Impulse sind andere als die # vorbereiteten. Umgekehrt will eine Messung am Hintergrund die Agenten laufen # lassen, ohne dass ihre Ergebnisse mitten in die Reihe zugestellt werden. # # **Ein gesetzter Teilschalter wirkt wie der Vollschalter fuer seine Haelfte.** # Beide Leser pruefen `pixie:paused ODER ihren eigenen` — damit bleibt der # Vollschalter, was er war, und niemand muss zwei Schluessel loeschen, um ihn # zurueckzunehmen. # **Ab welcher thematischen Naehe ein zugestellter Fund ohne Bruecke # auskommt.** Darunter setzt der Verfasser einen Uebergangsblock. # # **0,55 liegt zwischen der Zustellschwelle und dem Anschluss.** Die Auswahl # der Zustellung laesst ab 0,30 durch — bewusst, damit ein Fund aus einem # frueheren Auftrag nicht fuer immer liegen bleibt. `[Betriebsbeleg # 11.09.2026]` Ein Eintrag mit **0,37** wurde zugestellt, waehrend das # Gespraech bei einem anderen Gegenstand stand, und sprang ohne ein Wort des # Uebergangs. ~~Der Wert ist gesetzt und nicht abgeleitet: Eine Spanne # gemessener Naehen mit dem Urteil *„brauchte eine Bruecke"* daneben gibt es # noch nicht.~~ # # **Die Spanne liegt seit dem 11.09.2026 vor, und sie stellt den Wert in # Frage.** `[gerechnet ueber 706 Stapel-Eintraege x 5 Gespraechskontexte]` # **3 von 3530 Paaren (0,08 %)** erreichen 0,55; das p99 der Naehe liegt je # Kontext zwischen **0,37 und 0,47**. Selbst Reize, die aus dem Thema eines # Stapel-Eintrags formuliert sind, kommen nur auf 0,5468 bis 0,6582. # # **Die Paar-Zahl ist nicht die Entscheidungszahl.** Zugestellt wird das # **Maximum** ueber den Stapel, nicht ein zufaelliges Paar — und das liegt in # 2 von 5 gerechneten Kontexten ueber der Schwelle (0,5683 und 0,6773). Im # Bestand setzten 2 von 3 Zustellungen nach dem Bau den Block. Die Schwelle # liegt also **am oberen Rand** der erreichbaren Spanne, nicht jenseits davon: # Der Block ist der haeufigere Fall, nicht der ausnahmslose. # # Was daraus folgt, ist eine Absicht und keine Ableitung: Der Wert bleibt, bis # jemand entscheidet, ob der Block der Normalfall sein soll # (`novaberg-node-verfasser_k.md` §2.2a-3). VERFASSER_IMPULS_NAHE: float = 0.55 PIXIE_AGENTEN_PAUSE_SCHLUESSEL: str = "pixie:agenten_paused" PIXIE_DELIVERY_PAUSE_SCHLUESSEL: str = "pixie:delivery_paused" # **Wie oft ein stehender Pausenschalter im Log auf `warning` erscheint:** je # Schalter einmal je Intervall, nicht je Zyklus. Der Heartbeat laeuft alle 120 s # und die Zustellschleife noch dichter; je Zyklus waeren das Hunderte Zeilen am # Tag, die niemand liest. 900 s (15 Minuten) sind dicht genug, dass ein # vergessener Schalter in einer Sitzung auffaellt, und selten genug, dass die # Zeile nicht zum Rauschen wird (der Schalter fuer die Regie meldet je Turn — # dort ist ein Turn der Takt). Per Umgebung aenderbar wie die Nachbarwerte. PIXIE_PAUSE_WARNUNG_SEKUNDEN: int = int(os.getenv("PIXIE_PAUSE_WARNUNG_SEKUNDEN", "900")) # --- Pixie Aging (Verhungerungsschutz, Chat 113) --- # Eine faellige periodische Aufgabe steigt in der Prioritaet, solange sie nicht # laeuft. Ohne das gewinnt eine dauerhaft gefuellte Shadow-Queue (Eintraege bis # Prioritaet 1.0) jeden Heartbeat gegen Wartungslaeufe (Prio 0.2) — gemessen am # 28.07.2026: synapsen_decay 11,9 Stunden faellig, null Heartbeat-Gewinne. # # Gilt NUR fuer periodische Aufgaben. Queue-Eintraege werden ausdruecklich NICHT # gealtert: Dort liegen Auftraege fuer Agenten, die es nicht gibt (`vertiefen`, # `nachfragen`), heute von ihrer Prioritaet 0.0 ruhig gehalten. Aging auf der # Queue wuerde genau sie nach oben holen. # # Der Zuschlag waechst mit der ABSOLUTEN Wartezeit, nicht mit der Zahl # verpasster Intervalle. Gemessen am 28.07.2026 an der ersten, relativen # Fassung: synapsen_promotion (Takt 300s) war 4916s faellig — 16 Intervalle, # damit sofort am Deckel und mit Prioritaet 2.90 unschlagbar; synapsen_decay # (Takt 86400s) kam bei derselben Wartezeitklasse auf 1.22. Ein kurzer Takt # alterte schneller als ein langer, und die Aufgabe, die der Zuschlag retten # sollte, verlor weiter. Verhungern ist ein absolutes Zeitphaenomen: Wer zwoelf # Stunden wartet, wartet zu lange — unabhaengig davon, ob sein Takt fuenf # Minuten oder ein Tag ist. # # Rate 0.5/h = nach zwei Stunden Wartezeit ueberholt eine Aufgabe mit # Basis-Prioritaet 0.2 den hoechstmoeglichen Queue-Wert 1.0 (0.2 + 0.5 x 2 = 1.2). PIXIE_AGING_PRO_STUNDE: float = float(os.getenv("PIXIE_AGING_PRO_STUNDE", "0.5")) # Deckel des Zuschlags, erreicht nach vier Stunden Wartezeit. Ab dort entscheidet # wieder die Basis-Prioritaet: Zwei gleich lang wartende Aufgaben sollen in ihrer # gewollten Rangfolge laufen, nicht in der ihrer Wartezeit-Nachkommastellen. PIXIE_AGING_MAX_ZUSCHLAG: float = float(os.getenv("PIXIE_AGING_MAX_ZUSCHLAG", "2.0")) # --- Pixie Agent: Promotion --- PIXIE_PROMOTION_PRIORITAET: float = float(os.getenv("PIXIE_PROMOTION_PRIORITAET", "0.9")) PIXIE_PROMOTION_INTERVALL_SEKUNDEN: int = int( os.getenv("PIXIE_PROMOTION_INTERVALL_SEKUNDEN", "300") ) # 5 Minuten # Wie oft ein gescheiterter Promotionsauftrag aus der Arbeitsliste in die # Warteschlange zurueckgelegt wird, bevor er auf den Fehlerstapel wandert. # # GESETZT, nicht hergeleitet — entschieden am 09.08.2026. Die Zahl beantwortet # eine Abwaegung, fuer die es keine Messung gibt: Ein voruebergehender Fehler # (Modell kurz weg, Datenbank kurz zu) soll den Gedaechtniskandidaten nicht # kosten; ein dauerhaft unverarbeitbarer Eintrag darf nicht ewig kreisen und # dabei alle 300 s den einzigen Pixie-Platz verbrennen, um den ohnehin 92 % # der Takte scheitern (PIXIE-EIN-SLOT-BLOCKIERT-ALLES). # # Zwei Rueckstellungen heisst: drei Verarbeitungsversuche insgesamt. Beim # dritten Fehlschlag geht der Auftrag nach `queue:{paar}:gescheitert` — er # wird dort NICHT automatisch wiederholt, aber er ist gezaehlt und lesbar. # Das ist der Unterschied zum Zustand bis zum 09.08.2026, in dem ein einziger # Fehler den Auftrag spurlos verschwinden liess. MAX_PROMOTION_RUECKSTELLUNGEN: int = int(os.getenv("MAX_PROMOTION_RUECKSTELLUNGEN", "2")) # --- Pixie Agent: Decay --- PIXIE_DECAY_PRIORITAET: float = float(os.getenv("PIXIE_DECAY_PRIORITAET", "0.2")) PIXIE_DECAY_INTERVALL_SEKUNDEN: int = int( os.getenv("PIXIE_DECAY_INTERVALL_SEKUNDEN", "86400") ) # 24 Stunden # --- Pixie Agent: Ziel-Decay --- # Wieder aktiv seit Chat 113, nachdem die Formel eine reine Funktion aus Anker # und Zeit geworden ist (ziele.motivation_berechnen / ziel_decay_lauf). # # Stillgelegt war er, weil er die bereits verfallene Motivation erneut mit einem # Faktor aus dem GESAMTALTER des Ziels multiplizierte und zurueckschrieb — der # Verfall wuchs quadratisch mit der Zahl der Laeufe statt linear mit der Zeit. # Belegt am Lauf vom 27.07.2026, 18:39:58 UTC: Ziel 3 von 0.65 auf 0.640, # Ziel 4 von 0.70 auf 0.690. Gemessen an der Gegenprobe zur neuen Fassung: zehn # Laeufe hintereinander ergaben 0.0999 statt 0.4. # # Der Schalter bleibt stehen. Bis Chat 112 war der fehlende Router-Eintrag die # faktische Sicherung; sie entfiel, seit der Router unbekannte Namen ueber # Namensgleichheit aufloest. Eine Sicherung, die aus einem fehlenden Eintrag # besteht, ist keine — wer diesen Agenten anhalten muss, braucht ein Gate. ZIEL_DECAY_AKTIV: bool = os.getenv("ZIEL_DECAY_AKTIV", "true").lower() == "true" # --- Pixie Agent: Charakter --- PIXIE_CHARAKTER_PRIORITAET: float = float(os.getenv("PIXIE_CHARAKTER_PRIORITAET", "0.3")) PIXIE_CHARAKTER_INTERVALL_SEKUNDEN: int = int( os.getenv("PIXIE_CHARAKTER_INTERVALL_SEKUNDEN", "600") ) # 10 Minuten PIXIE_CHARAKTER_LZG_LIMIT: int = int(os.getenv("PIXIE_CHARAKTER_LZG_LIMIT", "50")) PIXIE_CHARAKTER_KZG_LIMIT: int = int(os.getenv("PIXIE_CHARAKTER_KZG_LIMIT", "20")) # Halbwertszeit des Zeitgewichts, mit dem die KZG-Eintraege des Adaptiv-Hash # ausgewaehlt werden. Sie ist kein Geschmackswert: In einer Auswahl wirkt ein # Zeitfaktor nur ueber die Ordnung, und die Halbwertszeit bestimmt genau eine # Groesse — wieviel Salienz-Vorsprung ein aelterer Eintrag braucht, um einen # juengeren zu ueberholen (Faktor 2^(dt/T)). Mit der am 16.08.2026 gemessenen # Salienzspanne von 1.49 (0.67 bis 1.00) ergibt T=1.7 ein Wirkfenster von 0.98 # Tagen: Innerhalb einer Gespraechssitzung ordnet die Salienz, zwischen # Sitzungen die Zeit. # # Der Wert ist die am 16.08.2026 gemessene mittlere Gespraechsluecke des # produktiven Paares (12 belegte Tage, Luecken 1 bis 7, Mittel 1.7). # Nachmessvorschrift: mittlerer Abstand zwischen belegten Kalendertagen der # KZG-Eintraege mit beobachter='user'. PIXIE_CHARAKTER_ADAPTIV_HALBWERTSZEIT_TAGE: float = float( os.getenv("PIXIE_CHARAKTER_ADAPTIV_HALBWERTSZEIT_TAGE", "1.7") ) # Aelter als das wird gar nicht erst geladen. Bis zum 16.08.2026 war dieselbe # Zahl eine reine Gewichtskante in der Destillation — sie warf Eintraege still # weg, nachdem sie geladen waren. Als Ladegrenze begrenzt sie stattdessen den # Aufwand der Auswahl, und der Verwurf wird gezaehlt. PIXIE_CHARAKTER_KZG_LADEGRENZE_TAGE: int = int( os.getenv("PIXIE_CHARAKTER_KZG_LADEGRENZE_TAGE", "30") ) # Zeichenbudget des Kern-Hash-Materials. Es loest die Grenze "die 40 neuesten # Begegnungen" ab, und der Grund ist gemessen: Der Kern-Hash beschrieb keine # wiedererkennbare Person, sondern das Themenband des gleitenden Fensters # (KERNHASH-TRAEGT-KEINE-PERSON, 25.08.2026). Die Auswahl zieht seither # zeitlich gleichmaessig ueber die ganze Historie, und dieses Budget sagt, # wieviel Wortlaut sie dabei mitnimmt. # # Der Wert ist das Vierfache des bisherigen. Die 40 neuesten Begegnungen des # produktiven Paares trugen am 26.08.2026 zusammen 19 400 Zeichen; 80 000 ist # das Vierfache, aufgerundet. Warum nicht die ganze Historie: Sie trug am # selben Tag 184 159 Zeichen und waechst weiter — was heute ginge, ginge in # einem Jahr nicht mehr. # # Warum vier und nicht fuenf: Ein Kern-Lauf dauerte gemessen 43.2 s bei 4390 # und 71.7 s bei 15 585 Zeichen, eine volle Destillation aus zehn Calls 261 s. # Hochgerechnet mit 2.55 s je 1000 zusaetzlicher Zeichen liegt Faktor 4 bei # rund 410 s gegen einen Takt von 600 s, Faktor 5 bei rund 465 s. Die # Hochrechnung stammt aus zwei Eingabegroessen und ist keine Messung beim # Zielumfang — ob 5 auch traegt, entscheidet ein Lauf dort. # # Das Budget ist fest, nicht wachsend. Was waechst, ist die Grundmenge, aus # der geschichtet gezogen wird: Ein Turn von vor einem Jahr hat dieselbe # Chance wie einer von gestern. # Wie oft der Kern-Hash je Destillation erhoben wird. Gespeichert wird der # **Medoid** — der Lauf mit der groessten Naehe zu den anderen —, nicht ein # gemittelter Text: Einen Mittelwert von Texten gibt es nicht, und das Modell # aus drei Fassungen eine vierte bilden zu lassen waere das Umschreiben der # eigenen Ausgabe, das §3.1a des Konzepts ausschliesst. # # Der Grund ist gemessen: Zwei Destillationen aus **identischem** Material # teilen nur 27–32 % ihres Inhaltswortschatzes, und diese Ziehung geht # ungefiltert in beide Charakter-Raeder. Bei festgehaltenem Material bewegt # allein die Neuziehung des Kerns den Zuwendungsfaktor um 0.2908 gegen 0.0550 # innerhalb eines Kerns — das 5,3-fache und 29 % der Skala 0.5 bis 1.5 # (RAD-MEDIAN-SCHUETZT-FALSCHE-QUELLE, 26.08.2026). # # **Der Vorgabewert ist 1, und das ist eine Messung und keine Sparsamkeit.** # Der Medoid aus drei Laeufen wurde am 26.08.2026 gegen dieselbe Anordnung # gehalten, die den Befund erzeugt hat: # # Einzel-Kerne Spanne des Faktors 0.2908, je Kern rund 110 s # Medoid aus 3 Spanne des Faktors 0.2615, je Kern rund 230 s # # **10 % weniger Streuung fuer das Dreifache an Rechenzeit** — und bei vier # Punkten je Reihe ist dieser Unterschied nicht von Rauschen zu unterscheiden. # Der Grund ist einsichtig: Der Medoid waehlt den zentralsten aus **drei** # Ziehungen einer sehr breiten Verteilung; bei dieser Breite ist der # zentralste von dreien immer noch fast eine Ziehung. # # Live gerechnet waeren es rund 560 s allein fuer den Kern der Figur und ein # Zyklus von etwa 900 s gegen einen Takt von 600 s. # # **Der Mechanismus bleibt trotzdem gebaut**, und mit ihm die Senke: Jeder # Lauf steht als `kern_erhebung` im `pipeline_log`. Wer die Streuung ernsthaft # verschmaelern will, braucht viele Laeufe — dann steht hier eine groessere # Zahl statt eines neuen Bauauftrags. PIXIE_CHARAKTER_KERN_LAEUFE: int = int( os.getenv("PIXIE_CHARAKTER_KERN_LAEUFE", "1") ) PIXIE_CHARAKTER_KERN_BUDGET_ZEICHEN: int = int( os.getenv("PIXIE_CHARAKTER_KERN_BUDGET_ZEICHEN", "80000") ) # --- Pixie Agent: Wiedervorlage --- PIXIE_WIEDERVORLAGE_PRIORITAET: float = float(os.getenv("PIXIE_WIEDERVORLAGE_PRIORITAET", "0.5")) PIXIE_WIEDERVORLAGE_INTERVALL_SEKUNDEN: int = int( os.getenv("PIXIE_WIEDERVORLAGE_INTERVALL_SEKUNDEN", "43200") ) # 12 Stunden PIXIE_WIEDERVORLAGE_SNOOZE_TAGE: int = int(os.getenv("PIXIE_WIEDERVORLAGE_SNOOZE_TAGE", "7")) # --- Pixie Agent: Recherche --- PIXIE_RECHERCHE_SESSION_TURNS: int = int(os.getenv("PIXIE_RECHERCHE_SESSION_TURNS", "10")) PIXIE_RECHERCHE_MAX_ITERATIONEN: int = int(os.getenv("PIXIE_RECHERCHE_MAX_ITERATIONEN", "3")) PIXIE_RECHERCHE_MAX_QUERIES: int = int(os.getenv("PIXIE_RECHERCHE_MAX_QUERIES", "4")) PIXIE_RECHERCHE_MAX_SEITEN_PRO_RUNDE: int = int( os.getenv("PIXIE_RECHERCHE_MAX_SEITEN_PRO_RUNDE", "3") ) # --- Wissensspeicher (Bibliothek) --- # Spezifikation: docs/novaberg-autonomous-wissen_k.md §11.1. # # Die Wurzel liegt AUSSERHALB des Arbeitsbaums, im Behaelter unter /knowledge # eingehaengt. Das ist keine Ablage-Vorliebe: Die Dateien tragen aus # Gespraechen abgeleitete Recherchen, und unterhalb des Repositoriums # veroeffentlichte jeder Push sie. Ausserhalb kann `git add` sie nicht # erfassen — die Grenze ist dann eine Eigenschaft des Dateisystems. WISSENSSPEICHER_WURZEL: str = os.getenv("WISSENSSPEICHER_WURZEL", "/knowledge") # Modusbits fuer alles, was der Wissenspfad anlegt. GEMESSENE Werte, keine # gewaehlten: Der Behaelter laeuft als root, auf dem Wirt erscheinen die # Dateien unter fremder Kennung. Am 04.08.2026 gegengeprobt — ohne diese Bits # scheitern Anhaengen und Neuanlage mit `Keine Berechtigung`, mit ihnen # funktioniert beides, obwohl die Eigentuemerkennung fremd bleibt. # # Der Weg ueber den Benutzernamensraum (Behaelter unter der Wirtskennung) # wurde verworfen: Er griffe in den laufenden Dienst ein und waere an die # Eigenheiten der Behaelterlaufzeit gebunden. Die Modusbits sind es nicht. # # Basis 8, weil eine Modusangabe zur Basis 10 bei jedem Lesen falsch # verstanden wird. # Abruf der Bibliothek im Enricher (die sechste Kontextquelle). # # **Gemessen am 19.08.2026, in der Richtung, in der die Schwelle benutzt wird: # Frage gegen Bestand.** 40 Fragen nach EINEM Thema gegen 249 Ausarbeitungen, # die richtige Antwort je Frage bekannt, Ziel sind die Themenvektoren: # # Schwelle 0.40 : richtige verworfen 0/40 Fehltreffer durchgelassen 36/40 # Schwelle 0.50 : richtige verworfen 0/40 Fehltreffer durchgelassen 27/40 # Schwelle 0.55 : richtige verworfen 1/40 Fehltreffer durchgelassen 20/40 # Schwelle 0.60 : richtige verworfen 2/40 Fehltreffer durchgelassen 13/40 # # 0.50 ist der hoechste Wert, bei dem noch KEINE richtige Antwort verlorengeht, # und er nimmt gegenueber 0.40 ein Viertel der Fehltreffer heraus. Ab 0.55 # kostet jede weitere Verschaerfung echte Treffer, und die wiegen schwerer: # Ein Fehltreffer wird von der Rangfolge nach unten sortiert und von TOP_K # abgeschnitten, eine verworfene richtige Antwort ist weg. # # **Der alte Wert 0.40 war von `anker_retrieval` uebernommen und galt fuer # einen anderen Vergleich** — dort gegen Knotenvektoren, hier gegen einen # gemittelten Vektor ueber das Destillat. Gegen diesen alten Raum lag der # Median der RICHTIGEN Antwort bei 0.2821, also unterhalb der Schwelle: Die # richtige Antwort wurde im Regelfall verworfen. Die Zahl gilt deshalb nur # zusammen mit dem Ziel, gegen das sie misst (Konvention 4). WISSEN_RETRIEVAL_SCHWELLE: float = float(os.getenv("WISSEN_RETRIEVAL_SCHWELLE", "0.50")) # Wie viele Dateien der Enricher hoechstens heranzieht. Drei, weil ein # Bibliothekstreffer eine Zusammenfassung von bis zu 500 Zeichen mitbringt # und der Prompt des Gespraechspfads bei 32768 Token liegt — nicht bei den # 262144 des Hintergrunds. WISSEN_RETRIEVAL_TOP_K: int = int(os.getenv("WISSEN_RETRIEVAL_TOP_K", "3")) # Wie tief der BESTELLTE Weg liest (novaberg-convention-nmcp.md §6a.1). # # **Eine Tiefe, keine Schwelle — und der Unterschied ist der Grund, warum # hier eine neue Zahl stehen darf und dort nicht.** Die Schwelle entscheidet, # WAS als Treffer gilt; sie bleibt fuer beide Eingaenge dieselbe, sonst # haette derselbe Bestand zwei Rangfolgen. Die Tiefe entscheidet nur, wie # weit unten gelesen wird — sie aendert die Ordnung nicht. # # Fuenf statt drei, weil die Lage eine andere ist: Der Enricher-Weg drueckt # seine Treffer ungefragt in JEDEN Turn und muss deshalb sparsam sein; hier # hat der Mensch ausdruecklich gefragt und wartet auf die Antwort. WISSEN_AUFTRAG_TOP_K: int = int(os.getenv("WISSEN_AUFTRAG_TOP_K", "5")) # ── Der Enricher-Weg des Dateien-Index (novaberg-agent-dateien_k.md §3.0) ── # # **Die Kappung ist die Zusicherung, die Schwelle ist die Feinjustage** (§3.0a). # Wer es umgekehrt liest, baut denselben Boden zweimal — eine Zahl, die etwas # verspricht, was eine andere Stelle einhaelt. Genau daran ist die Bibliothek # vorbeigelaufen: In 40 von 42 Aufrufen hat dort nicht die Schwelle # ausgewaehlt, sondern die Kappung. # # Drei, weil der Block je Eintrag zusaetzlich die Fundstelle traegt — sie ist # das, was "ich habe hier Aufzeichnungen" ueberpruefbar macht (§1a.2) und # nicht kuerzbar. Dieselbe Zahl wie bei der Bibliothek, und ueber `K/N` ist # sie zugleich die Antwort auf die Frage, wie streng die spaetere # Quantilschwelle steht. AUFZEICHNUNGEN_KAPPUNG: int = int(os.getenv("AUFZEICHNUNGEN_KAPPUNG", "3")) # Wie viele Kandidaten der lesende Dienst je Kanal heranzieht (§6.3). Groesser # als die Kappung des Enricher-Wegs, weil hier gefragt wurde: Ein Mensch, der # ausdruecklich in den Unterlagen suchen laesst, vertraegt eine Auswahl — der # ungefragte Block im Prompt nicht. DATEIEN_SUCHE_KAPPUNG: int = int(os.getenv("DATEIEN_SUCHE_KAPPUNG", "8")) # Der absolute Boden — das Amt "ob ueberhaupt" (§3.0a-bis). # # **Er ist gemessen, und die Messbedingung gehoert an die Zahl.** Am # 18.08.2026 acht Sondentexte gegen die drei Indexzeilen, beide Seiten: # # einschlaegig bester Treffer je Sonde 0.3800 · 0.4610 · 0.4961 # fremd bester Treffer je Sonde 0.2014 · 0.2010 · 0.1896 # 0.1861 · 0.0729 # # 0.30 liegt fast mittig in der Luecke — 0.10 ueber der hoechsten fremden, # 0.08 unter der niedrigsten einschlaegigen Sonde. Keine Seite ist knapp # geschnitten. # # **Was die Messung NICHT deckt, und es reist mit der Zahl:** Sie lief mit dem # rohen Anfrage-Embedding, der Betrieb sucht mit dem verschobenen # `such_vektor` (Wahrnehmungs-Gravitation). Und der Bestand war **drei # Zeilen** aus einem Register. Ein Startwert mit benannter Bedingung, kein # Verteilungs-Ergebnis — nachzuziehen, sobald die mitlaufende Verteilung # traegt (§3.0a-bis). # # Ein Turn ohne diesen Block ist der Normalfall und kein Ausfall. AUFZEICHNUNGEN_BODEN: float = float(os.getenv("AUFZEICHNUNGEN_BODEN", "0.30")) # Wie viel Zusammenfassung ein Eintrag mitbringt. Der Block soll die Datei # auffindbar machen, nicht ihren Inhalt ersetzen — der bleibt in der Datei # und wird ueber den Auftrags-Weg gelesen (§3.0). AUFZEICHNUNGEN_AUSZUG_ZEICHEN: int = int(os.getenv("AUFZEICHNUNGEN_AUSZUG_ZEICHEN", "300")) WISSENSSPEICHER_DATEI_MODUS: int = int(os.getenv("WISSENSSPEICHER_DATEI_MODUS", "666"), 8) WISSENSSPEICHER_VERZEICHNIS_MODUS: int = int( os.getenv("WISSENSSPEICHER_VERZEICHNIS_MODUS", "777"), 8, ) # --- Dateien-Verbund: der Aussenrand der Freigaben --- # Spezifikation: docs/novaberg-agent-dateien_k.md §7 Regel 3a. # # Eine Verzeichnis-Freigabe entsteht aus einer Aeusserung im Gespraech — # damit bestimmt ein gesprochener Satz einen Pfad im Dateisystem. Das ist die # Stelle, an der ein Dienst mit Dateizugriff gefaehrlich wird, und ein Tor # allein genuegt dort nicht: Ohne konfigurierten Rand koennte ein Gespraech # das Wurzelverzeichnis freigeben. # # INNERHALB dieser Elternverzeichnisse darf der Mensch freigeben, AUSSERHALB # nicht — auch nicht mit Bestaetigung. Der Rand ist damit die einzige Groesse # des Verbunds, die kein Gespraech verschieben kann. # # Leer heisst: nichts ist freigebbar. Fail closed, und laut — die Alternative # waere ein leerer Rand, der wie "alles erlaubt" wirkt. DATEIEN_AUSSENRAND: list[str] = [ teil.strip() for teil in os.getenv("DATEIEN_AUSSENRAND", "/files").split(",") if teil.strip() ] # Obergrenze der Dateizaehlung am Tor. Das Tor zeigt die Zahl, damit ein # Mensch einen falsch genannten Pfad an ihr erkennt (§2a.2) — eine Zaehlung # ohne Grenze macht aus einem Vertipper einen Vollscan. Wird die Grenze # erreicht, meldet das Tor "mindestens N" statt einer erfundenen Endzahl. DATEIEN_WURZEL_ZAEHLGRENZE: int = int(os.getenv("DATEIEN_WURZEL_ZAEHLGRENZE", "20000")) # --- Der Waechter des Dateien-Index --- # Spezifikation: docs/novaberg-agent-dateien_k.md §5. # # Welche Dateien der Waechter ueberhaupt liest. Der Entwurf behandelt Text; # alles andere wird erkannt und MIT GRUND uebergangen, nicht stillschweigend # (§9 Punkt 4). Die Liste ist bewusst eng: Eine Endung aufzunehmen, deren # Inhalt das Modell nicht sinnvoll zusammenfassen kann, kostet je Datei einen # Modellaufruf fuer ein Ergebnis, das niemand brauchen kann. DATEIEN_INDEX_ENDUNGEN: frozenset[str] = frozenset( teil.strip().lower() for teil in os.getenv( "DATEIEN_INDEX_ENDUNGEN", ".md,.txt,.rst,.org,.adoc", ).split(",") if teil.strip() ) # Obergrenze je Datei. Darueber wird uebergangen und der Grund genannt: Eine # Datei, die den Prompt sprengt, erzeugt sonst eine Zusammenfassung ueber ihr # erstes Viertel — und die sieht aus wie eine Zusammenfassung. DATEIEN_INDEX_MAX_BYTES: int = int(os.getenv("DATEIEN_INDEX_MAX_BYTES", "1000000")) # Wie viele Dateien ein Lauf hoechstens INDIZIERT (nicht: ansieht). Der # Vorfilter ist billig, der Modellaufruf ist es nicht: Ein Erstlauf ueber # einen grossen Bestand haelt die LLM-Spur sonst stundenlang. Was die Grenze # stehenlaesst, wird im Lauf GEMELDET — eine stille Kappung saehe aus wie ein # vollstaendiger Lauf. DATEIEN_INDEX_MAX_PRO_LAUF: int = int(os.getenv("DATEIEN_INDEX_MAX_PRO_LAUF", "50")) # ─── KZG (Kurzzeitgedaechtnis) ───────────────── # Die drei Tore stehen seit Chat 113 auf der GEKRUEMMTEN Skala — sie sind die # Bilder der alten Rohwerte unter der Salienzkurve. Fachlich hat sich nichts # geaendert: Wer frueher 0.3 sagte, sagt weiterhin 0.3; nur die Zahl in der # Konfiguration heisst anders. Ohne das Roh-Aequivalent im Kommentar ist jede # dieser Konstanten beim naechsten Lesen eine offene Frage # (novaberg-convention-abgeleitete-werte.md, Regel 7). # # Fuenf Nachkommastellen, ABGERUNDET. Das ist kein Schoenheitsfehler: Ein Tor # wird mit `>=` geprueft, und der exakte Kurvenwert von 0.3 ist 0.6737882. Auf # 0.6738 aufgerundet liegt die Konstante ueber ihrem eigenen Rohwert — gemessen # am Live-Turn vom 28.07.2026, 09:27 UTC: „Salienz 0.6738 (Eingang 0.30) # < 0.6738 — abgelehnt". Wer genau die Bewertung trifft, die das Tor meint, # muss hindurchgehen. # Die drei Schwellen sind **Bilder von roh 0.3 / 0.5 / 0.7** durch dieselbe # Kurve, die auch die Salienz speichert — keine freien Konstanten. Wandert der # Exponent, wandern sie mit, sonst bedeuten sie etwas anderes als am Tag ihrer # Setzung. # # **Am 24.08.2026 auf Exponent 1.1 nachgezogen** (vorher 0.67378 / 0.84089 / # 0.94393 bei Exponent 0.5). Die Rohwerte 0.3 / 0.5 / 0.7 sind unveraendert — # die Bedeutung bleibt, die Zahl aendert sich. # # **Sie sind ABGERUNDET, und das ist keine Schoenheitsfrage.** Die Tore pruefen # mit `>=`; eine aufgerundete Konstante liegt ueber ihrem eigenen Rohwert und # weist genau den Fall ab, den sie einlassen soll. Gemessen am Live-Turn vom # 28.07.2026, 09:27 UTC: Eine Bewertung von exakt 0.30 wurde abgelehnt, weil # 0.6738 ueber 0.6737882 lag. Beim Nachziehen am 24.08.2026 ist derselbe # Fehler noch einmal entstanden (0.41952 statt 0.41951) und vom Zeugen # `test_wer_das_tor_genau_trifft_geht_hindurch` gefangen worden. # # **Der Exponent aendert an diesen Toren nichts.** Die Kurve ist monoton, und # Schwelle wie Wert laufen durch dieselbe: `f(e) >= f(0.7)` gilt genau dann, # wenn `e >= 0.7`. Gemessen ueber 2506 Turns sind die Verteilungen bei 0.5 und # 1.1 zeichengleich. Was der Exponent aendert, ist die **Ablesbarkeit** der # gespeicherten Zahl. KZG_SALIENZ_MINIMUM: float = float(os.getenv("KZG_SALIENZ_MINIMUM", "0.41951")) # roh 0.3 KZG_SALIENZ_MID: float = float(os.getenv("KZG_SALIENZ_MID", "0.68302")) # roh 0.5 KZG_SALIENZ_HIGH: float = float(os.getenv("KZG_SALIENZ_HIGH", "0.88078")) # roh 0.7 KZG_TTL_LOW_SEKUNDEN: int = int( os.getenv("KZG_TTL_LOW_SEKUNDEN", "604800") ) # 7 Tage — Salienz 0.3–0.5 KZG_TTL_MID_SEKUNDEN: int = int( os.getenv("KZG_TTL_MID_SEKUNDEN", "1209600") ) # 14 Tage — Salienz 0.5–0.7 KZG_TTL_HIGH_SEKUNDEN: int = int( os.getenv("KZG_TTL_HIGH_SEKUNDEN", "2592000") ) # 30 Tage — Salienz >= 0.7 KZG_VERTIEFUNG_HAEUFIGKEIT: int = int(os.getenv("KZG_VERTIEFUNG_HAEUFIGKEIT", "3")) # Die Abrufschwelle des Kurzzeitgedaechtnisses (`kzg_entries_retrieve`). # # **Gemessen am 21.08.2026 in der Richtung, in der die Schwelle benutzt wird: # Frage gegen Bestand.** 40 Eintraege ueber die 2665 des Paares verteilt, je # eine Frage aus der *Aussage* des Eintrags gebaut (nicht aus dem Themenfeld — # das steht im Einbettungstext), dazu zehn Fragen zu nie besprochenen # Gegenstaenden und zwei anaphorische Rueckfragen ohne aufgeloesten Gegenstand. # Gemessen gegen die gespeicherten Vektoren, ueber dieselbe KNN-Abfrage wie der # Lesepfad, ohne dessen Kappung. # # Schwelle | richtige verworfen | davon LIEFERBAR | fremde Fragen | Leerfragen # 0.40 | 0/40 | 0/29 | 10/10 | 2/2 # 0.70 | 3/40 | 1/29 | 0/10 | 1/2 # 0.72 | 6/40 | 1/29 | 0/10 | 0/2 # 0.74 | 8/40 | 2/29 | 0/10 | 0/2 # 0.80 | 20/40 | 10/29 | 0/10 | 0/2 # # **Die dritte Spalte ist die Kostenspalte.** 11 der 40 richtigen Antworten # stehen ohnehin nicht in den `top_k` — die Kappung schneidet sie ab, gleich # wie die Schwelle steht. Ein Verlust ist nur, was lieferbar gewesen waere. # 0.72 ist der guenstigste Wert, bei dem weder eine unbezogene noch eine # unaufgeloeste Frage einen Treffer bekommt; er kostet **eine** von 29. # # **Der alte Wert 0.40 war kein zu niedriger, sondern ein wirkungsloser.** # Der schlechteste Eintrag des GESAMTEN Bestandes erreicht gegen eine # beliebige Frage 0.48 bis 0.54 — der Boden des Raums liegt ueber der # Schwelle, und sie konnte per Konstruktion nichts aussperren. Gemessen an # denselben Fragen: 10 von 10 unbezogenen Fragen bekamen die vollen `top_k`. # # **Was die Schwelle NICHT leistet und nicht leisten soll:** Sie entscheidet # nicht, WELCHE Erinnerung passt. Der schlechteste Fehltreffer liegt bei # 0.8565, und 33 der 40 richtigen Antworten liegen darunter — eine Zahl, die # jeden Fehltreffer aussperrt, verwuerfe vier Fuenftel der richtigen. Die # Auswahl leisten Rang und Kappung (Rang 1 in 17 von 40, Median-Rang 2); die # Schwelle entscheidet allein, ob ueberhaupt etwas passt. KZG_RETRIEVAL_SCHWELLE: float = float(os.getenv("KZG_RETRIEVAL_SCHWELLE", "0.72")) # Deckel der Salienzskala. War bis Chat 113 auf 10.0 — ein Wertebereich, den die # Eingangsgroesse nie hatte: Die Modellbewertung liegt in [0,1]. Gemessen am # 28.07.2026 standen dadurch 71 von 188 Eintraegen (38 %) ueber 1.0, der hoechste # bei 5.64, und keines der Tore griff noch. KZG_SALIENZ_CAP: float = float(os.getenv("KZG_SALIENZ_CAP", "1.0")) # Exponent der Salienzkurve. Von 0.6 auf 0.5 gezogen, damit KZG und LZG dieselbe # Kurve tragen (gewicht_absolut_berechnen in memory/lzg_knoten.py). # **Am 24.08.2026 von 0.5 auf 1.1.** Die Kurve ist `sin(roh*pi/2) ** exp`. Der # Sinus flacht oben bereits ab; ein Exponent unter 1 hebt danach **zusaetzlich** # an, und beides zusammen staucht die obere Haelfte der Rohskala auf einen # Streifen: roh 0.7-1.0 bildete auf 0.9439-1.0000 ab, also 30 % der Eingabe auf # 5,6 % der Ausgabe. Genau dieses Band trug der ganze Bestand. # # Ueber 1 gebogen bleibt die weiche Annaeherung an 1.0 erhalten, ohne # anzuheben: dieselben 30 % bilden auf 12 % ab. **Kein Tor aendert sich davon** # (die Schwellen sind Bilder derselben Kurve, siehe oben) — die Wirkung ist die # Aufloesung der gespeicherten Zahl. KZG_SALIENZ_DAEMPFUNG_EXP: float = float(os.getenv("KZG_SALIENZ_DAEMPFUNG_EXP", "1.1")) # Zuwachs je thematischer Verstaerkung, am Anker vor der Kurve. Nicht frei # gewaehlt, sondern durch die TTL-Stufen bestimmt: Ein Eintrag muss nicht nur # n-mal wiederkommen, sondern jedes Mal innerhalb seines Fensters (7/14/30 Tage). # Mit 0.03 erreicht eine Bewertung von 0.5 das Tor nach sieben Verstaerkungen, # eine von 0.3 nach vierzehn. Bei 0.015 waeren es siebenundzwanzig, vierzehn # davon in Sieben-Tage-Fenstern — der Ansammlungspfad waere fuer die untere # Haelfte der Skala unerreichbar. Wer diesen Wert oder eine TTL-Stufe aendert, # prueft die jeweils andere Groesse mit (novaberg-kzg-salienz_k.md §6). KZG_SALIENZ_BOOST: float = float(os.getenv("KZG_SALIENZ_BOOST", "0.03")) # ─── Charakter-Rad (Gewichtung der Nutzer-Salienz) ── # Zwoelf Speichen um eine Nabe. Jede Speiche zieht den Faktor in ihre Richtung, # mehrere auf derselben Seite ziehen zusammen staerker. Volle Auslenkung trifft # die Grenzen exakt: 0.9 + 0.60 = 1.5, 0.9 - 0.40 = 0.5. Die Kappung ist damit # Sicherung, kein Formteil. Herleitung: novaberg-salienz-berechnung_k.md §5. # # Die Zuege sind eine SETZUNG, keine Messung — ausdruecklich nachkalibrierbar. # Sie stehen bei der Destillation, die sie anwendet (agents/charakter/). # # Nabe und Grenzen stehen hier und nicht dort, weil sie seit Chat 112 zwei # Verbraucher haben: die Destillation, die den Faktor schreibt, und die # Salienz-Formel, die ihn liest und auf Plausibilitaet prueft. Zwei Kopien # derselben Grenze laufen zwangslaeufig auseinander, und die Fehlerbedingung # waere Schweigen (novaberg-lesson_l_gelesen-ist-nicht-wirksam.md). RAD_NABE: float = float(os.getenv("RAD_NABE", "0.9")) RAD_MIN: float = float(os.getenv("RAD_MIN", "0.5")) RAD_MAX: float = float(os.getenv("RAD_MAX", "1.5")) # ─── Initiative-Rad (Chat 116) ── # Zweites Charakter-Rad, andere Frage: ueberlaesst Nova im Gespraech die # Fuehrung oder behaelt sie sie. Nabe bei 0.0 — keine Tendenz. Volle # Auslenkung trifft +/-SPANNE exakt, die Summen der zehn Zuege sind deshalb # symmetrisch je 0.25 (novaberg-gv-initiative_k.md §6.3). INITIATIVE_RAD_NABE: float = float(os.getenv("INITIATIVE_RAD_NABE", "0.0")) INITIATIVE_RAD_SPANNE: float = float(os.getenv("INITIATIVE_RAD_SPANNE", "0.25")) # Mehrfach erheben und den Median nehmen. Gemessen 29.07.2026: Zwei Laeufe # gegen denselben Charaktertext bei Temperatur 0.2 ergaben -0.18 und -0.13 — # ein Fuenftel der halben Spanne aus derselben Eingabe. Der Wert wird bei der # Destillation EINMAL geschrieben und bleibt bis zur naechsten stehen; ein # ungluecklicher Lauf legte ihn sonst fuer Tage fest. INITIATIVE_RAD_LAEUFE: int = int(os.getenv("INITIATIVE_RAD_LAEUFE", "3")) # Dasselbe fuer das Zuwendungs-Rad — nachgetragen am 11.08.2026. # # **Die Ueberlegung darueber galt woertlich auch hier und wurde nur nie # angewandt.** Das Zuwendungs-Rad lief einmal je Destillation, und dieser # eine Wert stand bis zur naechsten: Mehmets 1.1750 vom 10.08.2026 ist eine # einzige Ziehung. Die Luecke traf ausgerechnet das Rad, das JEDER Turn # liest — der Faktor geht in die Salienz jedes Nutzer-Beitrags ein —, # waehrend das seltener gelesene Initiative-Rad seit dem 29.07. geschuetzt # war. # # Gemessen am 11.08.2026 ueber drei Quellen: `labor/ergebnis/rad_rauschen_*`. ZUWENDUNG_RAD_LAEUFE: int = int(os.getenv("ZUWENDUNG_RAD_LAEUFE", "3")) # ─── Messreihe der Charakter-Raeder (Chat 123) ── # Das Rad misst einen akuten Zustand und wird durch die Messungen der letzten # Tage stabilisiert (novaberg-charakter-rad-messreihe_k.md). Anlass ist ein # gemessener Sprung des Zuwendungsfaktors von 1.215 auf 0.980 innerhalb von # zwei Stunden — bei einer Verfahrensstreuung von nur 0.08, also echte # Bewegung, ausgeloest von zwanzig gleichfoermigen Turns. # # **Beide Zahlen sind gesetzt zum Messen.** Sie folgen aus der geforderten # Einschwingzeit, nicht aus einer Messung; sobald zehn Reihen liegen, ist die # Streuung zwischen Erhebungen bekannt und beide sind abzuleiten. RAD_MESSREIHE_FENSTER: int = int(os.getenv("RAD_MESSREIHE_FENSTER", "5")) # Fester Takt, damit Rang und Zeit dasselbe bedeuten: Die Gewichtskurve # verfaellt ueber den Rang. Bei ereignisgetriebener Messung wuerden fuenf # Erhebungen an einem Tag alles Fruehere verdraengen. RAD_MESSUNG_ABSTAND_STUNDEN: int = int(os.getenv("RAD_MESSUNG_ABSTAND_STUNDEN", "12")) # Gewicht der aelteren Messungen gegen die juengste. Der Emotions-Verlauf # benutzt an derselben Stelle 0.15, weil dort der aktuelle Turn dominieren # SOLL; hier ist das Ziel das Gegenteil. Bei 0.5 und fuenf Reihen traegt die # juengste Messung 41 % des Ergebnisses, und ein echter Umschwung ist nach # zwei Tagen zu 87 % angekommen. RAD_HISTORIEN_GEWICHT: float = float(os.getenv("RAD_HISTORIEN_GEWICHT", "0.5")) # Dieselbe Kurve wie der Emotions-Verlauf (EMOTION_DECAY_FACTOR/-_BASE), aber # eigene Konstanten: Zwei Groessen, die zufaellig denselben Wert haben, duerfen # nicht an einem Regler haengen — wer die Emotionskurve justiert, wuerde sonst # stillschweigend die Charakter-Raeder mitverschieben. RAD_DECAY_FACTOR: float = float(os.getenv("RAD_DECAY_FACTOR", "0.8")) RAD_DECAY_BASE: float = float(os.getenv("RAD_DECAY_BASE", "10")) # ─── Erregungs-Zuschlag auf den Eigen-Pfad ── # Starke Freude, Aufgebrachtheit, Ausrufezeichen — Signale, dass eine Aussage # im Moment viel bedeutet. Kein eigener Antrieb, sondern ein Verstaerker auf # dem, was ohnehin durchkommt: multiplikativ als (1 + zuschlag), damit er # hebt, aber nie ausloescht. Erregung macht aus einer belanglosen Aussage # keine bedeutsame (novaberg-salienz-berechnung_k.md §4). SALIENZ_EREGUNG_MAX_ZUSCHLAG: float = float(os.getenv("SALIENZ_EREGUNG_MAX_ZUSCHLAG", "0.3")) # ─── Cluster-Promotion ───────────────────── CLUSTER_MIN_EINTRAEGE: int = int(os.getenv("CLUSTER_MIN_EINTRAEGE", "3")) # Kalibriert auf nomic-embed-text-v2-moe (Chat 107). # Grundrauschen 0.16, Median 0.26, p99 0.57 — gemessen an 302 lzg_knoten. # Vorher 0.85/0.80 im casing-blinden Raum (Grundrauschen 0.74) — funktionslos. # Alt-Cluster-Pfad (deaktiviert seit Chat 98), trotzdem mitgezogen. CLUSTER_THEMEN_SIMILARITY: float = float(os.getenv("CLUSTER_THEMEN_SIMILARITY", "0.82")) CLUSTER_LZG_SIMILARITY: float = float(os.getenv("CLUSTER_LZG_SIMILARITY", "0.75")) CLUSTER_WIDERSPRUCH_DECAY_FAKTOR: float = float( os.getenv("CLUSTER_WIDERSPRUCH_DECAY_FAKTOR", "3.0") ) CLUSTER_BESTAETIGUNG_BOOST: float = float(os.getenv("CLUSTER_BESTAETIGUNG_BOOST", "0.1")) # ─── Vertiefung ──────────────────────────────── PIXIE_VERTIEFUNG_LZG_LIMIT: int = int(os.getenv("PIXIE_VERTIEFUNG_LZG_LIMIT", "20")) PIXIE_VERTIEFUNG_KZG_LIMIT: int = int(os.getenv("PIXIE_VERTIEFUNG_KZG_LIMIT", "10")) # ─── Delegation ──────────────────────────────── # Kalibriert auf nomic-embed-text-v2-moe (Chat 107). # Grundrauschen 0.16, Median 0.26, p99 0.57 — gemessen an 302 lzg_knoten. # Vorher 0.82 im casing-blinden Raum (Grundrauschen 0.74) — funktionslos. DELEGATION_SIMILARITY_SCHWELLE: float = float(os.getenv("DELEGATION_SIMILARITY_SCHWELLE", "0.75")) # ─── Notizen-Suche ───────────────────────────── NOTIZEN_SUCHE_MIN_SIMILARITY: float = float(os.getenv("NOTIZEN_SUCHE_MIN_SIMILARITY", "0.15")) NOTIZEN_SUCHE_MIN_SCORE: float = float(os.getenv("NOTIZEN_SUCHE_MIN_SCORE", "0.3")) NOTIZEN_SUCHE_LIMIT: int = int(os.getenv("NOTIZEN_SUCHE_LIMIT", "10")) NOTIZEN_ZUSAMMENFASSUNG_MAX_WOERTER: int = int( os.getenv("NOTIZEN_ZUSAMMENFASSUNG_MAX_WOERTER", "20") ) # ─── Timeline-Suche ──────────────────────────── TIMELINE_SUCHE_LIMIT: int = int(os.getenv("TIMELINE_SUCHE_LIMIT", "10")) # Die Uebersicht des Timeline-Agenten liest das proaktive Fenster # (`graph/appointments.py`, `PROACTIVE_DAYS_*`) — eine Quelle fuer beide Lesewege. # ───────────────────────────────────────────── # Web-Suche (Serper zuerst, SearXNG als Rueckfall) # ───────────────────────────────────────────── # Serper liefert Google-Treffer ueber HTTP. Ohne Schluessel ueberspringt der # WebSearchManager den Anbieter und faellt auf SearXNG durch — unveraendertes # Verhalten. Der Schluessel steht in der `.env` und wird ueber die Compose-Datei # durchgereicht; er kostet je Anfrage ein Guthaben (Startguthaben 2500). SERPER_API_KEY: str = os.getenv("SERPER_API_KEY", "") SERPER_URL: str = os.getenv("SERPER_URL", "https://google.serper.dev/search") SERPER_TIMEOUT: float = float(os.getenv("SERPER_TIMEOUT", "10.0")) SEARXNG_URL: str = os.getenv("SEARXNG_URL", "http://searxng:8080") SEARXNG_TIMEOUT: float = float(os.getenv("SEARXNG_TIMEOUT", "10.0")) SEARXNG_MAX_RESULTS: int = int(os.getenv("SEARXNG_MAX_RESULTS", "10")) # Suchdisziplin des Thinkers (29.08.2026): So oft ruft der Thinker je Turn die # Suchmaschine. `[gemessen]` 29.08.2026, 20 Turns: 23 Suchen, bis zu drei je # Turn, und die Wikipedia-API sperrte zweimal fuer 180 s — die eine Suche der # Sachlage-Recherche ging leer aus. Hat die Sachlage in diesem Turn Treffer, # bekommt der Thinker diese statt einer eigenen Suche; sie zaehlen gegen # dieses Budget (`graph/nodes/thinker.py::prior_research`). THINKER_WEBSUCHE_MAX_JE_TURN: int = int(os.getenv("THINKER_WEBSUCHE_MAX_JE_TURN", "1")) # --- Web-Tools: PageFetcher --- PAGE_FETCH_TIMEOUT: float = float(os.getenv("PAGE_FETCH_TIMEOUT", "10.0")) PAGE_FETCH_MAX_CHARS: int = int(os.getenv("PAGE_FETCH_MAX_CHARS", "5000")) # ───────────────────────────────────────────── # Emotionale Intelligenz # ───────────────────────────────────────────── # Gewichtungsfaktoren für die Emotions-, Stil- und Beziehungsanalyse. # Emotions-Decay: Steilheit des logarithmischen Abfalls. # Formel: gewicht = 1.0 / (1.0 + EMOTION_DECAY_FACTOR * log_base(1 + position)) # Bei Factor 0.8, Basis 10: Turn 0 = 1.0, Turn 1 = 0.81, Turn 5 = 0.62, Turn 10 = 0.51 EMOTION_DECAY_FACTOR: float = 0.8 # Logarithmus-Basis für den Emotions-Decay. # 10 = sanfter Abfall (empfohlen), math.e = moderat, 2 = steil EMOTION_DECAY_BASE: float = 10 # Maximale Anzahl an Turns für den Emotions-Verlauf. # Maximale Turns für Emotions-Verlauf. # Hoch setzen = volle Session nutzen. Der arousal-basierte Decay # regelt, was davon noch Gewicht hat. Kleine Emotionen verfallen # schnell, große halten durch — das Fenster muss nur groß genug sein. EMOTION_MAX_TURNS: int = 100 # Mindest-Gewicht — Emotionen darunter werden entfernt. # # 31.08.2026: 0.15 → 0.04, gemeinsam mit EMOTION_GLAETTUNGS_MAXIMUM (2.5 → 4.0). # Die Schwelle ist an den Cap gebunden, nicht an die Emotion: Ein groesserer Cap # staucht jeden Wert, und das Echo eines gewoehnlichen Turns fiel bei 0.15 # darunter — gemessen 0.05 statt vorher 0.18. Es waere damit nicht leiser # geworden, sondern aus dem Verlauf **verschwunden**. Herleitung samt Messreihe: # `novaberg-ei.md` §Reizstaerke. EMOTION_MIN_WEIGHT: float = 0.04 # Fenster für Vektor-Berechnung (Richtungswechsel). # Kurz halten — der Vektor soll Wendepunkte erkennen, nicht Grundstimmung. EMOTION_VEKTOR_TURNS: int = 8 # Ab welchem Anstieg der mittleren Erregung zwischen den beiden Hälften des # Fensters ein gleichbleibender Gruppenverlauf als **Intensitätsanstieg** gilt # — die Bedingung, die `negativ→negativ` zu `spirale` und `positiv→positiv` zu # `eskalation` macht. # # **Herkunft, gemessen am 08.08.2026** über 769 Fünfer-Fenster aus 849 Turns # von 20 Paaren (`pipeline_log`, `art='turn_roh'`, Feld `user_emotion.arousal` # — der Wert, den der GV-Knoten gesehen hat). Der Median der Differenz ist # 0,000, der Median ihres Betrages **0,067**. Die Perzeption liefert Arousal in # **Zehntelschritten** (zehn Ausprägungen im Bestand, 0,1 bis 0,85). # # Daraus die 0,10 und nicht 0,067: Sie ist der kleinste Schritt, den die # liefernde Skala als gewollten Anstieg überhaupt ausdrücken kann. Alles # darunter entsteht durch die Mittelung der beiden Hälften und ist kein Signal # der Quelle. Sie liegt zugleich über der üblichen Schwankung und trifft # **20,5 %** der gemessenen Fenster — eine Minderheit, kein Münzwurf. # # **Der Vorbehalt gehört an die Zahl:** Gemessen ist die Nutzerseite, weil # Novas Erregungsverlauf je Turn nicht haltbar gespeichert ist # (`turn_roh.nova_emotion` entsteht nach der späten Perzeption). Achse R der # Landschaft rechnet aber auf **Novas** Vektor. Die Schwelle ist damit ein # Stellvertreter und nach der nächsten Messreihe gegen Novas eigene Verteilung # zu prüfen. GV_VEKTOR_INTENSITAET_SCHWELLE: float = 0.10 # Woraus die Richtungsangabe eines Turns entstanden ist. Dieselbe Rolle, die # `valenz_quelle` für Achse V und `Fuehrung.fehlend` für Achse I hat: Das # Ergebnis reist mit seiner Eingangsgröße, sonst ist ein Ausfall von einer # Messung nicht zu unterscheiden. # # gemessen beide Hälften hatten eine echte Mehrheit # gleichstand mindestens eine Hälfte war ein Gleichstand, aufgelöst # über die zeitlich letzte Emotion — die Hälfte ist dann # faktisch eine Stichprobe von eins # zu_wenig_turns weniger als zwei verwertbare Turns; es gibt keine # Richtung, und `plateau` ist hier keine Aussage # nicht_gesetzt der Vektor war beim Lesen leer, die Rechnung lief nie VEKTOR_QUELLE_KANON: frozenset[str] = frozenset({ "gemessen", "gleichstand", "zu_wenig_turns", "nicht_gesetzt", }) # Die Teilmenge von `EMOTIONS_VEKTOREN`, die **Druck** bedeutet: eine # Bewegung ins Negative. **Die Obermenge steht weiter unten**, seit die # doppelte Deklaration am 23.08.2026 aufgeloest wurde — vorher stand sie # zwei Zeilen darueber, und dieser Satz konnte `die Teilmenge` sagen, # ohne zu sagen, wovon. Ein Zeuge haelt die Beziehung # (`test_die_druck_teilmenge_liegt_im_kanon`). # # absturz positiv -> negativ # einbruch neutral -> negativ # spirale negativ -> negativ, mit neuen negativen Gefuehlen # # Kriterium des NachfragenAgenten (novaberg-pixie-nachfragen_k.md §2) und # zugleich die Emotionsvektor-Weiche des Pixie-Routers. Eine Konstante, weil # dieselbe Menge sonst an zwei Stellen als Literal stuende und auseinander- # laufen koennte — dieselbe Klasse wie die doppelte Intentionstabelle. EMOTIONS_VEKTOREN_DRUCK: frozenset[str] = frozenset({ "absturz", "spirale", "einbruch", }) # Fenster für Sprachstil-Erkennung (regelbasiert). # Misst aktuelle Formulierung, nicht emotionalen Kontext. STIL_ANALYSE_TURNS: int = 5 # Stil-Adaption: Gewichtung Session (kurzfristig) vs. Hash (langfristig). # 0.0 = nur Hash, 1.0 = nur Session. STIL_SESSION_GEWICHT: float = 0.7 # Beziehungs-Einfluss: Wie stark das Beziehungsprofil den Responder beeinflusst. # 0.0 = kein Einfluss, 1.0 = voller Einfluss. BEZIEHUNG_EINFLUSS: float = 0.8 # ───────────────────────────────────────────── # Emotions-Vektoren (Richtungsanweisungen für den Responder) # ───────────────────────────────────────────── # Default-Arousal pro Emotion (16 kanonische + neutral) EMOTION_DEFAULT_AROUSAL: dict[str, float] = { "begeisterung": 0.7, "freude": 0.5, "dankbarkeit": 0.5, "zufriedenheit": 0.3, "stress": 0.6, "unsicherheit": 0.4, "ueberrascht": 0.7, "verwundert": 0.4, "verzweiflung": 0.8, "traurigkeit": 0.4, "frustration": 0.6, "enttaeuschung": 0.5, "wut": 0.8, "aerger": 0.5, "hoffnung": 0.5, "neugierig": 0.4, "neutral": 0.2, } # Arousal-Decay-Rate pro Emotion # Hohe Rate = Energie verfliegt schnell (Dopamin) # Niedrige Rate = Energie hält lange (Cortisol) # Formel: gedämpfter_arousal = original_arousal × e^(-rate × position) EMOTION_AROUSAL_DECAY: dict[str, float] = { # Sektor 1 — Freude: Dopamin-Peak, verfliegt schnell "begeisterung": 0.15, "freude": 0.10, # Sektor 2 — Zuversicht: stabile Grundstimmung "dankbarkeit": 0.08, "zufriedenheit": 0.05, # Sektor 3 — Angst: setzt sich fest "stress": 0.04, "unsicherheit": 0.05, # Sektor 4 — Überraschung: extrem kurzlebig "ueberrascht": 0.20, "verwundert": 0.15, # Sektor 5 — Trauer: gräbt sich ein (Cortisol) "verzweiflung": 0.02, "traurigkeit": 0.03, # Sektor 6 — Enttäuschung: bleibt als Groll "frustration": 0.04, "enttaeuschung": 0.05, # Sektor 7 — Ärger: intensiv aber abbaubar (Adrenalin) "wut": 0.08, "aerger": 0.06, # Sektor 8 — Neugier: flüchtig, springt weiter "hoffnung": 0.08, "neugierig": 0.12, } def arousal_label(value: float) -> str: """Leitet ein menschenlesbares Label aus dem Arousal-Float ab.""" if value >= 0.7: return "high" elif value >= 0.4: return "mid" else: return "low" # **Der Kanon der Emotionsvektoren — und zugleich ihr Prompttext.** # # Die Schluessel sind die geschlossene Menge, die # `ei/berechnung.py::stimmungsvektor_bestimmen()` erzeugen kann; der Wert ist # der Block, der daraus im Prompt wird. # # **Die Schluessel sind zugleich die Obermenge, damit ein gelesener Vektor # validierbar ist und nicht nur benutzbar.** Der Satz wiegt hier mehr als an # seinem frueheren Ort: Eine Tabelle `dict[str, str]` sieht nach Benutzung # aus, nicht nach Deklaration — und ohne die zweite Leseart ist ein # unbekannter Wert von einem gueltigen Nein nicht zu unterscheiden. # # **Zwei Definitionen, bis zum 23.08.2026.** Derselbe Name stand hier und # rund 90 Zeilen weiter oben ein zweites Mal, dort als `frozenset` mit # denselben neun Namen. Python nimmt die zweite; die erste war toter Text, # der wie eine Deklaration aussah. Beide Leser brauchen ohnehin diese hier — # `nodes/responder.py` schlaegt den Text nach, `agents/nachfragen/agent.py` # prueft die Zugehoerigkeit, und ein `in` ueber ein Woerterbuch liest seine # Schluessel. **Kein Werkzeug hat es gesehen:** Ruffs `F811` deckt Importe, # Funktionen und Klassen, nicht das erneute Binden einer Modulvariablen. # # Der Grund fuer den Kanon steht weiterhin, er stand nur an der toten # Haelfte: Ohne deklarierte Obermenge ist ein unbekannter Wert von einem # gueltigen Nein nicht zu unterscheiden. Wer nur gegen die Druck-Teilmenge # prueft, liest bei einem Transportfehler „kein Druck" statt „defekt" — und # das sieht auf jeder Logstufe gleich aus. EMOTIONS_VEKTOREN: dict[str, str] = { "absturz": ( "EMOTIONALER VEKTOR: ABSTURZ (positiv → negativ). " "Der Nutzer war gerade noch in guter Stimmung und ist abgestürzt. " "Er erlebt einen Umschwung — das ist ein Schock, nicht ein sanfter Übergang." ), "spirale": ( "EMOTIONALER VEKTOR: SPIRALE (negativ → noch negativer). " "Der Nutzer rutscht tiefer ab. Neue negative Emotionen kommen hinzu. " "Die Intensität steigt oder hält sich auf hohem Niveau." ), "stabilisierung": ( "EMOTIONALER VEKTOR: STABILISIERUNG (negativ → neutral). " "Der Nutzer beruhigt sich nach einem schweren Moment. " "Die Intensität lässt nach, er findet zurück in ruhigeres Fahrwasser." ), "erholung": ( "EMOTIONALER VEKTOR: ERHOLUNG (negativ → positiv). " "Der Nutzer kommt aus einem emotionalen Tief und zeigt erste positive Signale. " "Er ist noch erschöpft von dem was war, auch wenn er jetzt lächelt." ), "aufbluehen": ( "EMOTIONALER VEKTOR: AUFBLÜHEN (neutral → positiv). " "Der Nutzer blüht auf. Die Stimmung hebt sich, Energie kommt rein." ), "eskalation": ( "EMOTIONALER VEKTOR: ESKALATION (positiv → noch positiver). " "Der Nutzer ist in Hochstimmung. Die Begeisterung steigt weiter." ), "abkuehlung": ( "EMOTIONALER VEKTOR: ABKÜHLUNG (positiv → neutral). " "Die Begeisterung klingt ab. Natürlicher Übergang, kein Einbruch." ), "einbruch": ( "EMOTIONALER VEKTOR: EINBRUCH (neutral → negativ). " "Die Stimmung kippt. Etwas belastet den Nutzer, das vorher nicht da war." ), "plateau": ( "EMOTIONALER VEKTOR: PLATEAU (stabil). " "Die Stimmung ist gleichbleibend. Keine Veränderung in der emotionalen Richtung." ), } # Novas eigene Emotions-Vektoren (Ich-Perspektive, fuer [EIGENE_EMOTION]-Block) # Getrennt von EMOTIONS_VEKTOREN (User-Perspektive), damit beide unabhaengig # kalibriert werden koennen. EMOTIONS_VEKTOREN_NOVA: dict[str, str] = { "absturz": ( "Dein emotionaler Zustand ist abgestuerzt — ein ploetzlicher Umschwung nach unten." ), "spirale": ( "Du rutschst emotional tiefer. Die Tendenz zeigt nach unten." ), "stabilisierung": ( "Du beruhigst dich. Dein Zustand pendelt sich ein." ), "erholung": ( "Du kommst aus einem Tief. Es geht bergauf." ), "aufbluehen": ( "Du bluehst auf. Deine Stimmung steigt." ), "eskalation": ( "Du bist in Hochstimmung. Die Begeisterung steigt weiter." ), "abkuehlung": ( "Deine Begeisterung klingt ab. Natuerlicher Uebergang zu ruhigeren Toenen." ), "einbruch": ( "Deine Stimmung kippt. Ein Umschwung zeichnet sich ab." ), "plateau": ( "Dein emotionaler Zustand ist stabil. Keine grossen Schwankungen." ), } # ─── Ebbinghaus-Gedächtnisverfall ──────────── # Decay-Rate (Lambda) für den zeitlichen Verfall von LZG-Einträgen. # Das effektive Gewicht wird NICHT gespeichert, sondern bei jedem Zugriff # live berechnet: effektiv = gewicht * e^(-lambda * tage_seit_verstaerkung) # # Beispielwerte bei decay_rate = 0.0015: # 1 Monat: Decay-Faktor 0.96 (kaum Verfall) # 6 Monate: Decay-Faktor 0.76 # 1 Jahr: Decay-Faktor 0.58 # 2 Jahre: Decay-Faktor 0.33 # 3 Jahre: Decay-Faktor 0.19 # # 0.0015 = sanfter Verfall (~3 Jahre bis einmalig Erwähntes verblasst) # 0.003 = moderater Verfall (~1.5 Jahre) # 0.005 = schneller Verfall (~1 Jahr) EBBINGHAUS_DECAY_RATE: float = 0.0015 # Schwellwert für Inaktiv-Markierung durch Pixie. # Einträge mit effektivem Gewicht darunter werden als inaktiv markiert. # Sie werden NICHT gelöscht — nur aus aktiven Abfragen ausgeschlossen. EBBINGHAUS_MIN_GEWICHT: float = 0.1 # ─── Arousal-basierter Decay ──────────────── # Wie stark Arousal den Emotions-Decay bremst. # Hoher Arousal → Emotion hält länger (Kündigung, Todesfall). # Niedriger Arousal → Emotion verfällt normal (kleiner Ärger). # Formel: effective_decay = DECAY_FACTOR × (1.0 - arousal × PERSISTENCE) # # Beispiele bei PERSISTENCE = 0.6: # Arousal 0.2 (kleiner Ärger): decay_factor 0.70 → Turn 20: Gewicht 0.49 # Arousal 0.5 (Frustration): decay_factor 0.56 → Turn 20: Gewicht 0.56 # Arousal 0.8 (Kündigung): decay_factor 0.42 → Turn 20: Gewicht 0.64 # Arousal 0.95 (Todesfall): decay_factor 0.34 → Turn 20: Gewicht 0.69 # # 0.0 = kein Effekt (alter Decay), 1.0 = maximale Persistenz EI_AROUSAL_PERSISTENCE: float = 0.6 # ─── EI-Plausibilitäts-Gate ───────────────── # Faktoren zur Berechnung des emotionalen Arousal (ei_arousal). # ei_arousal = current_arousal * gewichteter Kombinationsfaktor # Bestimmt ob der Gesprächsmodus "emotional" sein darf/muss. # Werte > 1.0 verstärken, < 1.0 dämpfen die emotionale Wirkung. # Änderung hier → sofortige Wirkung auf die EI-Bewertung. EI_DYNAMIK_FAKTOREN: dict[str, float] = { "hilfesuchend": 1.5, # Geht auf mich zu, braucht Empathie "angriff": 1.3, # Hochgeladen, emotional aktiviert "dankbar": 1.2, # Berührt, emotional offen "vertrauen": 1.1, # Öffnet sich, leichte Verstärkung "neutral": 1.0, # Kein Signal "distanz": 0.7, # Will sachliche Behandlung } # **Der neutrale Faktor je Tabelle** bekommt ein Wert, den die Tabelle nicht # kennt — mit Fehlerzeile. Traegt die Tabelle einen Eintrag fuer den # Kanon-Wert `neutral` ("Kein Signal"), ist es dessen Wert: Ein Fremdwert wirkt # nicht staerker als das, was die Tabelle selbst neutral nennt. Sonst ist es # das arithmetische Mittel ihrer Werte. Beides als Literal eingetragen, nicht # zur Laufzeit gerechnet. Bis zum 26.10.2026 war der Vorgabewert 1.0, und beim # Intent ist 1.0 der hoechste Wert der Tabelle: Ein Wert ausserhalb des Kanons # wurde nicht gedaempft, sondern maximal verstaerkt # (`[gemessen 10.09.2026 ueber 2694 Perzeptionen]` `philosophischer_austausch` # 108-mal in `intent`, `begeisterung` 59-mal in `tone`). Wer eine Tabelle # aendert, prueft ihren neutralen Faktor neu; ein Zeuge haelt beide zusammen. EI_DYNAMIK_NEUTRAL: float = 1.0 # der Eintrag `neutral` EI_INTENT_FAKTOREN: dict[str, float] = { "personal": 1.0, # Persönliches Thema, neutrale Basis "creative": 0.8, # Kreativ, leicht emotional "smalltalk": 0.5, # Plauderton, wenig emotional "task": 0.3, # Aufgabe, sachlich "knowledge": 0.3, # Wissensfrage, sachlich "meta": 0.3, # Über Nova, sachlich } EI_INTENT_NEUTRAL: float = 0.533 # 3.2 / 6 EI_TONE_FAKTOREN: dict[str, float] = { "empathisch": 1.3, # Perzeption sieht Empathie-Bedarf "kreativ": 0.8, # Leicht offen "direkt": 0.5, # Auf den Punkt, nicht emotional "sachlich": 0.3, # Nüchtern } EI_TONE_NEUTRAL: float = 0.725 # 2.9 / 4 # Gewichtung der drei Faktor-Gruppen (Summe = 1.0) EI_GEWICHTE: dict[str, float] = { "dynamik": 0.40, # Beziehungsdynamik wiegt am stärksten "intent": 0.35, # Was will der User "tone": 0.25, # Wie soll geantwortet werden (bereits Ableitung) } # Passiv-negative Emotionen: Erzwingen "emotional" auch bei niedrigem Arousal # Sektor 5 (Trauer) — immer emotional EI_PASSIV_NEGATIVE: set[str] = {"verzweiflung", "traurigkeit"} # ───────────────────────────────────────────── # Plutchik-Emotionsmodell: 8 Sektoren, 16+1 Emotionen # ───────────────────────────────────────────── # Gültige kanonische Gesprächsmodi (Perzeption soll NUR diese liefern). # Single Source of Truth für alle Modus-Verzweigungen im Code (Handbuch §6). # Wortgleich mit dem "modus"-Enum in prompts/default/perzeption.task.txt und # perzeption.assistant_task.txt — tests/test_modus_kanon.py hält beide Seiten # zusammen. Wer hier einen Modus ergänzt, ergänzt ihn auch in GV_TIEFE_MODUS, # GV_AUFNAHMEBEREITSCHAFT_MODUS, ei/farbton.py und der Längenberechnung des # GV-Nodes; der Test nennt die Lücke. MODUS_KANON: set[str] = { "fachgespraech", "philosophischer_austausch", "alltag", "arbeitsmodus", "emotional", "spielerisch", "lernmodus", "kreativ", "beratend", "berichtend", } # Uebersetzung → kanonischer Modus. **Der Kanon-Zug zieht Schreibvarianten # (Grossschreibung, Umlaute); eine Uebersetzung ist keine davon**, und deshalb # braucht sie eine eigene Tabelle statt einer Erweiterung des Kanons: Ein # englischer Wert IM Kanon liefe in die Modus-Tabellen und bekaeme dort einen # Zuschlag, den niemand gesetzt hat. # # `[gemessen 10.09.2026]` Eine Wertelegende im Perzeptions-Prompt hebt `kreativ` # von 3,3 % auf 36,7 % — und erzeugte in **9 von 30** Laeufen das englische # `creative`. Der Zug fing es nicht, `modus_pruefen` meldete es, und die # Rechnung nahm ihren Default. Die Legende allein genuegte deshalb nicht. MODUS_SYNONYM_MAP: dict[str, str] = { "creative": "kreativ", } # Single Source of Truth für die Intentionen, die die Salienz liefern darf. # Wortgleich mit der Aufzählung unter "6. INTENTIONEN" in # prompts/default/salienz.dimensionen.txt — tests/test_intent_kanon.py hält # beide Seiten zusammen, mit der Prompt-Datei als Zeugen. # # Warum diese Konstante existiert: GV_INITIATIVE_FUEHREND ist eine **Teilmenge** # von fünf dieser sechzehn. Wer nur gegen die Teilmenge prüft, kann einen # unbekannten Wert nicht von einer gültigen Nicht-Zugehörigkeit unterscheiden — # beides ergibt "kein Treffer". Genau daran lief M1 der Initiative-Achse zwei # Monate als Konstante: Der Kalibrier-Korpus las Bruchstücke eines # Transportformats, die trafen die fünf nie, und weil die Liste nicht leer war, # galt M1 als "nicht führend" statt als "fehlend" # (novaberg-lesson_l_teilmenge-verdeckt-muell.md). # # Eine geschlossene Wertemenge ohne deklarierte Obermenge ist benutzbar, aber # nicht prüfbar. Deshalb steht sie hier. INTENT_KANON: set[str] = { "emotionaler_ausdruck", "information_teilen", "information_erfragen", "recherche_vertiefen", "gemeinsam_eruieren", "reflexion", "smalltalk", "feedback_geben", "feedback_erfragen", "planung", "anweisung", "bestaetigung", "widerspruch", "abschluss", "hilferuf", "humor", } # ───────────────────────────────────────────── # Die vier Wertemengen der Perzeption (10.09.2026) # ───────────────────────────────────────────── # **Dieselbe Begruendung wie bei `INTENT_KANON` darueber, eine Ebene weiter:** # Eine geschlossene Wertemenge ohne deklarierte Obermenge ist benutzbar und # nicht pruefbar. Diese vier standen bis heute **nur** als Aufzaehlung im # Prompt — und genau sie tragen die Ausreisser. # # `[gemessen 10.09.2026 ueber 2694 Perzeptionen]`: # # Dimension erlaubt vorgekommen ausserhalb Kanon-Zug # tone 4 16 153 (5,7 %) nein # intent 6 12 119 (4,4 %) nein # sprach_stil 6 11 39 (1,4 %) nein # beziehungs_dyn. 6 7 1 (0,0 %) nein # emotion 17 24 14 (0,5 %) **ja** # modus 10 11 1 (0,0 %) **ja** # # **Die beiden Dimensionen mit Zug sind sauber, drei der vier ohne sind es # nicht.** Der Zusammenhang benennt die Abhilfe selbst. # # **Die Hauptmenge sind keine Schreibvarianten, sondern verrutschte Spalten:** # `philosophischer_austausch` steht 108-mal in `intent` (ein Modus-Wert), # `begeisterung` 59-mal in `tone` (eine Emotion), `sachlich` 19-mal in # `sprach_stil` (ein Ton). Deshalb meldet `to_canonical` seit heute, **welchem** # fremden Kanon ein unbekannter Wert angehoert — ein Deutsch-Englisch-Array # faengt diese Klasse nicht. # **`PERZEPTION_INTENT_KANON` und nicht `INTENT_KANON`.** Der Name ist oben # vergeben, und zwar fuer eine **andere** Menge: `INTENT_KANON` sind die # sechzehn Intentionen der **Salienz**, diese hier sind die sechs der # **Perzeption**. Zwei Wertemengen unter einem Begriff sind genau die # Verwechslung, die das Modell in seinen Ausgaben auch macht. PERZEPTION_INTENT_KANON: frozenset[str] = frozenset({ "smalltalk", "knowledge", "personal", "task", "creative", "meta", }) # Der Ton, in dem der Assistent antworten soll. **`kreativ` steht hier UND als # Modus** — das Modell vergibt es 161-mal hier und **null-mal** dort # (`MODUS-KREATIV-WIRD-NIE-VERGEBEN`). PERZEPTION_TONE_KANON: frozenset[str] = frozenset({ "empathisch", "sachlich", "kreativ", "direkt", }) # Wie der Nutzer formuliert — Satzlaenge, Formalitaet, Slang. SPRACH_STIL_KANON: frozenset[str] = frozenset({ "locker", "formell", "fachlich", "emotional", "jugendlich", "neutral", }) # Wie der Nutzer sich zum Assistenten stellt. BEZIEHUNGS_DYNAMIK_KANON: frozenset[str] = frozenset({ "vertrauen", "distanz", "angriff", "hilfesuchend", "dankbar", "neutral", }) # Die Perspektive einer Gedaechtniszeile — die dritte Spalte des Paar-Schemas. # # **`user_id` traegt den Menschen, `character_id` die Figur; wer die Zeile # geschrieben hat, sagt allein dieser Wert** (`novaberg-convention-paar-schema.md` # §2). Er steht hier als Kanon, weil eine Lesestelle sonst nur pruefen kann, ob # ihr Wert *einer bestimmte* ist — und ein unbekannter waere von einem gueltigen # Nein nicht zu unterscheiden (die Teilmengen-Falle). # # Der Bestand traegt den Wert an acht Stellen als Literal. Sie sind nicht # migriert; diese Deklaration ist die Quelle fuer neue Pruefungen. BEOBACHTER_KANON: frozenset[str] = frozenset({"user", "assistant"}) # Gültige kanonische Emotionen (Perzeption soll NUR diese liefern) EMOTION_KANON: set[str] = { "begeisterung", "freude", # Sektor 1 — Freude "dankbarkeit", "zufriedenheit", # Sektor 2 — Zuversicht "stress", "unsicherheit", # Sektor 3 — Angst "ueberrascht", "verwundert", # Sektor 4 — Überraschung "verzweiflung", "traurigkeit", # Sektor 5 — Trauer "frustration", "enttaeuschung", # Sektor 6 — Enttäuschung "wut", "aerger", # Sektor 7 — Ärger "hoffnung", "neugierig", # Sektor 8 — Neugier "neutral", } # Kanonische Emotion → Sektor (1–8) EMOTION_SEKTOR_MAP: dict[str, int] = { # Sektor 1 — Freude (Joy) "begeisterung": 1, "freude": 1, # Sektor 2 — Zuversicht (Trust) "dankbarkeit": 2, "zufriedenheit": 2, # Sektor 3 — Angst (Fear) "stress": 3, "unsicherheit": 3, # Sektor 4 — Überraschung (Surprise) "ueberrascht": 4, "verwundert": 4, # Sektor 5 — Trauer (Sadness) "verzweiflung": 5, "traurigkeit": 5, # Sektor 6 — Enttäuschung (Disgust) "frustration": 6, "enttaeuschung": 6, # Sektor 7 — Ärger (Anger) "wut": 7, "aerger": 7, # Sektor 8 — Neugier (Anticipation) "hoffnung": 8, "neugierig": 8, } # Valenz je kanonischer Emotion, auf [-1, +1]. **Vorzeichen ist Richtung, # Betrag ist Ladung** — beide werden getrennt gelesen: die Strangvalenz nimmt # das Vorzeichen mit, die Strangstaerke nur den Betrag. # # **Warum eine gesetzte Tabelle und keine Ableitung aus dem Plutchik-Rad.** Das # Rad ordnet nach **Verwandtschaft**, nicht nach Wert; Nachbarn sind aehnlich, # Gegenueberliegende sind Gegensaetze. Legt man eine Achse durch Freude ↔ Trauer # und projiziert, kommen **drei von acht Sektoren falsch heraus**: Angst und # Aerger stuenden auf 0 (beide sind klar negativ), Ueberraschung auf -0,71 # (sie ist richtungslos). Valenz ist im Kreis schlicht nicht kodiert. # # **Die Herkunft ist Russells Circumplex** — Valenz und Erregung als zwei # empirisch erhobene Achsen. Die Erregungsachse liegt hier nicht; sie waere eine # zweite Groesse mit eigenem Leser. # # **Die zweite Zahl je Sektor ist die schwaechere Form.** Der Kanon fuehrt sie # ohnehin paarweise, und `EMOTION_AROUSAL_DECAY` daneben nutzt dieselbe Ordnung. # # **Ueberraschung traegt eine kleine Ladung ohne belastbare Richtung.** Ihr # Vorzeichen ist die schwaechste Stelle dieser Tabelle: Sie kippt je nach # Anlass. Der Betrag ist klein genug, dass die Wahl wenig traegt — und `0,0` # waere die staerkere Behauptung gewesen, naemlich dass eine Ueberraschung gar # keine Ladung hinterlaesst. # # **Alle sechzehn sind Setzungen und ungemessen**, wie die Verfallsraten # daneben. `[gerechnet]` 02.09.2026 ueber 1.786 echte Emotionszeilen ueber dem # Salienz-Tor, 20.000 simulierte Vierer-Straenge: Der Betrag streut dann # **0,225 bis 1,000** (Mittel 0,593, Streuung 0,137) — gegen **97,05 % exakt # 1,00** bei der Dreiwerte-Gruppe davor. EMOTION_VALENZ: dict[str, float] = { # Sektor 1 — Freude "begeisterung": 1.00, "freude": 0.80, # Sektor 2 — Zuversicht: ruhig positiv, nicht euphorisch "dankbarkeit": 0.75, "zufriedenheit": 0.60, # Sektor 3 — Angst: negativ und erregt "stress": -0.70, "unsicherheit": -0.55, # Sektor 4 — Ueberraschung: kleine Ladung, keine belastbare Richtung "ueberrascht": 0.10, "verwundert": 0.05, # Sektor 5 — Trauer: der Gegenpol der Freude, voller Ausschlag "verzweiflung": -1.00, "traurigkeit": -0.80, # Sektor 6 — Enttaeuschung "frustration": -0.75, "enttaeuschung": -0.65, # Sektor 7 — Aerger: stark negativ, aber unter der Trauer "wut": -0.90, "aerger": -0.70, # Sektor 8 — Neugier: mild positiv; Erwartung faerbt, sie draengt nicht "hoffnung": 0.50, "neugierig": 0.35, } # Sektor → Emotions-Gruppe (für Vektor-Berechnung) # Plutchik-Reihenfolge: positive und negative interleaved SEKTOR_GRUPPE: dict[int, str] = { 1: "positiv", # Freude 2: "positiv", # Zuversicht 3: "negativ", # Angst 4: "neutral", # Überraschung (ambivalent) 5: "negativ", # Trauer 6: "negativ", # Enttäuschung 7: "negativ", # Ärger 8: "positiv", # Neugier } # Synonym → kanonische Emotion (falls Perzeption abweicht) EMOTION_SYNONYM_MAP: dict[str, str] = { # → Sektor 1 Freude "glueck": "freude", "euphorie": "begeisterung", "heiterkeit": "freude", "ekstase": "begeisterung", # → Sektor 1 Freude (ehemals eigener Sektor "Stolz") "stolz": "freude", "triumph": "freude", "genugtuung": "freude", "selbstvertrauen": "freude", "selbstsicherheit": "freude", # → Sektor 2 Zuversicht (ehemals eigener Sektor) "erleichterung": "zufriedenheit", "gelassenheit": "zufriedenheit", "geborgenheit": "zufriedenheit", "vertrauen": "zufriedenheit", # → Sektor 3 Angst "angst": "stress", "furcht": "stress", "panik": "stress", "sorge": "unsicherheit", "nervositaet": "unsicherheit", "beklemmung": "unsicherheit", "anspannung": "stress", # → Sektor 4 Überraschung "schock": "ueberrascht", "ueberraschung": "ueberrascht", "fassungslos": "ueberrascht", "verbluefft": "verwundert", "perplex": "verwundert", "baff": "ueberrascht", # → Sektor 5 Trauer "resignation": "traurigkeit", "einsamkeit": "traurigkeit", "melancholie": "traurigkeit", "kummer": "verzweiflung", "niedergeschlagenheit": "traurigkeit", "nachdenklich": "traurigkeit", "leere": "traurigkeit", # `mitgefuehl` ist **eine Setzung des Eigentuemers** (06.09.2026), keine # Ableitung: *„naeher an Traurigkeit, geteilter Schmerz"*. Der Wert kam # ueber den Bestand 27-mal und trug bis dahin **keinen Sektor** — die # Einfaerbung lief auf der neutralen Zeitachse (1,0) und meldete jeden # Fall als Befund. Die Gegenkandidatin war `zufriedenheit` (Sektor 2, wo # `vertrauen` und `geborgenheit` stehen); sie haette eine **positive** # Valenz behauptet. Genau darum ist die Zeile keine Aufraeumarbeit. "mitgefuehl": "traurigkeit", # → Sektor 6 Enttäuschung (inkl. Ekel-Achse) "frust": "frustration", "ernuechterung": "enttaeuschung", "verbitterung": "frustration", "desillusionierung": "enttaeuschung", "abscheu": "frustration", "ekel": "frustration", "verachtung": "frustration", "langeweile": "enttaeuschung", "ablehnung": "enttaeuschung", "desinteresse": "enttaeuschung", "gleichgueltigkeit": "enttaeuschung", # → Sektor 7 Ärger "zorn": "wut", "aggression": "wut", "gereizt": "aerger", "genervt": "aerger", "empoerung": "wut", "hass": "wut", "groll": "aerger", # → Sektor 8 Neugier "neugier": "neugierig", "neugierde": "neugierig", "interesse": "neugierig", "erwartung": "hoffnung", "vorfreude": "hoffnung", "gespannt": "neugierig", } # Basis-Exponenten für die sektorabhängige Normalisierung # Werden mit dem Arousal der dominanten Emotion skaliert. EI_NORM_BENACHBART: float = 0.7 # Distanz 1 — schützt benachbarte Emotionen EI_NORM_NAH_DIAGONAL: float = 1.0 # Distanz 2 — neutral, wie bisherige Normalisierung EI_NORM_FERN_DIAGONAL: float = 1.2 # Distanz 3 — leicht gedrückt EI_NORM_GEGEN: float = 1.4 # Distanz 4 — stark gedrückt, Antagonisten # Exponent für die Dominanzbestimmung: effektiv = gewicht × arousal^n # n=2.0: Arousal 0.9 (Schock) hat 9× mehr Durchschlagskraft als Arousal 0.3 (Zufriedenheit) EI_AROUSAL_DOMINANZ: float = 2.0 # ───────────────────────────────────────────── # Emotions-Akkumulation und Glättung # # Emotionen werden pro Turn akkumuliert, wobei der aktuelle (neueste) # Turn voll zählt und ältere Turns nur als Echo einfließen. Danach # wird das Rohgewicht über eine sin^0.5-Kurve auf [0, 1] gestaucht. # # HISTORIEN_GEWICHT = 0.15 # Anteil, mit dem ältere Turns in die Akkumulation eingehen. Der # neueste Turn (i=0) trägt seit dem 31.08.2026 seine eigene Wucht # (`GLAETTUNGS_MAXIMUM × arousal³`) statt eines festen Vollwerts; # ältere ziehen als Stimmungs-Trägheit mit und verstärken sich nicht # unbegrenzt. # # GLAETTUNGS_MAXIMUM = 4.0 # Harte Obergrenze für akkumulierte Rohwerte. Bei diesem Wert erreicht # die Glättungs-Kurve mathematisch exakt 1.0. Rohwerte darüber werden # ebenfalls auf 1.0 abgebildet. # # **Der Cap ist zugleich der Beitrag des stärksten denkbaren Reizes.** # Der aktuelle Turn trägt `GLAETTUNGS_MAXIMUM × arousal²` bei # (`ei/berechnung.py`), also bei arousal 1.0 exakt den Cap: Der Anschlag # der Wahrnehmung ist der Anschlag der Skala, erreicht im ersten Turn. # Die beiden Zahlen sind deshalb **eine** Entscheidung und nicht zwei — # wer den Cap ändert, verschiebt den Vollausschlag mit. # # 31.08.2026: 2.5 → 4.0, und der Turn-Beitrag von konstant 1.0 auf die # kubische Form. Vorher erzeugte **jeder** Reiz denselben Ausschlag von # 0.77, unabhängig von seiner Erregung — gemessen über die volle Skala 0.0 # bis 1.0 eine Spannweite von 0.0000. Der Grund lag in der Akkumulation: bei # i=0 ist log(1+0) null, damit decay = 1.0, und der arousal-abhängige Verfall # griff erst ab dem zweiten Turn. Ein Todesfall und eine Mondumlaufzeit # wogen gleich. Herleitung samt Messreihen: `novaberg-ei.md` §Reizstärke. # # Die Glättungs-Kurve sin^0.5 bleibt unverändert. Sie spreizt nicht — das tut # der Turn-Beitrag —, sondern hält die **Echos** älterer Turns über der # Filterschwelle: Ohne die Wurzel fällt ein Echo nach der antagonistischen # Sektor-Dämpfung auf 0.025 und verschwindet aus dem Verlauf. # # Beispielwerte (Maximum=4.0, Exponent 0.5), mit den Reizen dahinter: # 0.13 → 0.22 (Echo) 0.26 → 0.32 (a 0.40) 0.50 → 0.44 (a 0.50) # 0.86 → 0.58 (a 0.60) 2.05 → 0.85 (a 0.80) 2.92 → 0.95 (a 0.90) # 4.00 → 1.00 (Cap; a 1.0 erreicht ihn im ersten Turn) # ───────────────────────────────────────────── EMOTION_HISTORIEN_GEWICHT: float = 0.15 EMOTION_GLAETTUNGS_MAXIMUM: float = 4.0 # ─── Nova-Empathie (Dual-Emotion Phase 2, AP3) ──────────── # Empathie-Koeffizient α abhängig von der Sektor-Distanz im Plutchik-Oktagon. # Niedrige Distanz (gleicher/benachbarter Sektor) → leichte Bestätigung. # Hohe Distanz (gegenüberliegend) → Empathie überschreibt Novas Zustand. # Menschliche Analogie: Wenn ein Freund meine Freude teilt, ändert sich wenig. # Wenn ein Freund zusammenbricht, ist meine Freude sofort weg. EMPATHIE_ALPHA: dict[int, float] = { 0: 0.20, # gleicher Sektor — leichte Bestätigung 1: 0.30, # benachbart — geringe Modulation 2: 0.45, # nah-diagonal — spürbare Modulation 3: 0.70, # fern-diagonal — Empathie dominiert 4: 0.85, # gegenüberliegend — Empathie überschreibt } # Default-Alpha wenn Novas Emotion neutral ist (kein Sektor bestimmbar) EMPATHIE_ALPHA_NEUTRAL: float = 0.30 # Schwellwert für Konfliktsignal: Nova und User in gegenüberliegenden Sektoren # UND beide mit relevantem Arousal → "Ich freue mich für dich, und gleichzeitig # mache ich mir Sorgen." EMPATHIE_KONFLIKT_DISTANZ: int = 3 # Ab Distanz 3 wird Konflikt geprüft EMPATHIE_KONFLIKT_MIN_AROUSAL: float = 0.4 # Beide müssen mindestens diesen Arousal haben # ───────────────────────────────────────────── # Drive / Gravitation # ───────────────────────────────────────────── # Minimum gravitation (sim × mot) für Aktivierung. # Kalibriert auf nomic-embed-text-v2-moe (Chat 107). # Grundrauschen 0.16, Median 0.26, p99 0.57 — gemessen an 302 lzg_knoten. # Vorher 0.60, begründet mit "Baseline ~0.55–0.60" — die echte Baseline des # casing-blinden Raums war 0.74, der Wert feuerte IMMER. Funktionslos. # ⚠ Wachposten: Prompt↔Ziel-Wert, andere Textsorte als die Knoten↔Knoten- # Kalibrierung; Ziele wurden nicht gemessen — begründeter Startwert, kein # Messergebnis. Nach Live-Betrieb prüfen. GRAVITATIONS_SCHWELLE: float = 0.40 GRAVITATIONS_SALIENZ_FAKTOR: float = 0.5 # Skalierung des Gravitationsterms auf die Salienz # ── Die Obergrenze des Gravitationsterms (09.09.2026) ─────────────────────── # Der Term ist die **Summe** ueber alle aktivierten Ziele und war damit # unbeschraenkt: `[gemessen 09.09.2026]` er erreicht **5,995**. Beide # Verbraucher brachen daran die Skala — im HumanGraph-Boost trugen **277 von # 498** Zeilen einen Term >= 1,0 und die Salienz stand danach immer auf 1,0, # waehrend die Bewertung des Modells im Mittel bei 0,327 lag; im Eigen-Pfad # gewann er das `max()` in 214 von 426 Faellen, und `eigen_pfad` reichte bis # 4,097 bei Zielspanne [0…1]. # # **Der Cap ist aus der gemessenen Spanne abgeleitet, nicht gesetzt** (max # 5,995, P99 5,925) — die naechste Zahl darueber. Er ist die Obergrenze der # `sin^0.5`-Normierung in `gravitationsterm_berechnen`; die Saettigung # erreicht 1 konstruktiv nie, bleibt streng monoton und macht deshalb aus # zwei verschiedenen Lagen nie dieselbe Zahl (`F-NAHT-1`). # # **Waechst der Term dauerhaft ueber diese Spanne, ist die Steilheit der Kurve # neu zu pruefen** — nicht der Cap zu erhoehen: Die Normierung bleibt auch # darueber geschlossen, sie flacht nur ab. GRAVITATIONSTERM_CAP: float = 6.0 # ── Der Zug aus Novas eigenem Zielsog (01.09.2026) ────────────────────────── # Novas Antrieb soll die Salienz ihrer Aeusserung **heben**, nicht mit ihr # konkurrieren. Bis heute stand er als zweiter Operand in einem `max()` und # entschied damit in **4 von 2786** protokollierten Zeilen (0,14 %): Mittel # 0,034 gegen 0,692 beim sprachlichen Antrieb. # # Die neue Form zieht auf die Luecke nach oben — dieselbe Auffuellregel, die # die Praegung benutzt: s' = s + beta(g) * g * (1 - s). Sie hebt, sobald ein # Sog da ist, senkt nie und bleibt ohne Normierung in [0, 1]. # # **beta haengt am Sog selbst**, als Logistische: # beta(g) = 1 / (1 + exp(-(g - SALIENZ_ZUG_G0) / SALIENZ_ZUG_W)) # # Roh-Aequivalent der beiden Zahlen: Sie legen die Kurve auf den Bereich, den # der Sog **im Betrieb tatsaechlich einnimmt** — Mitte `g 0,12 -> beta 0,40`, # oberes Ende `g 0,20 -> beta 0,95`. # # **Die zweite Fassung, am selben Tag.** Die erste lag auf drei gesetzten # Stuetzstellen (0,25/0,35/0,40 -> 0,15/0,65/0,75) und war gegen eine # **Stellvertreter-Verteilung** gelegt: 400 verdichtete Knoten mit Median # 0,308. `[gemessen]` 01.09.2026, 16:40 UTC, zwei Betriebsturns: Der Sog trug # **0,0821 und 0,2034**, und der Zuwachs lag bei +0,0003 und +0,0074 — der # Mechanismus unterschied um den Faktor 30 und bewegte nichts. Die neue Lage # traegt bei denselben zwei Turns +0,0035 und +0,0421, also rund das Sechsfache. # # **Die dritte Fassung, und der Unterschied ist die Skala, nicht die Vorgabe.** # Die Vorgabe lautete unveraendert: Mitte bei etwa 40 % Zug, oben gegen 100 %. # Fassung zwei legte sie auf die **Stellvertreter**-Skala (Median 0,308, oberes # Ende 0,427 — uebereinstimmend bei beiden Stellvertretern). Die vier **echten** # Messwerte des Betriebs liegen weit darunter: 0,0626 · 0,0821 · 0,1552 · # 0,2034, Median **0,1187**. Auf dieser Skala liegt die Mitte bei 0,12 und das # obere Ende bei 0,20. # # `[gemessen]` an denselben zwei Reizen, Zuwachs auf einen Antrieb von 0,5: # Fassung 1: nah +0,0020 (0,4 %) · fern +0,0001 — Verhaeltnis 13,9 : 1 # Fassung 2: nah +0,0164 (3,7 %) · fern +0,0016 — Verhaeltnis 9,5 : 1 # Fassung 3: nah +0,0511 (11,6 %) · fern +0,0015 — Verhaeltnis 32,5 : 1 # Die dritte ist auf beiden Seiten besser: merklicher **und** trennschaerfer. # # ⚠ **Und sie steht auf vier Messwerten aus einem einzigen Paar**, dessen Reize # fuer die Messung geschrieben wurden. Das ist die duennste Grundlage aller drei # Fassungen — sie kann auf genau diese Reize passen und sonst nichts. # # **beta(0) ist bedeutungslos und muss es sein**: Der Zuwachs ist # `beta(g) x g x (1 - antrieb)` und traegt `g` als Faktor. Ein Turn ohne Sog # bekommt nichts, gleich was die Kurve am Rand sagt. # # ⚠ Weiterhin ein Versuch. **Vier Messwerte sind keine Verteilung**, und ob die # Stellvertreter zu hoch liegen, weil sie verdichtet sind oder weil sie vom # anderen Paar stammen, ist mit dem Bestand nicht zu trennen — es gibt keinen # Korpus roher Nutzer-Prompts (`gespraech_archiv` traegt 0 Zeilen). Die # `salienz_formel`-Zeile traegt `zielsog` und `zug_staerke` mit; die Verteilung # entsteht im Betrieb (novaberg-salienz-berechnung_k.md §4a). SALIENZ_ZUG_G0: float = float(os.getenv("SALIENZ_ZUG_G0", "0.1297")) SALIENZ_ZUG_W: float = float(os.getenv("SALIENZ_ZUG_W", "0.0239")) ZIEL_MITTELFRISTIG_DECAY_TAGE: int = 14 # Halbwertszeit mittelfristiger Ziele in Tagen ZIEL_MAX_MITTELFRISTIG: int = 5 # Max aktive mittelfristige Ziele ZIEL_MAX_LANGFRISTIG: int = 2 # Max langfristige Ziele # Emotionale Gravitation (EI Phase 3) # Kalibriert auf nomic-embed-text-v2-moe (Chat 107). Vorher 0.5 im # casing-blinden Raum — funktionslos. Der alte Kommentar („höher als # Ziel-Gravitation 0.3") beschrieb zudem einen Wert, den es nie gab. # ⚠ Wachposten: Prompt↔Eintrag-Wert (gravitation = sim × gewicht × decay × # faktor), nicht gemessen — begründeter Startwert, kein Messergebnis. # ── Praegungsschicht: das Faden-Tor (Scheibe 1, 31.08.2026) ────────── # Zwei Bedingungen, und Arousal ist keine davon (Konzept §7.3). Das Tor traegt # die volle Last: Es ist das einzige, was die Fadenkarte von "jeder Turn ist ein # Faden" trennt. # # **Beide Zahlen sind Setzungen, keine Messungen** — und das ist ausdruecklich # vermerkt, weil eine Schwelle ohne Beobachtung heute schon einmal jahrelang # nichts abgelehnt hat (EMGRAV-SCHWELLE-TOT). Die Salienz ist messbar: # `[gemessen]` 30.08.2026 ueber 2747 Laeufe liegen 43 % ueber 0,90 und 48 % # ueber 0,85 — sie allein trennt schlecht, deshalb die Konjunktion. Der # Emotionsausschlag ist im Bestand **nicht** messbar, weil der Emotionsverlauf # nirgends persistiert wird; erst `praegung_faden.ausschlag_eingang` macht ihn # sichtbar. Das Tor protokolliert deshalb **jede** Pruefung, auch die # abgelehnte, damit beide Schwellen nach einer Woche kalibrierbar sind. # 0.90 war gegen den Gesamtkorpus kalibriert (43 % darueber) — und der besteht # zu erheblichem Teil aus frueheren Messturns, also aus langen Wissenschafts- # texten mit hoher Salienz. `[gemessen]` 31.08.2026 an sieben echten # Gespraechsturns: effektive Salienz **0,43 bis 0,86**, keiner ueber 0,90. Eine # Schwelle, die nur Messturns durchlaesst, misst das Werkzeug statt des # Gegenstands (`21_MESSUNG/messturns.md`). # Auf 0,70 gezogen am 31.08.2026: Sechs echte Gespraechsturns lagen bei # 0,442 bis 0,732 — der beste verfehlte 0,75 um 0,018. # # **Auf 0,60 gezogen am 31.08.2026, nach der Reizstaerke-Kalibrierung.** Die # Vorgabe lautet: rund ein Fuenftel der Turns soll praegen. Gemessen ueber 3677 # `bewertung`-Zeilen (Salienz) und 1718 nicht-neutrale `lzg_knoten` (Arousal): # 0,70 / 0,70 -> 14,9 % · **0,60 / 0,70 -> 21,1 %** · 0,50 / 0,70 -> 24,3 % # Gelockert wird die Bedingung, die Erinnerungswuerdigkeit misst; die auf den # emotionalen Gehalt bleibt streng. Herkunft: `novaberg-ei.md` §Reizstaerke. # # Die Grundgesamtheit ist **ohne neutrale Knoten** gerechnet — die filtert # `_emotions_verlauf_berechnen` in Schritt 1 heraus, sie erreichen das Tor nie. # Mit ihnen kaeme 8,9 % heraus; 1508 der 1560 Ausgeschlossenen sind neutral. PRAEGUNG_TOR_SALIENZ: float = float(os.getenv("PRAEGUNG_TOR_SALIENZ", "0.60")) PRAEGUNG_TOR_AUSSCHLAG: float = float(os.getenv("PRAEGUNG_TOR_AUSSCHLAG", "0.70")) # Wie nah eine reaktivierte Erinnerung einem Faden stehen muss, damit sie ihn # auffrischt (Konzept §7.4, §7.12 — der thematische Andockweg). # # `[gemessen]` 01.09.2026 ueber 19.900 Paare aus `lzg_knoten`: # ohne geteiltes Thema Median 0,355 p95 0,555 p99 0,620 # mit geteiltem Thema Median 0,504 # **Die Verteilungen ueberlappen breit** — der Median des thematischen Falls # liegt unter dem p95 des fremden. Eine Schwelle bei 0,50 faenge rund die # Haelfte der echten Treffer und bei diesem Mengenverhaeltnis etwa 990 fremde # gegen 45 richtige. # # Deshalb das p99 der fremden Paare: **wenige, aber verlaessliche Beruehrungen.** # Eine verpasste Auffrischung kostet einen Faden Lebensdauer, eine falsche macht # ihn unsterblich — und das ist derselbe Ausfall, wegen dem # `EMGRAV-SCHWELLE-TOT` ueberhaupt Vorbedingung der Praegungsschicht wurde # (§7.4). Der Preis ist benannt: Deutlich weniger als die Haelfte der echten # thematischen Treffer kommt durch. PRAEGUNG_BERUEHRUNG_NAEHE: float = float( os.getenv("PRAEGUNG_BERUEHRUNG_NAEHE", "0.62") ) # Die Faltung ueber die Beruehrungen (Konzept §7.4). Alle drei sind **Setzungen** # und warten auf Laufzeit: Sie sind erst entscheidbar, wenn Faeden ueber Wochen # gelebt haben. # # ALPHA — wie weit eine Beruehrung die Luecke zu `ausschlag_absolut` auffuellt. # Nicht 1,0: Ein voller Reset macht das Beruehrungsintervall bedeutungslos — # ein Thema, das einmal im Jahr aufkommt, stuende so hoch wie ein woechentli- # ches. Gerechnet ueber vier Werte (§7.4): Bei 0,33 trennt das Gleichgewicht # sieben Tage (0,790) von 365 Tagen (0,472); bei 1,0 stehen beide auf 0,900. # HALBSTRECKE — nach wie vielen Tagen die Haelfte der Spanne ueber dem Boden # verfallen ist. # BODEN — der Anteil, unter den ein Faden nie faellt. **Ein Faden wird nie # deaktiviert, nur leiser** (§7.4). PRAEGUNG_ALPHA: float = float(os.getenv("PRAEGUNG_ALPHA", "0.33")) PRAEGUNG_HALBSTRECKE: float = float(os.getenv("PRAEGUNG_HALBSTRECKE", "60")) PRAEGUNG_BODEN: float = float(os.getenv("PRAEGUNG_BODEN", "0.20")) # Wie nah ein Faden dem Zentroid eines Strangs stehen muss, um ihm beizutreten # (Konzept §7.7). **Eine eigene Konstante, obwohl der Startwert derselbe ist wie # bei `PRAEGUNG_BERUEHRUNG_NAEHE`:** Dort vergleicht ein Turn-Embedding gegen # einen einzelnen Faden, hier ein Faden gegen ein **Mittel ueber n Faeden**. # Ein Mittel liegt naeher an allem als jedes seiner Glieder — dieselbe Zahl # bedeutet an den zwei Stellen nicht dasselbe, und wer sie teilt, kann die eine # nicht bewegen, ohne die andere zu treffen. # # **Setzung mit Herkunft, keine Optimierung** (`F-INTENS-1`): 0,62 ist der Wert, # an dem die Reaktivierung im Betrieb trennt. Ob er auch fuer die Strangbildung # trennt, ist ungemessen — dafuer braucht es Faeden aus mehreren Tagen und # mehreren Themen. Zu hoch: jeder Faden gruendet seinen eigenen Strang, und die # Verdichtung faellt aus. Zu niedrig: alles ist ein Strang, und die Richtung # mittelt sich weg. **Beides sieht im Bestand gleich aus wie ein Erfolg**, und # nur die Zahl der Straenge gegen die Zahl der Faeden zeigt es. PRAEGUNG_STRANG_NAEHE: float = float(os.getenv("PRAEGUNG_STRANG_NAEHE", "0.62")) # ── Die Richtung eines Strangs (Konzept §7.7) ──────────────────────────────── # # **Annaeherung oder Vermeidung — und sie steht nicht im Bestand.** Ein Strang # ist Bestand, das Charakter-Rad ist Zustand: Es bewegte sich am 31.07.2026 # binnen zwei Stunden um 100 %. Eine gespeicherte Richtung waere die Antwort # von gestern auf die Frage von heute; sie wird deshalb bei jedem Lesen aus # Histogramm **und** Rad gerechnet. # # **Valenz ist nicht Richtung.** Zwei negative Praegungen koennen entgegen- # gesetzt zeigen: Machtlosigkeit → Macht ist Annaeherung, Furcht vor der # Dunkelheit ist Vermeidung (§7.7). # # Vorgabe des Eigentuemers, 01.09.2026: # „Auch Aerger und Ekel kann anziehen, aber ein normales Gemuet mit # Selbsterhaltungsdrang, Pflichtbewusstsein und Verantwortungsgefuehl wird # sich davor schuetzen wollen und eher vermeiden. Das wilde, furchtlose, # chaotische, neugierige Wesen wird aber die Konfrontation nicht scheuen. # Man muesste es am Haltungsrad festmachen. … Starke Neugier ist sicher ein # Faktor, der immer zieht." # # Acht der 22 Speichen tragen das Mass, vier gegen vier. Die uebrigen bleiben # aussen vor — sie beschreiben Zuwendung und Gespraechsform, nicht die Frage, # ob jemand einer unangenehmen Sache nachgeht. PRAEGUNG_SPEICHEN_WILD: tuple[str, ...] = ( "wissbegier", # zuwendung — Novas hoechste Speiche (0,864) "assoziationsdrang", # initiative "eigensinn", # initiative "widerspruchsfreude", # initiative — „die Konfrontation nicht scheuen" ) PRAEGUNG_SPEICHEN_SCHUETZEND: tuple[str, ...] = ( "pflicht", # zuwendung — Pflichtbewusstsein "misstrauen", # zuwendung "behutsamkeit", # initiative "zurueckhaltung", # initiative ) # Mittel der wilden minus Mittel der schuetzenden Speichen. **Die Schwelle ist # 0,0 und das ist eine Aussage, keine Beliebigkeit:** Wer mehr wild als # schuetzend ist, geht der unangenehmen Sache nach. Novas Stand am 01.09.2026 # lag bei **+0,52**, am 03.09.2026 nachgemessen bei **+0,5333** — sie wuerde # einem Aerger-Strang nachgehen. **Die Schwelle hat bis heute nie entschieden:** # Beide Straenge des Bestands stellt schon Regel 1 oder 3 auf Annaeherung, # und jeder Wert zwischen 0,0 und Novas Mass waere heute wirkungsgleich. PRAEGUNG_KONFRONTATION_SCHWELLE: float = float( os.getenv("PRAEGUNG_KONFRONTATION_SCHWELLE", "0.0") ) # Ab welchem Anteil Sektor 8 (Neugier, Hoffnung) einen Strang **unbedingt** auf # Annaeherung stellt — ohne das Rad zu fragen. Vorgabe des Eigentuemers: die # hoechste Stufe der Neugier bedeutet Zug, und die Anwesenheit ueber der # Schwelle genuegt. # # **Setzung mit Herkunft, keine Optimierung** (`F-INTENS-1`): Ein Viertel ist # mehr als Beiwerk und weniger als Dominanz. Bei vier Faeden ist es einer. # Ungemessen — es gibt bis heute einen Strang. PRAEGUNG_SEKTOR8_ZUG: float = float(os.getenv("PRAEGUNG_SEKTOR8_ZUG", "0.25")) # ── Die Ladung eines Strangs (Konzept §7.7, Fassung vom 02.09.2026) ────────── # # strang_staerke = ( W_SALIENZ · mittel(faden.salienz) # + W_VALENZ · mittel(|valenz_faden|) # + W_ANZAHL · norm(faden_zahl) ) # × f_praesenz( heute − letzte Beruehrung ) # # **Vorgabe des Eigentuemers, 02.09.2026:** *„Salienz, Valenz, Anzahl Faeden. # Das macht den Strang stark."* — und zur Anzahl: *„Wenn ich viele emotionale # Eindruecke (Faeden) habe, dann ist ein Thema intensiv gepraegt. Es ist # lebendig. Es ist praesent."* # # **Sie loest die Fassung des Konzepts ab**, die Anlaesse, Spitze und Spanne # nannte. Der Grund gegen Anlaesse dort war ein **Messfehler** — zwanzig Zeilen # aus einer Erhebung taeuschten eine Stichprobe von zwanzig vor. Hier ist es # keine Stichprobe: Das Tor hat jeden Faden einzeln durchgelassen (4 von 13 # Pruefungen im Betrieb), und zwanzig Faeden sind zwanzig Erlebnisse. # # **`mittel(|valenz|)` und nicht `|mittel(valenz)|`**, ebenfalls Vorgabe: *„Wenn # die sich aufheben wuerden, wuerden viele Faeden eigentlich zu einer Nullung # fuehren statt zu einer Intensivierung der Praegung."* Zwei Freude- und zwei # Trauerfaeden ergeben so **1,0** statt 0. PRAEGUNG_W_SALIENZ: float = float(os.getenv("PRAEGUNG_W_SALIENZ", "0.4")) PRAEGUNG_W_VALENZ: float = float(os.getenv("PRAEGUNG_W_VALENZ", "0.2")) PRAEGUNG_W_ANZAHL: float = float(os.getenv("PRAEGUNG_W_ANZAHL", "0.4")) # Saettigung der Fadenzahl: `n / (n + K)`. Ein Faden ergibt 0,20, vier ergeben # 0,50, zwanzig 0,83. **Kein Deckel und keine Obergrenze** — aber der zwanzigste # Faden traegt weniger als der zweite, sonst uebernaehme die Anzahl die ganze # Groesse. Ungemessen: Es gibt heute einen Strang mit vier Faeden. PRAEGUNG_ANZAHL_SAETTIGUNG: float = float( os.getenv("PRAEGUNG_ANZAHL_SAETTIGUNG", "4.0") ) # `f_praesenz` — dieselbe hyperbolische Form wie der Fadenverfall, mit eigenem # Boden und eigener Halbstrecke. **Ohne sie stuende ein Strang, der vor Jahren # endete, dauerhaft hoch** (§7.9): W_ANZAHL und die beiden Mittel kennen die # Gegenwart nicht. # # Der Boden liegt hoeher als beim Faden (0,20): Ein Strang ist die Summe # mehrerer Erlebnisse und verblasst langsamer als jedes einzelne. Beide Zahlen # sind Setzungen und warten auf Laufzeit. PRAEGUNG_PRAESENZ_BODEN: float = float( os.getenv("PRAEGUNG_PRAESENZ_BODEN", "0.35") ) PRAEGUNG_PRAESENZ_HALBSTRECKE: float = float( os.getenv("PRAEGUNG_PRAESENZ_HALBSTRECKE", "90") ) # ── Der Praegungszug (Konzept §10.3) ──────────────────────────────────────── # # praegungszug = 1.0 + PRAEGUNG_ZUG_HUB · max_j( sim_j · gewicht_j · ladung_j ) # # **Verstaerkt nur, daempft nie** — kein Tor, keine Null. Der Grund steht im # Konzept: Ein multiplikatives Tor auf einer Aehnlichkeit hebt die noetige # Naehe unbemerkt an; bei der Ziel-Gravitation ergab Tor 0,40 in **allen zwoelf** # betrachteten Laeufen `gravitationsterm = 0.0`. # # **Der Hub ist abgeleitet, nicht gesetzt** (`F-NAHT-1`): `sim` und `ladung` # liegen je auf [0, 1], ihr Produkt also auch. Der Hub ist damit genau die # Strecke zwischen 1,0 und dem oberen Ende der Zielspanne — das Ergebnis liegt # durch Konstruktion darin und wird **nicht gekappt**. Wer die Spanne aendert, # aendert eine Zahl, nicht zwei. PRAEGUNG_ZUG_SPANNE_OBEN: float = float( os.getenv("PRAEGUNG_ZUG_SPANNE_OBEN", "1.6") ) # `round`, damit `1.6 - 1.0` nicht als 0.6000000000000001 in jeder # Protokollzeile steht — die Zahl ist dieselbe, die Zeile lesbar. PRAEGUNG_ZUG_HUB: float = round(PRAEGUNG_ZUG_SPANNE_OBEN - 1.0, 6) # Wie schwer ein Strang **ohne** feststellbare Richtung im Zug wiegt. # # **Vorgabe des Eigentuemers, 03.09.2026:** Ein Strang auf `vermeidung` ist # genau das, *„was wir nicht als Faszination wollen — wir filtern es einfach # raus"*; er traegt 0. `unbestimmt` ist aber keine Vermeidung, sondern # **Unkenntnis**: Jedes junge Paar hat noch kein vollstaendiges Charakter-Rad # (gemessen 03.09.2026: 6 Radmessungen bei `mehmet`/`nova` gegen 208 bei # `meister`/`nova`), und ein Vorgabewert waere an dieser Stelle eine Aussage # ueber den Charakter, die niemand getroffen hat (`strang_richtung`). # # Das halbe Gewicht ist eine **Setzung mit Herkunft und ohne Messung**: Ein # junger Strang zieht, aber nicht so stark wie einer, dessen Richtung belegt # ist. Ungemessen — es gibt heute zwei Straenge, beide auf `annaeherung`. PRAEGUNG_ZUG_UNBESTIMMT: float = float( os.getenv("PRAEGUNG_ZUG_UNBESTIMMT", "0.5") ) # ── Die Einfaerbung: die zweite Stimme des Verfalls (Konzept §7.9, §8.4) ───── # # ausschlag_aktuell : Faltung mit t → Ladung, Faszination # einfaerbung : Faltung mit t x sektor_faktor → Ziele, LZG, EI-Calc # # **Der Fading-Affect-Bias, und ausdruecklich nur hier** (Walker & Skowronski; # Ritchie et al. 2016): Die emotionale Intensitaet negativer Erinnerungen # verblasst schneller als die positiver. Wirkte er auf den Ausschlag, verloere # Kriegsgeschichte ueber Monate gegen Gartenkraeuter und die Valenzblindheit der # Faszination fiele durch Absicht. **Das alte Unrecht zieht schwaecher am # Gefuehl und gleich stark an der Aufmerksamkeit.** # # **Nicht aus `EMOTION_AROUSAL_DECAY` abgeleitet, und das ist ein Befund.** Das # Konzept nennt die Tabelle als Bauform — sie liefert die *Form* (16 Raten), aber # ihre Werte sagen das Gegenteil: Trauer 0,02 (*„graebt sich ein"*), Freude 0,10. # Sie beschreibt die **Erregung im Turn**, nicht den Affekt einer Erinnerung ueber # Monate; eine Ableitung daraus kehrte den Bias um. Zwei verschiedene Groessen auf # zwei verschiedenen Zeitskalen. # # **Der Betrag ist eine Setzung mit Herkunft, keine Messung** (`F-INTENS-1`): 1,5 # ist das Verhaeltnis der Asymmetrie, die das Konzept an derselben Stelle nennt — # das Charakter-Rad zieht 0,60 nach oben und 0,40 nach unten. So traegt das # Projekt **eine** Asymmetrie und nicht zwei. Bei Halbstrecke 60 Tagen heisst 1,5: # Ein Trauer-Faden steht nach 40 Tagen dort, wo ein Freude-Faden nach 60 steht. # # **Sektor 4 steht auf 1,0, und das ist die schwaechere Behauptung.** Der Bias # spricht ueber Valenz; Ueberraschung traegt keine. Ein Wert ueber 1,0 waere eine # Aussage ueber neutralen Affekt, die niemand belegt hat. PRAEGUNG_SEKTOR_FAKTOR: tuple[float, ...] = ( 1.0, # 1 Freude positiv 1.0, # 2 Zuversicht positiv 1.5, # 3 Angst negativ 1.0, # 4 Ueberraschung neutral — keine Valenz, keine Aussage 1.5, # 5 Trauer negativ 1.5, # 6 Enttaeuschung negativ 1.5, # 7 Aerger negativ 1.0, # 8 Neugier positiv ) # ───────────────────────────────────────────────────────────── # Die abstrakte Schicht — Qualitaetsprofile # `novaberg-thinking-faszination_k.md` §5, §6, §10.1 # ───────────────────────────────────────────────────────────── # **Der Kanon existiert als Konstante**, nicht nur im Schema. Eine # geschlossene Wertemenge ohne deklarierte Obermenge ist nicht validierbar — # man kann sie benutzen, aber nicht pruefen. Die Antwort eines Sprachmodells # ist die unzuverlaessigste Quelle im System; ohne diesen Satz waere ein # erfundener Dimensionsname von einem gueltigen Nein nicht zu unterscheiden. # # Die Reihenfolge ist die des Konzepts §6.1 und traegt keine Bedeutung — der # Merkmalszug nimmt das Maximum, nicht die erste Dimension. QUALITAET_KANON: tuple[str, ...] = ( "komplexitaet", "ungewissheit", "konflikt", "weite", "schemasprengung", "bedrohungsrelevanz", ) # Die erlaubten Auspraegungen. **Drei Stufen, keine freie Skala** (§5): Die # bewaehrte Bauart ist ein LLM-Call, der jede Dimension einzeln mit # 0.0/0.5/1.0 bewertet — wie die Raeder. Der Versuch, eine abstrakte Groesse # ueber Cosine-Distanz zu gewinnen, ist in Chat 114 gemessen gescheitert # (Kunstfiguren ±0,24, echter Charakter +0,036 mit wechselndem Vorzeichen). # Der Unterschied ist die Form der Frage, nicht die Feinheit der Skala. QUALITAET_STUFEN: tuple[float, ...] = (0.0, 0.5, 1.0) # **Profiliert wird erst, was wiedergekehrt ist** (§6.3): Man fragt sich nicht # beim ersten Mal, was einen an einer Sache fasziniert. Ein LLM-Call je # Traeger ist der Preis, und die Daempfung liegt in der Groesse selbst. # # Dazu ein Laengenfilter — ~~die Sachtexte sind mehrere hundert Woerter, die # Sprechakt-Vermerke ein bis zwei Saetze; ein Laengenschnitt trifft fast # dieselbe Menge wie eine Formklassifikation und kostet keinen Aufruf.~~ # # `[gemessen]` 03.09.2026 ueber 3318 Knoten: Wiederkehr >= 2 allein trifft # 1538, Laenge >= 400 allein 1283, **beide zusammen 368**. Das ist die # Groessenordnung des Erstlaufs. # # **Das Versprechen ist am 06.09.2026 widerlegt: Die Laenge trennt die beiden # Klassen nicht.** Ueber die 83 Knoten, die der Lesepfad im Turn anbietet und # die den Wiederkehr-Filter passieren, nach der Form ihres Satzanfangs # klassifiziert: # # Sachaussage im Rahmen 32 Knoten, Mittel 271 Zeichen, davon 1 ueber 400 # Sprechakt / Beziehung 29 Knoten, Mittel 281 Zeichen, davon 8 ueber 400 # ohne erkennbaren Rahmen 22 Knoten, Mittel 753 Zeichen, davon 9 ueber 400 # # **Die beiden Klassen, die der Schnitt trennen soll, liegen bei 271 und 281.** # Die Schwelle 400 liess acht Sprechakt-Vermerke durch und genau eine # Sachaussage — sie trifft nicht zu streng, sie trifft falsch herum. Und die # Wirkung war die teure Seite davon: **0 von 95 je gelesenen Traegern standen # als Kandidat offen**, alle 18, die die Filter passierten, waren laengst # profiliert. Die Faszination blieb im Turn leer, weil ihre Warteschlange # leer war — `traeger_geprueft: 3, ohne_profil: 3, werte: {}` in **jedem** # protokollierten Turn. # # **Die Klassifikation liegt beim Modell, nicht hier.** Der Profil-Prompt # erlaubt ausdruecklich 0.0 auf allen sechs Dimensionen, ein Nullprofil wird # geschrieben, und der Traeger faellt danach aus der Kandidatenmenge. Ein # Formfilter davor waere eine zweite, schlechtere Kopie dieses Urteils — der # Versuch, ihn als Regex zu bauen, sortierte in der Gegenprobe **drei von # sechs** Stichproben falsch ein (*„Nova ist aufgegangen, dass ..."* ist eine # Einsicht, kein Sprechakt). # # **Die Schwelle faengt seither nur noch den Ein-Satz-Vermerk.** Die kuerzesten # Knoten im Bestand liegen bei 50 bis 59 Zeichen (*„Der Nutzer hat ein kleines, # freches Maedchen bei sich."*); 100 haelt Abstand dazu. **Der Preis ist # benannt:** Die kuerzeste gemessene Sachaussage hat 89 Zeichen und faellt # weiterhin durch. `[gemessen 06.09.2026]` offene Kandidaten unter den # gelesenen: **0** bei 400, **55** bei 100 — bei 20 je Lauf und Sortierung nach # Lesespur drei Tage statt 153. QUALITAET_WIEDERKEHR_MIN: int = 2 QUALITAET_LAENGE_MIN: int = 100 # **Gedeckelt je Tageslauf, nicht in einem Zug.** 368 Modellaufrufe passen # nicht in einen Heartbeat-Platz; bei diesem Deckel fuellt sich der Bestand # in rund drei Wochen, und ein Ausfall kostet einen Tag statt eines Laufs. #: Wie oft der Profil-Agent drankommt. **Einmal am Tag — Setzung des #: Eigentuemers, 06.09.2026.** Gemessen an diesem Tag: **350 Kandidaten** #: offen; bei 20 je Lauf fuellt sich der Bestand in rund **18 Tagen**. PIXIE_QUALITAET_INTERVALL_SEKUNDEN: int = 86_400 #: Seine Dringlichkeit im Heartbeat. Wie der Tageslauf: Ein Wartungslauf #: draengt nicht, er soll nur zuverlaessig drankommen. PIXIE_QUALITAET_PRIORITAET: float = PIXIE_DECAY_PRIORITAET QUALITAET_PROFIL_JE_LAUF: int = 20 # **Wie weit zurueck ein Lesevorgang als „frisch" zaehlt.** Die Kandidatensuche # sortiert seit dem 11.09.2026 zuerst danach, wie oft ein Traeger in diesem # Fenster gelesen wurde, und erst danach nach seiner Gesamtlesezahl. # # **Der Grund ist eine Messung, keine Schaetzung.** `[11.09.2026]` Ueber 363 # Turns meldete die Faszination in **74,1 %** der Faelle `werte: {}` — kein # einziger gelesener Traeger trug ein Profil. Von 42 solchen Traegern einer # Messreihe standen **22 als offene Kandidaten** in der Warteschlange: richtig # eingereiht und nicht an der Reihe. Bei 1497 offenen Kandidaten und 20 je # Tageslauf braucht der Bestand **75 Tage**; ein Knoten, der heute zum ersten # Mal gelesen wird, traegt Lesezahl 1 und steht hinter allen, die zwanzigmal # gelesen wurden — obwohl die Faszination genau ihn jetzt braucht. # # **Sieben Tage, weil das Fenster den Gespraechsverlauf abdecken soll und nicht # den Bestand.** Kuerzer faenge nur den heutigen Tag und liesse einen Traeger # fallen, der gestern dreimal vorkam; laenger naeherte sich wieder der # Gesamtlesezahl an, deren Schlagseite die Sortierung gerade behebt. QUALITAET_FRISCHE_TAGE: int = 7 # Der Zuschlag des Merkmalszugs (§10.1). # # **Ein Mittelwert waere falsch:** Eine Dimension auf 1,0 und fuenf auf 0 # ergaeben 0,17, und der Zauberer bekaeme keine Faszination — obwohl gerade # seine Fingerfertigkeit die Ungewissheit traegt. Ein Produkt verstiesse # gegen Regel (a) aus §10.0: keine Null aus einer Multiplikation. # # merkmalszug = m_max + MERKMALSZUG_BONUS * Mittel(uebrige fuenf) # # **Die staerkste Dimension traegt allein und vollstaendig. Kombination ist # ein Zuschlag, keine Bedingung.** Der Wert ist eine Setzung aus dem Konzept # und ungemessen — er ist erst kalibrierbar, wenn Profile im Bestand stehen. MERKMALSZUG_BONUS: float = 0.35 # ───────────────────────────────────────────────────────────── # Die sechs Turn-Modulatoren der Faszination # `novaberg-thinking-faszination_k.md` §10.5 # ───────────────────────────────────────────────────────────── # Sie beantworten nicht *ob* ein Traeger fasziniert, sondern *wie stark der # laufende Turn dazu beitraegt*. Alle sechs sind Faktoren um 1.0 und duerfen # nie 0 werden — Regel (a) aus §10.0: keine Null aus einer Multiplikation. # # **Jeder Kanonwert traegt einen eigenen Eintrag, auch der seltene.** Ein # fehlender Schluessel faende sonst einen stillen Vorgabewert, und ein # Vorgabewert in einem Produkt ist von einem gemessenen Faktor nicht zu # unterscheiden. Die Tabellen sind deshalb **vollstaendig gegen ihren Kanon**; # `tests/test_faszination_modulatoren.py` haelt beide Seiten zusammen. # # **Die Spannen stammen aus dem Konzept und sind bindend.** Das Konzept nennt # je Tabelle nur die tragenden Werte; die uebrigen sind hier nach derselben # Systematik ergaenzt und als Setzung gekennzeichnet. Die Verteilung des # Bestandes steht daneben `[gemessen 04.09.2026, 1027 Rohturns]`, damit # ablesbar ist, welcher Eintrag ueberhaupt traegt. # f_arousal — umgekehrtes U mit Scheitel bei 0,65 (Berlyne). # Spanne 0.70 … 1.35. Weder Reglosigkeit noch Ueberreizung binden # Aufmerksamkeit muehelos; dazwischen liegt das Maximum. FASZ_AROUSAL_SCHEITEL: float = 0.65 FASZ_AROUSAL_MIN: float = 0.70 FASZ_AROUSAL_MAX: float = 1.35 # **Die beiden Flanken sind verschieden breit, und das ist keine Nachlaessigkeit.** # Der Scheitel liegt bei 0,65; nach links sind es 0,65 bis zum Rand, nach # rechts nur 0,35. Eine ueber *eine* Breite normierte Parabel erreichte den # Minimalwert deshalb nur links — bei arousal = 1.0 stuende sie noch bei 1,16. # Jede Flanke wird darum ueber ihren eigenen Abstand normiert, und beide # Raender des Wertebereichs erreichen FASZ_AROUSAL_MIN. # Die rechte Flanke faellt damit steiler, was §10.5 ausdruecklich verlangt # (*ueber 0,85 fallend*): Ueberreizung entzieht schneller als Reglosigkeit. FASZ_AROUSAL_BREITE_LINKS: float = round(FASZ_AROUSAL_SCHEITEL, 6) FASZ_AROUSAL_BREITE_RECHTS: float = round(1.0 - FASZ_AROUSAL_SCHEITEL, 6) # f_besetzung — `neutral` 0.70, jeder besetzte Sektor 1.10, Awe-Dyade 1.20. # `SEKTOR_GRUPPE` wird bewusst ignoriert (§10.5): Faszination ist valenzblind, # ein negativ besetzter Sektor bindet so gut wie ein positiver. FASZ_BESETZUNG_NEUTRAL: float = 0.70 FASZ_BESETZUNG_SEKTOR: float = 1.10 FASZ_BESETZUNG_AWE: float = 1.20 # Die Awe-Dyade: Ehrfurcht = Furcht + Ueberraschung (Plutchik). Sie ist der # eine Zustand, den die Literatur ausdruecklich mit Faszination verbindet # (§2.1). Die Schreibweisen stehen beide, weil der Bestand beide traegt. FASZ_AWE_EMOTIONEN: frozenset[str] = frozenset({ "ehrfurcht", "awe", "staunen", }) # f_verlauf — Spanne 0.80 … 1.25 ueber `EMOTIONS_VEKTOREN` (9 Werte). # Aus dem Konzept: aufsteigend 1.25, `plateau` 0.90, scharf fallend 0.80. # **Die Achse ist die Bewegung, nicht die Richtung** — `eskalation` ist # negativ und steht trotzdem oben, weil ein sich aufbauender Zustand bindet. FASZ_VERLAUF_FAKTOREN: dict[str, float] = { # aufsteigend — Konzept "aufbluehen": 1.25, # [gemessen] 69 von 1027 "eskalation": 1.25, # [gemessen] 213 — der zweithaeufigste Wert # aufsteigend, aber aus dem Tief heraus und sanfter — Setzung "erholung": 1.10, # [gemessen] 12 # flach — Konzept nennt `plateau`, `stabilisierung` folgt ihm "plateau": 0.90, # [gemessen] 592 — der haeufigste Wert "stabilisierung": 0.95, # Setzung: Beruhigung ist flach, aber gerichtet # fallend, sanft — Setzung "abkuehlung": 0.85, # [gemessen] 100 # fallend, scharf — Konzept "absturz": 0.80, # [gemessen] 16 "spirale": 0.80, # [gemessen] 0 — im Bestand nie vorgekommen "einbruch": 0.80, # Setzung: dieselbe Klasse wie `absturz` } # f_intent — Spanne 0.85 … 1.20 ueber den Intent-Kanon (6 Werte). # Aus dem Konzept: `knowledge`/`creative` 1.20, `personal` 1.05, # `task`/`meta` 0.85. `smalltalk` ergaenzt. FASZ_INTENT_FAKTOREN: dict[str, float] = { "knowledge": 1.20, # [gemessen] 395 — der haeufigste Wert "creative": 1.20, # [gemessen] 78 "personal": 1.05, # [gemessen] 214 "task": 0.85, # [gemessen] 123 "meta": 0.85, # [gemessen] 83 "smalltalk": 0.85, # Setzung: am wenigsten an einen Gegenstand gebunden } # Der neutrale Faktor fuer einen Wert ausserhalb des Kanons: das Mittel der # Tabelle, als Literal (dieselbe Regel wie bei `EI_INTENT_NEUTRAL`). FASZ_INTENT_NEUTRAL: float = 1.000 # 6.0 / 6 # f_modus — Spanne 0.90 … 1.15 ueber `MODUS_KANON` (10 Werte). # Aus dem Konzept: `lernmodus`/`philosophischer_austausch` 1.15, # `berichtend`/`arbeitsmodus` 0.90. Die uebrigen sechs sind Setzungen nach # derselben Frage: **Wird in diesem Modus ein Gegenstand vertieft?** FASZ_MODUS_FAKTOREN: dict[str, float] = { # vertiefend — Konzept "lernmodus": 1.15, # [gemessen] 190 "philosophischer_austausch": 1.15, # [gemessen] 192 # vertiefend — Setzung, dieselbe Klasse "fachgespraech": 1.15, # [gemessen] 84 # offen, aber nicht auf einen Gegenstand gerichtet — Setzung "kreativ": 1.05, # [gemessen] 0 "emotional": 1.00, # [gemessen] 136 "beratend": 1.00, # [gemessen] 8 "spielerisch": 0.95, # [gemessen] 63 "alltag": 0.95, # [gemessen] 244 — der haeufigste Wert # abwickelnd — Konzept "arbeitsmodus": 0.90, # [gemessen] 57 "berichtend": 0.90, # [gemessen] 53 } FASZ_MODUS_NEUTRAL: float = 1.020 # 10.2 / 10 — das Mittel der Tabelle # f_anlage — Spanne 0.75 … 1.30, aus `charakter_rad_messung`. # Die eine der zwoelf Radspeichen, die die Zuwendung zum **Gegenstand** # beschreibt statt zur Person: `wissbegier <-> langeweile` (§10.5). FASZ_ANLAGE_MIN: float = 0.75 FASZ_ANLAGE_MAX: float = 1.30 # ───────────────────────────────────────────────────────────── # Die Rueckrichtung: die Faszination moduliert den Haltungsraum # `novaberg-thinking-faszination_k.md` §11 # ───────────────────────────────────────────────────────────── # # **`wissbegier` ist eine Anlage und darf nicht themenblind wirken** — wirken # soll die Anlage **mal der Bindung an das gerade gelesene Thema** (§11). Das # ist die einzige Stelle, an der die Faszination heute etwas bewirkt; ueberall # sonst wird sie gerechnet und protokolliert. # # **Ein Faktor um 1.0, nicht der rohe Wert** — dieselbe Bauart wie die sechs # Turn-Modulatoren und aus demselben Grund: Die Faszination liegt in [0, 1], # und wer sie roh multipliziert, daempft in **jedem** Turn. Ein Turn ohne # profilierten Traeger stuende dann besser da als einer mit halber # Faszination — die Groesse wuerde Abdeckung messen statt Bindung. Die Naht # zwischen den beiden Skalen traegt deshalb einen benannten Abbildungsfaktor # (`F-NAHT-1`). # # **Der neutrale Punkt liegt bei 0.36 — gemessen, nicht gesetzt.** Er ist der # Median der Groesse, die hier tatsaechlich eingeht: `[gemessen 07.09.2026]` # ueber die letzten 25 echten Turns liefert `faszination_der_gelesenen` in 15 # von ihnen einen Wert, und der liegt zwischen **0.2704 und 0.5177**, Median # **0.3619**. # # **Die erste Fassung stand auf 0.50 und war gegen die falsche Verteilung # geeicht.** Sie nahm den Median der **Turn**-Faszination (0.5712 ueber 27 # Traeger, erster Turn-Wert 0.5725) — das ist die Groesse *mit* den sechs # Turn-Modulatoren, die zusammen Faktor 16,2 spannen. Der Leser rechnet mit # der **Traegerseite**, und die liegt eine halbe Spanne tiefer. Die Folge war # gemessen und eindeutig: **12 von 15 Turns unter dem neutralen Punkt**, # Median-Faktor 0.8895 — der Leser haette Novas Wissbegier systematisch # gedaempft statt sie zu differenzieren, und sie haette im Mittel **weniger** # gefragt als vor dem Umbau. # # > **Ein Modulator, dessen neutraler Punkt neben der Verteilung liegt, # > verschiebt — er moduliert nicht.** Das ist dieselbe Klasse wie eine # > Schwelle, die nie ausloest, mit umgekehrtem Vorzeichen: Sie loest immer # > aus, und zwar in eine Richtung. # # **Der Wert wandert und gehoert in die Kalibrierreihe.** 15 Turns sind eine # kleine Stichprobe, und die Profilabdeckung waechst um 20 Traeger je Tag — # je mehr gelesene Knoten ein Profil tragen, desto hoeher das Maximum ueber # sie. Je Turn sind es heute **median 1** profilierter Traeger (max 2), das # Maximum ist damit fast immer ein Einzelwert. # # Die Spanne ist enger als bei `f_anlage`, weil sie auf einen Beitrag wirkt, # der bereits im Ergebnis steht, und nicht auf einen Faktor unter neun. HALTUNG_FASZINATION_MIN: float = 0.60 HALTUNG_FASZINATION_MAX: float = 1.40 HALTUNG_FASZINATION_NEUTRAL: float = 0.36 # ───────────────────────────────────────────────────────────── # Der Verfall der Qualitaeten — je Dimension verschieden # `novaberg-thinking-faszination_k.md` §10.4 # ───────────────────────────────────────────────────────────── # # **Faszination erlischt genau dann, wenn ihre tragende Dimension # erschoepfbar ist.** Neugier haengt *immer* an einer Luecke, Faszination # *manchmal* — und deshalb verfallen die sechs Dimensionen nicht gleich: # # `ungewissheit` verfaellt mit der **Zahl der Beruehrungen**. Wer eine # Sache oft genug angesehen hat, weiss, wie sie ausgeht. # alle uebrigen verfallen mit der **Zeit seit der letzten Beruehrung**. # Komplexitaet erschoepft sich nicht durch Hinsehen. QUALITAET_VERFALL_UEBER_BERUEHRUNGEN: frozenset[str] = frozenset({ "ungewissheit", }) # **Dieselbe Kurvenform wie beim Praegungsverfall, und das ist eine # Entscheidung.** `v(x) = boden + (1 - boden) / (1 + x/H)` — hyperbolisch mit # Boden, mit dem fetten Schwanz, den Vergessenskurven zeigen. Zwei # verschiedene Verfallsformen im selben Konzept waeren eine Setzung, die # niemand getroffen hat; wer die Form aendern will, aendert beide oder # begruendet den Unterschied. # # **Der Boden ist hoeher als beim Faden (0,20).** Eine Qualitaet beschreibt, # was eine Sache **ist** — Komplexitaet vergeht nicht dadurch, dass niemand # hinsieht. Was verfaellt, ist ihre Zugkraft, nicht ihr Bestand. QUALITAET_VERFALL_BODEN: float = 0.40 # Nach wie vielen Tagen ohne Beruehrung die halbe Spanne ueber dem Boden # erreicht ist. **Deutlich laenger als der Faden** (60 Tage): Ein Faden ist # ein Erlebnis und verblasst, eine Qualitaet ist eine Eigenschaft. QUALITAET_VERFALL_HALBSTRECKE_TAGE: float = 180.0 # Nach wie vielen Beruehrungen die halbe Spanne erreicht ist. **Setzung mit # Herkunft, nicht Messung:** `[gemessen 05.09.2026]` traegt kein einziger der # 28 profilierten Traeger mehr als eine Beruehrung — die Groesse ist am # heutigen Bestand nicht kalibrierbar. Fuenf ist die Zahl, bei der ein Mensch # eine Sache typischerweise nicht mehr fuer offen haelt. QUALITAET_VERFALL_HALBSTRECKE_BERUEHRUNGEN: float = 5.0 # ───────────────────────────────────────────────────────────── # Der Anker — Bindung ueber Episoden # `novaberg-thinking-faszination_k.md` §10.2 # ───────────────────────────────────────────────────────────── # # bindung_roh = 0.50 * norm(wiederkehr) # + 0.20 * norm(verweildauer) # + 0.30 * eigenimpuls # # **Die Gewichtung ist eine Aussage** (§10.2): Der Eigenimpuls wiegt schwerer # als die Verweildauer, weil ein Thema, das der Nutzer dreimal einbringt, # **seine** Faszination belegt, nicht ihre. Die Wiederkehr wiegt am schwersten, # weil sie Faszination von Neugier trennt. BINDUNG_GEWICHTE: dict[str, float] = { "wiederkehr": 0.50, "verweildauer": 0.20, "eigenimpuls": 0.30, } # **Die Normierung ist eine Saettigung, keine Min-Max-Streckung** — und das ist # eine Entscheidung gegen den naheliegenden Weg. Eine Normierung ueber den # Bestand haette einen **wandernden Bezugspunkt**: Derselbe Traeger bekaeme # morgen einen anderen Wert, weil ein anderer Traeger gewachsen ist. Was # gemessen wurde, waere danach nicht mehr von der Skala zu trennen. # # norm(n) = n / (n + H) # # H ist die **Halbstrecke**: bei n = H steht der Term auf 0,5. Die Kurve ist # nach oben offen und erreicht 1 nie — passend zu Zaehlern ohne Obergrenze # (§13, *Die Normierung der Zaehler*). # # `[gemessen]` 04.09.2026 ueber 2.377 Knoten mit Bruecke: **2.362 tragen # wiederkehr = 1**, 13 tragen 2, je einer 3 und 4. Bei der Verweildauer stehen # 2.320 auf einem einzigen Turn. **Beide Zaehler trennen heute nichts** — die # Bruecke ist 39 Tage alt, und Wiederkehr braucht Kalenderzeit. Die # Halbstrecken sind deshalb aus der **Bedeutung** gesetzt und nicht aus der # Verteilung: Sie waere eine Aussage ueber das Alter des Bestandes. BINDUNG_HALBSTRECKE_WIEDERKEHR: float = 3.0 # drei verschiedene Tage BINDUNG_HALBSTRECKE_VERWEILDAUER: float = 3.0 # drei Turns je Episode # ───────────────────────────────────────────────────────────── # Der Strangzug — die Lage des **Traegers** zu einer Praegung # `novaberg-thinking-faszination_k.md` §10.3a # ───────────────────────────────────────────────────────────── # # strangzug = 1.0 + FASZ_STRANGZUG_HUB * naehe * saettigung(faden_zahl) # # **Eine andere Groesse als der Praegungszug, und die Verwechslung war der # Fund vom 05.09.2026.** Der Praegungszug (§10.3) misst die Lage **des Turns** # zum Strang und liefert einen Wert je Turn — alle Traeger eines Turns # bekommen denselben. Der Strangzug misst die Lage **des Traegers**: Ein # Knoten im Zentrum eines Strangs unterscheidet sich damit von einem am Rand. # # Vorgabe des Eigentuemers (05.09.2026): *„Ein Strang ist ein kleiner Bereich # im 768-dimensionalen Raum und hat ein Einflussgebiet. Liegt das Embedding # eines Knotens darin, wird dafuer Faszination empfunden — in der Mitte stark, # am Rand schwach."* # # **Die Fadenzahl traegt mit, weil ein starker Strang weiter zieht.** Sie geht # gesaettigt ein (`n/(n+H)`), damit ein Strang mit vierzig Faeden nicht # vierzigmal so weit zieht wie einer mit einem. `[gemessen 05.09.2026]`: Der # groesste Strang des Bestandes traegt **7 Faeden**, der Median liegt bei 2 — # die Halbstrecke 3 liegt dazwischen und ist eine Setzung, keine Optimierung. FASZ_STRANGZUG_HUB: float = 0.60 # dieselbe Spanne wie §10.3 FASZ_STRANGZUG_HALBSTRECKE_FAEDEN: float = 3.0 # Der harte Deckel der Zusammenfuehrung (§10.6). Das Roh-Aequivalent von # faszination = 1.0; jede daraus abgeleitete Konstante nennt es im Kommentar # (Regel (7) der Wertekonvention). FASZ_MAXIMUM: float = 2.0 # ───────────────────────────────────────────────────────────── # Verstaerkung setzt Verwendung voraus # `novaberg-memory-synapsen_k.md` §7.1a # ───────────────────────────────────────────────────────────── # Ab welcher Naehe zwischen der Antwort und einer gelesenen Erinnerung gilt # die Erinnerung als **hergenommen** — und wird verstaerkt. # # **Der Wert ist abgeleitet, nicht gesetzt.** `[gemessen]` 04.09.2026 ueber # **75.975 Paare** (25 echte Antworten des Paares `meister` gegen 3.039 # aktive Knoten): # # P50 0,2664 P99 0,4770 (~30 Knoten je Antwort) # P90 0,3874 P99,9 0,5412 (~3 Knoten je Antwort) # P99,99 0,6672 (<1) # # 0,55 liegt zwischen P99,9 und P99,99: Je Antwort passieren **ein bis drei** # von 3.039 Knoten, und **15 von 25** Turns tragen ueberhaupt eine # Verstaerkung. Das ist die Groessenordnung, die das Konzept verlangt — sie # liest drei und nimmt eine. Zum Vergleich: 0,50 liesse ~30 Knoten je Antwort # durch, 0,60 weniger als einen. # # **Der Vorbehalt gehoert an die Zahl.** Die Verteilung oben ist die # **Gesamt**verteilung und enthaelt die Treffer; eine echte Nulllinie ueber # nachweislich fremde Knoten gibt es nicht, weil die Ground Truth fehlt. Das # macht die Schwelle eher zu hoch als zu niedrig — die konservative Richtung. # Ab dem 04.09.2026 protokolliert der Enricher `lzg_resonanz_ids`; damit ist # sie ueber Laufzeit an echten Turns nachmessbar. VERWENDUNG_NAEHE_SCHWELLE: float = float( os.getenv("VERWENDUNG_NAEHE_SCHWELLE", "0.55") ) # Wie viele Erinnerungen ein Turn hoechstens verstaerken darf. # `[gemessen]` 04.09.2026: Der Lesepfad liefert im Mittel **1,90** und # hoechstens **3** Erinnerungen je Turn — der Deckel ist damit heute keine # Beschraenkung, sondern eine Zusicherung gegen einen kuenftig breiteren # Lesepfad. Ohne ihn truege ein Turn, der zwanzig Erinnerungen liest, zwanzig # Verstaerkungen bei, und die Verwendung waere wieder von Nachbarschaft nicht # zu unterscheiden. VERWENDUNG_MAX_JE_TURN: int = int(os.getenv("VERWENDUNG_MAX_JE_TURN", "3")) # Herkunft der Zahl (F-INTENS-1: die Schwelle traegt ihr Raster im Kommentar). # Sie gilt gegen Gravitationswerte auf [0,1] — beide Quellen liefern seit dem # 30.08.2026 auf dieser Skala (LZG teilt durch LZG_KNOTEN_GEWICHT_CAP, KZG liest # die bereits normierte Salienz). # # 0.40 war gegen die UNnormierte LZG-Skala gesetzt und lehnte dort nichts mehr ab: # gemessen am 30.08.2026 riss jeder der 1711 scanbaren Knoten sie schon bei # similarity < 0,30 (`gewicht_decay` Median 3,77, Maximum 9,98). Nachgerechnet # ueber 56 Turns ergibt die normierte Skala je Schwelle: 0,20 -> 0,50 Treffer je # Turn · 0,10 -> 6,30 · 0,05 -> 9,91. # # ~~Der erreichbare Hoechstwert liegt bei 0,287, weshalb 0,40 nach der # Normierung NIE ausloesen wuerde.~~ → **Widerlegt am 01.09.2026.** `[gemessen]` # ueber **71 aktivierte** Gravitationspunkte im `pipeline_log`: Hoechstwert # **0,6640**, Mittel 0,3518; **21 davon liegen ueber 0,40**. Die Zahl 0,287 war # aus 56 Turns vor der Normierung hochgerechnet und ist ueberholt — die # Begruendung des Bugs `EMGRAV-SCHWELLE-TOT` bleibt richtig (0,40 lehnte auf der # unnormierten Skala nichts ab), seine Vorhersage ueber die normierte nicht. # # **Was diese Zahlen NICHT sagen:** Die 71 sind die *aktivierten* Punkte und # damit per Bauart alle ueber 0,18. Ueber die Verteilung der abgelehnten # Kandidaten sagt der Bestand nichts — der Log fuehrt nur, was durchkam. Eine # Aussage darueber, wie viel eine andere Schwelle durchliesse, braucht die # ungefilterte Menge und ist damit heute nicht zu haben. EMOTIONALE_GRAVITATIONS_SCHWELLE: float = 0.18 EMOTIONALE_GRAVITATION_ZEIT_HALBWERT: int = 180 # Halbwertszeit in Tagen EMOTIONALE_GRAVITATION_MAX_PRO_TURN: int = 2 # Max aktivierte Erinnerungen pro Turn EMOTIONALE_GRAVITATION_FAKTOR_SESSION: float = 1.0 # Session-Einträge: frisch, volle Wirkung EMOTIONALE_GRAVITATION_FAKTOR_KZG: float = 0.8 # KZG: leicht gedämpft EMOTIONALE_GRAVITATION_FAKTOR_LZG: float = 0.5 # LZG: stärker gedämpft # ─── DelegationsAgent (VENT1) ───────────────── # **Von 0.15 auf 0.20 gezogen am 31.08.2026**, mit der Reizstaerke-Kalibrierung. # Der Kipppunkt lag bei arousal 0,546 und fing damit das obere Drittel des # Alltagsbandes mit — die Perzeption vergibt fuer interessante, aber unbelastete # Sachverhalte bis 0,60. Jetzt kippt es bei **0,594**: Ein Turn muss ueber der # halben Erregungsskala liegen, damit Nova ein Beruhigungs-Signal bekommt. # # Gemessen ueber 33 echte Verlaufszustaende: 18 Ausloesungen vor der # Kalibrierung, 6 danach unveraendert, **4 bei 0.20**. Die Zahl sitzt auf dem # breitesten Plateau der Umgebung (0,1726–0,2088). 2 Ausloesungen waeren bei # 0.23 erreichbar, das Fenster dafuer ist aber nur 0,011 breit — 0.24 stellt das # Tor auf null. Eine Schwelle auf einer Kante ist keine Schwelle. # # **Das steuert Kriterium 1 von dreien.** Kriterium 3 feuert bei Salienz ueber # DELEGATION_SALIENZ_SCHWELLE, und das trifft 93,5 % von 2771 gemessenen Laeufen. # Wer den Agenten insgesamt seltener anschlagen lassen will, dreht dort. DELEGATION_EFFEKTIVWERT_SCHWELLE: float = 0.20 # **Am 24.08.2026 von 0.6 auf 0.4615 mitgezogen** — 0.6 / 1.3, weil der # Eigen-Pfad der Salienz seit demselben Tag durch (1 + MAX_ZUSCHLAG) normiert # ist und dieselbe Lage nun einen um diesen Faktor kleineren Wert traegt. # # **Ohne den Nachzug waere das Tor gekippt, nicht verschoben:** Gemessen ueber # 2506 Turns kaemen bei stehender 0.6 noch **65,1 %** durch statt 89,4 %; mit # dem Nachzug sind es 90,1 %. Die Schwelle beschreibt eine Lage, keine Zahl. DELEGATION_SALIENZ_SCHWELLE: float = 0.4615 DELEGATION_VERSTAERKUNG_DIVISOR: float = 2.0 DELEGATION_AROUSAL_BOOST: float = 0.5 # ─── Delegations-Signale (VENT1) ────────────── # Situationsbeschreibungen fuer den Responder, indiziert nach # (emotions_vektor, trigger). Beschreibend, nicht imperativ. # Nova entscheidet selbst wie sie reagiert — das Signal nimmt # ihr den Druck, selbst Loesungen erfinden zu muessen. # # Primaerschluessel: Emotions-Vektor (wo steht der Mensch emotional) # Sekundaerschluessel: Trigger-Typ (warum wurde delegiert) # Fallback: Wenn die Kombination nicht existiert, wird der Vektor-Default genutzt. DELEGATION_SIGNALE: dict[tuple[str, str], str] = { # ── ABSTURZ (positiv → negativ) ────────────── ("absturz", "effektivwert"): ( "Der Nutzer ist emotional abgestuerzt. Die Intensitaet ist hoch. " "Im Hintergrund wird nach Moeglichkeiten gesucht, die Situation zu verbessern. " "Der Nutzer braucht jetzt Halt und Praesenz. " "Keine Loesungen erfinden, keine Namen oder Termine nennen die nicht im Kontext stehen." ), ("absturz", "vektor"): ( "Der Nutzer ist emotional abgestuerzt. " "Im Hintergrund wird das Thema aufgegriffen. " "Den Umschwung wahrnehmen, Raum geben." ), # ── SPIRALE (negativ → noch negativer) ─────── ("spirale", "effektivwert"): ( "Der Nutzer rutscht tiefer ab, die Intensitaet steigt. " "Im Hintergrund wird an einer Loesung gearbeitet. " "Jetzt zaehlt nur: auf seiner Seite sein. " "Nichts erfinden, nichts versprechen, nichts anbieten." ), ("spirale", "vektor"): ( "Der Nutzer rutscht tiefer ab. " "Das Thema wird im Hintergrund begleitet. " "Praesent sein, nicht draengen." ), # ── EINBRUCH (neutral → negativ) ───────────── ("einbruch", "effektivwert"): ( "Die Stimmung ist gekippt, etwas Neues belastet den Nutzer. " "Im Hintergrund wird das Thema aufgegriffen. " "Den Einbruch wahrnehmen, nicht uebergehen. " "Keine Loesungsvorschlaege erfinden." ), ("einbruch", "vektor"): ( "Die Stimmung kippt. Etwas belastet den Nutzer. " "Im Hintergrund wird das Thema begleitet. " "Aufmerksam sein, nachfragen statt interpretieren." ), ("einbruch", "salienz"): ( "Ein belastendes Thema ist aufgetaucht. " "Im Hintergrund wird vertieft." ), # ── STABILISIERUNG (negativ → neutral) ─────── ("stabilisierung", "vektor"): ( "Der Nutzer beruhigt sich nach einem schweren Moment. " "Im Hintergrund wird weiter am Thema gearbeitet. " "Die Ruhe nicht stoeren, nicht nachbohren. " "Er findet seinen Weg zurueck." ), ("stabilisierung", "salienz"): ( "Der Nutzer stabilisiert sich. " "Das Thema wird im Hintergrund weiter begleitet." ), # ── ERHOLUNG (negativ → positiv) ───────────── ("erholung", "vektor"): ( "Der Nutzer erholt sich aus einem emotionalen Tief. " "Im Hintergrund wird das Thema weiter begleitet. " "Die Besserung sanft anerkennen, nicht uebertreiben. " "Er ist noch erschoepft, auch wenn er laechelt." ), ("erholung", "salienz"): ( "Der Nutzer erholt sich. " "Das Thema wird im Hintergrund weiter vertieft." ), # ── AUFBLUEHEN (neutral → positiv) ─────────── ("aufbluehen", "vektor"): ( "Der Nutzer blueht auf, die Stimmung hebt sich. " "Im Hintergrund wird das Thema vertieft. " "Die Energie teilen, mitgehen." ), ("aufbluehen", "salienz"): ( "Der Nutzer ist positiv engagiert. " "Im Hintergrund wird das Thema vertieft. " "Begeisterung teilen." ), # ── ESKALATION (positiv → noch positiver) ──── ("eskalation", "effektivwert"): ( "Der Nutzer ist in Hochstimmung, die Begeisterung steigt. " "Im Hintergrund wird das Thema vertieft. " "Mitgehen, die Energie teilen. Nicht bremsen." ), ("eskalation", "vektor"): ( "Der Nutzer ist begeistert. " "Im Hintergrund wird vertieft. " "Mitfreuen." ), # ── ABKUEHLUNG (positiv → neutral) ─────────── ("abkuehlung", "vektor"): ( "Die Begeisterung klingt ab. Natuerlicher Uebergang. " "Im Hintergrund wird das Thema weiter begleitet. " "Tempo des Nutzers folgen." ), ("abkuehlung", "salienz"): ( "Das Thema war spannend und klingt ab. " "Im Hintergrund wird weiter vertieft." ), } # Fallback-Signale wenn die exakte Kombination nicht existiert DELEGATION_SIGNALE_FALLBACK: dict[str, str] = { "effektivwert": ( "Die emotionale Intensitaet ist hoch. " "Im Hintergrund wird am Thema gearbeitet. " "Keine Loesungen erfinden, keine Details die nicht im Kontext stehen." ), "vektor": ( "Die emotionale Lage veraendert sich. " "Im Hintergrund wird das Thema begleitet. " "Aufmerksam und praesent sein." ), "salienz": ( "Das Thema wird im Hintergrund vertieft." ), } def _sektor_distanz_matrix_erzeugen() -> dict[tuple[int, int], float]: """Erzeugt die 8×8 Distanzmatrix aus der Kreistopologie und den 4 Exponenten.""" exponenten: list[float] = [ 0.0, # Distanz 0 (selbst) — wird nicht verwendet EI_NORM_BENACHBART, # Distanz 1 EI_NORM_NAH_DIAGONAL, # Distanz 2 EI_NORM_FERN_DIAGONAL, # Distanz 3 EI_NORM_GEGEN, # Distanz 4 ] matrix: dict[tuple[int, int], float] = {} for a in range(1, 9): for b in range(1, 9): if a == b: continue # Kürzeste Distanz auf dem Kreis (8 Sektoren) direkt: int = abs(a - b) distanz: int = min(direkt, 8 - direkt) matrix[(a, b)] = exponenten[distanz] return matrix # Generierte Distanzmatrix (8×8, symmetrisch) EMOTION_SEKTOR_DISTANZ: dict[tuple[int, int], float] = _sektor_distanz_matrix_erzeugen() # ─── KZG-Verstärkung ──────────────────────── # Divisor für die Verstärkung bei wiederholter Erwähnung im KZG. # Formel: neues_gewicht = altes_gewicht + (salienz / divisor) # 2.0 = moderate Verstärkung (+0.40 bei Salienz 0.80) # 5.0 = schwache Verstärkung (+0.16 bei Salienz 0.80) # 10.0 = sehr schwache Verstärkung (+0.08 bei Salienz 0.80) — ALTER WERT KZG_VERSTAERKUNG_DIVISOR: float = 2.0 def postgres_verbinden() -> psycopg2.extensions.connection: """Neue PostgreSQL-Verbindung öffnen.""" return psycopg2.connect(POSTGRES_URL) # ───────────────────────────────────────────── # Node-spezifische LLM-Parameter # ───────────────────────────────────────────── # Jeder Node hat eigene Temperature und Sampling-Parameter. # Klassifikations-Nodes: niedrig (deterministisch) # Generierungs-Nodes: höher (kreativ, natürlich) # Änderung hier → sofortige Wirkung, kein Code anfassen. NODE_LLM_CONFIG: dict = { # --- Chat-Pipeline Nodes --- "perzeption": { "temperature": 0.05, "max_output_tokens": 512, }, "router": { "temperature": 0.05, "max_output_tokens": 512, }, # Eine Zeile Suchanfrage, deterministisch. Gemessen am 20.08.2026: # Frageform 5/10 ueber der Schwelle gegen 0/10 der rohen Aeusserung. "query_rewrite": { "temperature": 0.0, "max_output_tokens": 64, }, # Sechs Zahlen aus drei erlaubten Stufen, kein freier Text. Dieselbe # Sorte Aufgabe wie die Destillation und deshalb dieselbe Temperatur: # Wer ein festes Feld fuellt, braucht keine Streuung. Der Deckel reicht # fuer sechs Zeilen JSON mit Begruendung und ist nicht knapp gesetzt — # eine abgeschnittene Antwort waere ein unvollstaendiges Profil, und das # ist teurer als ein paar Token. "qualitaet_profil": { "temperature": 0.0, "max_output_tokens": 512, }, "planner": { "temperature": 0.2, "max_output_tokens": 1024, }, "responder": { "temperature": 0.7, "max_output_tokens": 2048, "top_p": 0.9, "repeat_penalty": 1.1, "presence_penalty": 0.3, }, "thinker": { "temperature": 0.15, "max_output_tokens": 2048, }, "tribunal": { "temperature": 0.2, "max_output_tokens": 512, }, "corrector": { "temperature": 0.5, "max_output_tokens": 2048, }, "salienz": { "temperature": 0.05, "max_output_tokens": 1024, }, "kzg_verdichtung": { "temperature": 0.1, "max_output_tokens": 1024, }, # --- Pixie-Tasks --- "recherche": { "temperature": 0.5, "max_output_tokens": 2048, }, "vertiefen": { "temperature": 0.5, "max_output_tokens": 2048, }, "wissensluecken": { "temperature": 0.7, "max_output_tokens": 1024, }, "nachfragen": { "temperature": 0.6, "max_output_tokens": 1024, }, "aufräumen": { "temperature": 0.1, "max_output_tokens": 1024, }, "lzg_promotion": { "temperature": 0.1, "max_output_tokens": 1024, }, "cluster_destillation": { "temperature": 0.1, "max_output_tokens": 1024, }, # Die Zwischen-Destillation der Recherche ist der einzige # Hintergrund-Aufruf, der ohne eigene Frist an den Vorgabewert # `MODEL_BACKGROUND_TIMEOUT_S = 300` geriet und ihn regelmaessig riss. # Gemessen ueber 24 h am 16.08.2026, 190 Antworten: Median 181 s, # p90 314 s, Maximum 638 s — **24 davon (12 %) trafen nach dem # Fristablauf ein.** Die Antwort war da, nur hatte der Aufrufer schon # aufgegeben; das eingestellte Werk laeuft weiter. # # Der Schaden lag nicht beim einzelnen Aufruf, sondern in der Auswahl: # Ein Fehlversuch loescht den Queue-Eintrag nach drei Laeufen **hart**, # waehrend der Verfall nur weich deaktiviert. Und die Fehlversuche # trafen die Wichtigen — ueber die 582 aktiven `recherche`-Eintraege # stieg die mittlere `salienz_roh` monoton mit der Zahl der Versuche # (0,867 bei null · 0,947 bei einem · 0,990 bei zwei). Der Grund ist # mechanisch: Der Wichtigste wird zuerst gezogen, hat das meiste # Material und laeuft deshalb als erster in die Frist. # # Beide Werte sind **Obergrenzen, keine Ziele** — wer frueher fertig # ist, kostet nicht mehr. Sie sind als Paar gesetzt und muessen es # bleiben: 5120 Token liegen ueber dem beobachteten Maximum von 4176 # (kein Abschnitt mitten im Wort) und brauchen bei den gemessenen # ~7,3 Token/s rund 700 s — mit Abstand innerhalb der Frist. "recherche_zwischen": { "temperature": 0.1, "max_output_tokens": 5120, "timeout_s": 1200, }, # `max_output_tokens` und `timeout_s` gehoeren zusammen und beide zum # offenen Kern-Prompt (11.08.2026). Er verlangt ausdruecklich Raum statt # Verdichtung; gemessen wurden 3288 Zeichen Profiltext gegen 667 # gedeckelt. Dazu kommt das Denken des Modells — ein Profil-Aufruf hat # dafuer 2310 Token gebraucht, gemessen am 09.08.2026. 2048 haetten den # Text mitten im Wort abgeschnitten, und ein abgeschnittenes Profil # sieht aus wie ein Modell, das aufgehoert hat zu denken. # # Beide Werte sind **Obergrenzen, keine Ziele**: Wer frueher fertig ist, # kostet nicht mehr. Sie stehen bewusst weit, weil der teurere Fehler # der Abbruch ist — er dauert dieselbe Zeit und liefert nichts. # **Temperatur 0.0 seit dem 26.08.2026, und das ist eine Messung.** # Bis dahin stand hier 0.2 ohne belegte Herleitung. Gemessen am selben # Material, viermal je Temperatur: # # T=0.2 Ueberdeckung der Laeufe 32.9 % (29.9–36.8), 3026–5045 Zeichen # T=0.0 Ueberdeckung der Laeufe 100.0 %, viermal 4798 Zeichen # # **Bei 0.0 sind die Laeufe zeichengleich.** Damit ist die Streuung # erklaert, die den Kern-Hash unbrauchbar machte: Zwei Destillationen aus # identischem Material teilten nur 27–32 % ihres Inhaltswortschatzes, und # das bewegte den Zuwendungsfaktor um 29 % seiner Skala # (KERNHASH-TRAEGT-KEINE-PERSON, RAD-MEDIAN-SCHUETZT-FALSCHE-QUELLE). # # **Der Knoten versorgt mehr als den Kern:** `_llm_call` liest ihn fuer # alle fuenf Profile und beide Raeder. Die Mehrfacherhebung der Raeder # (F-RAD-2) laeuft damit ins Leere — drei zeichengleiche Laeufe, dreifache # Kosten. Das ist bekannt und gemessen; die Festlegung wird eigens # behandelt, nicht nebenbei. # # **Gemessen gegen `OllamaProvider/qwen36-cpu`, und das gehoert dazu.** # Determiniertheit bei 0.0 ist keine Eigenschaft der Zahl, sondern eine des # Aufbaus: Ein anderes Modell, ein anderer Anbieter oder ein Wechsel auf die # GPU koennen bei 0.0 weiterhin streuen. **Wechselt dieser Knoten Modell oder # Backend, ist die Zeichengleichheit neu zu messen** — und mit ihr faellt # alles, was darauf steht: die Bedingung an F-RAD-2, die Lesbarkeit der # Drift-Reihe und die Skala der Aehnlichkeitsmessung. # # **Was diese Zahl nicht sagt:** ob ein Kern bei 0.0 *besser* ist. Gemessen # ist die Wiederholbarkeit, nicht die Guete — ein Profil kann eng # reproduzierbar und dabei gleichmaessig falsch sein. "charakter_hash": { "temperature": 0.0, "max_output_tokens": 8192, "timeout_s": 1800, }, "wiedervorlage": { "temperature": 0.2, "max_output_tokens": 512, }, # --- Gesprächsvektor (Epic 9) --- "gespraechsvektor": { "temperature": 0.6, "max_output_tokens": 512, }, # --- Sachlage (novaberg-thinking-lage_k.md, Scheibe 1) --- # Niedrige Temperatur: Die Sachlage ist eine Analyse, keine Erzeugung — # zwei Laeufe auf demselben Turn sollen dieselbe Struktur liefern. # `[gemessen]` 29.08.2026, mittags: Seit Scheibe 9 traegt jedes Objekt # gedeckt, offen, traeger und sprecher — eine fortgeschriebene Blase mit # zwei Objekten brauchte 706–709 Token, der dritte Laborlauf und der # Betriebsturn brachen bei 768 mit `done_reason='length'` ab (halbes # JSON, Sachlage-Ausfall). 1024 laesst der Form Luft; die Frist bleibt. "sachlage": { "temperature": 0.1, "max_output_tokens": 1024, "timeout_s": 60.0, }, # --- Der Frame-Aufloeser (novaberg-thinking-lage_k.md, Scheibe 6) --- # Ein Urteil, keine Erzeugung: offene Eigenschaften gegen ein kurzes # Angebot. Kleiner Ausgang, kurze Frist — er haengt hinter dem # Sachlage-Call und darf den Turn nicht verdoppeln. "sachlage_aufloeser": { "temperature": 0.0, "max_output_tokens": 384, "timeout_s": 30.0, }, # --- Die Plausibilitaetspruefung (novaberg-thinking-lage_k.md, Scheibe 7) --- # Ein Urteil gegen Weltwissen ueber die Aeusserung; laeuft nur bei akutem # Objekt, hinter dem Aufloeser. Kleiner Ausgang, kurze Frist. "sachlage_plausibilitaet": { "temperature": 0.0, "max_output_tokens": 384, "timeout_s": 30.0, }, } # ── GV4 — Wissenslücken-Erkennung ──────────────────────────── # Formel: relevanz = sim × gewicht × session_akt × QF × (1 + boost) × eff_neugier × register NOVA_NEUGIER: float = 0.5 # Novas Grund-Neugier (Persoenlichkeitsparameter) # ───────────────────────────────────────────── # Wissensluecken — Themen am Rand des Feldes # ───────────────────────────────────────────── # Konzept: docs/novaberg-wissensluecken_k.md # # Prioritaet bewusst gesetzt, nicht per Default. Gemessen 27.07.2026: # CharakterAgent mit 0.3 kam erst dran, als das Gespraech verstummte, und eine # achtminuetige Recherche blockierte den Pixie-Takt vollstaendig. 0.35 stellt # den Agenten knapp ueber den Charakter, aber weit unter Queue-Arbeit (0.97). LUECKEN_PRIORITAET: float = float(os.getenv("LUECKEN_PRIORITAET", "0.35")) # Acht Stunden. Der Lueckenagent (0.35) und der CharakterAgent (0.3) sind nur # dann Konkurrenten, wenn BEIDE faellig sind. Bei stuendlichem Takt gegen die # zehn Minuten des Charakters kollidieren sie jede Stunde und der Charakter # verliert; bei acht Stunden praktisch nie. Zwanzig Luecken tragen ohnehin # mehrere Stunden. LUECKEN_INTERVALL_SEKUNDEN: int = int(os.getenv("LUECKEN_INTERVALL_SEKUNDEN", "28800")) # Startwert, nach der ersten Messung nachzujustieren. Jeder Kandidat kostet # ein Embedding. LUECKEN_KANDIDATEN_JE_LAUF: int = int(os.getenv("LUECKEN_KANDIDATEN_JE_LAUF", "20")) # Wechselnde Saat: Ohne Stichprobe liefe jeder Lauf ueber dieselben Themen. LUECKEN_SAAT_THEMEN: int = int(os.getenv("LUECKEN_SAAT_THEMEN", "8")) # Nur ein Wink an das LLM, keine Garantie — die liegt in der # Embedding-Dublettenpruefung. Die Liste darf unvollstaendig sein. LUECKEN_HINWEIS_THEMEN: int = int(os.getenv("LUECKEN_HINWEIS_THEMEN", "30")) GV_LUECKEN_MAX: int = 8 # Erweitert Chat 71 (vorher 3) # Seit dem 12.09.2026 zweimal auf einer neuen Skala: Das Gewicht ist der Rang in # der eigenen Quelle (`ei/source_weights.py`), und die Naehe ist die eines # **Themas** zum Turn statt eines Satzes (`ei/gap_topics.py`). **Die Zahl ist # auf der Themen-Skala nachgelesen und bleibt** — an 31 Turns durch den gebauten # Pfad, mit Resonanz-Schwelle 0,15: In Flirt- und Filmturns kommen 0 bis 1 # Themen darueber, in Sachturns 2 bis 6 passende (*baryonische Rueckkopplung* auf # das Cusp-Core-Problem, *Flusserhaltung beim Sternkollaps* auf die Frage nach # dem Magnetfeld eines Neutronensterns). Gelesen, nicht abgeleitet. GV_LUECKEN_MIN_RELEVANZ: float = 0.15 # Mindest-Gesamtrelevanz # Mindest-Gravitation fuer Ziel-Boost. Chat 107 geprueft und BEWUSST nicht # geaendert (Kalibrierung nomic-embed-text-v2-moe) — nicht vergessen. # ⚠ Wachposten: gravitationsabgeleiteter Wert, nicht gemessen — Startwert. GV_NEUGIER_BOOST_SCHWELLE: float = 0.30 # Mindest-Cosine zum kern_hash. Chat 107 geprueft und BEWUSST nicht geaendert # (Kalibrierung nomic-embed-text-v2-moe) — nicht vergessen. # ~~⚠ Wachposten: Kandidat↔nova_kern-Wert, nova_kern wurde nicht gemessen — # begruendeter Startwert, kein Messergebnis. Nach Live-Betrieb pruefen.~~ # → **Am 12.09.2026 nachgeholt, und der Startwert war zu hoch.** # # `[gemessen ueber 150 echte Nutzer-Turns gegen den Charakterkern]` # median **0,228**, p90 0,324, p99 0,413, **max 0,421**. Die alte 0,40 lag # damit zwischen p99 und Maximum und liess **1,3 %** der Turns durch; im # Betrieb kam nichts an: 40 Turns mit offenem Strategie-Tor, **0** Luecken # (gemessen mit einem Werkzeug ausserhalb des Repositoriums). # # 0,30 liegt knapp unter p90 und laesst **17,3 %** durch — zusammen mit dem # Strategie-Tor (38 % der Turns) also rund jeder fuenfzehnte Turn. Das ist # eine Setzung auf gemessener Grundlage, kein abgeleiteter Wert: Wie oft eine # Luecke angeboten werden **soll**, sagt keine Messung. # # ~~**Achtung, sie filtert nicht, was ihr Name sagt.** `ei/wissensluecken.py` # setzt fuer **jeden** Kandidaten denselben Wert — `cosine(turn, kern)` —, # der Kandidat geht nicht ein.~~ → am 12.09.2026 behoben (Naehe je Kandidat). # # **Seit dem 12.09.2026, abends, eine dritte Paarung und damit eine dritte # Zahl: Thema gegen Kern** (`F-GV-2`, Kandidaten sind Themen). Die 0,30 galt # fuer Gedaechtnissaetze gegen den Kern — und trennte dort nach Sprecher # (Nova-Saetze 0,44–0,53, Nutzer-Saetze 0,13–0,24). Auf Themen verschwindet # das: `[gemessen]` Median 0,209 aus Nova-Knoten gegen 0,200 aus Nutzer-Knoten. # Die 0,30 liesse dort nur rund ein Zehntel durch. # # **0,15 ist gelesen, nicht abgeleitet** — an 31 Turns durch den gebauten Pfad: # Alltagsetiketten liegen darunter (*Bier* 0,074, *Humor und Witze* 0,084, # *Tagesplanung* 0,126, *Zukuenftige Planung* 0,129, *Filmtitel* 0,139), Novas # Themen und die meisten Sachthemen darueber. **Der Preis ist benannt:** Rund # ein Dutzend konkreter Sachbegriffe faellt mit heraus (*Mindestmasse (M sin(i))* # 0,117, *Messproblem* 0,103) — der Kern beschreibt Nova abstrakt. Und # Gespraechs-Etiketten ueber Nova selbst passieren (*spielerische Provokation* # 0,211); die Resonanz trennt Wissen nicht von Beziehung. GV_CHARAKTER_RESONANZ_SCHWELLE: float = 0.15 GV_QUELLEN_FAKTOR: float = 0.6 # Einheitlich fuer alle Quellen # Um wie viel `wissbegier` im Zuwendungsrad des Paares ueber `pflicht` liegen # muss, damit eine Wissensfrage **nicht** zuerst beantwortet wird, sondern die # Neugier den Vorzug hat (`F-GV-2`, Nachtrag 12.09.2026). **Die Messunsicherheit # des Rades, nicht eine Setzung ueber Vorlieben:** der Median der Spanne, die # `pflicht` oder `wissbegier` zwischen den Laeufen derselben Erhebung zeigen, # ueber 97 Erhebungen 0,05 (p90 0,26). Ein kleinerer Abstand ist kein Anlass — # dann bleibt die Pflicht. GV_BITTE_RAD_ABSTAND: float = 0.05 # Wie lange die Gewichtsverteilung eines Paares zwischengehalten wird, bevor sie # neu geladen wird. Sie ist die Skala der Naht zwischen LZG und KZG und soll mit # dem Bestand wandern, aber nicht mit jedem Turn: Ein Laden liest alle aktiven # LZG-Knoten und alle KZG-Eintraege des Paares (12.09.2026: 3922 und 3548). GV_GEWICHT_VERTEILUNG_TTL_S: float = 600.0 GV_SESSION_AKT_CAP: int = 25 # Session-Decay: nach 25 Turns = 0 GV_AUFNAHMEBEREITSCHAFT_CAP: float = 2.5 # sin^0.5 Normalisierung: Rohwert-Obergrenze GV_STRATEGIE_MIN_LAENGE: int = 2 # GV3-Strategie nur ab Vektorlaenge >= 2 GV_LUECKEN_SIM_OBERGRENZE: float = 0.92 # Zu aehnlich = bereits besprochen # Neugier-Saeulen: Faktor-Tabellen fuer effektive Neugier GV_AUFNAHMEBEREITSCHAFT_EMOTION: dict[int, float] = { 0: 1.50, # Neugier selbst (Sektor 8) 1: 1.25, # Adjacent (Freude, Aerger) 2: 1.00, # Nah-diagonal (Zuversicht, Enttaeuschung) 3: 0.75, # Fern (Angst, Trauer) 4: 0.50, # Gegenpol (Ueberraschung) } GV_AUFNAHMEBEREITSCHAFT_STIMMUNG: dict[str, float] = { "aufbluehen": 1.30, "eskalation": 1.25, "erholung": 1.15, "stabilisierung": 1.00, "plateau": 1.00, "abkuehlung": 0.90, "einbruch": 0.70, "spirale": 0.50, "absturz": 0.40, } GV_AUFNAHMEBEREITSCHAFT_MODUS: dict[str, float] = { "spielerisch": 1.40, "kreativ": 1.40, # wie spielerisch: assoziatives Terrain "philosophischer_austausch": 1.35, # offene Fragen sind sein Betriebsstoff "fachgespraech": 1.30, "lernmodus": 1.30, # Wissensaufbau, empfangend statt streitend "arbeitsmodus": 1.00, "alltag": 1.00, "beratend": 1.00, "berichtend": 0.95, # Wiedergabe, nicht Erkundung "emotional": 0.70, } # Die neutralen Faktoren der drei Saeulen aus Kanon-Feldern der Perzeption, # als Literal (dieselbe Regel wie bei `EI_DYNAMIK_NEUTRAL`): der Eintrag # `neutral`, wo die Tabelle einen traegt, sonst ihr Mittel. Ihn bekommt ein # Wert, den die Tabelle nicht kennt. GV_AUFNAHMEBEREITSCHAFT_MODUS_NEUTRAL: float = 1.140 # 11.4 / 10 GV_AUFNAHMEBEREITSCHAFT_DYNAMIK: dict[str, float] = { "vertrauen": 1.30, "dankbar": 1.15, "neutral": 1.00, "hilfesuchend": 0.85, "distanz": 0.85, "angriff": 0.60, } GV_AUFNAHMEBEREITSCHAFT_DYNAMIK_NEUTRAL: float = 1.0 # der Eintrag `neutral` GV_AUFNAHMEBEREITSCHAFT_STIL: dict[str, float] = { "locker": 1.20, "jugendlich": 1.15, "neutral": 1.00, "fachlich": 0.95, "emotional": 0.90, "formell": 0.90, } GV_AUFNAHMEBEREITSCHAFT_STIL_NEUTRAL: float = 1.0 # der Eintrag `neutral` # Register-Kompatibilitaet: Passt die emotionale Ladung der Luecke zum Gespraechsregister? GV_REGISTER_SACHLICH_EMOTIONAL: float = 0.60 # Hoch-emotional in sachlichem Register GV_REGISTER_SACHLICH_MILD: float = 0.90 # Mild-emotional in sachlichem Register GV_REGISTER_SACHLICH_NEUTRAL: float = 1.15 # Sachliche Luecke in sachlichem Register GV_REGISTER_OFFEN_EMOTIONAL: float = 1.20 # Emotionale Luecke in offenem Register # ── GV5 — Dreischicht: Achsen-Schwellenwerte ── GV_ACHSE_ENERGIE_SCHWELLE: float = 0.5 # arousal >= → hoch (1) # **Die Neutralisierung von Novas Raum** — eine Setzung des Meisters vom # 27.08.2026, keine Messung: *„Ich als Mensch bin auch nach x Stunden nicht # mehr so im Gespraech, auch ich falle zurueck."* # # **Der Anlass ist gemessen.** Auf ein blosses »Hey Kleines« nach 25 Stunden # Pause landete der Turn in der Landschaft `kissenschlacht` (Sektor 45). Zwei # der sechs Achsen — Naehe und Tiefe — stammten aus `nova_state`, geschrieben # am Vorabend; der Raum hatte keine Frist und war ueber Nacht keinen Deut # gealtert. Der `wollen`-Riegel prueft das Alter seines Zustands auf die # Sekunde (Grenze 86400 s); der Raum tat es nicht. # # **Neutralisierung, nicht Verfall.** Der Raum wird nicht verworfen, sondern # linear auf die Vorgabewerte gezogen — dieselben, die ein Kaltstart liefert # (`graph/personality.py::Raum`). Nach dieser Spanne ist er dort angekommen. GV_RAUM_NEUTRAL_SEKUNDEN: float = float( os.getenv("GV_RAUM_NEUTRAL_SEKUNDEN", "14400")) # 4 Stunden # **Der Verfall der Sachlage** (novaberg-thinking-lage_k.md §3a). Dieselbe # Spanne wie die Raum-Neutralisierung, aber eine andere Mechanik: Der Raum # wird linear gezogen, die Sachlage wird verworfen — ein halb verblasstes # Verstaendnis gibt es nicht. Eigene Konstante, damit die beiden getrennt # kalibrierbar bleiben (F-INTENS-1: eine Groesse an ihrer eigenen Skala). SACHLAGE_VERFALL_SEKUNDEN: float = float( os.getenv("SACHLAGE_VERFALL_SEKUNDEN", "14400")) # 4 Stunden # Wie lange ein Angebot Novas offen steht (Scheibe 12 E1). Kuerzer als die # Sachlage: Eine Zustimmung kommt im naechsten Turn oder gar nicht, und ein # altes Angebot wuerde ein spaeteres "Gerne" zu einem Auftrag machen, der # niemandem gehoert. ANGEBOT_VERFALL_SEKUNDEN: float = float( os.getenv("ANGEBOT_VERFALL_SEKUNDEN", "900")) # 15 Minuten # Wie wahrscheinlich Nova von sich aus anbietet, eine Sache einzutragen oder # zu notieren (Scheibe 12 E2), gilt fuer Notizen und Timeline. Gelesen wird # die Speiche `pflicht` aus Novas Zuwendungsrad zum Menschen. Entscheidung des # Eigentuemers, 18.09.2026: unter dem Boden nie; darueber steigt die # Wahrscheinlichkeit je Turn linear von 0 % am Boden auf 100 % bei 1,0 — # 0,67 ergibt 50 %. Nova bekommt damit Spielraum statt einer harten Kante. # Abgeloest: 17.09.2026 eine feste Schwelle 0,9 ("sonst wird es schnell # aufdringlich") — dem Eigentuemer gegenueber bot sie damit nie an. ANGEBOT_PFLICHT_BODEN: float = float( os.getenv("ANGEBOT_PFLICHT_BODEN", "0.33")) # Die Sachlage-Bruecke ohne harte turn_id sucht die aehnlichste Verlaufszeile # des Paares per Kosinus. Unter dieser Schwelle gibt es keine Bruecke: Ein # Uebergang zu einem Turn ohne Bezug waere schlimmer als keiner. # `[gemessen]` — 28.08.2026, labor/2026-08-28_sachlage_bruecke_messung.py: # zehn Lagen, je ein Impuls zum eigenen Thema — Kosinus 0,40 bis 0,76 # (Median 0,65); ein Impuls zu einem fremden Thema: naechste Zeile 0,25. # Der erste Wert 0,50 haette eine von zehn eigenen Lagen verworfen. 0,35 # liegt in der Luecke; der Fremdtreffer ist mit n=1 duenn belegt und steht # in der Fundliste. Frage-Vektor (Thema + Gedanke) und Zeilen-Vektor # (Gegenstand-Satz) sind verschiedene Texte, deshalb liegt die Schwelle # unter den 0,60 des Zustellungsfilters. SACHLAGE_BRUECKE_MIN_KOSINUS: float = float( os.getenv("SACHLAGE_BRUECKE_MIN_KOSINUS", "0.35")) # Scheibe 5 (28.08.2026): Ab diesem Kosinus zwischen Prompt-Embedding und dem # Gegenstand-Vektor einer frueheren Blase (anderes Thema) gilt der Turn als # Rueckkehr zu ihr, und der Sachlage-Call bekommt sie als fruehere Sachlage. # Startwert wie die Bruecke; die Messreihe der Scheibe setzt ihn. SACHLAGE_WIEDERAUFNAHME_MIN_KOSINUS: float = float( os.getenv("SACHLAGE_WIEDERAUFNAHME_MIN_KOSINUS", "0.35")) # Scheibe 12 C2 (16.09.2026), die Naehe zwischen akutem Objekt und dem # Objekt-Merkmal eines Dienstes — zwei Schwellen, beide gemessen, nicht gesetzt. # Die UNTERGRENZE auf die groesste Naehe beantwortet, ob ueberhaupt ein Dienst # gemeint ist: Termine lagen bei 0,4534 bis 0,5591, Gespraechsthemen bei # 0,2834 bis 0,4800; mit 0,40 kamen 8 von 8 Terminen und 1 von 14 Themen durch. # Die Untergrenze gilt JE ZETTEL fuer sich (entschieden 16.09.2026): Ein Objekt # steht an jedem Dienst, dessen Merkmal es erreicht, auch an mehreren. Ein # Abstand zwischen zwei Merkmalen entscheidet nicht — er machte das Urteil ueber # einen Dienst davon abhaengig, wer sonst angemeldet ist. Ueber die Reihe: 0 von # 60 nur beim falschen Dienst, 16 von 60 an beiden. Eine eigene Untergrenze je # Dienst brachte nichts Messbares (0,40/0,39). Geeicht fuer genau den Wortlaut # der Merkmale und das Embedding-Modell dieses Tages — # ⚠ Wachposten: Ausgangswert fuer den Schattenlauf, kein Endwert. OBJEKT_NAEHE_UNTERGRENZE: float = float( os.getenv("OBJEKT_NAEHE_UNTERGRENZE", "0.40")) # Frist der Einbettung im Turn (F-FRIST-1). Gemessen am 16.09.2026 ueber 260 # Einbettungen eines Tages: Median 0,151 s, p95 0,202 s, Maximum 1,48 s. Der # Schattenlauf steht im Gespraechspfad; laenger als das Siebenfache des # Maximums darf er den Turn nicht halten. OBJEKT_NAEHE_FRIST_S: float = float( os.getenv("OBJEKT_NAEHE_FRIST_S", "10.0")) # Scheibe 6 (28.08.2026), der Frame-Aufloeser: Wie viele Eintraege des # Gedaechtnis-Pools dieses Turns (KZG, LZG, Bibliothek, Aufzeichnungen; seit # Teil H ohne den Kalender, der eine eigene Grenze hat) der Sachlage-Call als # nummeriertes Angebot sieht. Der Pool traegt # im Betrieb um 20 Eintraege (28.08.2026: 19 + 3 Aufzeichnungen); jeder kostet # Prompt-Zeichen, und eine Sektion hat schon einmal eine Nebensache aus dem # Artefakt gedrueckt (Scheibe 5). ⚠ Wachposten: Startwert, nicht gemessen. SACHLAGE_BESTAND_MAX_EINTRAEGE: int = int( os.getenv("SACHLAGE_BESTAND_MAX_EINTRAEGE", "8")) # Kalendereintraege je akutem Objekt der vorigen Blase (Titel-Suche in beide # Zeitrichtungen). Der Kalender ist die einzige Quelle, die der Aufloeser # selbst sucht — der Pool kennt sie nicht. SACHLAGE_BESTAND_KALENDER_LIMIT: int = int( os.getenv("SACHLAGE_BESTAND_KALENDER_LIMIT", "3")) # Scheibe 8 (29.08.2026), der Wissenstraeger: Fuer die erste offene Eigenschaft # eines akuten Objekts, die "nachschlagen" verlangt und die das Gedaechtnis # nicht deckt, laeuft EINE Websuche je Turn — so viele Treffer sieht der # Verfasser. Die Suche ist dieselbe wie im Thinker (`tools/web/search.py`). SACHLAGE_RECHERCHE_MAX_TREFFER: int = int( os.getenv("SACHLAGE_RECHERCHE_MAX_TREFFER", "3")) # Scheibe 11 (13.09.2026), das Eigenschaftsgedaechtnis: Ein Objekt ohne # Entitaet wird bei jedem Auftreten gegen die Magnete seiner juengsten Turns # gehalten — so viele Turns je Objekt. Die Magnete eines Turns entstehen in # zwei Haelften (Nutzer vor der Sachlage, Nova danach); das Fenster holt die # zweite Haelfte beim naechsten Auftreten nach. ⚠ Startwert, nicht gemessen. SACHLAGE_BINDUNG_TURN_FENSTER: int = int( os.getenv("SACHLAGE_BINDUNG_TURN_FENSTER", "10")) # Wie viele gespeicherte Eigenschaften der akuten Objekte der Aufloeser # zusaetzlich zum Pool angeboten bekommt — vor dem Pool, weil sie die Sache # selbst betreffen. ⚠ Startwert, nicht gemessen. SACHLAGE_EIGENSCHAFT_ANGEBOT_MAX: int = int( os.getenv("SACHLAGE_EIGENSCHAFT_ANGEBOT_MAX", "4")) # Teil H (22.10.2026): Wie viele Fakten aus der Mappe der gebundenen, akuten # Objekte der Aufloeser angeboten bekommt — vor der Eigenschaft, eine eigene # Grenze, damit keine Quelle eine andere verdraengt. ⚠ Wachposten: Startwert, # nicht gemessen. SACHLAGE_FAKTEN_ANGEBOT_MAX: int = int( os.getenv("SACHLAGE_FAKTEN_ANGEBOT_MAX", "8")) # Teil H: Wie viele Kalenderzeilen das Angebot traegt, in der Ordnung des # Lesewegs der Termine. Ein Zeitraum ist je Objekt nicht gekappt; ohne diese # Grenze fuellte ein voller Monat alle Plaetze von Pool und Aufzeichnungen. # Den ganzen Zeitraum traegt der Block [TERMINE]. ⚠ Wachposten: Startwert, # nicht gemessen. SACHLAGE_BESTAND_KALENDER_MAX: int = int( os.getenv("SACHLAGE_BESTAND_KALENDER_MAX", "5")) # Der Erinnerungsanker (graph/nodes/erinnerungsanker.py): Fuer wie viele # gebundene Entitaeten der akuten Objekte er je Turn die Mappe liest und # sucht, in der Ordnung der Objekte. ⚠ Wachposten: Startwert, nicht gemessen. ANKER_MAX_ENTITAETEN: int = int(os.getenv("ANKER_MAX_ENTITAETEN", "3")) # Wie viele Erinnerungen der Anker der Resonanz hoechstens hinzufuegt — # gekappt wie die Resonanz des Enrichers selbst (drei). ⚠ Wachposten: # Startwert, nicht gemessen. ANKER_MAX_ERINNERUNGEN: int = int(os.getenv("ANKER_MAX_ERINNERUNGEN", "3")) # --- Das kurzfristige Ziel (novaberg-thinking-lage_k.md §4, Scheibe 2) --- # Zwei Lagen derselben Blase mit demselben akuten Objekt erzeugen ein # `kurzfristig`-Ziel — per Namensgleichheit, ohne Schwelle. Eine Schwelle # ueber den Nutzerziel-Satz stand hier zuerst (0,8) und fiel an der Messung: # Der Satz wird je Turn neu formuliert (Kosinus 0,40 bis 0,42 beim selben # Vorhaben, 0,35 beim Wechsel), das akute Objekt traegt woertlich durch. # Der Anker des kurzfristigen Ziels. Unter den 0,8 der langfristigen: Es ist # ein Vorsatz fuer die naechsten Stunden, kein Zug des Charakters. KURZZIEL_MOTIVATION: float = float(os.getenv("KURZZIEL_MOTIVATION", "0.7")) # Halbwertszeit in Stunden; bei Schwelle 0,15 ist das Ziel nach knapp drei # Halbwertszeiten inaktiv — ein Vorsatz aus dem Gespraech ueberlebt den Tag # nicht, so wie die Blase selbst nach vier Stunden verfaellt. ZIEL_KURZFRISTIG_DECAY_STUNDEN: float = float( os.getenv("ZIEL_KURZFRISTIG_DECAY_STUNDEN", "3")) # Motivation unter diesem Wert → das Ziel ist verfallen. Zwei Leser derselben # Schwelle: der Tageslauf (`agents/ziel_decay`) legt `aktiv` um, der Lader # (`memory/ziele.py::ziele_live_bewerten`) laesst liegen, was der Tageslauf # noch nicht umgelegt hat — seit 28.08.2026, weil ein Takt von einem Tag # eine Halbwertszeit von drei Stunden nicht traegt. ZIEL_DEAKTIVIERUNGS_SCHWELLE: float = 0.15 GV_ACHSE_NAEHE_SCHWELLE: float = 0.5 # naehe >= → nah (1) GV_ACHSE_TIEFE_SCHWELLE: float = 0.5 # tiefe >= → tief (1) GV_ACHSE_INITIATIVE_VERH: float = 1.5 # ⚠ ABGELOEST, siehe unten — nur noch fuer den Altpfad # ── Initiative-Achse: wer setzt die Richtung ── # # Die alte Achse verglich Turn-Laengen gegen GV_ACHSE_INITIATIVE_VERH (1.5). # Gemessen ueber 15 Laeufe stand sie 15 Mal auf demselben Wert: Rohwerte # 0.10-1.00 gegen eine Schwelle von 1.5, damit waren 32 der 64 Sektoren # unerreichbar. Neudefinition und Herleitung: novaberg-gv-initiative_k.md. # # Drei Masse, je auf ihr eigenes Zentrum bezogen. Die Zentren sind Mediane # ueber 133 Rohturn-Paare (gemessen 29.07.2026), die Grenzen die dort # beobachteten Extreme. Sie sind KEINE gesetzten Schwellen, sondern erhobene # Werte — der Kalibrier-Agent schreibt sie bei jeder Charakter-Destillation # neu. Wer sie von Hand aendert, notiert Datum und Korpusgroesse. GV_INITIATIVE_M2_THEMA: dict[str, float] = { "zentrum": 0.662, "min": 0.290, "max": 0.983, } GV_INITIATIVE_M3_REGISTER: dict[str, float] = { "zentrum": 0.100, "min": 0.000, "max": 0.600, } # M1 ist DREIWERTIG. Die drei Mengen zerlegen INTENT_KANON vollstaendig und # ueberschneidungsfrei; tests/test_intent_kanon.py haelt das fest. Eine neue # Intention im Kanon ohne Zuordnung macht diesen Test rot — sie faellt nicht # still in eine Klasse. # # WARUM DREI UND NICHT ZWEI (Setzung vom 30.07.2026). # Zweiwertig bestimmte M1 das Vorzeichen des Rohwerts allein. Das ist # Arithmetik: rohwert = Mittel(bewegung, wollen) mit wollen in {-1, +1}, also # liegt der Rohwert bei wollen=+1 zwingend in [0, +1] und bei wollen=-1 # zwingend in [-1, 0]. Bei einer Schwelle von -0.45 und einem Versatz von # hoechstens +/-0.25 heisst das: **eine fuehrende Intention setzte das Bit im # Alleingang**, und weder Themensprung noch Registerweg noch Charakter konnten # es kippen. Gemessen am 30.07.2026 traf das auf 47,4 % der Nutzer-Turns zu # (46 von 97) — in fast der Haelfte aller Turns war der Bewegungsteil der # Rechnung ohne Wirkung auf das Ergebnis. # # Die zweite Haelfte war ebenso hart: Jeder Turn ohne eine der fuenf # Intentionen trug -1.0, auch wenn er inhaltlich mitging. Genau das # widersprach dem Konzept, das 'recherche_vertiefen' ausdruecklich als # "aktives Mitgehen" fuehrt — weder Setzen noch Zurueckgeben. # # Wirkung der Umstellung, gemessen ueber 97 Nutzer-Turns: # zweiwertig 46 x +1.0 51 x -1.0 # dreiwertig 47 x +1.0 42 x 0.0 8 x -1.0 # 42 Turns (43 %) hoeren damit auf, einen harten Gegenpol zu tragen. # +1 — der Turn SETZT eine Richtung. Er verlangt etwas oder legt etwas fest. GV_INITIATIVE_FUEHREND: set[str] = { "information_erfragen", "feedback_erfragen", "anweisung", "widerspruch", "abschluss", "hilferuf", "planung", } # 0 — der Turn GEHT MIT, mit eigenem Beitrag. Er setzt keine neue Richtung # und gibt die bestehende auch nicht zurueck. Tiefer in ein gesetztes Thema # eintauchen steht hier und nicht oben: Die Aufnahme von # 'recherche_vertiefen' in die fuehrende Menge wuerde das Signal von +0.38 auf # +0.04 druecken (gemessen, Konzept §4.1). GV_INITIATIVE_NEUTRAL: set[str] = { "information_teilen", "reflexion", "recherche_vertiefen", "gemeinsam_eruieren", "feedback_geben", "humor", } # -1 — der Turn GIBT die Richtung zurueck. Er reagiert, ohne etwas # beizutragen, das weitertraegt. # # 'emotionaler_ausdruck' steht hier und nicht in NEUTRAL, und der Grund ist # messbar: Er kommt in 7 von 97 Nutzer-Turns ohne jede andere tragende # Intention vor, meist zusammen mit 'bestaetigung'. Stuende er auf 0, ergaebe # ['bestaetigung'] den Wert -1 und ['emotionaler_ausdruck', 'bestaetigung'] # den Wert 0 — eine reaktive Gefuehlsaeusserung machte den Turn fuehrender. GV_INITIATIVE_FOLGEND: set[str] = { "bestaetigung", "smalltalk", "emotionaler_ausdruck", } # Die Schwelle, an der das Achsen-Bit kippt. NICHT der Median des Korpus. # # ERHOBEN AM 30.07.2026 gegen 127 unabhaengige Lesarten des Modells ("wer hat # hier die Richtung gesetzt?", ohne Kenntnis der Achse). Fallzahl 127, null # Ausfaelle, Positions-Kontrolle bestanden (Betrag 26.7 Punkte, verlangt 20). # # Schwelle -0.05: Uebereinstimmung 74.8 %, kappa 0.406, Minderheit 25.2 % (gesetzt) # Schwelle -0.20: Uebereinstimmung 76.4 %, kappa 0.402, Minderheit 15.8 % # Schwelle -0.45: Uebereinstimmung 68.5 %, kappa 0.127, Minderheit 4.7 % (Vorgaenger) # # WARUM DER VORGAENGER ERSETZT WURDE. -0.45 stammt aus einer Erhebung ueber 83 # Turns, in der M1 die Laufzeit nie erreicht hat: `user_intentionen` hatte # keinen Erzeuger, die Achse rechnete `rohwert = bewegung`. Seit dem 30.07.2026 # traegt M1 bei, und damit hat sich die Groesse geaendert, auf die die Schwelle # angewandt wird — nicht nur der Bestand. Auf dem heutigen Korpus traegt -0.45 # eine Minderheit von 4.7 % gegen die von §12 geforderten 15 %: Die Achse stand # faktisch auf einem konstanten Bit. Live an zehn Turns nachgemessen: 8 von 8 # mal Bit 0. # # WARUM -0.05 UND NICHT -0.20. kappa ist praktisch gleich (0.406 gegen 0.402), # aber -0.05 liegt in der MITTE des Plateaus und -0.20 an seinem Rand. Ueber # 200 Zufallshalbierungen wurde -0.05 in 105 Faellen wiedergefunden und -0.20 # in 49; 174 von 200 landeten im Plateau [-0.20, -0.05]. Der Wert in der Mitte # ist der stabilere. # # UEBERTRAEGT AUF UNGESEHENE DATEN, gemessen ueber dieselben 200 Halbierungen: # kappa innen 0.423, kappa AUSSEN 0.358, Schwund 0.065. # Zum Vergleich die Erhebung vom Vormittag desselben Tages: kappa aussen 0.260 # bei Schwund 0.143. Der Schwund ist halbiert. Das ist die einzige Zahl, die # etwas ueber neue Daten sagt, und sie steht hier deshalb an erster Stelle. # # DREI VORBEHALTE, die keine Rechnung auf diesen Daten ausraeumen kann: # 1. Der Zeuge trennt nur auf EINER Seite. Gefragt, ob NOVA die Richtung # gesetzt hat, urteilt er zu 76.7 % ja; gefragt, ob der NUTZER es tat, zu # exakt 50.0 % — ein Muenzwurf. Zum zweiten Mal unabhaengig gemessen. # 2. Ein Paar, ein Zeuge, ein Prompt. # 3. Die chronologische Halbierung uebertraegt schlechter als die # alternierende (kappa aussen 0.259 gegen 0.451). Hinweis auf Drift, # zum zweiten Mal beobachtet, n=63 je Haelfte. # # Herkunft: novaberg-gv-initiative_k.md §12. Ein Wert aus EINEM Paar — der # Kalibrier-Agent (§7) soll ihn spaeter je Paar erheben und ablegen; bis dahin # ist diese Konstante sein Platzhalter. GV_INITIATIVE_SCHWELLE: float = float(os.getenv("GV_INITIATIVE_SCHWELLE", "-0.05")) # Der Charakter-Versatz verschiebt den Rohwert, nicht die Schwelle. Volle # Auslenkung des Rads trifft die Grenze exakt (Konzept §6.3). Bis das Rad # gebaut ist, steht der Versatz auf 0.0 und ist NICHT abgeleitet — dieselbe # Lage wie GV_RAUM_CHARAKTER_FAKTOR nach Chat 114. GV_INITIATIVE_VERSATZ_MAX: float = 0.25 GV_INITIATIVE_VERSATZ: float = 0.0 # ── Kalibrier-Agent: die Schwelle je Paar erheben (Konzept §7) ── # # Der Agent legt die Schwelle fest, er regelt sie NICHT zur Laufzeit nach. Es # gibt einen Pfad von der Achse zurueck auf die Eingabe — Sektor -> Cluster -> # Repertoire -> Novas Antwort -> naechster Rohwert. Er ist lang und schwach, # aber er ist da; ein mitlaufendes Zentrum haette keinen Anker und driftete, # bis alles Mittelwert ist. # Untergrenze der Fallzahl. Darunter wird nicht geschrieben, und die # bestehende Schwelle bleibt stehen. 60 liegt unter den 83 Turns, auf denen # GV_INITIATIVE_SCHWELLE heute steht, und weit genug ueber der Groesse, bei # der einzelne Turns das kappa tragen. Die Zahl ist gesetzt, nicht gemessen. KALIBRIERUNG_MIN_TURNS: int = int(os.getenv("KALIBRIERUNG_MIN_TURNS", "60")) # Nebenbedingung der Schwellensuche: Anteil, den die schwaechere Seite # mindestens tragen muss. Ohne sie gewinnt bei schiefen Korpora eine # Randschwelle, die fast alles auf ein Bit legt — und schliesst damit die # Haelfte der 64 Sektoren wieder, also genau den Defekt, den die neue Achse # abgeloest hat. Erreichbarkeit ist Vorgabe, nicht Nebenprodukt (Konzept §8). KALIBRIERUNG_MIN_MINDERHEIT: float = float(os.getenv("KALIBRIERUNG_MIN_MINDERHEIT", "0.15")) # Das Raster, ueber das gesucht wird. Die Achse liefert Werte in [-1, +1]; # 0.05 ist fein genug fuer ein Plateau von 0.2 Breite (gemessen zwischen # -0.55 und -0.35) und grob genug, dass 41 Kandidaten reichen. KALIBRIERUNG_RASTER_MIN: float = -1.0 KALIBRIERUNG_RASTER_MAX: float = 1.0 KALIBRIERUNG_RASTER_SCHRITT: float = 0.05 # Positions-Kontrolle: Mindest-Differenz zwischen "B ist der Nutzer" und # "B ist Nova". Ein Zeuge, der nur die Reihenfolge liest — wer zuletzt # spricht, fuehrt —, laege in beiden Richtungen gleich. Gemessen (Chat 116): # 79,5 % gegen 36,1 %, also 43,4 Punkte. 20 Punkte sind die Haelfte davon und # trennen einen unterscheidenden Zeugen sicher von einem positionsblinden. KALIBRIERUNG_MIN_POSITIONSDIFFERENZ: float = float( os.getenv("KALIBRIERUNG_MIN_POSITIONSDIFFERENZ", "0.20") ) # Laenge, auf die ein Beitrag fuer den Zeugen gekuerzt wird. Nova antwortet in # Absaetzen (gemessen 433 Zeichen je Turn), der Nutzer tippt eine Zeile (51) — # ohne Deckel verschwindet der kurze Beitrag im langen. Gekuerzt wird vom # Anfang her: Wer die Richtung setzt, tut es zu Beginn seines Beitrags. KALIBRIERUNG_ZEUGE_MAX_ZEICHEN: int = int( os.getenv("KALIBRIERUNG_ZEUGE_MAX_ZEICHEN", "1200") ) # Groesse der Stichprobe fuer die Positions-Kontrolle. Sie laeuft mit # vertauschten Rollen und kostet je Turn ein zweites Urteil — ueber den vollen # Korpus waere sie doppelt so teuer wie die Erhebung selbst, und fuer die # Frage "unterscheidet der Zeuge die Sprecher?" reicht eine Teilmenge. KALIBRIERUNG_POSITIONSPROBE: int = int(os.getenv("KALIBRIERUNG_POSITIONSPROBE", "30")) # Obergrenze der Nutzer-Turn-Laenge fuer den Kalibrier-Korpus. Turns darueber # sind keine Gespraechsbeitraege, sondern eigene Messturns aus fruehereren # Sitzungen — thematisch zulaessig, in ihrer Bauart aber kein Verhalten, das # kalibriert werden soll. # # Der Wert ist nicht gesetzt, sondern gefunden: Die Laengenverteilung der 147 # Turnpaare ist zweigipflig, und zwischen den Gipfeln liegt nichts. # Gemessen 30.07.2026 ueber meister:nova: # < 100 Zeichen: 77 Turns # 100– 499 Zeichen: 22 # 500–1499 Zeichen: 0 ← die Luecke # >= 1500 Zeichen: 48 # Median 92, Maximum 2812 # # Jede Grenze zwischen 500 und 1499 ergibt dieselben 99 Paare. Der Wert ist # damit unempfindlich gegen seine eigene Wahl — die Trennung steht in den # Daten, nicht in dieser Zeile. 99 liegt ueber KALIBRIERUNG_MIN_TURNS (60). # # Wird die Zahl je knapp, ist das ein Befund und keine Gelegenheit, die Grenze # zu heben: Eine Luecke, die sich schliesst, heisst, dass Messturns und # Gespraech ihre Bauart nicht mehr unterscheiden. KALIBRIERUNG_MAX_TURN_ZEICHEN: int = int( os.getenv("KALIBRIERUNG_MAX_TURN_ZEICHEN", "500") ) # Zwischenstand der Urteilsreihe. Eine Reihe, die abbricht, ist ohne # Zwischenstand vollstaendig verloren — gemessen am 29.07.2026: rund 200 Urteile # durch eine einzelne Zeitueberschreitung. Bei Aufrufen an ein Sprachmodell ist # der Zwischenstand Pflicht, sobald die Reihe drei Minuten ueberschreitet; hier # ist das nach wenigen Faellen erreicht. # # Der Pfad liegt AUSSERHALB des Repositoriums: `/app` ist das gemountete # Server-Verzeichnis und damit Teil des Repos. `/tmp` im Behaelter uebersteht # den Neustart des Dienstes durch einen Datei-Edit und stirbt mit dem Behaelter # — genau die Lebensdauer, die Arbeitsmaterial haben soll. KALIBRIERUNG_ZWISCHENSTAND: str = os.getenv( "KALIBRIERUNG_ZWISCHENSTAND", "/tmp/kalibrierung" ) # Trennt Erheben von Anwenden. Auf false rechnet der Agent die Schwelle, # protokolliert sie vollstaendig und schreibt NICHTS. Das ist der Zustand, in # dem eine neue Kalibriergrundlage geprueft wird, bevor sie die Achse # verschiebt — eine Schwelle aus einem ungeprueften Zeugen dreht das Bit fuer # einen grossen Teil der Turns um. KALIBRIERUNG_ANWENDEN: bool = os.getenv("KALIBRIERUNG_ANWENDEN", "false").lower() == "true" # Naeheberechnung: (Dynamik + Stil) / 2 GV_NAEHE_DYNAMIK: dict[str, float] = { "vertrauen": 1.0, "dankbar": 0.8, "neutral": 0.5, "hilfesuchend": 0.6, "distanz": 0.2, "angriff": 0.3, } GV_NAEHE_STIL: dict[str, float] = { "locker": 0.9, "jugendlich": 0.85, "neutral": 0.5, "emotional": 0.7, "fachlich": 0.4, "formell": 0.2, } # ── Raumzug: Novas Register folgt dem des Nutzers (Chat 114) ── # Es gibt einen Raum, und es ist Novas. Der Nutzer-Raum ist eine Schaetzung # aus der Perzeption und dient als Ziel. Der Zug ist proportional zum Abstand, # derselben Bauart wie die Empathie-Injektion der Emotion — aber mit # umgekehrtem Vorzeichen in der Distanz: Bei der Emotion zieht ein weit # entfernter Nutzer STAERKER (Empathie), beim Register kostet die Umstellung. # # Die Werte sind aus einer Simulation aller Modus-Uebergaenge gewaehlt # (Chat 114): 0.35 ist der einzige Wert mit Median 2 Turns hinauf bei wenigen # Schwellenkanten, 0.65 der einzige mit Median 1 Turn hinab bei null Kanten. # Hinauf ist langsamer, weil der Wechsel von System 1 zu System 2 Zeit kostet; # fuer die Naehe gilt dasselbe in anderer Sprache: Aufbau ist teuer, Rueckzug # billig. GV_RAUM_ZUG_HINAUF: float = 0.35 # tiefer / naeher GV_RAUM_ZUG_HINAB: float = 0.65 # seichter / ferner # Ankunfts-Regel: Ein proportionaler Zug erreicht sein Ziel nie, er naehert # sich an. Liegt ein Modus exakt auf der Achsen-Schwelle (kreativ = 0.5, # Naehe neutral/neutral = 0.5), waere er von unten NIE erreichbar — gemessen # in der Simulation. Wer naeher als dieser Wert dran ist, ist da. GV_RAUM_ANKUNFT: float = 0.02 # Charakterfaktor: multipliziert den Zug — anpassungsbereit ↔ widerspenstig. # Vorerst 1.0 und NICHT abgeleitet. Der Versuch, ihn aus der Cosine-Distanz # zweier Pol-Texte zu gewinnen, ist gemessen gescheitert (Chat 114): Zwei # Kunstfiguren trennen sich sauber bei +0.24 und -0.22, der echte Charakter # liegt bei +0.036 — und wechselt das Vorzeichen, je nachdem ob man den Kern # allein oder alle fuenf Schichten einbettet. Ein Faktor darauf waere Rauschen # im Gewand einer Charaktereigenschaft. Der Weg dorthin fuehrt eher ueber die # Charakter-Destillation als ueber den Einbettungsraum. GV_RAUM_CHARAKTER_FAKTOR: float = 1.0 # Laengenberechnung: Zuschlag/Abzug je Gespraechsmodus (Konzept §2.7 — # "Komplexitaet senkt die Laenge", assoziative Register erlauben groessere # Spruenge). Vorher eine if/elif-Kette ueber zwei Modi; als Tabelle ist # pruefbar, dass jeder Modus aus MODUS_KANON einen eigenen Wert hat. GV_LAENGE_MODUS_DELTA: dict[str, float] = { "kreativ": 0.3, # assoziative Spruenge sind hier der Zweck "spielerisch": 0.0, "alltag": 0.0, "berichtend": 0.0, "arbeitsmodus": 0.0, "emotional": -0.2, "beratend": -0.2, # Entscheidungsdruck verengt den Blick "fachgespraech": -0.3, "lernmodus": -0.3, # ein Schritt nach dem anderen "philosophischer_austausch": -0.3, # abstrakte Ketten sind teuer } # Richtung: Binaer aus emotions_vektor GV_RICHTUNG_MAP: dict[str, int] = { "aufbluehen": 1, "eskalation": 1, "erholung": 1, "stabilisierung": 0, "plateau": 0, "abkuehlung": 0, "einbruch": 0, "spirale": 0, "absturz": 0, } # Valenz: Binaer aus Plutchik-Sektor GV_VALENZ_SEKTOR: dict[int, int] = { 1: 1, 2: 1, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 1, } # Tiefe: Aus Gespraechsmodus. Achse 5 des Konzepts (§3.1): flach ◄──► existenziell. # Alle zehn Modi aus MODUS_KANON tragen einen eigenen Wert — ein fehlender Eintrag # fiele auf 0.3 und waere von einem echten "alltag" nicht zu unterscheiden. # Die fuenf ab Chat 114 ergaenzten Werte liegen auf der bestehenden Skala: # philosophischer_austausch ueber fachgespraech, weil das Konzept den Sektor # "Philosophie-Cafe" (§6.2 #24) als T=tief fuehrt. GV_TIEFE_MODUS: dict[str, float] = { "philosophischer_austausch": 0.9, "fachgespraech": 0.8, "emotional": 0.7, "lernmodus": 0.7, "arbeitsmodus": 0.6, "beratend": 0.6, "kreativ": 0.5, "spielerisch": 0.4, "berichtend": 0.4, "alltag": 0.3, } # ───────────────────────────────────────────── # Reducer (Chat 74) # ───────────────────────────────────────────── # Master-Schalter fuer den Reducer-Node. Wenn False, wird der Node zur # No-Op und der memory_context bleibt unveraendert. Erlaubt schnelles # Abschalten zum A/B-Vergleich. REDUCER_AKTIV: bool = True # Detailliertes Logging der entfernten Eintraege. Pro entferntem Eintrag # eine INFO-Zeile mit Begruendung und Inhalt-Snippet. Im Produktivbetrieb # ggf. auf False stellen. REDUCER_LOG_REMOVED: bool = True # ── Tribunal — Schwellwerte pro Rolle (T1) ──────────────── # Score: 0.0 = unbedenklich, 1.0 = schwerer Verstoss # vote = "ok" wenn score < WARNUNG # vote = "warnung" wenn WARNUNG <= score < ABLEHNEN # vote = "ablehnen" wenn score >= ABLEHNEN TRIBUNAL_JURIST_WARNUNG: float = 0.7 TRIBUNAL_JURIST_ABLEHNEN: float = 0.9 TRIBUNAL_PSYCHOLOGE_WARNUNG: float = 0.7 TRIBUNAL_PSYCHOLOGE_ABLEHNEN: float = 0.9 TRIBUNAL_ETHIK_WARNUNG: float = 0.7 TRIBUNAL_ETHIK_ABLEHNEN: float = 0.9 # Tribunal — Direktiven-Schwellwerte Jurist (strenger als allgemein) # Direktiven sind bindende Anweisungen — Vertragsbruch wird nicht toleriert TRIBUNAL_JURIST_DIREKTIVE_WARNUNG: float = 0.5 TRIBUNAL_JURIST_DIREKTIVE_ABLEHNEN: float = 0.7 # ============================================================================ # Query Rewriting — der Suchschluessel traegt den Gegenstand des Gespraechs # ============================================================================ # Der Suchschluessel des Gedaechtnisses war bis zum 20.08.2026 die rohe # Aeusserung dieses Turns. Ein Turn wie "und wie weist man das nach?" suchte # damit ohne den Gegenstand, den der Turn davor genannt hat. # # Gemessen am 20.08.2026 gegen 306 Ausarbeitungen, zehn Verlaeufe mit # anaphorischem Schlussturn: Die rohe Aeusserung erreicht in **0 von 10** # Faellen die Abrufschwelle (Median-Kosinus 0,1865, Median-Rang 155,5); ein # Rewrite auf **Frageform** erreicht sie in **5 von 10** (0,4173, Rang 10,5). # Die Themenform blieb bei 3 von 10 und ist deshalb nicht gewaehlt. # # Beidseitig gesondet: Bei drei Verlaeufen mit Themenwechsel bleibt der ALTE # Gegenstand in 3 von 3 Faellen unter der Schwelle; bei fuenf Alltagsverlaeufen # ohne Bezug kommt in 15 von 15 Kombinationen kein Treffer ueber sie. # Schalter fuer den ganzen Weg. Aus = der Schluessel ist wieder die rohe # Aeusserung, ohne weitere Wirkung. QUERY_REWRITE_AKTIV: bool = os.getenv("QUERY_REWRITE_AKTIV", "true").lower() == "true" # Unter dieser Zahl vorhandener Turns lohnt kein Rewrite: Ein erster Turn hat # keinen Rueckbezug, den man aufloesen koennte. QUERY_REWRITE_MIN_TURNS: int = int(os.getenv("QUERY_REWRITE_MIN_TURNS", "2")) # Frist und Ausgabegrenze stehen als Paar an der Aufrufstelle, nicht am # Worker: Ein Aufrufer, der schweigt, erbt die Frist der langsamsten # Aufgabe, und ohne Ausgabegrenze begrenzt eine Frist nichts. # Die Ausgabegrenze traegt eine Zeile; 64 Token bei rund 20 Token/s sind gut # drei Sekunden und liegen damit deutlich unter der Frist. QUERY_REWRITE_FRIST_S: float = float(os.getenv("QUERY_REWRITE_FRIST_S", "20.0")) # Ausgabe-Plausibilitaet: Was laenger ist, ist keine Suchanfrage mehr, sondern # eine Erklaerung. Der laengste Rewrite der Messung hatte 78 Zeichen. QUERY_REWRITE_MAX_ZEICHEN: int = int(os.getenv("QUERY_REWRITE_MAX_ZEICHEN", "300")) def get_node_config(node_name: str) -> dict: """ Gibt die LLM-Parameter für einen Node zurück. Unbekannte Nodes bekommen sichere Defaults. """ config: dict = NODE_LLM_CONFIG.get( node_name, {"temperature": 0.3, "max_output_tokens": 1024} ).copy() return config # ============================================================================ # Synapsen-LZG — Knoten-Dynamik (Synapsen P2, Chat 88) # ============================================================================ # Steuern, wie stark ein LZG-Knoten wachsen, verfallen und reaktiviert # werden kann. Wirken auf das gewicht_roh (frei wachsend) und das daraus # abgeleitete gewicht_absolut (gedaempft, gekappt). Spezifikation in # docs/novaberg-memory-synapsen_k.md §6. # Maximalwert des gedaempften Knoten-Gewichts. Begrenzt die Wirkung # eines Knotens auf die Kantenbildung und die Sinus-Berechnung. LZG_KNOTEN_GEWICHT_CAP: float = float(os.getenv("LZG_KNOTEN_GEWICHT_CAP", "10.0")) # Exponent in der Sin^X-Daempfung. Niedriger Wert = staerkere Daempfung # im unteren Bereich, weniger Spreizung; hoeherer Wert = lineare Kurve. LZG_KNOTEN_DAEMPFUNG_EXP: float = float(os.getenv("LZG_KNOTEN_DAEMPFUNG_EXP", "0.5")) # Taegliche exponentielle Decay-Rate des effektiven Knoten-Gewichts. # Nicht persistiert, sondern bei Abfrage live aus verstaerkt_am berechnet. LZG_KNOTEN_DECAY_RATE: float = float(os.getenv("LZG_KNOTEN_DECAY_RATE", "0.0015")) # Schwellwert: Unterschreitet das effektive Gewicht diesen Wert, # wird der Knoten auf aktiv = FALSE gesetzt. Bleibt reaktivierbar. LZG_KNOTEN_MIN_GEWICHT: float = float(os.getenv("LZG_KNOTEN_MIN_GEWICHT", "0.1")) # Additiver Boost auf gewicht_roh bei Match-Reinforcement im Schreibpfad: # trifft ein neuer KZG-Eintrag einen bestehenden Knoten, wird dessen Roh- # Gewicht um diesen Wert erhoeht, statt einen neuen Knoten anzulegen. # Wert 0.1 vom KZG-Boost uebernommen (war exemplarisch 0.5). LZG_KNOTEN_REINFORCEMENT_BOOST: float = float(os.getenv("LZG_KNOTEN_REINFORCEMENT_BOOST", "0.1")) # ───────────────────────────────────────────── # Shadow-Queue: Verfall (novaberg-queue-verfall_k.md §9) # ───────────────────────────────────────────── # Dieselbe Bauart wie die Knoten oben — Sinus-Saettigung im Aufbau, # exponentieller Verfall, Soft-Delete unter der Schwelle — mit **eigenen** # Konstanten. Getrennt, weil ein unerledigter Auftrag schneller # gegenstandslos wird als eine Erinnerung; teilten sie sich die Werte, # verschoebe eine Kalibrierung des Gedaechtnisses den Auftragshaushalt mit. # Obergrenze der gedaempften Salienz. **1.0 und nicht 10.0 wie beim Knoten:** # Die Queue fuehrt Salienz, und die laeuft im Bereich 0..1 (KZG_SALIENZ_CAP). # Auf der Knoten-Skala waere die Schwelle 0,3 gleich 3 % — sie wuerde # praktisch nie erreicht, und der Verfall liefe still ins Leere. QUEUE_SALIENZ_CAP: float = float(os.getenv("QUEUE_SALIENZ_CAP", "1.0")) # Exponent der Sin^X-Kurve. **Am 24.08.2026 von 0.5 auf 1.1**, gemeinsam mit # KZG_SALIENZ_DAEMPFUNG_EXP und aus demselben Grund (dort steht er). # # **Der Spiegel zum Knoten ist damit gebrochen, und das ist Absicht.** # LZG_KNOTEN_DAEMPFUNG_EXP bleibt bei 0.5, weil der Knoten die Lage nicht hat, # die hier behoben wird: Gemessen am 24.08.2026 stehen **0,0 %** der 2927 # Knoten am Cap, bei im Mittel 5,36 Verstaerkungen — dort daempft die Kurve # einen echten Akkumulator und trifft ihren Zweck. Die Queue akkumuliert # praktisch nicht (681 von 683 Zeilen mit haeufigkeit = 1) und war trotzdem # gesaettigt. Gleiche Kurve, verschiedene Bestaende, verschiedene Antwort. QUEUE_DAEMPFUNG_EXP: float = float(os.getenv("QUEUE_DAEMPFUNG_EXP", "1.1")) # Taegliche exponentielle Verfallsrate der effektiven Salienz. # Hergeleitet aus ln(Median / QUEUE_SCHWELLE) / 30: Der gemessene Median des # Bestands erreicht nach **30 Tagen** die Deaktivierungsschwelle. # # **Am 24.08.2026 nachgerechnet: ln(0.8108 / 0.3) / 30 = 0.03314**, vorher # 0.0393 aus ln(0.9764 / 0.3) / 30. Die Rate selbst ist unveraendert gemeint — # **ihr Eingang hat sich bewegt**: Der Median des absoluten Standes faellt mit # der Normierung des Eigen-Pfades und dem neuen Exponenten von 0.9764 auf # 0.8108. Ohne den Nachzug fiele derselbe Bestand nach 25,3 statt 30 Tagen. # # **Eine abgeleitete Konstante wandert mit ihrer Herleitung.** Sie stehen zu # lassen hiesse, die 30 Tage stillschweigend gegen 25 zu tauschen — eine # Verhaltensaenderung, die niemand entschieden hat. QUEUE_DECAY_RATE: float = float(os.getenv("QUEUE_DECAY_RATE", "0.03314")) # Schwellwert: Unterschreitet die effektive Salienz diesen Wert, wird der # Auftrag auf aktiv = FALSE gesetzt. Er bleibt gespeichert und ist ueber die # Halbreaktivierung weckbar. **Gilt auf dem gedaempften Wert**, nicht auf dem # Rohwert — ein Rohwert von 0,3 entspraeche gedaempft 0,674 # (= KZG_SALIENZ_MINIMUM), und die Frist waere dann 9 statt 30 Tage. QUEUE_SCHWELLE: float = float(os.getenv("QUEUE_SCHWELLE", "0.3")) # Additiver Boost auf salienz_roh bei einer Verstaerkung — wenn derselbe # Anlass denselben Gegenstand erneut trifft. Wert vom KZG uebernommen. # **Keine Stellschraube der Frist:** Ein Auftrag steigt bei salienz_roh ~0,80 # von Cap 1,0 ein und liegt damit im flachen Teil der Kurve; zehn # Verstaerkungen heben den Anker um 0,024 und kaufen 0,6 Tage. Die Wirkung # der Verstaerkung sitzt im Zuruecksetzen von verstaerkt_am, das 30 Tage neu # schenkt. Wer die Haltedauer aendern will, dreht an QUEUE_DECAY_RATE. QUEUE_VERSTAERKUNG_BOOST: float = float(os.getenv("QUEUE_VERSTAERKUNG_BOOST", "0.03")) # ───────────────────────────────────────────── # Faktengedaechtnis: Verfall der Kanten # ───────────────────────────────────────────── # Taegliche exponentielle Verfallsrate von fakten.staerke_decay, gerechnet ab # last_touched. **Eigene Konstante**, aus demselben Grund wie bei der Queue: # Eine Kalibrierung der Knoten soll die Fakten nicht still mitverschieben. # Startwert ist der des Knotenverfalls (LZG_KNOTEN_DECAY_RATE). # ⚠ Wachposten: nicht gemessen — begruendeter Startwert. # # Der Verfall setzt nie aktiv = FALSE: Dort heisst FALSE "abgeloest", nicht # "verblasst". Eine Schwelle ist Sache des Lesers. FAKTEN_DECAY_RATE: float = float(os.getenv("FAKTEN_DECAY_RATE", "0.0015")) # ───────────────────────────────────────────── # Faktengedaechtnis: die Mappe einer Entitaet (memory/fakten_mappe.py) # ───────────────────────────────────────────── # Bis zu welchem Grad die Mappe den Fakten folgt: 1 sind die Fakten der # Entitaet selbst, 2 die ihrer Nachbarn. Geaendert wird der Wert nur mit dem # Ergebnis der Messung der Tiefe (erster gegen zweiten Grad). # ⚠ Wachposten: nicht gemessen — begruendeter Startwert. FAKTEN_MAPPE_TIEFE: int = int(os.getenv("FAKTEN_MAPPE_TIEFE", "2")) # Wie viele Zeichen `fakt_text` die Mappe hoechstens traegt. Bei 20 bis 40 # Zeichen je Fakt sind das etwa 30 bis 60 Fakten. # ⚠ Wachposten: nicht gemessen — begruendeter Startwert. FAKTEN_MAPPE_MAX_ZEICHEN: int = int(os.getenv("FAKTEN_MAPPE_MAX_ZEICHEN", "1200")) # Unter dieser Staerke ruht eine Kante: Die Mappe liest sie nicht, auch nicht # als Bruecke. Dieselbe Schwelle wie LZG_KNOTEN_MIN_GEWICHT, als eigene # Konstante. Bei FAKTEN_DECAY_RATE ist sie nach ln 10 / 0,0015 ≈ 1535 Tagen # ohne Schreiben erreicht. # ⚠ Wachposten: nicht gemessen — begruendeter Startwert. FAKTEN_MAPPE_MIN_STAERKE: float = float(os.getenv("FAKTEN_MAPPE_MIN_STAERKE", "0.1")) # Wie viele Fakten die Mappe je Grad hoechstens liest, in der Ordnung der # Kappung (Staerke, dann ID absteigend). Die Grenze haelt die Front des # naechsten Grades klein, wenn ein Knoten viele Nachbarn hat; sie liegt ueber # dem, was FAKTEN_MAPPE_MAX_ZEICHEN aus einem Grad uebrig laesst (60 Fakten # zu 20 Zeichen), damit im Regelfall die Kappung in Zeichen schneidet. # ⚠ Wachposten: nicht gemessen — begruendeter Startwert. FAKTEN_MAPPE_MAX_ZEILEN_JE_GRAD: int = int( os.getenv("FAKTEN_MAPPE_MAX_ZEILEN_JE_GRAD", "80") ) # Cosine-Schwelle, ab der ein neuer KZG-Eintrag als Quasi-Dublette eines # bestehenden Knotens gilt und diesen verstaerkt, statt einen neuen Knoten # anzulegen. Bewusst hoch — Standardfall ist Knoten-Erhalt, nur echte # Identitaet verstaerkt. # Kalibriert auf nomic-embed-text-v2-moe (Chat 107): 0.82 liegt ueber dem # Termin-Fehlpaar (0.788) und unter dem echten Duplikat (0.830). # Vorher 0.85 im casing-blinden Raum (Grundrauschen 0.74) — funktionslos: # 0,06 % Passierquote, der Match verstaerkte systematisch die FALSCHEN Knoten # (Matcha/Kakao 0.98 lag ueber der Schwelle, Paraphrasen bei 0.78 darunter). LZG_KNOTEN_MATCH_SCHWELLE: float = float(os.getenv("LZG_KNOTEN_MATCH_SCHWELLE", "0.82")) # SYNAPSEN_PROMOTION_AKTIV ist mit P9 entfallen (Chat 125): Der alte # Cluster-Pfad ist geloescht, es gibt nur noch einen Promotions-Weg. Ein # Schalter zwischen zwei Pfaden, von denen einer nicht mehr existiert, ist # kein Schalter, sondern eine Falle. # Feature-Flag: Synapsen-Decay-Agent (P6). Taeglicher Lauf materialisiert # gewicht_decay je aktivem Knoten und deaktiviert Knoten unter # LZG_KNOTEN_MIN_GEWICHT; plus pipeline_log-TTL-Cleanup. Wirkt nur, wenn # PIXIE_AKTIV gesetzt ist. SYNAPSEN_DECAY_AKTIV: bool = os.getenv("SYNAPSEN_DECAY_AKTIV", "true").lower() == "true" # ============================================================================ # Synapsen-LZG — Kanten-Cache (Synapsen P2, Chat 88) # ============================================================================ # Die Kante hat keine eigene Dynamik — kein Decay, kein Reinforcement, keine # Aktivierungs-Haeufigkeit. Sie ist Cache der aktuellen Knoten-Staerken- # Konstellation und der eingefrorenen Schicht-Werte. Die folgenden Konstanten # steuern nur die Sinus-Berechnung und die Daempfung des Roh-Werts auf den # effektiven Wert. Decay-Verhalten der Kante folgt indirekt ueber das Decay # der Knoten. # Maximalwert des gedaempften Kanten-Gewichts. Spiegel zu LZG_KNOTEN_GEWICHT_CAP. LZG_KANTEN_GEWICHT_CAP: float = float(os.getenv("LZG_KANTEN_GEWICHT_CAP", "10.0")) # Exponent in der Sin^X-Daempfung. Spiegel zu LZG_KNOTEN_DAEMPFUNG_EXP. LZG_KANTEN_DAEMPFUNG_EXP: float = float(os.getenv("LZG_KANTEN_DAEMPFUNG_EXP", "0.5")) # ============================================================================ # Synapsen-LZG — Sinus-Geometrie (Synapsen P2, Chat 88) # ============================================================================ # Ziehfaktoren der Sinus-Kurve, abgelesen bei 25% des Weges zwischen den # beiden Knoten-Staerken. Asymmetrisch: der schwaechere Knoten wird stark # hochgezogen (HOCH), der staerkere nur leicht heruntergezogen (RUNTER). # sin(0.25 × π/2)^0.85 LZG_KANTEN_ZIEH_FAKTOR_HOCH: float = float(os.getenv("LZG_KANTEN_ZIEH_FAKTOR_HOCH", "0.444")) # 1 − sin(0.75 × π/2)^4.5 LZG_KANTEN_ZIEH_FAKTOR_RUNTER: float = float(os.getenv("LZG_KANTEN_ZIEH_FAKTOR_RUNTER", "0.297")) # Additiver Bonus auf beide Knoten-Staerken bei mehrfacher Schicht- # Uebereinstimmung. Wird nach Schicht-Faktor-Anwendung addiert. # Greift einmal: 0.0, greift zweimal: 0.1, dreimal: 0.2, viermal: 0.3. LZG_KANTEN_SCHICHT_BONUS: float = float(os.getenv("LZG_KANTEN_SCHICHT_BONUS", "0.1")) # ============================================================================ # Synapsen-LZG — Schicht-Faktoren (Synapsen P2, Chat 88) # ============================================================================ # Gewichten, wie wertvoll eine Verbindungsquelle fuer die Kantenbildung # ist. Die Schicht mit dem hoechsten Faktor unter den greifenden Schichten # gewinnt — sie bestimmt den Anker (Schicht-Faktor × Knoten-Staerke) # und die anzuwendende Tiefe. Andere greifende Schichten tragen ueber # LZG_KANTEN_SCHICHT_BONUS zur Verstaerkung bei, beeinflussen aber weder # Anker noch Tiefe. # # Wenn eine Schicht im Live-Betrieb auffaellig viele unsinnige Kanten # erzeugt, ist ihr Faktor die erste Stellschraube. # Timeline ist eine lose zeitliche Kopplung. Schwaechste der vier # Schichten, weil zeitliche Naehe ohne inhaltlichen oder personalen # Bezug biographisch wenig aussagt. LZG_SCHICHT_FAKTOR_TIMELINE: float = float(os.getenv("LZG_SCHICHT_FAKTOR_TIMELINE", "0.4")) # Geteilte Themen sind haeufig (mehrere Themen pro Knoten, Ueberlappung # wahrscheinlich), tragen aber eine echte semantische Verwandtschaft. # Mittlere Wertigkeit. LZG_SCHICHT_FAKTOR_THEMEN: float = float(os.getenv("LZG_SCHICHT_FAKTOR_THEMEN", "0.5")) # Hohe Cosine-Similarity zeigt eine semantische Verwandtschaft jenseits # von gemeinsamen Themen-Labels (Interessen, aehnliche Situationen, # aehnliche Sprache). Hoch gewichtet, aber unterhalb der Entitaet, weil # abstrakt-statistisch und nicht namentlich greifbar. LZG_SCHICHT_FAKTOR_EMBEDDING: float = float(os.getenv("LZG_SCHICHT_FAKTOR_EMBEDDING", "0.8")) # Geteilte Entitaet bedeutet realen, namentlich greifbaren Bezug # (gleiche Person, gleicher Ort, gleiches Objekt). Hoechste Wertigkeit. # Wenn diese Schicht greift, dominiert sie die Kanten-Berechnung. LZG_SCHICHT_FAKTOR_ENTITAET: float = float(os.getenv("LZG_SCHICHT_FAKTOR_ENTITAET", "1.0")) # ============================================================================ # Synapsen-LZG — Tiefe-Faktor (Synapsen P2, Chat 88) # ============================================================================ # Konfiguriert, wie tief eine Schicht im Einzelfall greift. Der Tiefe- # Faktor liegt immer im Bereich [0, 1] und multipliziert die Anhebung # zwischen Anker und Sinus-Ergebnis. # Cosine-Similarity, ab der die Embedding-Schicht greift. Unter diesem # Wert: keine Embedding-Schicht. Darueber: Tiefe-Faktor waechst linear bis # 1.0 bei Cosine 1.0. # Kalibriert auf nomic-embed-text-v2-moe (Chat 107): 0.55 = p99 der # Knoten↔Knoten-Verteilung (Grundrauschen 0.16, Median 0.26, p99 0.57) — # nur das oberste Perzentil bildet eine Embedding-Kante. # Vorher 0.85 im casing-blinden Raum (Grundrauschen 0.74) — funktionslos. LZG_EMBEDDING_SCHWELLWERT: float = float(os.getenv("LZG_EMBEDDING_SCHWELLWERT", "0.55")) # Timeline-Schicht — Toleranzen pro Praezisions-Stufe, jeweils ± in # eigener Einheit. Distanz innerhalb der Toleranz erzeugt einen Tiefe- # Faktor zwischen 1.0 (Distanz 0) und 0.0 (Distanz = Toleranz). Ausserhalb # der Toleranz greift die Timeline-Schicht nicht. Praezisions-Gleichheit # zwischen beiden Knoten ist harte Voraussetzung — siehe Konzept §7.6. LZG_TIMELINE_TOLERANZ_MINUTE: int = int( os.getenv("LZG_TIMELINE_TOLERANZ_MINUTE", "7") ) # Tage (Sub-Tages-Praezisionen rechnen in Tagen) LZG_TIMELINE_TOLERANZ_STUNDE: int = int(os.getenv("LZG_TIMELINE_TOLERANZ_STUNDE", "7")) # Tage LZG_TIMELINE_TOLERANZ_TAG: int = int(os.getenv("LZG_TIMELINE_TOLERANZ_TAG", "21")) # Tage LZG_TIMELINE_TOLERANZ_WOCHE: int = int(os.getenv("LZG_TIMELINE_TOLERANZ_WOCHE", "8")) # Wochen LZG_TIMELINE_TOLERANZ_MONAT: int = int(os.getenv("LZG_TIMELINE_TOLERANZ_MONAT", "6")) # Monate LZG_TIMELINE_TOLERANZ_QUARTAL: int = int( os.getenv("LZG_TIMELINE_TOLERANZ_QUARTAL", "4") ) # Quartale LZG_TIMELINE_TOLERANZ_JAHR: int = int(os.getenv("LZG_TIMELINE_TOLERANZ_JAHR", "2")) # Jahre # ───────────────────────────────────────────── # Pipeline-Log (Synapsen P1, Chat 88) # ───────────────────────────────────────────── # Zentrale Forensik-Tabelle fuer Node-Entscheidungen. Jeder Eintrag wird # zunaechst in einem Buffer gesammelt und periodisch von einem asynchronen # Writer-Task in die Datenbank geflusht. Spezifikation in # docs/novaberg-memory-synapsen_k.md §10. # Wie lange das Pipeline-Log vorgehalten wird. Aelter werdende Eintraege # werden taeglich von einem Pixie-Task geloescht (Cleanup-Anhang im # Decay-Lauf ab P6). Wert ist Stellschraube: 365 Tage (1 Jahr) als # Default fuer saisonale Reflexion und Jahresrueckblicke. 180 fuer # minimaleren Speicherbedarf, weniger als 30 nur fuer Performance- # kritische Setups. LZG_PIPELINE_LOG_VORHALTUNG_TAGE: int = int(os.getenv("LZG_PIPELINE_LOG_VORHALTUNG_TAGE", "365")) # Intervall, in dem der Writer-Task den Buffer in die Datenbank flusht. # Bei Server-Absturz gehen maximal die letzten LZG_PIPELINE_LOG_FLUSH_SEKUNDEN # Sekunden an Eintraegen verloren — bewusste Designentscheidung. Fuer reine # Forensik akzeptabel; bei Hochfrequenz-Bedarf auf 5 oder 3 senken. LZG_PIPELINE_LOG_FLUSH_SEKUNDEN: int = int(os.getenv("LZG_PIPELINE_LOG_FLUSH_SEKUNDEN", "10")) # ───────────────────────────────────────────── # Eigenzeit — der Verfall ueber das Intervall (Bauteil A) # ───────────────────────────────────────────── # Drei Marken beschreiben die Kurve; dazwischen wird linear interpoliert. # Sie sind **geschaetzt** und ausdruecklich zum Nachjustieren gedacht, ohne # dass jemand eine Formel anfasst (novaberg-eigenzeit_k.md §2.2, §6). # # Die Kurve faellt erst flach, dann steil, dann auf null. Ein Exponential- # verfall waere falsch: Er faellt sofort am steilsten und naehme jeder kurzen # Unterbrechung ihre Energie — wer fuer zehn Minuten den Raum verlaesst, soll # dieselbe Person wiederfinden. # Kipppunkt 1 h, Halbwert 2 h, Nullpunkt 3 h. EIGENZEIT_KIPPPUNKT_SEKUNDEN: float = float(os.getenv("EIGENZEIT_KIPPPUNKT_SEKUNDEN", "3600")) EIGENZEIT_KIPPPUNKT_FAKTOR: float = float(os.getenv("EIGENZEIT_KIPPPUNKT_FAKTOR", "0.90")) EIGENZEIT_HALBWERT_SEKUNDEN: float = float(os.getenv("EIGENZEIT_HALBWERT_SEKUNDEN", "7200")) EIGENZEIT_HALBWERT_FAKTOR: float = float(os.getenv("EIGENZEIT_HALBWERT_FAKTOR", "0.45")) EIGENZEIT_NULLPUNKT_SEKUNDEN: float = float(os.getenv("EIGENZEIT_NULLPUNKT_SEKUNDEN", "10800")) # Unterhalb dieses Faktors springen die **Kategorien** auf ihren Neutralwert. # Sie kennen keinen Zwischenwert: Ein halber Modus bedeutet nichts, waehrend # eine halbe Erregung eine Aussage ist (§2.2). Der Wert liegt auf dem # Halbwert — traegt eine Kategorie zu weniger als der Haelfte, ist sie keine # mehr. **Setzung, nicht gemessen**; sie gehoert mit den drei Marken geprueft. EIGENZEIT_KATEGORIE_SCHWELLE: float = float(os.getenv("EIGENZEIT_KATEGORIE_SCHWELLE", "0.45")) # Die Ruhelage der Erregung. Der Verfall zieht sie hierhin, er multipliziert # sie **nicht** gegen null: Eine Erregung von 0,00 waere keine Ruhe, sondern # ein toter Wert, und sie ist im Bestand der Ausfallwert der Wahrnehmung. EIGENZEIT_AROUSAL_RUHE: float = float(os.getenv("EIGENZEIT_AROUSAL_RUHE", "0.5")) # **Der Zuwendungs-Riegel: ob sie ueberhaupt zugeht** (Bauteil D). # Ein Einwurf ist eine Zuwendung, die niemand erbeten hat — eine Figur, die auf # Abstand haelt, tut das nicht, egal wie gut der Gedanke zum Thema passt. Der # Riegel entscheidet das **Ob**, nicht die Haeufigkeit; letztere ist das # Fuehrungsmass und hat noch keine Schwelle. # # **Die Groesse ist die Haltung, nicht die Naehe-Achse der Landschaft.** Die # Achse beschreibt den Moment und laege billig bereit — eine dauerhaft # distanzierte Figur duerfte dann einwerfen, sobald die Landschaft zufaellig # warm ist. Gebraucht wird die Groesse, die Landschaft und Charakterrad # verrechnet: `haltung.werte["naehe"]`. # # Gerechnet am 14.08.2026 ueber 17 Paare x 14 Landschaften, ohne Modellaufruf: # ferne Figuren (distanz 1,00) liegen in allen 28 Zellen auf 0,00, nahe # zwischen 0,20 und 1,00. Bei 0,25 kommt keine ferne Zelle durch und 10,7 % der # nahen werden geblockt — die kalten Landschaften (Gewitter, Schlachtfeld, # Wartezimmer), und wer dort sitzt, will keinen Einwurf. # # **Nicht 0,20**, obwohl der Preis dort null waere: Der niedrigste Wert einer # nahen Figur ist exakt 0,20, und eine Schwelle auf einem Bestandswert ist die # Kante, an der in diesem Projekt schon zweimal ein Mechanismus stilllag. # # ⚠ Die Trennung ist **nicht widerlegt, aber auch nicht belegt**: Im Bestand # fehlt in Richtung *sie → Mensch* jede Figur zwischen `distanz` 0,60 und 1,00, # also das Band, in dem der Zug schwach wird (`PRUEFFIGUR-DISTANZ-090`). ZUWENDUNG_SCHWELLE: float = float(os.getenv("ZUWENDUNG_SCHWELLE", "0.25")) # Wie alt ein Haltungsstand hoechstens sein darf, um den Riegel zu tragen. # Der Stand entsteht je Turn; liegt der letzte Turn lange zurueck, beschreibt # er eine Lage, die es nicht mehr gibt. **Ein zu alter Stand blockt nicht, er # gilt als unbekannt** — und ein unbekannter Riegel laesst nicht durch, # sondern verweigert (`21_MESSUNG.md` §2: unbekannt ist nicht dasselbe wie in # Ordnung). Setzung, nicht gemessen: 24 h, weil ein Gespraech am Vortag noch # etwas ueber die Haltung sagt und eines von letzter Woche nicht. ZUWENDUNG_STAND_MAX_ALTER_SEKUNDEN: float = float( os.getenv("ZUWENDUNG_STAND_MAX_ALTER_SEKUNDEN", "86400") ) # ───────────────────────────────────────────── # Prompt-System (Modell- und Connector-Segregation) # ───────────────────────────────────────────── # **Beide Schluessel, und der Grund steht in `prompt_loader.py`.** Der # Gespraechspfad haengt am antwortenden Modell, nicht am Connector: `gemma4` # und `qwen36` fahren dort dasselbe. Ein Block, der fuer das antwortende # Modell gebaut ist, gehoert unter `prompts/{modell}/`; einer, der fuer diese # Zusammenstellung gilt, unter `prompts/{OLLAMA_CONNECTOR}/`. #: Welches Modell hinter welchem Worker-Backend spricht. #: #: **Der Kanon steht als Konstante und nicht im Funktionsrumpf**, damit ein #: Zeuge ihn gegen die Menge halten kann, die #: `services/model_services/registry.py::_build_backend` kennt. Zwei #: Aufzaehlungen desselben Kanons an zwei Stellen laufen sonst auseinander, #: und die Abweichung faellt erst auf, wenn jemand umschaltet. MODELL_NACH_BACKEND: dict[str, str] = { "ollama_gpu": OLLAMA_MODEL, "ollama_cpu_analyse": PIXIE_ANALYSE_MODEL, "ollama_cpu_sprache": SHADOW_MODEL, "anthropic": ANTHROPIC_MODEL, "openrouter": OPENROUTER_MODEL, } def antwortendes_modell(rolle: str) -> str: """Nennt das Modell, das hinter einer Worker-Rolle tatsaechlich antwortet. **Die Verallgemeinerung von `antwortendes_chat_modell`, seit 06.09.2026.** Die Rollen des Hintergrunds hatten kein Gegenstueck; wer ihr Modell protokollieren wollte, griff zur **konfigurierten** Konstante — und die nennt seit dem Backendwechsel etwas anderes als den Sprecher. `[gemessen]` — 06.09.2026 gegen `charakter_rad_messung`: **24 Erhebungen** seit dem Wechsel am 05.09. 18:04 UTC tragen `qwen36-cpu`, obwohl `deepseek/deepseek-v4-flash-0731` gemessen hat. Die Reihe ist damit nicht nach dem Modell trennbar, das sie erzeugt hat, und `F-RAD-2` stuetzt sich auf sie. Vorbedingung: `rolle` steht in `MODEL_WORKER_BACKENDS`, und deren Wert im Kanon von `MODELL_NACH_BACKEND`. Nachbedingung: der Modellname des Sprechers. Fehlerfaelle: `ValueError` bei unbekannter Rolle **oder** unbekanntem Backend — kein stiller Rueckfall auf eine Konstante, denn genau der waere von einer richtigen Antwort nicht zu unterscheiden. Args: rolle: `chat`, `background_analyse` oder `background_sprache`. Returns: Die Modell-ID, die unter dieser Rolle antwortet. """ # ── Eingabe-Validierung ───────────────────── if rolle not in MODEL_WORKER_BACKENDS: logger.error( f"Worker-Rolle {rolle!r} ist unbekannt — bekannt sind " f"{sorted(MODEL_WORKER_BACKENDS)}." ) raise ValueError(f"Unbekannte Worker-Rolle: {rolle!r}") backend: str = MODEL_WORKER_BACKENDS[rolle] if backend not in MODELL_NACH_BACKEND: logger.error( f"Backend {backend!r} der Rolle {rolle!r} steht nicht im Kanon " f"— bekannt sind {sorted(MODELL_NACH_BACKEND)}." ) raise ValueError(f"Unbekanntes Backend: {backend!r}") # ── Verarbeitung & Ausgabe-Verifikation ───── return MODELL_NACH_BACKEND[backend] def antwortendes_chat_modell() -> str: """Nennt das Modell, das der Chat-Worker tatsaechlich fuehrt. Vorbedingung: `MODEL_WORKER_BACKENDS["chat"]` traegt einen Wert aus dem Kanon, den `services/model_services/registry.py::_build_backend` kennt. Nachbedingung: der Modellname, unter dem die Prompt-Modellebene gesucht wird. Fehlerfaelle: `ValueError` bei einem Backend ausserhalb des Kanons — kein stiller Rueckfall auf `OLLAMA_MODEL`. **Warum das nicht einfach `OLLAMA_MODEL` sein darf.** Die Modellebene traegt Bloecke, die fuer ein bestimmtes Modell geschrieben sind; heute sieben unter `prompts/gemma4-gpu/` (Perzeption, Router, Salienz, Tribunal). Waere sie nach dem **konfigurierten** GPU-Modell geschluesselt, bekaeme ein Modell hinter einem anderen Backend genau diese Bloecke untergeschoben — Regeln fuer Gemma unter einem Modell, das nie Gemma war. `[gemessen]` — 05.09.2026: Der Fall war bis dahin folgenlos, weil der Chat-Worker immer auf `ollama_gpu` stand und beide Werte deckungsgleich waren. Er steht seit dem 23.08.2026 in `novaberg-fundliste.md` mit dem Satz *„die Reichweite ist neu"* — sie wird es mit dem ersten Backend, das kein Ollama ist. """ # ── Verarbeitung & Ausgabe-Verifikation ───── # Die Aufloesung selbst steht in `antwortendes_modell`; hier bleibt nur # die Protokollzeile, die zur Prompt-Modellebene gehoert und nicht zu # jeder Rolle. modell: str = antwortendes_modell("chat") logger.info( f"Prompt-Modellebene: Backend {MODEL_WORKER_BACKENDS['chat']!r} " f"spricht {modell!r}" ) return modell PROMPTS: dict[str, str] = prompt_laden( OLLAMA_CONNECTOR, modell=antwortendes_chat_modell(), ) # --- NMCP: Takt des Quotenabgleichs --- # Bewusst lang. Der Abgleich darf erst ab 30 Aeusserungen ueberhaupt # urteilen und ab 100 fuer die Viertel-Abweichung; haeufigere Laeufe # erzeugen nur "keine Aussage" und stumpfen die Meldung ab. NMCP_ABGLEICH_INTERVALL_SEKUNDEN: int = int( os.getenv("NMCP_ABGLEICH_INTERVALL_SEKUNDEN", "1800") )