feat(#294): Sender→Label Memory — Online-Feedback-Loop für Mail-Tags #295

Merged
admin-mrrm merged 2 commits from feat/294-sender-label-memory into main 2026-05-15 07:24:26 +02:00
Owner

Closes #294

Was

Das System lernt aus User-Bestätigungen: bestätigt der User wiederholt einen Tag für einen Sender (z.B. noreply@dhl.deSendung), wird der Tag beim nächsten Batch direkt vergeben — NLI wird übersprungen.

Warum

Nach #268 (Threshold), #290 (Label-Wording, regression) und #292 (Subject-Weighting, regression) sind die manuellen NLI-Hebel ausgereizt. Pivot auf Online-Loop laut Issue #294.

Architektur

Backend:

  • Drizzle-Tabelle mail_sender_label_memory (id, owner, senderAddr, tagId, confirmCount, removeCount, lastSeenAt) + Unique-Index auf (owner, senderAddr, tagId)
  • SenderMemoryService.incrementConfirm/Remove via Upsert (ON CONFLICT DO UPDATE)
  • Hook in MailTagsService.confirmTag+confirm; removeTag+remove — der Sender wird aus mail_messages_cache.from_addr gezogen
  • Batch-Endpoint POST /mail/sender-memory/lookup mit {senderAddrs: string[]} (1-100)

Mobile:

  • Pro Page einmal Memory abfragen (nicht pro Mail)
  • Match-Kriterium: confirmCount ≥ 2 UND confirm > remove; bei mehreren Tag-Treffern gewinnt höchste confirm - remove-Marge
  • Match → assignTag direkt, NLI skip, weiterzählen
  • Lookup-Fehler → Log + Fallthrough auf NLI (kein Hard-Fail)

Shared:

  • normalizeSenderAddr in @mrrmlab/shared-types — beide Seiten nutzen die exakt gleiche Logik ("John <X@Y>"x@y, lowercase, trim)
  • Drift wäre stiller Lookup-Mismatch → bewusst zentral

Was NICHT in dieser PR

  • Web-Classifier-Integration (Follow-up — Web hat eigene Embedding-Pipeline)
  • UI zum Anzeigen / Pflegen der Memory
  • Auto-Decay (alte Einträge verfallen)
  • Domain-basiertes Fallback (*@dhl.de)

Tests

  • API Unit: 232 Tests grün — inkl. 15 neue für SenderMemoryService, 4 neue für MailTagsService-Hooks (confirm/remove rufen Memory mit Sender, skip wenn fromAddr null)
  • API Integration: 42 Tests grün — inkl. 4 neue End-to-End (lookup leer / confirm zählt / remove zählt / 400 bei leerem Array)
  • Mobile Unit: 49 Tests grün — inkl. 8 neue (page-Lookup, NLI-Skip bei Match, Fallthrough bei kein Match / threshold ungenügt / mehr Removes, beste Marge gewinnt, kein Lookup wenn keine Sender, robust gegen Netzwerkfehler)
  • Typechecks API + Mobile + Shared-Types grün

Manueller Test-Plan

  1. Nach Merge: Build neu + Metro reload
  2. Eine Mail einer bekannten Quelle (z.B. DHL) bestätigen
  3. Eine zweite Mail derselben Quelle bestätigen → Counter steht jetzt auf 2
  4. Inbox neu kategorisieren → die nächste DHL-Mail sollte ohne NLI-Inferenz direkt mit „Sendung" getaggt sein (in Logs: keine [NLI]-Zeile, dafür direktes assignTag)
Closes #294 ## Was Das System lernt aus User-Bestätigungen: bestätigt der User wiederholt einen Tag für einen Sender (z.B. `noreply@dhl.de` → `Sendung`), wird der Tag beim nächsten Batch direkt vergeben — NLI wird übersprungen. ## Warum Nach #268 (Threshold), #290 (Label-Wording, regression) und #292 (Subject-Weighting, regression) sind die manuellen NLI-Hebel ausgereizt. Pivot auf Online-Loop laut Issue #294. ## Architektur **Backend:** - Drizzle-Tabelle `mail_sender_label_memory` (id, owner, senderAddr, tagId, confirmCount, removeCount, lastSeenAt) + Unique-Index auf `(owner, senderAddr, tagId)` - `SenderMemoryService.incrementConfirm/Remove` via Upsert (`ON CONFLICT DO UPDATE`) - Hook in `MailTagsService.confirmTag` → `+confirm`; `removeTag` → `+remove` — der Sender wird aus `mail_messages_cache.from_addr` gezogen - Batch-Endpoint `POST /mail/sender-memory/lookup` mit `{senderAddrs: string[]}` (1-100) **Mobile:** - Pro Page einmal Memory abfragen (nicht pro Mail) - Match-Kriterium: `confirmCount ≥ 2` UND `confirm > remove`; bei mehreren Tag-Treffern gewinnt höchste `confirm - remove`-Marge - Match → `assignTag` direkt, NLI skip, weiterzählen - Lookup-Fehler → Log + Fallthrough auf NLI (kein Hard-Fail) **Shared:** - `normalizeSenderAddr` in `@mrrmlab/shared-types` — beide Seiten nutzen die exakt gleiche Logik (`"John <X@Y>"` → `x@y`, lowercase, trim) - Drift wäre stiller Lookup-Mismatch → bewusst zentral ## Was NICHT in dieser PR - Web-Classifier-Integration (Follow-up — Web hat eigene Embedding-Pipeline) - UI zum Anzeigen / Pflegen der Memory - Auto-Decay (alte Einträge verfallen) - Domain-basiertes Fallback (`*@dhl.de`) ## Tests - **API Unit:** 232 Tests grün — inkl. 15 neue für `SenderMemoryService`, 4 neue für `MailTagsService`-Hooks (confirm/remove rufen Memory mit Sender, skip wenn fromAddr null) - **API Integration:** 42 Tests grün — inkl. 4 neue End-to-End (lookup leer / confirm zählt / remove zählt / 400 bei leerem Array) - **Mobile Unit:** 49 Tests grün — inkl. 8 neue (page-Lookup, NLI-Skip bei Match, Fallthrough bei kein Match / threshold ungenügt / mehr Removes, beste Marge gewinnt, kein Lookup wenn keine Sender, robust gegen Netzwerkfehler) - Typechecks API + Mobile + Shared-Types grün ## Manueller Test-Plan 1. Nach Merge: Build neu + Metro reload 2. Eine Mail einer bekannten Quelle (z.B. DHL) bestätigen 3. Eine zweite Mail derselben Quelle bestätigen → Counter steht jetzt auf 2 4. Inbox neu kategorisieren → die nächste DHL-Mail sollte ohne NLI-Inferenz direkt mit „Sendung" getaggt sein (in Logs: keine `[NLI]`-Zeile, dafür direktes assignTag)
feat(#294): Sender→Label Memory für Online-Tag-Learning
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
c0e9c44443
Pivot von manueller NLI-Kalibrierung (#268, #290, #292 alle ausgereizt)
auf einen Online-Feedback-Loop: bestätigt der User wiederholt einen Tag
für einen Sender, wird der Tag beim nächsten Batch direkt vergeben und
NLI gespart.

Backend:
- Tabelle `mail_sender_label_memory` (owner, senderAddr, tagId,
  confirmCount, removeCount, lastSeenAt) + Unique-Index
- `SenderMemoryService.incrementConfirm/Remove/lookup` mit Upsert-Logik
- Hooks in `confirmTag` / `removeTag`: zählen das Sender→Tag-Signal mit
- Endpoint `POST /mail/sender-memory/lookup` (Batch, 1-100 Adressen)
- Integration-Tests gegen echte DB

Mobile:
- `categorizeFolderItems` fragt pro Seite einmal Memory ab; bei klarem
  Match (confirmCount ≥ 2, mehr Confirm als Remove, höchste Marge)
  → Tag direkt setzen, NLI überspringen
- Robust gegen Lookup-Fehler (Log + Fallthrough auf NLI)

Shared:
- `normalizeSenderAddr` nach @mrrmlab/shared-types — beide Seiten nutzen
  die exakt gleiche Normalisierung (Drift wäre stiller Lookup-Mismatch)
- API-Client: `MailResource.lookupSenderMemory` + Zod-Schemas

Tests: API 232 unit + 42 integration; Mobile 49 (alles grün).
fix(#294): manuelle Tag-Zuweisung (assignTag confirmed) zählt jetzt auch im Sender-Memory
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
3ce62bf1ff
Beim manuellen Test fiel auf: assignTag(status='confirmed') aus der Mobile-UI
(reader.tsx, review.tsx) löste keinen Memory-Hook aus — nur confirmTag tat das.
Dadurch wurden direkte User-Aktionen ("Werbung" an PayPal-Newsletter anhängen)
nicht gelernt.

Fix: assignTag ruft incrementConfirm auf, wenn status=confirmed. Bei
status=suggested (NLI-Batch) bleibt es bei reinem Insert ohne Memory-Effekt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
admin-mrrm/mrrmlabapp!295
No description provided.