Ctrl K
ComunidadGuías…

docs/novaberg-agent-notes.md

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

Archivo original ↗Preguntar a KiBot
# Novaberg — User-Agent: NotizenAgent

**Projekt:** Novaberg — The Nova Anima Resonance System
**Dokument:** Modul NotizenAgent — Merkzettel, Listen, Snippets (konsolidiert)
**Stand:** 8. November 2026, 14:34 UTC (`date -u`; §7: die Ablehnung der Frage nach einer nicht gefundenen Notiz endet mit `dismissed` — Code-Tree `33a7b83`, Kennung wird nachgetragen). Davor 6. November 2026, 20:46 UTC (`date -u`; eine ältere Kennung im Stand nachgetragen — Code-Tree `f901e08` ist `e1e0724`). Davor 6. November 2026, 20:43 UTC (`date -u`; §6: `NOTIZ-SUCHTEXT-HINKT-NACH` genannt). Davor 26. Oktober 2026, 18:41 UTC (`date -u`; §6: die Bindung jeder Kennung an den Menschen im Betrieb gemessen nach dem Commit von `89bcba8`, 50 von 50 Schritten). Davor 26. Oktober 2026, 18:33 UTC (`date -u`; die Commit-Kennungen nachgetragen — Code-Tree `30b81af` ist `89bcba8`, die Doku dazu `64ba8dd`; Code-Tree `9c1df7a` ist `b03db21`, die Doku dazu `de16bea`). Davor 26. Oktober 2026, 08:25 UTC (`date -u`; §3: die Antwort des Modells wird geprüft gelesen — `null` wie ein fehlender Schlüssel, ein falscher Typ und eine Antwort ohne Objekt laut; Code-Tree `9c1df7a`, Kennung wird nachgetragen → committet als `b03db21`, die Doku dazu als `de16bea`, am Modell gemessen, im Betrieb nicht; `KLASSIFIKATION-NULL-FELD-WIRFT` geschlossen). Davor 26. Oktober 2026, 08:17 UTC (`date -u`; §6: jede Kennung ist in Lesen und Schreiben an den Menschen gebunden, ein geschlossener Satz Schreibungen mit Zeilenzahl, ein Schreibweg ohne genau eine getroffene Zeile endet laut, kein `NotizenRepository.update` mehr — Code-Tree `30b81af`, Kennung wird nachgetragen → committet als `89bcba8`, die Doku dazu als `64ba8dd`, im Betrieb nicht gemessen; `SCHREIBWEG-KENNUNG-OHNE-MENSCH` geschlossen). Davor 26. Oktober 2026, 07:51 UTC (`date -u`; §7: die Deutung der Antwort im Betrieb über den echten Weg gemessen, `ZURUECKHALTUNG-GILT-ALS-ZUSTIMMUNG` geschlossen). Davor 24. Oktober 2026, 22:21 UTC (`date -u`; die Commit-Kennung nachgetragen — Code-Tree `f901e08` ist `e1e0724`, die Doku dazu `d2d6ec8`). Davor 24. Oktober 2026, 21:35 UTC (`date -u`; §7: die Deutung der Antwort auf die Rückfrage zum Anlegen ist geschlossen — nur eine Antwort aus Zustimmung legt an; Code-Tree `f901e08`, Kennung wird nachgetragen → committet als `e1e0724`, die Doku dazu als `d2d6ec8`, im Betrieb nicht gemessen). Davor 24. Oktober 2026, 16:50 UTC (`date -u`; §7: die Commit-Kennung der Deutung am Tor der Direktiven nachgetragen — Code-Tree `2457549` ist `6846120`). Davor 24. Oktober 2026, 14:20 UTC (`date -u`; §7: die Direktiven lesen eine zurückhaltende Antwort als unklar, die Notizen nicht — Vermerk, am Code des Notizen-Dienstes nichts geändert). Davor 18. Oktober 2026, 01:01 UTC (`date -u`; §7: die Antwort auf eine Rückfrage wird an Wortgrenzen gelesen, die Ablehnung zuerst — Tree `9153022`, gemessen an 41 Antwortformen). Davor 17. Oktober 2026, 14:36 UTC (`date -u`; §5 *Create*: ein Name nur mit Beleg, sonst aus dem Inhalt — committet `914217c`, gemessen 8 von 8). Davor 16. Oktober 2026, 16:02 UTC (`date -u`; die Grenze zur Timeline: ein Notizauftrag, den der Router den Notizen zustellt, erreicht sie zuerst, auch neben einem akuten älteren Termin — gebaut im Planner). Davor 19. September 2026, 13:08 UTC (Regel A der Vorprüfung durch eine Ausnahme für die laufende Liste ersetzt, `b31f2f5`; eine Ablehnung der Klassifikation trägt `kein_auftrag`, `968c56b`). Davor 18. September 2026, 21:14 UTC (eine blanke Zustimmung bindet an die angebotene Sache, ein Nein bleibt am Objekt, und jede Schreibung meldet ihre Verifikation — auch das Umbenennen). Davor 17. September 2026, 17:52 UTC (Notizen nur auf ausdrücklichen Auftrag: die Vorprüfung der Klassifikation lehnt Aussagen ab — Aussagen geschrieben 12 → 8 von 12; die Sperre am Empfang gemessen und zurückgenommen). Davor 17. September 2026, 14:27 UTC (die Klassifikation bekommt den Objektbezug als `[OBJEKT]`-Block — Scheibe 12 D2b; eine Aussage über einen Bedarf wird trotzdem geschrieben, Fundliste). Davor 17. September 2026, 10:58 UTC (**Merk-Bitten mit Zeitbezug erreichen die Notizen** — der Planner fragt nach dem Objekt, nicht nach der Zeitangabe, Scheibe 12 D1). Davor 16. September 2026, 19:43 UTC (Grenze zur Timeline: die Anmeldung trägt ein Objekt-Merkmal, geeicht und eingebettet, noch ohne Leser). Davor 08. Mai 2026, Chat 80 (NOTIZEN-VOR-TURN-BEZUG — Inhalts-Auflösung, kleinste Wirkstufe)
**Pfad:** novaberg/docs/novaberg-agent-notes.md
**Quellen:** nova-02-m-f.md (Modul), nova-14-k.md (CRUD-Haertung), nova-15-k.md (Domain Language)

---

## 1. Aufgabe

Der NotizenAgent verwaltet Freiform-Inhalte: Einkaufslisten, ToDos, Merkzettel, Entwuerfe, Ideensammlungen. Alles, was der Nutzer explizit ablegen will und ~~was nicht in das Fakten- oder Timeline-Schema passt~~ → was nicht ins Timeline-Schema passt; eine Merk-Bitte über eine Person ergibt seit dem 20.10.2026 eine Notiz und, im Nachlauf, einen Fakt (`novaberg-memory-fakten_e.md` A Nr. 27). Das LLM versteht den Inhalt — die DB speichert nur Text. "Streich die Milch von der Einkaufsliste" wird vom LLM als neue Version der Liste generiert.

