Ctrl K
ComunidadGuías…

docs/novaberg-node-router.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Novaberg — Node: Router

**Projekt:** Novaberg — The Nova Anima Resonance System
**Dokument:** Node-Referenz Router
**Stand:** 22. Oktober 2026, 20:29 UTC (`date -u`; der Vermerk unter der Graph-Zeile: zwischen Sachlage und Router steht der Erinnerungsanker — gebaut und committet, Code-Tree `04f823e`). Davor 17. Oktober 2026, 13:13 UTC (`date -u`; §3.2: der Leseschritt hinter dem Router ist die Knotenfunktion `read_after_route` — gebaut, noch nicht committet). Davor 16. Oktober 2026, 16:02 UTC (`date -u`; das Ziel des Routers führt im Planner unter den nahen Diensten — Nachtrag bei der Messung zum `[LAGE]`-Block). Davor 15. Oktober 2026, 21:36 UTC (`date -u`; §3.2: die Timeline-Anfrage liest seit heute derselbe Knoten hinter dem Router, `direction` gilt auch für `range` — gebaut). Davor 18. September 2026, 21:14 UTC (die Zustimmung auf ein offenes Angebot stellt der Router selbst zu, wenn das Modell es nicht tut; ein eigener Auftrag verwirft eine offene Rückfrage; beide Weichen im Pipeline-Log). Davor 17. September 2026, 20:38 UTC (**E1: der `[LAGE]`-Block trägt den Zustimmungssatz nur bei offenem Angebot**, dazu ein Riegel gegen die blanke Zustimmung ohne Angebot — `utils/offers.py`; im Betrieb gemessen). Davor: 17. September 2026, 14:27 UTC (**der `[LAGE]`-Block** — akute Objekte mit Bekanntem und Diensten vor den Aushängen; eine Zustimmung auf ein Angebot ist ein Auftrag; nicht auf Impuls-Turns, nicht nach einem Sachlage-Ausfall — Scheibe 12 D2a). Davor 17. September 2026, 10:58 UTC (das Urteil der Objekt-Nähe wird als `objekt_urteil` geschrieben und vom Planner gelesen — Scheibe 12 D1). Davor 16. September 2026, 20:58 UTC (die Objekt-Nähe urteilt **je Zettel für sich** — jeder Dienst über der Untergrenze ist Empfänger, der Abstand entscheidet nichts mehr). Davor 16. September 2026, 20:18 UTC (**die Objekt-Nähe im Schatten** — vor jeder Entscheidung rechnet der Knoten, welchem Dienst ein akutes Objekt der Lage nahe ist, protokolliert es und benutzt es nicht; letzter Abschnitt). Davor 14. September 2026, 19:52 UTC (der Timeline-Aushang verlangt einen Auftrag; unter `gemma4-a4b-gpu` gemessen, was der Empfang zustellt; die Position im Graphen berichtigt; der Verlauf in 100 Zeichen je Beitrag als Gefahr markiert — §2, §4.2, letzter Abschnitt). Davor: 25. August 2026 (der Sprecher kommt aus dem Feld, nicht aus der Position; der Enricher filtert nicht mehr). Davor: 23. August 2026 (die `[REGELN]` haben einen Override ueber dem Default — dreistufige Prompt-Segregation). Davor: 17. August 2026 (das Brett kommt von der Dienst-Fläche)
**Pfad:** novaberg/docs/novaberg-node-router.md
**Quellen:** nova-01-m-b.md
**Datei:** `graph/nodes/router.py`

---

## 1. Aufgabe

Der Router entscheidet, welche Ressourcen für die Verarbeitung des User-Prompts benötigt werden. Er trifft Routing-Entscheidungen — keine Analysen. Die Analyse hat die Perzeption bereits erledigt; der Router sieht deren Ergebnisse (Intent, Emotion, Arousal, Modus, Beziehungsdynamik) und entscheidet auf dieser Basis: Brauche ich Gedächtnis? Web-Suche? Timeline? Betrifft das eine Agent-Domäne?

**Resume-Check (seit Chat 23):** Vor jeder Routing-Entscheidung prüft der Router über `redis_manager.get_json()`, ob ein Agent auf eine Antwort wartet (`pending_agent:{user_id}`, TTL 300s). Falls ja, wird sofort `management_action=resume` gesetzt und der LLM-Call übersprungen. Der Prompt des Users geht direkt an den wartenden Agenten.

---

## 2. Position im Graph

```
CharacterGraph (Pfad 2): Enricher → EI-Calc → ▶ Router ◀ → [Planner ⇄ Agent] → GV-Node → ...
```

Nur im CharacterGraph. Im HumanGraph (Pfad 1) gibt es keinen Router — der User-Pfad routet nicht.

> **Berichtigt am 14.09.2026 — die Zeile oben ist veraltet.** Im Code steht `reducer → sachlage_node → router` (`graph/character_graph.py`): Die Sachlage läuft **unmittelbar vor** dem Router, damit beide Pfade dasselbe Verstehen sehen. **Der Router liest sie nicht** — weder `state["sachlage"]` noch ein Objekt daraus. → **Seit dem 22.10.2026** (gebaut, getestet und committet, Code-Tree `04f823e`) steht zwischen beiden der Erinnerungsanker: `reducer → sachlage_node → erinnerungsanker → router` (`graph/character_graph.py`). Er ergänzt vor dem Router `memory_entries`, `memory_context`, `memory_context_verfasser` und `lzg_resonanz` um Fakten und Erinnerungen der gebundenen Personen (`novaberg-memory-fakten_b.md` §6, *Der Erinnerungsanker*); der Router liest die Sachlage weiter nicht. Dass der Empfang die Objekte des Gesprächskontexts liest, ist entworfen, nicht gebaut (`novaberg-thinking-lage_k.md` §4, Scheibe 12).

