Codex-Brief: UID-Delta-Watcher (hoechste Prioritaet)

Der Volllauf laedt strukturell jede Quellmail, bevor er weiss, ob er sie hat -
die Identitaet ist der Body-Hash. Belegt am Lauf e.klarner: 46.415 gelesen,
10.023 geschrieben. Der Nachtlauf zieht so ~42 GB/Nacht.

Die UID ist die einzige Kennung ohne Body und bricht die Ringabhaengigkeit.
UIDVALIDITY-Pruefung ist dabei nicht Beiwerk, sondern Voraussetzung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DonVoo 2026-07-16 20:13:05 +02:00
parent a7ffab1182
commit 645ce25848

108
uid-delta-brief.md Normal file
View file

@ -0,0 +1,108 @@
# Codex-Brief — UID-Delta-Watcher
**Hoechste Prioritaet.** Alles andere (`--archive-only`, `--since`) ist Kuer.
## Das Problem, gemessen
Der Volllauf laedt **jede** Quellmail vollstaendig, bevor er weiss, ob er sie
schon hat. Das ist kein Bug, sondern strukturell — die Identitaet *ist* der
Body-Hash:
```go
// 04-imap-source.go
seqSet.AddRange(1, 0) // alle Nachrichten
cmd := m.c.Fetch(seqSet, &imap.FetchOptions{
BodySection: []*imap.FetchItemBodySection{section}, // samt Body
})
fn(rawMessageFromFetch(buf, section)) // AlreadyCopied erst hier
BodySHA256: bodySHA256(m.Body) // braucht den Body
```
Belegt am Lauf e.klarner 2026-07-16, Stueck 3/3:
`total=46415 complete=10023 archived=10023` — **36.392 Mails wurden gelesen und
verworfen.** Bei 22 Konten bedeutet der Nachtlauf rund **42 GB pro Nacht**
(~1,3 TB/Monat) von einem Anbieter, der gerade Mail verliert.
Die Idempotenz verhindert doppelte **Schreib**vorgaenge in Archiv und Ziel. Sie
verhindert **keinen** Doppel-Download. Diese Unterscheidung bitte auch in der
Doku sauber halten — wir haben sie selbst kurz verwechselt.
## Die Idee
**Die UID ist die einzige Kennung, die ohne Body zu haben ist.** Sie bricht die
Ringabhaengigkeit. `UID FETCH <cursor+1>:* (UID)` kostet einen Roundtrip statt
20 GB.
Der Preis: UIDs sind Server-Zusagen, keine Wahrheiten. Sie gelten nur, solange
`UIDVALIDITY` konstant bleibt. **Deshalb ist die UIDVALIDITY-Pruefung kein
Beiwerk, sondern das, was die Abkuerzung ueberhaupt erst zulaessig macht.**
Ohne sie tauschen wir einen teuren Beweis gegen eine billige Annahme — bei
diesem Anbieter die schlechteste aller Ideen.
## Was zu bauen ist
Neue Tabelle, pro Konto und Ordner:
```sql
CREATE TABLE uid_cursor (
account_id INTEGER NOT NULL,
folder TEXT NOT NULL,
uidvalidity INTEGER NOT NULL, -- vom Server bei SELECT
last_uid INTEGER NOT NULL, -- hoechste verarbeitete UID
checked_at INTEGER NOT NULL,
PRIMARY KEY (account_id, folder)
);
```
Ablauf je Ordner:
1. `SELECT` (readonly) → liefert `UIDVALIDITY` und `UIDNEXT`.
2. **Ist `uidvalidity` != gespeicherter Wert → ALARM + Volllauf.**
Cursor **nicht** stillschweigend fortschreiben. Nicht „reparieren".
Loggen, melden, komplett neu einlesen. Der Fall ist selten und teuer —
genau darum darf er nicht leise passieren.
3. Kein Eintrag vorhanden → Volllauf, danach Cursor setzen.
4. Sonst: `UID FETCH (last_uid+1):* (UID)` → nur diese UIDs mit Body holen und
durch die **unveraenderte** Pipeline schicken:
`AlreadyCopied -> mbox.Append -> dst.Append -> MarkCopied`.
5. Cursor **erst nach erfolgreichem Durchlauf** aller neuen Mails setzen.
Bricht etwas ab, bleibt der alte Stand stehen. Lieber zweimal lesen als
einmal ueberspringen.
## Fallstricke
1. **Cursor niemals bei Fehlern fortschreiben.** Ein Abbruch nach Mail 5 von 20
darf den Cursor nicht auf 20 setzen. Im Zweifel: alter Wert bleibt.
2. **UID-Rueckgang** (`UIDNEXT` kleiner als `last_uid`) ist unmoeglich, solange
UIDVALIDITY gilt. Tritt er auf, ist der Server kaputt → ALARM, kein
Rechnen mit dem Wert.
3. **Der Watcher findet nur Neues, nichts Geloeschtes.** Das ist in Ordnung —
Geloeschtes soll das Archiv ja behalten. Aber es heisst: **der Watcher
ersetzt den Volllauf nicht als Beweis, nur als Alltag.** Ein
Voll-Abgleich (z. B. woechentlich) bleibt noetig.
4. **Neue Ordner** tauchen ohne Cursor auf → Fall 3, Volllauf fuer diesen
Ordner. Kein Sonderweg.
5. **Kein stiller Default.** Ohne Flag laeuft alles wie bisher. Der Watcher ist
ein eigener Modus (`--watch`, Intervall konfigurierbar, Vorschlag 5 min).
6. **Log muss den Modus zeigen**, z. B.
`watch <konto> <ordner>: uidvalidity=N cursor=M neu=K` — sonst sieht ein
Delta-Lauf spaeter wie ein vollstaendiger Lauf aus. Wir haben genug Zeit
damit verbracht, gruenen Meldungen zu misstrauen.
## Abnahme
1. `go test ./...` gruen.
2. Testkonto, zweiter Lauf ohne neue Mails: **`neu=0`, und im Netzwerk-Log darf
kein `BODY`-Fetch auftauchen.** Wird weiterhin alles geladen, ist der
ganze Sinn verfehlt.
3. Eine Mail zustellen → naechster Lauf holt genau **eine**.
4. UIDVALIDITY kuenstlich in der DB veraendern → **ALARM + Volllauf**, Cursor
wird nicht leise korrigiert. Dieser Test ist der wichtigste.
5. Abbruch mitten im Delta (Prozess killen) → Cursor unveraendert, naechster
Lauf holt dieselben Mails erneut, Idempotenz verhindert Doppelschreiben.
## Danach
Nachtlauf-Cron (`/etc/cron.d/mail-graveyard-nightly`) entfernen — er ist der
Uebergang, nicht das Ziel. Erst wenn Abnahme 2 und 4 belegt sind.