**Die Grenze zur Timeline ist seit dem 16.09.2026 entschieden** ~~und noch nicht gebaut~~ → **gebaut am 17.09.2026** (Scheibe 12 D1: Reihenfolge nach der Objekt-Nähe; gemessen erreichen 13 von 13 Merk-Bitten die Notizen zuerst, vorher 7; **seit dem 16.10.2026 steht unter den nahen Diensten der vom Router genannte vorn** — ein Notizauftrag geht auch dann zuerst an die Notizen, wenn ein älterer Termin akut ist und der Timeline näher steht, `NOTIZAUFTRAG-GEHT-AN-TIMELINE`, `novaberg-node-planner.md`, *Der Router-Vorzug*): Hierher gehoert eine **Sache oder ein Zustand mit Zeitbezug** — *„Merk dir, dass ich morgen Mehl brauche"*, mit `faellig_am` als Traeger der Zeit und, wenn erinnert werden soll, einem Anker ueber `timeline_id`. Eine **Handlung oder ein Ereignis mit Zeitpunkt** gehoert in die Timeline. ~~**Heute erreicht keine Merk-Bitte mit Zeitbezug diesen Dienst**~~ (bis 17.09.2026) — der Planner waehlt bei jeder Zeitangabe die Timeline (`PLANNER-ZEITWORT-UEBERSTIMMT-DIENSTWAHL`), obwohl der eigene Aushang genau dieses Beispiel nennt.

**Seit dem 16.09.2026 trägt die Anmeldung die Grenze als Objekt-Merkmal** — `objekt_merkmal` in `agents/notizen/agent.py`, *ein Gegenstand, ein Bedarf oder eine Eigenschaft, die nicht stattfindet*, mit Sachen geeicht, beim Start eingebettet (`agents/object_feature.py`). Es entscheidet heute keine Zustellung; der Leser kommt mit Scheibe 12 C2, und erst mit D fällt der Defekt oben (`novaberg-thinking-lage_k.md` §4).

Der NotizenAgent war der erste migrierte Agent unter Epic 11 (Phase 2, Chat 22–23). Er ersetzte den frueheren NotizenManager (Plugin-System) und demonstrierte das volle Agent-Pattern: LangGraph-Subgraph, 5-Node-Architektur, pg_trgm-Suche, Resume-Flow fuer Rueckfragen.

---

## 2. Architektur: 6-Node-Subgraph

```
Validate → Resume? → Classify → Search → CRUD → Confirm
```

| Node | Datei | Aufgabe |
|------|-------|---------|
| `validieren` | `agent.py` | Pflichtfelder pruefen. Akzeptiert `"agent"` als Aktion (→ Classify-Node). |
| `resume` | `resume.py` | Resume-Flow bei Pending-Agent. Drei Pfade: Disambiguierung, Duplikat, Fallback. |
| `klassifizieren` | `klassifikation.py` | LLM-Call: Bestimmt action, target, target_typ. Container-vs-Inhalt-Regel. Session-Kontext fuer Rueckbezuege. |
| `suchen` | `suche.py` | Gewichtete Multi-Feld-Suche mit pg_trgm. |
| `ausfuehren` | `crud.py` | CRUD-Operation durchfuehren. LLM-Call fuer Create/Update. |
| `bestaetigen` | `bestaetigung.py` | Status setzen. Ueberschreibt `rueckfrage`/`fehler` nicht blind. |

**Routing-Logik (in agent.py):**

```
validieren
  ├─ fehler → END
  ├─ resume → resume → ...
  ├─ create/read/update/delete/append → direkt weiter (abwaertskompatibel)
  └─ agent → klassifizieren
                ├─ fehler → END
                ├─ create → ausfuehren → bestaetigen → END
                └─ read/update/delete → suchen → ausfuehren → bestaetigen → END
```

### File-Split-Konvention

| Datei | Inhalt | Zeilen |
|-------|--------|--------|
| `agent.py` | Klasse, Properties, build_graph(), Routing, _validieren | ~120 |
| `klassifikation.py` | Classify-Node (LLM-Prompt, Session-Kontext) | ~110 |
| `resume.py` | resume(), Disambiguierung, Duplikat-Aufloesung, die Deutung der Antwort | ~~~145~~ → ~390 (18.10.2026) |
| `suche.py` | suchen(), SUCH_GEWICHTE, Score-Gap | ~180 |
| `crud.py` | ausfuehren(), _create, _update, _delete, _append | ~270 |
| `bestaetigung.py` | bestaetigen() | ~20 |

Oeffentliche Node-Einstiegspunkte ohne Unterstrich (z.B. `resume()`), private Helfer mit Unterstrich (z.B. `_resume_disambiguierung()`). Node-Funktionen sind Standalone (kein `self`), nur `_validieren` bleibt als Methode auf der Klasse.

---

## 3. Classify-Node — Container vs. Inhalt

Seit Chat 60: `character_id` wird im Agent-Kontext (`state["kontext"]["character_id"]`) durchgereicht und an `session_turns_retrieve()` übergeben. Der Session-Key enthält die Charakter-Dimension.

Der Classify-Node loest AGT7: Der Router konnte "Streich die Bananen von der Obstliste" nicht von "Streich die Obstliste" unterscheiden. Der Agent kann das — per LLM mit einer expliziten Regel:

**Praeposition (von/aus/auf/zu)** → Inhalt-Operation → `update`
- "Streich die Milch VON der Liste" → update
- "Setz Kaese AUF die Liste" → update

**Ohne Praeposition + direktes Objekt** → Container-Operation → `delete`
- "Streich die Einkaufsliste" → delete
- "Loesch die Obstliste" → delete

### 9 Aktionen (erweiterte Taxonomie, Chat 42)

| Aktion | Bedeutung | Beispiele |
|--------|-----------|-----------|
| `create` | Neue Notiz anlegen | "Erstelle eine Einkaufsliste" |
| `read` | Notiz anzeigen | "Was steht auf der Einkaufsliste?" |
| `update` | Generische Aenderung | "Aendere die Einkaufsliste: nur Obst" |
| `append` | Text anhaengen (ohne LLM-Neugenerierung) | "Haeng an die Notiz dran: ..." |
| `add_content` | Inhalt hinzufuegen (semantisch via LLM) | "Fuege Milch zur Einkaufsliste hinzu" |
| `remove_content` | Inhalt entfernen | "Streich Shampoo von der Liste" |
| `clear_content` | Alle Inhalte leeren | "Leere die Einkaufsliste" |
| `rename` | Notiz umbenennen | "Nenn die Liste 'Samstags-Einkauf'" |
| `delete` | Notiz komplett loeschen | "Loesch die Einkaufsliste" |

Zusaetzlich akzeptiert die Input-Validierung den Platzhalter `agent` (Router-Routing vor Klassifikation). Der Classify-Node kennt die 9 Aktionen, die Input-Validierung alle 9 + `agent`.

### Domain Language — [FACHSPRACHE]-Block (Chat 43, Epic 15)

Der Classify-Node normalisiert den User-Prompt in die Fachsprache des Notizen-Systems. Neues Feld `normalisiert` im JSON-Output:

| User sagt | Normalisiert |
|-----------|-------------|
| "Hau die Bananen raus" | "remove_content: Bananen aus Notiz 'Einkaufsliste' entfernen" |
| "Setz Kuemmel drauf" | "add_content: Kuemmel zu Notiz 'Einkaufsliste' hinzufuegen" |
| "Notiere dir: Durch diese hohle Gasse..." | ~~"create: Neue Notiz anlegen mit Inhalt: Durch diese hohle Gasse..."~~ → seit `914217c` (16.10.2026) mit einem Namen aus dem Inhalt ('Hohle Gasse'), §5 *Create* |
| "Hau die Scheiße raus" | "remove_content: Bananen von Notiz 'Einkaufsliste' entfernen" |
| "Schmeiss noch Milch drauf" | "add_content: Milch zu Notiz 'Einkaufsliste' hinzufuegen" |
| "Mach ne neue Liste fuer den Baumarkt" | "create: Neue Notiz 'Baumarkt' mit Typ 'einkauf' anlegen" |
| "Leg sie bitte an" *(nach Listen-Turn)* | "create: Neue Notiz mit Inhalt: Halloumi, Feta, Paneer" |
| "Schreib das auf" *(nach Aufzählungs-Turn)* | "create: Neue Notiz mit Inhalt: Bohrer, Dübel, Schrauben" |

Die CRUD nutzt `normalisiert` als primaere Anweisung, den Originaltext (`aufgabe`) als Inhaltsquelle. Trennung: Normalisierung steuert die Aktion, Originaltext liefert den woertlichen Inhalt.

```python
DOMAIN_LANGUAGE = {
    "aktionen": {
        "create": "Neuen Eintrag anlegen",
        "read": "Bestehenden Eintrag anzeigen",
        "update": "Bestehenden Eintrag inhaltlich aendern",
        "delete": "Eintrag deaktivieren (Soft-Delete)",
        "add_content": "Inhalt zu bestehendem Eintrag hinzufuegen",
        "remove_content": "Inhalt aus bestehendem Eintrag entfernen",
        "clear_content": "Gesamten Inhalt eines Eintrags leeren",
        "rename": "Titel eines Eintrags aendern",
    },
    "entitaeten": ["Notiz", "Liste", "Merkzettel", "Entwurf"],
    "format": "{action}: {beschreibung} (betrifft {entitaet} '{name}')",
}
```

Classify-Output (erweitert):

```json
{
    "action": "update",
    "target": "Einkaufsliste",
    "target_typ": "inhalt",
    "konfidenz": "hoch",
    "normalisiert": "remove_content: Bananen von Notiz 'Einkaufsliste' entfernen"
}
```

Primacy/Recency-Reihenfolge im Classify-Prompt: [IDENTITAET] → [AUFGABE] → [FACHSPRACHE] → [REGELN].

### Inhalts-Auflösung aus Vor-Turns (seit Chat 80)

Der Classify-Node erhält über `session_turns_retrieve(user_id, character_id)` die letzten 5 Vor-Turn-Paare als `[KONTEXT]`-Block. Bis Chat 80 war die Nutzung dieses Verlaufs explizit auf Target-Auflösung beschränkt — der Prompt verbot Inhalts-Auflösung.

Seit Chat 80 darf der Classify-Node den Verlauf für **Target-Auflösung und Inhalts-Auflösung** nutzen. Bezugs-Anweisungen wie *"Leg sie bitte an"* nach einem Listen-Turn werden aufgelöst, indem der Klassifikator den vollständigen Vor-Turn-Inhalt in das `normalisiert`-Feld extrahiert.

**Beispiel:**

- Vor-Turn: *"Halloumi, Feta, Paneer kommen mir in den Sinn"*
- Aktueller Prompt: *"Leg sie bitte als Notiz an"*
- Klassifikator-Output: `normalisiert = "create: Neue Notiz mit Inhalt: Halloumi, Feta, Paneer"`

**Logging:** DEBUG-Log nach jedem LLM-Call mit `normalisiert`-Feld. INFO-Log mit Heuristik *"Inhalts-Auflösung erkannt: aufgabe={N} Zeichen, normalisiert={M} Zeichen"* wenn `normalisiert` deutlich länger ist als `aufgabe`.

**Reichweite:** Diese Wirkstufe deckt **einen Vor-Turn-Sprung** in **CREATE-Aktionen** ab. Mehrschrittige Rekonstruktion über mehrere Turns oder Bezugsauflösung in UPDATE/RENAME-Pfaden ist **nicht** abgedeckt — siehe Bugs NOTIZEN-KONTEXT-REKONSTRUKTION und NOTIZEN-UPDATE-TARGET-LEER. Die strukturelle Lösung ist das Frame-Konzept (`novaberg-thinking-frames_k.md`) Phase 1b.

### Die Antwort wird geprüft gelesen (26.10.2026)

**Seit dem Code-Tree `9c1df7a` (26.10.2026, Kennung wird nachgetragen → committet als `b03db21`, die Doku dazu als `de16bea`) liest `klassifizieren` jedes Textfeld der Modellantwort über einen geprüften Leser** — `_read_answer` und `_read_text_fields` in `agents/notizen/klassifikation.py`, eine Kopie je Agent wie in Direktiven, Charakter-Identität und Timeline (`novaberg-agent-directives.md` §4.3, dort die Tabelle der Ausgänge). Bis dahin las es `ergebnis.get(feld, "")`; die Vorgabe greift nur bei einem fehlenden Schlüssel, und `null` in `target` oder `target_typ` ging still als `None` in die Parameter, `null` in `normalisiert` warf an `len(normalisiert)` (`KLASSIFIKATION-NULL-FELD-WIRFT`, im Archiv [`novaberg-bugs-archiv-bauart.md`](novaberg-bugs-archiv-bauart.md)). Seither gilt: Eine Antwort, die kein Objekt ist, endet laut mit Status `fehler` und Schritt `kein_objekt`; `null` und ein fehlender Schlüssel ergeben die Vorgabe; ein Text, auch ein leerer, geht unverändert durch; jeder andere Typ endet laut mit Schritt `feld_falscher_typ`. Die Textfelder sind `target` (leer), `target_typ` (`titel`) und `normalisiert` (leer, `_TEXT_FIELDS`), dazu `grund` nur beim Ausgang `rejected` (*„kein Grund angegeben“*, `_REASON_FIELDS`); `action` hat seinen eigenen Weg. **Belege:** `server/tests/test_classification_null_field.py`, Klasse `NotesTest`, je Feld mit Zwilling; der Zeuge der Vollständigkeit findet heute 6 Zugriffe auf die Antwort (Untergrenze). Server-Suite `Ran 5503 tests` · `OK (expected failures=2)`. Vor dem Commit am Modell `[gemessen 26.10.2026]`, an einer Rückhaltereihe mit Wissenschaftsthemen, zusammen mit der Timeline: 0 × `null`, 0 × falscher Typ, Ergebnis 48 von 48 gleich der Nulllinie — `null` trat an dieser Reihe nicht auf, die Wirkung tragen die Zeugen. Im Betrieb nicht gemessen. **Grenze:** `action` als Liste oder Objekt wirft weiter `TypeError`, eine unbekannte fällt still auf `read` zurück (Fundliste, 26.10.2026).