---

## 3. Routing-Felder

### 3.1 Ressourcen-Flags

| Feld | Typ | Wann `true` |
|------|-----|-------------|
| `needs_memory` | `bool` | Intent `personal`, emotionale Signale, Emotion ≠ `neutral`, wiederkehrende Themen |
| `needs_web` | `bool` | Aktuelle Fakten nötig (Intent `knowledge` + aktuelle Themen). SearXNG-Integration über den Thinker (seit Chat 12/15). |
| `needs_timeline` | `bool` | Frage nach Terminen/Daten ODER neuer Termin wird genannt. **Das Flag sagt, dass eine Zeitangabe vorkommt — nicht, welcher Dienst gemeint ist.** Der Planner behandelt es seit jeher als Dienstwahl; das ist seit dem 16.09.2026 ein Defekt (`PLANNER-ZEITWORT-UEBERSTIMMT-DIENSTWAHL`), weil der Gegenstand entscheidet und nicht das Zeitwort ~~(Defekt)~~ → **seit dem 17.09.2026 nicht mehr**: Der Planner liest es nur noch im Auffang (Scheibe 12 D1). |

### 3.2 Timeline-Query

Strukturierte Abfrage ~~für den TimelineManager~~ → **für den Leseweg hinter dem Router** (15.10.2026): `graph/base.py::_node_route` ruft nach `route()` ~~`memory/timeline_reading.py::read_for_turn`~~ → seit dem 17.10.2026 `graph/nodes/route_reading.py::read_after_route` (gebaut, noch nicht committet), und der Verfasser bekommt das Ergebnis als `[TERMINE]` (`novaberg-agent-timeline.md` §4a). Der TimelineManager las die Anfrage im Enricher, der vor dem Router läuft — sein gezielter Zweig war nie erreichbar (seit dem 18.09.2026 109 Router-Zeilen mit `timeline=True`, 0 gezielte Lesungen).

| Typ | Zweck | Felder |
|-----|-------|--------|
| `range` | Zeitraum-Abfrage | `from`, `to` als absolutes Datum. „morgen" = morgen 00:00–23:59. |
| `search` | Keyword-Suche | `keyword`, `direction` (forward/backward/both), `limit` |
| `store` | Neuen Termin speichern | `date` (berechnet), `title` — **der Router-Prompt nennt die Art nicht** (`type: range\|search`, gelesen am 15.10.2026); beim Lesen gilt eine solche Anfrage als unlesbar: ERROR und das proaktive Fenster |

Der Router berechnet relative Datumsangaben: „in 3 Wochen" = heute + 21 Tage. Die Zeitrichtung wird aus der deutschen Grammatik erkannt: Präsens/Futur → forward, Imperfekt/Perfekt → backward. **Seit dem 15.10.2026 gilt `direction` für `range` und `search` gleich** (`prompts/default/router.task.txt`), und `forward` auch bei Fragen nach Kommendem (*„was steht an“*, *„was habe ich noch“*, *„diese Woche noch“*) — dann fehlen abgelaufene Termine. Das ist Fassung c der Entscheidung in `TIMELINE-NENNT-ABGELAUFENEN-TERMIN-ALS-KOMMENDEN`. Kein Zeuge prüft den Text des Router-Prompts; wirken kann der Satz nur in der Messung.

### 3.3 Momentum

Misst die Gesprächsdynamik für den Shadow Delivery Service:

| Wert | Bedeutung | Beispiele |
|------|-----------|-----------|
| `low` | Abschluss, kurze Bestätigung | „okay", „danke", „passt", „verstanden", „cool" |
| `mid` | Normaler Flow | Fragen, Aussagen, moderate Interaktion |
| `high` | Aktives Engagement | Tiefe Fragen, emotionale Intensität, komplexe Aufträge |

**Guard:** Wenn der Prompt eine Frage enthält oder eine Aktion auslöst, ist Momentum niemals `low` — auch bei kurzen Prompts. „Ja, mach das" = `mid`. Zusätzlich: Wenn `management_action` gesetzt ist, wird `low` automatisch auf `mid` korrigiert (Python-Guard nach dem LLM-Call).

### 3.4 Agenten-Delegation (seit Chat 26, AGT6; Fix Chat 40)

Der Router trifft KEINE Management-Entscheidungen mehr für Agent-Domänen. Die gesamte MANAGEMENT-ERKENNUNG (72 Zeilen mit CRUD-Beispielen, Faustregel, Imperativ-Verben) wurde durch eine 4-Zeilen-Delegationsregel ersetzt:

```
AGENTEN-DELEGATION:
- "management_action", "management_target", "management_target_typ":
  Diese Felder werden AUSSCHLIESSLICH durch die Agentenregeln unten gesteuert.
  Wenn eine Regel zutrifft, MUSST du die Felder entsprechend setzen.
  Ohne passende Agentenregel: ALLE DREI Felder LEER lassen ("").
```