---

## 4. Suche — pg_trgm gewichtete Multi-Feld-Suche

### SUCH_GEWICHTE (suche.py)

| target_typ | w_name | w_text | w_themen |
|---|---|---|---|
| `titel` | 2.0 | 1.0 | 1.0 |
| `inhalt` | 1.0 | 2.0 | 0.5 |
| `thema` | 1.0 | 1.0 | 2.0 |

### SQL-Kern

```sql
SELECT id, name, typ, text, themen,
  (similarity(name, query) * w_name +
   similarity(text, query) * w_text +
   MAX(similarity(thema, query)) * w_themen) AS score
FROM notizen
WHERE aktiv = TRUE AND (similarity(name, q) > 0.15 OR similarity(text, q) > 0.15 OR ...)
ORDER BY score DESC
LIMIT 10
```

**Post-Filter:** Ergebnisse mit `score < 0.3` werden nach dem SQL-Query verworfen. Der WHERE-Clause nutzt einen niedrigeren Schwellenwert (0.15) pro Feld, um Kandidaten nicht vorzeitig auszuschliessen. Der kombinierte Score muss aber >= 0.3 sein.

### Score-Gap-Disambiguierung

Nach der Suche wird die Kandidaten-Liste auf die Gewinner-Gruppe reduziert:
1. Berechne `avg = Durchschnitt aller Scores`
2. Gehe die sortierte Liste durch, finde den ersten Gap >= avg
3. Alles oberhalb der Trennlinie = Gewinner-Gruppe
4. Count=1 → klarer Gewinner, direkt weiter
5. Count>1 → Disambiguierung, aber nur mit der reduzierten Menge

Formel: `gap >= avg` (nicht `>`). Validiert mit 20 Edge Cases.

### Fallback-Kette

Wenn die gewichtete Suche nach dem Min-Score-Filter (0.3) null Treffer liefert:
1. Exakter Name-Match (`find_by_stichwort`, ILIKE)
2. Volltext-Suche (`find_by_volltext`, tsvector)

### Rueckfrage bei nicht gefundener Notiz (Chat 43)

Bei `add_content` oder `remove_content` + Notiz nicht gefunden: Echte Rueckfrage statt Fehler. Der pending State bewahrt `original_aufgabe` (den rohen User-Prompt). Bei Bestaetigung setzt `_resume_nicht_gefunden()` die `aufgabe` auf den Originaltext zurueck — die CRUD extrahiert den Inhalt korrekt.

Vorher: "ja, bitte" als Notiz-Inhalt. Nachher: "Kuemmel" korrekt aus dem Originaltext extrahiert.

**Indexe:** `idx_notizen_user`, `idx_notizen_status`, `idx_notizen_themen`, `idx_notizen_entitaet_ids` (alle `db/init.sql`), dazu `idx_notizen_timeline_id` aus `agents/timeline/init.sql`.

> **Am 16.08.2026 berichtigt.** Hier standen ~~`idx_notizen_name_trgm` (Chat 23)~~ und ~~`idx_notizen_text_trgm` (Chat 24)~~ als GIN-Indexe. **Beide existieren nicht** — weder unter diesem noch unter einem ähnlichen Namen. Die Erweiterung `pg_trgm` ist in `db/init.sql` aktiviert, aber **auf `notizen` liegt kein einziger Trigramm-Index**; die fünf vorhandenen decken Besitzer, Status, Themen, Entitäten und den Timeline-Bezug ab. Wer sich auf unscharfe Namenssuche verlassen hat, hat sie nie gehabt.

---

## 5. CRUD-Operationen

### Create
1. `management_target` als Name (User-Wortlaut — Namens-Treue) — **seit `914217c` (16.10.2026) nur mit Beleg:** Ein Name gilt, wenn der Mensch ihn genannt hat (in der Anweisung in Apostrophen, oder bei einer Zustimmung der Name der angebotenen Sache), wenn er der Anfang des Inhalts ist oder mit dem Inhalt ein Wort ab 4 Zeichen teilt, verglichen mit gefalteten Umlauten (`crud.py::_name_grounds`). Sonst bekommt die Notiz die ersten höchstens 5 Wörter ihres Inhalts (`_derive_name`), mit einer Info-Zeile im Log. Vorher wurde *„Neue Notiz“* aus der Anweisung zum Namen, und der nächste Notizauftrag fragte wegen Gleichnamigkeit nach (`NOTIZ-BEFEHL-ALS-TITEL`, geschlossen). Gemessen in einer Simulation an einem Prüf-Nutzer am 17.10.2026: 8 von 8 Notizsätzen richtig, die vier neuen Notizen mit Namen aus ihrem Inhalt. **Grenzen:** Ein Meta-Wort, das auch im Inhalt steht, gilt als belegt; was der Klassifikator in Apostrophe setzt, gilt als genannt; zwei Notizen mit denselben fünf Anfangswörtern werden gleichnamig. Zeugen: `tests/test_note_name_from_content.py` (21).
2. LLM extrahiert Typ, Text und Themen aus dem User-Prompt (JSON-Output)
3. Zusammenfassung generieren (Heuristik: erste 20 Woerter)
4. Duplikat-Check: Existiert eine Notiz mit gleichem Namen? → Rueckfrage. Bei Resume mit "neue anlegen": Duplikat-Check wird uebersprungen (`skip_duplikat_check`).
5. INSERT ueber `NotizenRepository`

Das `normalisiert`-Feld steuert die CRUD, der Originaltext (`aufgabe`) liefert den woertlichen Inhalt fuer die LLM-Extraktion.

### Read
Gewichtete Multi-Feld-Suche. Bei `read` reicht erster Treffer. Sonderfall: `read` ohne Target listet alle aktiven Notizen als kompakte Uebersicht (Name, Typ, Zusammenfassung).

### Update / add_content / remove_content
Das LLM generiert die neue Version. Der Agent laedt die aktuelle Notiz und gibt beides an das LLM:

```
System: Du bearbeitest eine Notiz. Fuehre die gewuenschte Aenderung durch.
        Antworte NUR mit dem neuen, vollstaendigen Notiz-Text.

User:   Aktuelle Notiz: Milch, Eier, Brot, Butter
        Aenderungswunsch: Streich die Milch
        Neuer Notiz-Inhalt:
```

→ LLM: "Eier, Brot, Butter"

Kein Diff, kein Regex — das LLM versteht natuerliche Sprache besser als jeder Parser.

### Delete (Soft-Delete)
`status = 'archiviert'` UND `aktiv = FALSE`. Kein Hard-Delete. Beide Felder werden synchron gesetzt (AGT-FIX5, Chat 22).

### clear_content (deterministisch)
Leert den Inhalt einer Notiz. Kein LLM-Call noetig — rein deterministische Operation.

### rename
Aendert den Titel einer bestehenden Notiz.

### append
Text an bestehende Notiz anhaengen — ohne die bestehende Version neu zu generieren.

---

## 6. CRUD-Haertung (Chat 42, Epic 14)

### Dreistufige Erkennung

**Stufe A — Statische Keyword-Hints (Python, vor dem LLM-Call):**

```python
KEYWORD_HINTS = {
    "fuege.*hinzu|hinzufuegen|aufnehmen|nimm.*auf|ergaenz":    "add_content",
    "streich|nimm.*raus|entferne.*von|runter von":              "remove_content",
    "leere|alles loeschen|komplett leeren":                     "clear_content",
    "loesch|entfern|weg damit|streich .*komplett":              "delete",
    "aendere|aendern|aktualisier|korrigier":                    "update",
    "zeig|was hast|welche|liste|auflisten":                     "read",
}
```

**Stufe B — Lernende Verb-Mappings (PostgreSQL, pro User):** Bei unbekanntem Verb (z.B. "hau rein") klassifiziert das LLM, bei niedriger Konfidenz wird rueckgefragt, bei Bestaetigung wird das Mapping gelernt. Ab `konfidenz >= 3` keine Rueckfrage mehr.

**Stufe C — LLM-Klassifikation:** Bekommt Keywords + Verb-Mappings als `[ERKENNUNGSHILFE]`-Block. Mit Chat 44 (Domain Language) verschiebt sich die primaere Erkennung auf den `[FACHSPRACHE]`-Block. Keywords und Verb-Mappings dienen als sekundaere Konfidenz-Pruefung.

### Konfidenz

| Situation | Konfidenz | Aktion |
|-----------|-----------|--------|
| Keyword-Hint + LLM stimmen ueberein | Hoch | Direkt validieren |
| Verb-Mapping (>=3) + LLM stimmen ueberein | Hoch | Direkt validieren |
| Verb-Mapping (<3) + LLM stimmen ueberein | Mittel | Direkt validieren, Konfidenz++ |
| Nur LLM, kein Hint | Niedrig | Rueckfrage + ggf. Verb lernen |
| Hint und LLM widersprechen sich | Konflikt | Rueckfrage |

### Verifikation (DB-Read nach Write)

Nach jeder Schreiboperation liest die CRUD den DB-Zustand und vergleicht mit dem erwarteten Ergebnis. ~~Bei Fehler: `CrudErgebnis.erfolg` wird auf `False` korrigiert.~~ → **widerlegt am 16.09.2026, behoben am 18.09.2026:** `CrudErgebnis` hatte keinen einzigen Importeur, und der Dienst meldete trotz gescheiterter Verifikation `abgeschlossen` (`DIENST-MELDET-ABGESCHLOSSEN-OHNE-VERIFIKATION`). Seither laeuft jede verifizierende Rueckgabe durch `agents/write_outcome.py::verified_outcome`: nicht bestaetigt heisst Status `fehler` mit Begruendung und ohne Zusage.

### Jede Kennung ist an den Menschen gebunden (26.10.2026)

**Seit dem Code-Tree `30b81af` (26.10.2026, Kennung wird nachgetragen → committet als `89bcba8`, die Doku dazu als `64ba8dd`) trägt jede Lese- und Schreibabfrage über eine Kennung den Menschen in der Bedingung.** Bis dahin lasen `_read_by_id` und die Verifikation eine Notiz allein über ihre Kennung (`WHERE id = %s`), `_delete` und `_rename` schrieben so, und `_update`, `_append` und `_clear_content` schrieben über `NotizenRepository.update`, das die Zeile ebenso allein über die Kennung trifft (`SCHREIBWEG-KENNUNG-OHNE-MENSCH`, im Archiv [`novaberg-bugs-archiv-wissen.md`](novaberg-bugs-archiv-wissen.md)). Offen war kein Weg — die Kennung kommt aus der Suche des Menschen (`suche.py`) oder aus der Kandidatenliste einer Rückfrage, die dieselbe Suche gefüllt hat —, aber der Schutz hing an dieser einen Herkunft; ein künftiger Weg, der die Kennung anders beschafft, hätte still in die Notiz eines anderen Menschen geschrieben.

- **Lesen:** `crud.py::_read_by_id(notiz_id, user_id)` liest `WHERE id = %s AND user_id = %s`; eine fremde Kennung liefert dasselbe wie eine fehlende, `None`. Die Verifikation nach dem Schreiben (`_verifizieren_notiz`) liest über denselben Weg.
- **Schreiben, ein geschlossener Satz:** `_SCHREIBUNGEN` trägt drei ganze Abfragen, jede mit `WHERE id = %s AND user_id = %s` — `inhalt` (Text und Zusammenfassung, dazu `suchtext`, `last_touched`, `updated_at`: dieselben Spalten, die `NotizenRepository.update` setzt), `archiv` (`aktiv = FALSE`, `status = 'archiviert'`) und `name`. Geschrieben wird nur über `_notiz_schreiben(art, werte, notiz_id, user_id)`, das die Zahl der getroffenen Zeilen zurückgibt; keine Spalte und keine Bedingung kommt aus einer Eingabe. Eine unbrauchbare Kennung, ein leerer Mensch, eine unbekannte Schreibung oder eine falsche Zahl der Werte werfen `ValueError`, bevor geschrieben wird; eine Rückgabe, die keine Zeilenzahl ist, wirft `TypeError`. `suchtext` läuft dabei eine Schreibung hinterher: `NOTIZ-SUCHTEXT-HINKT-NACH` ([`novaberg-bugs-wissen.md`](novaberg-bugs-wissen.md)), mittel, offen.
- **Laut statt Erfolg:** Trifft ein Schreibweg nicht genau eine Zeile, endet er über `_keine_zeile_getroffen` mit `logger.error` und Status `fehler` samt Grund (*„Notiz ID … : … Zeilen getroffen statt einer“*), Schritt `keine_zeile`; einen Erfolgssatz gibt es dann nicht. Das gilt für alle fünf schreibenden Wege, `_update`, `_delete`, `_append`, `_clear_content` und `_rename`. Bei einer fremden Kennung endet der Weg damit vor der Verifikation.
- **Kein `NotizenRepository.update` mehr:** Die Notizen schreiben über ihren eigenen gebundenen Weg. Das Repository selbst ist unverändert; `find_by_id`, `update`, `invalidate` und `set_wiedervorlage` treffen eine Notiz weiter allein über die Kennung und werden aus anderen Diensten gerufen (Fundliste, 26.10.2026). Angelegt wird weiter über `NotizenRepository.insert` (§5 *Create*).
- **Ein Zugang zur Datenbank:** Der Dienst holt sie über `_datenbank()`, eine lokale Importstelle, zur Aufrufzeit aufgelöst — vorher standen drei.