> **Fix Chat 40 (nova-12-l-a):** Die ursprüngliche Formulierung "Setze sie NIEMALS eigenständig" blockierte bei Claude-Backend alle Plugin-Regeln — Claude interpretierte die Plugin-Regeln als "eigenständig". Positive Handlungsanweisung ("MUSST wenn Regel zutrifft") löst das Problem. Lesson: "NIEMALS" ist kein Proxy für "nur wenn erlaubt".

Jedes Agent-Plugin definiert seine eigenen Erkennungsregeln über `router_prompt`. Die Plugin-Prompts setzen sowohl `management_action` als auch `management_target`:

- **Domäne erkannt** → `management_action = "agent"`, `management_target = "{Agent-Name}"` (seit Chat 40)
- **Kontext-Erkennung:** Auch implizite Bezüge auf aktive Notizen/Listen aus dem Gesprächsverlauf triggern den Agent — ohne Imperativ-Verb (z.B. „Wir brauchen auch Erdbeeren" nach Obstlisten-Gespräch)

Der Agent klassifiziert die konkrete Aktion (create/read/update/delete) selbst über einen LLM-Call (Classify-Node in `agents/notizen/klassifikation.py`).

> **Architektur-Entscheidung (Chat 25/26, AGT6):** Die Sekretärin diagnostiziert nicht. Der Router erkennt die Domäne, der Agent die Aktion. Das vermeidet AGT7 („Streich X von Y" als Delete statt Update) und ROUTE3 (semantische statt Recency-basierte Auflösung).

#### Dispatch-Guard (Chat 65)

Generelle Sicherheitsregel in `prompts/default/router.task.txt`: Kein Agent-Dispatch ohne Kommando-Signal. Der User-Prompt muss ein Verb, einen Imperativ oder ein Schlüsselwort enthalten, das eine Aktion impliziert. Bloße Themen-Erwähnung ("Wir haben über Lumi gesprochen") darf keinen Agent triggern — nur explizite Änderungsanweisungen ("Schreib auf, dass Lumi...").

**Ursache (ROUTE-CHAR-NOTIZ, Chat 62/65):** Der CharacterGraph-Router dispatchte Konversation an den NotizenAgent, weil Themen-Erwähnung als Notiz-Update interpretiert wurde. Zwei Maßnahmen: (1) Genereller Dispatch-Guard im Router-Task-Prompt. (2) Plugin-Regel im `notizen_manager` verschärft.

### 3.5 Management-Felder (Referenz)

Die Felder existieren weiterhin im State, werden von Agent-Plugins gesetzt:

| Feld | Wer setzt es | Beschreibung |
|------|-------------|-------------|
| `management_action` | Agent-Plugin `router_prompt` | `"agent"` für Agent-Domänen, `"resume"` für Pending-Agent, `""` sonst |
| `management_target` | Agent-Plugin `router_prompt` (seit Chat 40) | Agent-Name — Planner matcht über Target-Match (Priorität 3) |
| `management_target_typ` | Agent (Classify-Node) | titel/inhalt/thema — vom Agent bestimmt |

Die 3-Stufen-Auflösung (Wortlaut → Kontext-Bezug → Leer) und Target-Typ-Klassifikation (Artikelbestimmtheit) leben jetzt im Classify-Prompt des NotizenAgenten (`agents/notizen/klassifikation.py`).

---

## 4. Dynamischer Prompt

### 4.1 Basis-Prompt + Perzeption-Kontext

Zusammengebaut in `_build_router_prompt()` aus `[BLOCKNAME]`-Bausteinen (Prompt-Segregation seit Chat 46):

| Block | Datei | Rolle |
|-------|-------|-------|
| `[IDENTITAET]` | `prompts/default/router.identity.txt` | Rollendefinition + injizierte Perzeptionsfelder (`{today}`, `{intent}`, `{emotion}`, `{arousal}`, `{modus}`, `{beziehungs_dynamik}`) |
| `[AUFGABE]` | `prompts/default/router.task.txt` | JSON-Format-Vorgabe (needs_memory, needs_web, needs_timeline, timeline_query, momentum, management_*) |
| `[LAGE]` | `prompts/default/router.lage.txt` + `build_situation_block` | Seit 17.09.2026: akute Objekte der Sachlage mit Klasse, bekannten Eigenschaften und den Diensten, an deren Zettel sie stehen; eine Zustimmung auf ein Angebot ist ein Auftrag. Zwischen `[KONTEXT]` und `[AGENTEN]`; nicht auf Impuls-Turns, nicht nach einem Sachlage-Ausfall, höchstens 5 Objekte. Rund 670 Zeichen |
| `[REGELN]` | `prompts/default/router.rules.txt` | Verbindliche Regeln, direkt vor der User-Message |

Das gibt dem Router den vollständigen Kontext der Perzeption als Primacy-Position, ohne den Prompt selbst erneut analysieren zu müssen. Datum und Uhrzeit werden ebenfalls injiziert (`{today}`, Format `dd.mm.YYYY, HH:MM Uhr`).


> **Diese Bloecke haben einen Override.** Unter dem antwortenden GPU-Modell laedt `prompts/{modell}/` ueber den Default — heute `prompts/gemma4-gpu/`. Der Override traegt **nur** die verschaerften Ausgaberegeln; alles Inhaltliche steht im Default und gilt fuer jedes Modell. Drei Ebenen seit dem 23.08.2026: `default` → `{modell}` → `{connector}` (`novaberg-architecture_l_connector.md` §2a).

Reihenfolge: `[IDENTITAET]` → `[AUFGABE]` → (optional `[KONTEXT]`) → (optional `[AGENTEN]`) → `[REGELN]`.

### 4.2 Session-Kontext (seit Chat 23, nummeriert seit Chat 24)

Die letzten **5 Wortwechsel** werden direkt aus Redis geladen und als `[KONTEXT]`-Block zwischen `[AUFGABE]` und `[AGENTEN]`/`[REGELN]` eingefügt. Die Turns werden über `format_session_turns_numbered()` aus `memory/session.py` formatiert — chronologisch aufsteigend nummeriert, höhere Nummer = aktueller.

> **~~5 User+Assistant-Turns~~ → 5 Wortwechsel, und der Unterschied ist gemessen (24.08.2026).** Ein Eigen-Impuls ist ein **alleinstehender** assistant-Turn; die alte Paarbildung übersprang ihn, die neue zählt Wortwechsel und nimmt Impulse **dazwischen** mit. Zählte stattdessen jede Gruppe, verdrängte der Impuls den Nutzer aus dem Fenster: bei **16 von 24** Zuständen einer echten Session weniger Nutzer-Turns, bei einem **keiner**. Jede Zeile nennt jetzt ihren Sprecher, ein Impuls als `NOVA (von sich aus)`.

```
[KONTEXT]
Nutze den Verlauf fuer Rueckbezug-Aufloesung und Management-Target-Erkennung.
Hoehere Nummern sind aktueller — loese Bezuege bevorzugt ueber die hoechsten Nummern auf.

[1] USER: ...
[1] NOVA: ...
[2] USER: Was steht auf der Obstliste?
[2] NOVA: Äpfel, Bananen, Kiwi.
```

**Zwei Zwecke:**
1. **Rückbezug-Auflösung** — „Setz Kirschen drauf" → Target aus Turn [2] = „Obstliste"
2. **Management-Target-Erkennung** — Kontextreferenzen können über den Verlauf aufgelöst werden (3-Stufen-Regel, Stufe 2)

Die nummerierte Formatierung unterstützt Recency-basierte Auflösung: Bei Mehrdeutigkeit gewinnt der aktuellere Turn.

> ⚠ **Jeder Beitrag erreicht den Router nur in seinen ersten 100 Zeichen** (`format_session_turns_numbered`, Vorgabe `max_chars = 100`, vom Router nicht überschrieben — ebenso bei acht weiteren Lesern). **Das ist kein Ausschnitt, sondern eine Gefahr:** Novas Antworten beginnen häufig mit einer Regieanweisung, `[gemessen 14.09.2026]` 157 bis 208 Zeichen lang in 4 von 4 Antworten — der Router sah von diesen Antworten nur den Anfang der Regieanweisung. Ein Angebot am Ende einer Antwort erreicht ihn nie, und ein *„Gerne"* darauf ist nicht zuordenbar. Der Befund steht in der Fundliste (14.09.2026); die Abhilfe ist Teil B von Scheibe 12 (`novaberg-thinking-lage_k.md` §4). → **Behoben am 16.09.2026:** Der Router sieht jeden Beitrag des Fensters vollständig — ein Angebot am Ende einer Antwort erreicht ihn jetzt. Die Grenze greift nur noch über das ganze Fenster und wirft älteste Gruppen ganz weg (`novaberg-mem-session.md` §3.7). `[gemessen]` Über 1439 Fenster: mitten abgeschnittene Beiträge 7213 → **0**; der Verlaufsblock im Router-Prompt wächst dabei von 1,5 % auf höchstens rund ein Drittel seiner realen Länge — kein Kontextfenster reißt. **Dass ein *„Gerne"* damit zuordenbar wird, ist nicht gemessen:** Der Verlauf trägt die Angabe jetzt, ob der Router sie nutzt, zeigt erst der Betrieb.

### 4.3 Plugin-Erweiterungen ([AGENTEN]-Block)

Jeder registrierte Manager kann über `router_prompt` dem Router Erkennungsregeln hinzufügen. `get_combined_router_prompt()` sammelt die Prompts aller Plugins und hängt sie als eigenen `[AGENTEN]`-Block an:

```
[AGENTEN]
Die folgenden Regeln stammen von registrierten Agenten.
Nur diese Regeln duerfen die Management-Felder setzen.

{plugin_additions}
```

Neue Fähigkeiten werden automatisch erkannt — ohne Änderung am Router. Die einleitende Regel „Nur diese Regeln duerfen die Management-Felder setzen" ist die Implementierung der 4-Zeilen-Delegation aus Abschnitt 3.4.

---

## 5. State-Felder

### Gelesen

| Feld | Quelle | Beschreibung |
|------|--------|-------------|
| `user_prompt` | API | Roher User-Input |
| `user_id` | API | User-ID für Redis-Keys (Session-Turns, Pending-Agent) |
| `intent` | Perzeption | Kommunikationsabsicht |
| `current_emotion` | Perzeption | Dominante Emotion |
| `current_arousal` | Perzeption | Energie-Intensität |
| `gespraechs_modus` | Perzeption | Kommunikationsregister |
| `beziehungs_dynamik` | Perzeption | Beziehungspositionierung |
| `sachlage` | Sachlage-Knoten | **nur im Schatten** (seit 16.09.2026): die akuten Objekte für die Objekt-Nähe; kein geschriebenes Feld hängt davon ab |
| `turn_id`, `character_id` | API | Zuordnung des Schatteneintrags im Pipeline-Log |

### Geschrieben

| Feld | Typ | Beschreibung |
|------|-----|-------------|
| `needs_memory` | `bool` | Gedächtnis laden? |
| `needs_web` | `bool` | Web-Suche nötig? |
| `needs_timeline` | `bool` | Timeline-Daten laden? — gelesen seit dem 15.10.2026 hinter dem Router im selben Knoten (§3.2) |
| `timeline_query` | `dict` | Strukturierte Timeline-Abfrage |
| `momentum` | `str` | Gesprächsdynamik (low/mid/high) |
| `management_action` | `str` | `"agent"` (Plugin-gesteuert), `"resume"` (Pending-Agent), oder `""` |
| `management_target` | `str` | Agent-Name — von Plugin-Prompt gesetzt (seit Chat 40) |
| `management_target_typ` | `str` | Leer — wird vom Agent-Classify-Node bestimmt |
| `objekt_urteil` | `dict` | Urteil der Objekt-Nähe (`shadow_nearness`), seit 17.09.2026 — der Planner ordnet damit die Dienste |
| `appointment_reading` | `AppointmentReading` | **Nicht von `route()`, sondern vom Knoten dahinter** (`GraphBase._node_route`, seit 15.10.2026): die Termine dieses Turns für den Block `[TERMINE]` des Verfassers (§3.2) |

---

## 6. LLM-Parameter

- **Temperature:** `0.05` (Fallback, konfigurierbar über `NODE_LLM_CONFIG["router"]`)
- **Format:** JSON (erzwungen via `format_json=True`)
- **Provider:** `get_chat_provider()` über die LLM-Abstraktionsschicht (seit Chat 17, LLM1)
- **Caller:** `"router"` (für Logging und Metriken)
- **Fallback:** Bei JSON-Parsing-Fehler → alle Flags `false`, Momentum `mid`, Management leer, `management_target_typ = "titel"`

---

## 7. Abhängigkeiten

| Import | Quelle | Zweck |
|--------|--------|-------|
| `get_combined_router_prompt` | `plugins` | Plugin-Prompt-Erweiterungen |
| `redis_client` | `config` | Redis-Verbindung für Session-Turns |
| `get_node_config` | `config` | Node-spezifische LLM-Parameter |
| `PROMPTS` | `config` | Dictionary mit `[BLOCKNAME]`-Bausteinen (seit Prompt-Segregation, Chat 46) |
| `session_turns_retrieve` | `memory/session` | Letzte Turns aus Redis laden |
| `format_session_turns_numbered` | `memory/session` | Nummerierte Turn-Formatierung (zentrale Funktion, seit Chat 24) |
| `model_service`, `ChatRequest` | `services/model_services` | Der Modelldienst, an den der Knoten eine typisierte Anfrage übergibt |
| `redis_manager` | `tools/redis_manager` | Pending-Agent-Check (Resume-Flow) |
| `shadow_nearness` | `agents/object_nearness` | Objekt-Nähe im Schatten — rechnet und protokolliert, schreibt kein Feld (seit 16.09.2026) |

> **Am 16.08.2026 berichtigt.** Hier stand ~~`get_chat_provider` aus `services/llm_provider`~~ als LLM-Abstraktionsschicht. **Das ist keine Umbenennung, sondern eine eingezogene Schicht:** Der Knoten spricht die Provider-Abstraktion nicht mehr an, sondern reicht eine `ChatRequest` an `model_service`; dieser besitzt die Spurwahl (LLM- gegen CPU-Last) und die Provider-Registry. `services/llm_provider` existiert unverändert weiter mit `LLMProvider`, `OllamaProvider` und `AnthropicProvider` — es wird nur nicht mehr von Knoten importiert, sondern ausschließlich von `model_services` selbst.

---

→ Perzeption (liefert Kontext): novaberg-node-perception.md
→ Enricher (nutzt Flags): novaberg-node-enricher.md
→ Planner (nutzt Management-Felder): novaberg-node-planner.md
→ Plugin-System (erweitert Prompt): novaberg-architecture.md
→ Shadow Delivery (nutzt Momentum): novaberg-pixie.md
→ NotizenAgent (Resume-Flow, gewichtete Suche): novaberg-agent-notes.md
→ Session-Turns (zentrale Formatierung): `memory/session.py` → `format_session_turns_numbered()`
→ Epic 11 Konzept (Agent-System): novaberg-graph.md
→ Lesson: "NIEMALS" ist kein Proxy: novaberg-node-router_l.md

---

## Das schwarze Brett statt der Manager-Sammlung (17.08.2026)

**Die Aushänge werden nicht mehr nur von den Managern eingesammelt.** Der `[AGENTEN]`-Block entsteht aus der Dienst-Fläche; ein Manager kommt nur zum Zug, wenn kein Dienst seinen Namen trägt (im Bestand ist das `fakten`).

Der Grund: Beide Flächen tragen denselben Text — der Dienst erbt ihn vom gleichnamigen Manager. Ihn zweimal auszugeben hieße, dieselbe Regel doppelt vorzulegen, und zwei Regeln, die dasselbe sagen, heben sich in der Wirkung auf.

**Drei Dinge sind neu im Block:**

- **Die Negativfälle** stehen bei jedem Dienst an derselben Stelle in derselben Form. Auf der Manager-Fläche standen sie als Prosa mitten im Aushang und nur bei einigen.
- **Die Anweisung, jeden Zettel für sich zu beurteilen** — ausdrücklich, weil das Modell mit allen Zetteln in einem Aufruf sie unvermeidlich gegeneinander abwägt, ob es soll oder nicht. Damit ist die Unabhängigkeit eine Bitte und keine Eigenschaft des Aufbaus; die Gegenprobe dazu steht aus.
- **Die Zustellung im Zweifel** — mit einer Rücknahme je Zettel für Anbieter ohne begründete Ablehnung.

**Was der Block nicht enthält:** eine Rangfolge, einen Vorrang, einen Hinweis auf Überlappungen. Ein Verhältniswissen zwischen zwei Zetteln wäre die zentrale Zuordnungstabelle, gegen die die Bauart gerichtet ist — ein Zeuge prüft auf Rangworte.

> **Der Dispatch-Guard in `router.task.txt` spricht dem Zweifelssatz entgegen** (*„Im Zweifel: kein Dispatch"*) und steht im Prompt davor. Beide Sätze sind für sich begründet; die Auflösung ist eine offene Entscheidung ~~und steht in der Fundliste~~ → **stand dort nicht; nachgetragen am 13.09.2026** am Eintrag zum nicht angelegten Termin.

Zusätzlich zählt der Knoten den Nenner des Quotenabgleichs — eine Äußerung je Graph, getrennt, weil die Impulsrate des Hintergrunds keinem Fachdienst gehört.

---

## Timeline nur auf Auftrag — was der Empfang unter `gemma4-a4b-gpu` zustellt (13./14.09.2026)

**Der Anlass.** Über vier Turns entstand im Gesprächskontext ein Termin mit Tag, Uhrzeit und Dauer — in Aussagen, ohne Kommando-Verb. Der Router setzte in 4 von 4 Turns `needs_timeline: true` und ließ `management_action` in 4 von 4 leer, obwohl der Timeline-Aushang damals Aussagen und beiläufige Erwähnungen ausdrücklich als Auslöser nannte; der TimelineAgent lief nie.

**Die Beweiskette.** Werkzeug: die geloggten System-Prompts der vier Turns, wörtlich wiederholt über den Provider des Betriebs (temperature 0.05, `num_predict` 512), je Fassung und Reiz mehrere Durchgänge; Pixie beim zweiten und dritten Lauf vollständig pausiert.

| Lauf | Frage | Ergebnis |
|---|---|---|
| 1 (13.09., 60 + 9 Aufrufe) | Sperrt der Dispatch-Guard? Liegt es am Modell? | `gemma4-a4b-gpu`: **0 von 40** Zustellungen, mit **und** ohne Guard. `gemma4-gpu`: **9 von 9**, an allen vier Turns. **Die Guard-Hypothese ist widerlegt; das Modell entschied.** Der Lauf mit `gemma4-gpu` endete nach neun Aufrufen durch einen Ausfall der Maschine. |
| 2 (13.09., 91 Aufrufe) | Welche Prompt-Fassung stellt unter `gemma4-a4b-gpu` Zeitangaben ohne Verb zu? | Ausnahme im Timeline-Aushang **13 von 15**, 0 von 21 Fehlalarmen; Ausnahme im Guard 6/10; allgemeine Ausnahme 1/5; unverändert 2/10. **Gebaut und am selben Abend zurückgenommen** — siehe unten. |
| 3 (14.09., 48 Reize × Router + Klassifikation) | Schreibt die Kette nur auf ausdrücklichen Auftrag? | Aufträge (*eintragen, erinnern, verschieben, merken*) **schreiben 12 von 12**, eine Frage nach Terminen **liest 3 von 3**, **Aussagen mit Zeitpunkt schreiben 0 von 21**, Zeitbezug ohne Auftrag 0 von 9; keine Zustellung an einen anderen Dienst. Die Klassifikation allein lehnt 15 von 21 Aussagen ab — die übrigen sechs hält der Router. |

**Der verworfene Entwurf.** Lauf 2 ergab eine Fassung, die Aussagen mit Zeitpunkt zustellt, und sie war gebaut und im Betrieb bestätigt: Ein Termin entstand aus einer Aussage. **Die Absicht war eine andere** — Entscheidung des Eigentümers am 13.09.2026: *„Nur ein ausdrücklicher Auftrag"* (`novaberg-thinking-lage_k.md` §4, Scheibe 12). Das Nicht-Zustellen der vier Turns war unter dieser Absicht richtig; der Defekt war die Antwort, die *„notiert"* sagte.

**Gebaut (14.09.2026).** Der Timeline-Aushang nennt Aufträge und Fragen statt Satzformen — *eintragen, erinnern, ändern, löschen, abfragen* — und sagt: *„Eine Zeitangabe allein ist kein Auftrag — auch nicht mit Uhrzeit und Ereignis."* Die Zeile *„ODER der Gesprächsverlauf ein aktives Zeitereignis enthält"* ist entfallen. Der TimelineAgent führt die Erwähnung als Negativfall, und seine Klassifikation weist sie als `rejected` aus (`novaberg-agent-timeline.md` §3). Damit widerspricht der Timeline-Aushang dem Dispatch-Guard **für Aufträge** nicht mehr; ob eine **Frage** an die Zeitachse (*„Was steht morgen an?"*) das Kommando-Signal des Guards erfüllt, sagen die beiden Texte nicht — gemessen wurde sie 3 von 3 zugestellt. Zeugen: `tests/test_timeline_aushang.py` (6, nach der zweiten Kontrolle 8), Gegenprobe gegen den vorigen Stand 5 von 16 rot wie vorhergesagt; Suite **3669 grün, 0 übersprungen**, nach der zweiten Kontrolle **3671** (`novaberg-agent-timeline.md` §3a).

**Offen:** Die Zustimmung zu einem Angebot Novas ist nach der Entscheidung vom 14.09.2026 ein Auftrag — der Router erkennt sie heute nicht (Verlauf in 100 Zeichen, keine Objekte der Lage). Das ist Scheibe 12.

---

## Die Objekt-Nähe im Schatten (16.09.2026)

**Seit Scheibe 12 C2 rechnet der Empfang, wem ein akutes Objekt der Lage gehört — und benutzt es nicht.** `route()` ruft als Erstes nach dem Quotenzähler `agents/object_nearness.py::shadow_nearness(state)`, **vor** dem Resume-Pfad, damit jeder Durchlauf einen Eintrag hat.

| Schritt | Was geschieht |
|---|---|
| Objekte | nur die akuten (`akut is True`) aus `state["sachlage"]`; die latenten werden gezählt |
| Objekttext | `build_object_text`: `Klasse: … . Name: … . Eigenschaften: …` — Namen der gedeckten und offenen Eigenschaften, keine Werte; zeichengleich mit der Formel der Eichung |
| Vektor | ein Stapel über den Embed-Worker, Frist `OBJEKT_NAEHE_FRIST_S` = 10 s |
| Urteil | Kosinus zu jedem Objekt-Merkmal (`agents/object_feature.py`); **Empfänger ist jeder Dienst mit Nähe ≥ `OBJEKT_NAEHE_UNTERGRENZE` (0,40)** — `zugeordnet` bei einem, `mehrere` bei mehreren, `still_untergrenze` bei keinem. Der Abstand zur zweitgrößten Nähe steht als Diagnose im Eintrag. ~~Zugeordnet nur mit Abstand ≥ 0,04~~ — abgelöst am selben Abend, entschieden: je Zettel für sich |
| Protokoll | ein Eintrag im Pipeline-Log, Art `berechnung`, Knoten `router`, Quelle `objekt_naehe`, auf **jedem** Rückkehrpfad: `gerechnet`, `ohne_sachlage`, `ohne_objektliste`, `ohne_akute_objekte`, `ohne_merkmale`, `ausfall`; dazu `herkunft` der Sachlage |

~~**Kein Feld des Zustands wird geschrieben**~~ → **seit dem 17.09.2026 (D1) genau eines: `objekt_urteil`**, das der Planner liest; die Entscheidung *ob* bleibt beim Modellaufruf. Der Schatten wirft nie heraus — ein Ausfall ist ein Eintrag mit Fehlerart und ein `logger.error`. Die Zustellung entscheidet weiter der Modellaufruf allein; erst Teil D liest das Urteil.

**Eine übernommene Sachlage wird erneut beurteilt** — auf Impuls- und Ausfall-Turns trägt der Zustand die Objekte des Vorgängers, und der Eintrag heißt trotzdem `gerechnet`. Betriebszahlen filtern deshalb nach `herkunft` (`impuls_uebernommen`, `ausfall_uebernommen`). Gemessene Kosten im echten Turn: 146 ms.

**Gemessen am 16.09.2026:** Die Produktionsfunktionen über die 60 Fälle der Eichreihe reproduzieren die Eichung exakt — ~~**33 zugestellt, 32 richtig, 27 still**~~ (Regel mit Abstand) → **je Zettel: 21 nur beim richtigen Dienst, 16 an beiden, 0 nur beim falschen, 23 still**; Bestand 44 an einem, 22 an beiden von 313. Ein echter Turn (Thema Gravitationslinsen, 20:15 UTC) schrieb den Eintrag `gerechnet`, ein akutes Objekt, größte Nähe 0,2805 → `still_untergrenze`; der Router entschied danach unabhängig, die Bestände blieben gleich. Konzept: `novaberg-thinking-lage_k.md` §4, *Teil C2 — gebaut*.

---

## Die Lage im Router (17.09.2026, Scheibe 12 D2a)

**Der Anlass, gemessen:** Ohne Lage erkannte der Router **0 von 24** Zustimmungen auf ein Angebot (*»Soll ich dir den Termin eintragen?« — »Gerne«*) — `ROUTE-MISS1`. Der Verlauf trug das Angebot, aber nicht, worauf es sich bezog.

**Mit dem `[LAGE]`-Block** 13 und 14 von 24 in zwei Läufen, bei 0 Fehlalarmen auf Ablehnungen und Zustimmungen zu etwas anderem und unveränderten Bitten. Die falschen Ziele darunter fängt der Planner ab (Reihenfolge nach Nähe, 13 von 13 richtig zuerst). → **Seit dem 16.10.2026 nicht mehr, wenn ein akutes Objekt das falsche Ziel erreicht:** Dann führt der vom Router genannte Dienst unter den nahen (`novaberg-node-planner.md`, *Der Router-Vorzug*, benannte Grenze); erreicht keines das Ziel, entscheidet weiter das Objekt. **Ein zusätzlicher Satz im Dispatch-Guard** hob die Zustimmungen um eine und brachte drei Fehlalarme — verworfen. Belege: `labor/2026-09-17_router_lage/`.

**Wo der Block nicht steht:** auf einem Impuls-Turn (der Reiz ist Novas eigener Gedanke, zustimmen kann nur der Mensch) und nach einem Ausfall der Sachlage (die Objekte sind die des Vorturns) — geprüft an der Herkunft der Sachlage **und** des Urteils.

**Wärme:** Router-Aufrufe erzeugen kurze Spitzen der GPU-Junction bis 97 °C.

## Seit dem 18.09.2026 — Angebot, Rückfrage, Entscheidung

- **Das offene Angebot** (`utils/offers.py`, Scheibe 12 E1) liest der Router am Anfang des Knotens (`offer_load`). Der `[LAGE]`-Block trägt den Zustimmungssatz nur, wenn es offen ist (`prompts/default/router.lage.angebot.txt`).
- **Der Riegel:** Eine blanke Zustimmung (`is_bare_consent` — *»Gerne«*, *»Ja, bitte«*; ein *»Danke«* allein zählt nicht) stellt ohne offenes Angebot nicht zu.
- **Die Weiche:** Mit offenem Angebot und ohne Zustellung durch das Modell setzt der Router selbst `agent` an den Dienst des Angebots. Gemessen im Betrieb: 3 von 3; das Modell allein stellte in keinem Fall zu.
- **Die Sachen des Angebots** legt der Router für den Planner in den Zustand: `angebot_objekte`, `angebot_bezug`, `angebot_satz` (`novaberg-graph.md`).
- **Die Rückfrage-Wache:** Trägt der Turn einen eigenen Auftrag (`carries_own_request` — *»Trag … ein«*, *»Schreib …«*), gilt er nicht als Antwort auf eine offene Rückfrage; der Wartezustand fällt, der Turn wird normal geroutet.
- **Entscheidungs-Einträge** (`log_decision`): `router.rueckfrage` (resume / bleibt stehen für einen Impuls / verworfen für einen eigenen Auftrag) und `router.zustellung` mit dem, was das Modell wollte, offenem Angebot, blanker Zustimmung, Riegel und Weiche.

### Angebot und Zustimmung — die Bausteine

| Baustein | Ort | Aufgabe |
|---|---|---|
| `find_offers` | `utils/offers.py` | Sätze einer fertigen Antwort, in denen Nova anbietet zu schreiben (Formen *»Soll ich … eintragen?«*, *»Möchtest du, dass ich …«*, *»Ich kann dir das …«*). Liest Wortlaut: 7 von 7 konstruierte Nicht-Angebote gelten als Angebot (Fundliste 18.09.2026) |
| `Offer`, `offer_store` / `offer_load` / `offer_clear` | `utils/offers.py` | der offene Punkt je Paar in Redis (`angebot:{user}:{figur}`), mit Satz, Sachen (Name, Klasse, gedeckt) und Diensten; ein unlesbarer Eintrag gilt als keiner |
| `ANGEBOT_VERFALL_SEKUNDEN` | `config.py` | 900 s — ein Angebot gilt für den nächsten Turn, nicht auf Dauer |
| `offer_matches` | `utils/offers.py` | steht ein Angebot offen, auf das sich eine Zustimmung beziehen kann (die Lage entscheidet nicht mit) |
| `is_bare_consent` / `is_bare_refusal` | `utils/offers.py` | sagt die Äußerung nichts als *»ja«* bzw. *»nein«*; ein *»Danke«* allein ist keine Zustimmung, ein Satz mit eigener Sache keins von beiden |
| `carries_own_request` | `utils/offers.py` | trägt der Turn einen eigenen Schreibauftrag — dann ist er keine Antwort auf eine offene Rückfrage |
| `OfferCandidate`, `offer_candidate` | `utils/offers.py` | welche Sache für ein Angebot in Frage kommt (E2) — vor Pflicht und Ablehnung |
| `ANGEBOT_PFLICHT_BODEN` | `config.py` | 0,33 — darunter bietet Nova nie an; darüber ~~je Turn~~ → je Sache (seit 19.09.2026, `utils/offers.py::draw_recorded`) mit einer Wahrscheinlichkeit, die linear auf 100 % bei Pflicht 1,0 steigt (Notizen und Timeline; Entscheidung 18.09.2026, löst die Schwelle 0,9 ab) |
| `record_declined` / `declined_objects` | `memory/sachlage_properties.py` | das Nein als Eigenschaft `Speichern = abgelehnt` am Objekt (E3) |
| `consent_fields` | `agents/object_nearness.py` | Ziel und Zeitangabe aus der zugestimmten Sache; aufgelöste Tage mit der Uhrzeit aus dem Wortlaut |
| Prompts | `router.lage.angebot.txt`, `verfasser.angebot.txt`, `classify_timeline.zustimmung.txt`, `classify_notizen.zustimmung.txt` | der Zustimmungssatz im Empfang, das Angebot im Verfasser, die Bindung in den Klassifikationen |