**Belege:** `server/tests/test_write_path_binding.py` (neu): je schreibendem Weg eine fremde Kennung — 0 Zeilen, `fehler`/`keine_zeile`, die fremde Zeile unverändert — gegen den Zwilling mit der eigenen Kennung, der schreibt; ebenso Lesen und Verifikation. Der Speicher ist gedoppelt und wertet die Bedingung samt `AND` und `OR` aus. Ein Zeuge nach dem Kriterium findet jede Bedingung über die Kennung in `crud.py` — an jeder Stelle der Bedingung, mit Tabellennamen oder benanntem Platzhalter, in Literalen, die der Parser verbindet, mit `+` innerhalb eines Ausdrucks oder als f-String — und verlangt `user_id` als eigenes `AND`-Glied; er findet heute 4 Bedingungen und hält diese Zahl als Untergrenze. Ein zweiter Zeuge verlangt, dass `crud.py` keine Methode des Repositoriums ruft, die allein über die Kennung trifft. Server-Suite `Ran 5473 tests` · `OK (expected failures=2)`. ~~**Im Betrieb nicht gemessen** (an zwei Prüf-Nutzern: die Kennung des anderen trifft 0 Zeilen und endet laut, die eigene trifft 1).~~ → **Im Betrieb gemessen am 26.10.2026** `[gemessen]`, über die Funktion an zwei Prüf-Nutzern, 8 Funktionen, 20 von 20 Schritten wie Soll: Die fünf schreibenden Wege treffen mit der Kennung des anderen 0 Zeilen und enden mit `fehler`/`keine_zeile`, mit der eigenen genau 1; Lesen und Verifikation liefern für die fremde Kennung `None` bzw. `False`; jede Abfrage trug `user_id`, die Notiz des anderen blieb unverändert. Das Modell, das `_create` und `_update` fragen, war im Messprozess gedoppelt. **Grenzen:** Einen neuen, nicht gerufenen Weg, der die Kennung mit `IN`, `= ANY`, umgekehrt, in Anführungszeichen oder in `JOIN … ON` vergleicht oder die Abfrage über Variablen, `+=`, `join`, `.format` oder `%` baut, findet das Kriterium nicht; keine dieser Formen steht heute im Code (Fundliste, 26.10.2026).

### Keine Pflicht-Rueckfrage

NotizenAgent erfordert Rueckfrage nur bei niedriger Konfidenz oder Konflikt (anders als Direktiven/Charakter).

---

## 7. Resume-Flow

### 3 Pfade

| Pfad | Ausloeser | Aktion |
|------|----------|--------|
| Disambiguierung | Mehrere Treffer, User waehlt | Matching auf Kandidaten, gewaehlte Notiz verarbeiten |
| Duplikat | Notiz existiert bereits | User entscheidet: aktualisieren oder neu anlegen |
| nicht_gefunden | add_content/remove_content + Notiz fehlt | `original_aufgabe` bewahrt, bei "ja" wird Notiz angelegt mit korrektem Inhalt |

### Die Deutung der Antwort (seit 18.10.2026, Tree `9153022`)

Bis dahin verglich `resume.py` Teilzeichenketten, und `_resume_nicht_gefunden` prüfte die Zustimmung **vor** der Ablehnung: *„nein, bitte nicht“* trägt `bitte` und legte die Notiz an (`ZUSTIMMUNG-GILT-ALS-ABLEHNUNG`, im Archiv). Seither gilt:

- **nicht_gefunden** (`_resume_nicht_gefunden`): Die Ablehnung wird zuerst geprüft und gewinnt. Sie sucht die Stämme aus `REFUSAL_STEMS` (`nein`, `ne`, `nee`, `nicht`, `kein`, `lass`, `abbruch`, `abbrech`, `stopp`, `stop`) als Teilzeichenkette, aber im Text ohne die Zustimmungswörter (`CONSENT_WORDS`, herausgenommen als ganze Wörter über `_without_words`): *gerne* und *meinetwegen* stimmen zu, jedes andere `ne` lehnt ab, auch *„ja, leg ne neue an“* — die sichere Richtung, der Mensch gibt den Auftrag dann noch einmal. Die Zustimmung zählt nur ganze Wörter (*ja* trifft *Jahr* nicht). → **Seit dem Code-Tree `33a7b83` (08.11.2026, Kennung wird nachgetragen) endet die Ablehnung mit `dismissed`** statt `abgeschlossen`, mit *„Okay, '…' wird nicht angelegt.“*: `_nach_resume` (`agent.py`) beendet den Graphen darauf, `dispatch._handle_resume` löscht den Wartezustand, und `_build_return` trägt den Text ins `pipeline_log`; der Planner baut die Antwort aus `aufgabe_verworfen` (`TOR-ABLEHNUNG-MELDET-ABGESCHLOSSEN`, Archiv, `novaberg-bugs-archiv-charakter.md`). Fehlte `dismissed` in `_nach_resume`, ginge das Nein nach `suchen`, und die Frage käme erneut — gemessen am 07.11.2026, nicht nach `ausfuehren`, wie der Eintrag vermutet hatte. Auf ein Nein zur Auswahl und zur Duplikat-Frage gilt das nicht: Dort kommt die Frage erneut (Fundliste, 07.11.2026).
- **Disambiguierung** (`_resume_disambiguierung`): Ein Name wählt nur als ganzes Wort, ein leerer Name zählt nicht. Danach Position oder ID als ganzes Wort (`_candidate_keys`): *„11 Uhr“* wählt nicht mehr den ersten Kandidaten, und eine fehlende ID zählt nicht mit — ihr leerer Text stand früher in jeder Antwort und wählte den ersten. Bei zwei Kandidaten *erste*/*zweite* (auch *oberen*, *unteren*, *anderen*) als ganze Wörter.
- **Duplikat** (`_resume_duplikat`): *aktualisieren* und *neu* als ganze Wörter, die Formen einzeln (*neu* trifft *erneut* nicht).

**Die Direktiven weichen davon ab** (seit dem 24.10.2026, Code-Tree `2457549`, Kennung wird nachgetragen → committet als `6846120`; `novaberg-agent-directives.md` §3): Ihr Knoten `read_answer` nimmt dieselben Wortlisten als Kopie, liest aber eine Antwort, die etwas zurückhält — ein `?` an irgendeiner Stelle oder ein Stamm des Wartens (`wart`, `moment`, `augenblick`, `sekunde`, `halt`) —, als unklar, nicht als Zustimmung. ~~Hier im Notizen-Dienst gilt das nicht: *„Wie bitte?“* auf *„… soll ich anlegen?“* trägt *bitte* und legt an (Fundliste, 24.10.2026).~~ → **Seit dem Code-Tree `f901e08` (24.10.2026) gilt die geschlossene Annahme in beiden Diensten, die Stämme des Wartens entfallen** (unten).

**Die geschlossene Annahme** (seit dem Code-Tree `f901e08`, 24.10.2026, Kennung wird nachgetragen → committet als `e1e0724`, die Doku dazu als `d2d6ec8`; ~~im Betrieb nicht gemessen — `ZURUECKHALTUNG-GILT-ALS-ZUSTIMMUNG`, `ABLEHNUNG-OHNE-STAMM-BESTAETIGT` offen bis dahin~~ → im Betrieb gemessen am 26.10.2026 über den echten Weg, an einem Prüf-Nutzer: *„Wie bitte?“* auf die Frage nach einer nicht gefundenen Notiz legt nichts an und erneuert die Frage, *„Nein“* schließt; beide geschlossen (25. und 26.10.2026), nach den Zeugen und der Messung im Betrieb an `direktiven` und `notizen` am 26.10.2026): Zustimmung ist nur eine Antwort, die **aus Zustimmung besteht** (`_consists_of_consent`). Die Ablehnung wird zuerst geprüft und gewinnt, wie zuvor. Danach muss (a) mindestens ein Wort ein Zustimmungswort sein, (b) **jedes** Wort ein Zustimmungswort oder ein Füllwort aus `FILLER_WORDS` — Artikel, Pronomen, das Hilfsverb *ist*, Partikeln, die nicht zögern (*mal, doch, ruhig, sehr*), *danke*; nie ein Nomen, eine Zahl, ein Fragewort, ein Wort der Einschränkung oder des Aufschubs (*aber, sondern, statt, erst, nur, noch, lieber, später, morgen*) und nie *na*, *nun*, *also* —, (c) jedes Zeichen außer Buchstaben, Ziffern und Apostroph eines aus `DROPPED_SIGNS` (ASCII-Leerraum und `. , ! ; : - ( )`; jedes Fragezeichen jeder Schrift, jedes Emoji, jedes Symbol macht die Antwort unklar) und (d) die Wortstellung keine Frage: ein Verb aus `ASKING_VERBS` (*ist*, *passt*) vor einem Wort aus `SUBJECT_WORDS` im selben Satzteil (Satzteile enden nur an `, . ! ; :`, `CLAUSE_SEPARATORS`) macht die Antwort unklar — *„ist das ok“* fragt, *„das ist ok“* stimmt zu, der Imperativ (*„Ja, mach das“*) fragt nicht. Sonst unklar: dieselbe Rückfrage noch einmal, nie eine Ausführung. Die Ablehnung liest den Text mit Apostroph, die Zustimmung ohne (*„ger'ne“* lehnt ab). Eine Kopie je Dienst, bis `KLA-KLAERFALL-AM-OBJEKT` Schritte 5 und 6 sie ablösen. Zeugen für `nicht_gefunden` (`_resume_nicht_gefunden`): `ClosedConsentTest`, `ClosedSignsTest`, `RefusalFirstTest`, `ExcludedWordsTest`, `WordOrderTest` und `ClauseSeparatorsTest` in `server/tests/agents/notizen/test_resume.py`; Server-Suite `Ran 5394 tests` · `OK (expected failures=2)`; MESSUNG vor dem Commit an 305 ungezeigten Antworten: 0 Fehlausführungen. Preis: *„ja 👍“*, *„Ja…“*, *„na klar“*, *„gut, ja“* fragen ein zweites Mal; *„mach ich das“* stimmt zu. *„Wie bitte?“* auf *„… soll ich anlegen?“* legt nichts an. Die Wahl unter Kandidaten und die Frage *aktualisieren oder neu* sind nicht berührt.

**Nicht gelesen wird eine Verneinung** bei Duplikat und Wahl: *„nein, keine neue“* legt eine neue Notiz an, *„nicht die erste, die zweite“* wählt die erste. Und ~~Ablehnungswörter ohne Stamm in der Liste (*„vergiss es“*, *nö*, *niemals*) bestätigen neben einem Zustimmungswort~~ → seit `f901e08` nicht mehr (`ABLEHNUNG-OHNE-STAMM-BESTAETIGT`, im Betrieb nicht gemessen). Beides steht in der Fundliste (18.10.2026).

**Belege:** `server/tests/agents/notizen/test_resume.py`, 29 Testmethoden, darunter ein Klassen-Zeuge, der die alte Teilzeichenketten-Deutung aus `REFUSAL_STEMS` nachrechnet. Gemessen mit `labor/2026-10-18_zustimmung/deutung.py` an 41 Antwortformen, am Code vor und nach dem Bau: 20 gehen von Anlegen zu Ablehnung, *meinetwegen* von Ablehnung zu Anlegen. Ein Turn im Betrieb ist nicht gemessen.

### Kette

1. Agent setzt `status=rueckfrage` + formuliert Rueckfrage-Text
2. Dispatch speichert `pending_agent:{user_id}` in Redis (TTL 300s)
3. Router erkennt im naechsten Turn den Pending-Agent → `management_action=resume`
4. Planner sieht `resume` → Schleifen-Schutz `_agent_bereits_gelaufen()` (Chat 106), dann `agent_name` aus Redis
5. Agent._resume bearbeitet die Antwort

**Terminierung bei Rueckfrage-auf-Rueckfrage (Chat 106):** Liefert `_resume` erneut
`status=rueckfrage` (Antwort unklar, kein Match), schreibt der Dispatch den Pending-Key
sofort wieder nach Redis — die Terminierung besorgt dann der Planner-Guard: Der Agent
lief in diesem Turn bereits, der Turn endet, der Responder stellt die neue Rueckfrage,
der Pending-Key wartet auf den naechsten echten User-Turn. Ohne den Guard rekursierte
genau dieser Fall bis Recursion-Limit 25 (AGENT-RUECKFRAGE-LOOP, gefixt `f1b3a27`).
Details: `novaberg-node-planner.md` §3.1.

### Rueckfrage-Szenarien

| Situation | Rueckfrage |
|-----------|----------|
| Create ohne Name oder Text | "Mir fehlt noch: Stichwort/Name der Notiz" |
| Create mit existierendem Namen | "Es gibt bereits eine Notiz 'X'. Aktualisieren oder neu anlegen?" |
| Delete/Update mit mehreren Treffern | "Ich habe mehrere Notizen gefunden: 'X', 'Y'. Welche meinst du?" |
| add_content/remove_content + Notiz nicht gefunden | "Einkaufsliste existiert nicht. Soll ich sie anlegen?" |

---

## 8. Notiz-Typen

| Typ | Beispiele |
|-----|-----------|
| `einkauf` | Einkaufslisten |
| `todo` | Aufgabenlisten |
| `merkliste` | Gesammelte Links, Referenzen |
| `notiz` | Allgemeine Merkzettel |
| `entwurf` | Textentwuerfe, Formulierungen |
| `idee` | Brainstorming, Konzepte |

Der Typ wird vom LLM beim Erstellen klassifiziert — nicht vom Nutzer explizit angegeben.

---

## 9. DB-Schema

Tabelle: `notizen` (angelegt via `db/init.sql`; eine eigene `agents/notizen/init.sql` existiert nicht)

| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| `id` | SERIAL | Primaerschluessel |
| `user_id` | VARCHAR(50) | Gedaechtnis-Partition |
| `name` | VARCHAR(255) | Stichwort / Kurzname |
| `typ` | VARCHAR(50) | einkauf, todo, merkliste, notiz, entwurf, idee |
| `text` | TEXT | Vollstaendiger Inhalt |
| `zusammenfassung` | TEXT | Erste 20 Woerter (Heuristik) |
| `themen` | TEXT[] | PostgreSQL-Array fuer Themen-Tags |
| `status` | VARCHAR(20) | `aktiv` oder `archiviert` |
| `aktiv` | BOOLEAN | Partial Index fuer Soft-Delete |
| `created_at` | TIMESTAMPTZ | Erstellungszeitpunkt |
| `updated_at` | TIMESTAMPTZ | Letzte Aenderung |

pg_trgm Extension: `CREATE EXTENSION IF NOT EXISTS pg_trgm` in `db/init.sql`.

---

## 10. Konfiguration

| Parameter | Wert | Beschreibung |
|-----------|------|-------------|
| NOTIZEN_SUCHE_MIN_SIMILARITY | 0.15 | WHERE-Schwellenwert pro Feld (pg_trgm) |
| NOTIZEN_SUCHE_MIN_SCORE | 0.3 | Post-Filter fuer kombinierten Score |
| Score-Gap-Formel | `gap >= avg` | Disambiguierungs-Schwelle |
| Redis Pending TTL | 300s | Timeout fuer Rueckfrage-Flow |
| Zusammenfassung | Erste 20 Woerter | Heuristik fuer Notiz-Zusammenfassung |
| Konfidenz-Lernschwelle | 3 | Ab 3 Bestaetigungen keine Rueckfrage |

---

## 11. Offene Punkte

**Offen — durch Live-Test in Chat 80 entdeckt, strukturelle Lösung im Frame-Konzept Phase 1b:**

- NOTIZEN-KONTEXT-REKONSTRUKTION — Mehrschritt-Rekonstruktion über mehrere Turns
- NOTIZEN-CONTAINER-WECHSEL — Notiz↔Liste-Wechsel als legitime Aktion
- NOTIZEN-SKILL-MANIFEST — Nova kennt eigene Fähigkeiten in Sprach-Schicht nicht
- NOTIZEN-UPDATE-TARGET-LEER — Bezugs-Pronomen im UPDATE-Pfad

Details siehe `novaberg-bugs.md` und `novaberg-backlog.md`. Lösungsraum: `novaberg-thinking-frames_k.md` §16.2 (Phase 1b).

---

## Der Objektbezug in der Klassifikation (17.09.2026, Scheibe 12 D2b)

Wie bei der Timeline: `objekt_bezug` im Kontext, `[OBJEKT]`-Block (`prompts/default/classify_notizen.objekt.txt`). **Gemessen:** erfundene Zustimmungen ohne Bezug 6 von 12 als Auftrag, mit Bezug **12 von 12** (`labor/2026-09-17_objektbezug/`).

**Befund derselben Reihe, mit und ohne Bezug gleich:** *»Ich brauche noch Mehl und Hefe«* — eine Aussage ohne Bitte — wird in 6 von 6 Klassifikationen `add_content`, und der Router stellt sie in 6 von 6 zu. ~~Für die Timeline ist entschieden, dass nur ein ausdrücklicher Auftrag schreibt; für die Notizen nicht (Fundliste 17.09.2026).~~ → seit dem 17.09.2026 auch für die Notizen entschieden (nächster Abschnitt).

## Nur auf ausdrücklichen Auftrag (17.09.2026)

**Die Absicht:** Eine Aussage über einen Bedarf oder eine Sache (*»ich brauche noch …«*, *»die Nummer ist …«*) legt keine Notiz an; festgehalten wird nur, worum der Nutzer ausdrücklich bittet — wie bei der Timeline seit dem 13.09.2026.

**Gebaut:** die Vorprüfung der Klassifikation (`prompts/default/classify_notizen.task.txt`) sagt es und führt zwei Aussagen unter `rejected`. Zeuge: `tests/test_notizen_auftrag.py`.

**Gemessen und nicht übernommen:** derselbe Satz im Aushang (`plugins/notizen_manager/manager.py`) samt Negativfall. Er hielt am Empfang 4 von 4 zugestellten Aussagen zurück, nahm aber einer Zustimmung in 3 von 3 Durchgängen das Ziel.

**Wirkung, gemessen** (`labor/2026-09-17_notizen_auftrag/`, 12 Aufrufe je Gruppe): Aussagen geschrieben 12 → **8**, Bitten und Zustimmungen 24 → 24. **Nicht erreicht:** *»Ich brauche noch Mehl und Hefe«* bleibt `add_content`, eine Telefonnummer als Aussage bleibt `create` — beide 3 von 3. Der Befund der Fundliste ist damit nicht erledigt.

~~**Warum der Anlass bleibt, wahrscheinlich:** Dieselbe Vorprüfung trägt ältere Regeln, die Bedarfsaussagen als Auftrag lesen — *»Wir brauchen auch Erdbeeren«* und *»Ach, und Butter!«* als `add_content`, *»X brauchen wir nicht«* und *»brauch ich nicht mehr«* als Entfernen (`classify_notizen.task.txt`, 3 Stellen). Nicht gemessen, welche davon S2 trägt.~~ → **Gemessen und behoben am 19.09.2026** (`labor/2026-09-18_notizen_vorpruefung/`, `labor/2026-09-19_notizen_liste/`): Von den drei älteren Regeln trug **Regel A** (*»Wir brauchen auch Erdbeeren«* = `add_content`) es allein — das Modell las den Satzbau, nicht die laufende Liste. Sie allein zu streichen kippte die Ergänzung einer laufenden Liste; ersetzt durch eine **Ausnahme in der Vorprüfung** (*„Hat der User im Verlauf gerade etwas auf eine Liste setzen lassen, ist eine Bedarfsaussage eine Ergaenzung dieser Liste → add_content"*): Bedarfsaussagen ohne Liste 15 → 6 von 27 geschrieben, Listen 21 von 21. Seit `968c56b` ist eine Ablehnung der Klassifikation als Nicht-Auftrag gekennzeichnet (`Korrektur.kein_auftrag`); im Betrieb 19.09.2026: keine Notiz auf 3 von 3 Bedarfsaussagen, Nova bot an (`labor/2026-09-19_angebot_bedarf/`). **Offen:** 6 von 27 Bedarfsaussagen, die Telefonnummer als Aussage (Fundliste).

**Die Ergänzung einer laufenden Liste bleibt ein Auftrag — Entscheidung des Eigentümers am 17.09.2026:** Läuft eine Liste und folgt *»Wir brauchen auch Mülltüten«*, gehört das auf die Liste. Klausel 2 des Aushangs und die Regel in der Vorprüfung bleiben also stehen; in der Messung schrieb die Klassifikation diese Form in beiden Fassungen 3 von 3. **Damit ist auch die Rücknahme der Sperre am Empfang inhaltlich richtig:** Sie hielt genau diese Ergänzung in 3 von 3 zurück.

## Seit dem 18.09.2026 — Zustimmung, Ablehnung, Verifikation

- **Zustimmung:** Stimmt der Mensch einem Angebot blank zu, trägt die Klassifikation einen `[ZUSTIMMUNG]`-Block (`prompts/default/classify_notizen.zustimmung.txt`), und das Ziel kommt aus der angebotenen Sache, nicht aus dem Verlauf (`consent_fields`).
- **Ablehnung (E3):** Ein blankes Nein auf ein Angebot hält am Objekt `Speichern = abgelehnt` fest; eine so markierte Sache bietet Nova nicht wieder an.
- **Verifikation:** Alle schreibenden Rückgaben laufen durch `agents/write_outcome.py::verified_outcome` — bestätigt `abgeschlossen`, sonst `fehler` ohne Zusage. Das Umbenennen war der einzige Schreibpfad ohne Prüfung; es prüft jetzt den neuen Namen.