Read-only recon (kb/audits/mail-sync-2026-08-06.md, OKF type: audit).
Stan wyjsciowy zmierzony na zywo: korpus gmail urywa sie 2026-06-19, dziura
48 dni ~ 1800 maili przy tempie ~37/dobe; zero kodu IMAP/JMAP w repo (tylko
dokumenty), brak modelu stanu synca — w bazie 3 tabele, zadnej z UID.
Ustalenia blokujace, ktore latwo przeoczyc (kazde zawodzi cicho, bez bledu):
- koperty bez entities[type=headers] daja prefiks chunka "(brak tematu) | ?"
(build_prefix), wiec poller musi pisac headers przy INSERCIE, nie backfillem
- DEFAULT_SUMMARYLESS_SOURCES = ("gmail",) — fastmail zembeduje sie i zniknie
z /search, bo galaz summaryless filtruje po source
- etap mailowy dopiety do kb-ingest.timer (03:30) zapali KbEmbedBacklogGrowing
na stale: SOLARIA wtedy spi (potwierdzone: kb_ingest_embed_skipped 1)
- envelope.id = goly Message-ID globalnie, wiec mail obecny na obu kontach
trafia do bazy raz, z source konta ktore wygralo wyscig
Architektura: fetch na PIHA co godzine (24/7, archiwum kanoniczne, bez GPU),
indeksowanie osobno bramkowane probe'em Ollamy (embed z PIHA zmierzony:
HTTP 200 w 8 ms, ~60 chunkow/dobe — rsync na SOLARIE zbedny). Spoiwem jest
kolejka "koperty bez chunkow", nie --since (ts to naglowek nadawcy).
Decyzje operatora (a)-(g) z rekomendacjami. Dwie korekty zalozen:
- POSTGRES_PASSWORD NIE lezy plaintextem w repo — service.yaml wymienia tylko
nazwy zmiennych, env.example ma placeholdery, skan sledzonych YAML: 0 trafien.
Rekomendacja uzywa istniejacego /opt/homelab/kb/.env (root:root 600,
czytany przez systemd przed zrzuceniem uprawnien)
- Fastmail przez IMAP, nie JMAP — domyka otwarta od czerwca decyzje
"unifikacja adaptera" (kb-mail-pillar.md §9); wymaga korekty §2/§7 tamtego
dokumentu po zatwierdzeniu
Zaleznosci z reconem multiagentowym: zadnych blokujacych. Dyspozytor to
subsystem B (osobny projekt); jawna zaleznosc to wiki-kompilat
(kb-m5-faza3.md:620 — "pelna wiki po przyrostowce").
Aktualizacja kb-m5-faza-mailowa.md: Krok 7 = WYKONANE + wskaznik do reconu.
Nic nie zaimplementowano, nie zdeployowano ani nie pobrano.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
43 KiB
| okf | type | visibility | status | updated | as_of | links |
|---|---|---|---|---|---|---|
| 0.1 | audit | private | active | 2026-08-06 | 2026-08-06 |
Recon — przyrostówka IMAP (gmail + fastmail), 2026-08-06
Read-only recon. Realizuje Krok 7 fazy mailowej
(kb/phases/kb-m5-faza-mailowa.md §10, §12 poz. 7 — „OTWARTE — NEXT",
odblokowane zamknięciem Etapu B). Zakres: co trzeba dobudować, żeby korpus
mailowy przestał być fotografią z Takeoutu i sam się dosypywał z dwóch żywych
skrzynek.
Źródło kodu: task/mail-sync-recon @ b1b6692 (worktree cięty z mastera).
Dowody runtime zebrane 2026-08-06 ~13:50 UTC+2: SOLARIA = host tego reconu
(odczyty lokalne), PIHA po ssh (docker exec kb-postgres psql, systemctl,
curl). Nic nie zostało zaimplementowane, zdeployowane ani pobrane — wynik
to wyłącznie ten dokument. Twierdzenia o zewnętrznych API (limity Gmaila,
układ folderów Fastmaila) są oznaczone jako [do weryfikacji na żywo] —
nie sprawdzałem ich, bo wymagałoby to logowania na konta.
1. Jak powstał obecny korpus
1.1 Tor, którym przyszły dane — trzy joby, jeden kierunek
| # | Job | Co robi | Kiedy |
|---|---|---|---|
| 1 | jobs/gmail-bulk-import |
Takeout .mbox → archiwum .eml + wiersze envelope (tylko manifest załączników) |
jednorazowo, 2026-06/07 |
| 2 | jobs/gmail-header-backfill |
UPDATE: dopisuje entities[type=headers] do istniejących kopert |
jednorazowo, 2026-07 |
| 3 | jobs/mail-body-ingest |
drugi pełny przebieg po archiwum: body → quote-strip → chunk → embed → document_chunk + entities[type=threading] |
Etap A 2026-07-23, Etap B 2026-08-06 |
Wszystkie trzy są one-shotami uruchamianymi ręcznie. Żaden nie ma jednostki
systemd, żaden nie wie o istnieniu skrzynki — punktem wejścia jest plik na dysku
(.mbox, potem .eml). To jest dokładnie ta luka, którą przyrostówka wypełnia:
nie ma niczego, co rozmawia z serwerem pocztowym.
1.2 Archiwum .eml — struktura i format
Kanoniczne archiwum na PIHA, lustro (dla backfillu GPU) na SOLARII:
/home/oskar/kb/mail/archive/.
Układ katalogów wyznacza kb_mail.archive.save_eml
(packages/kb-mail/src/kb_mail/archive.py:29):
{archive_root}/{source}/{YYYY}/{MM}/{sanitized_message_id}.eml
sanitized_message_id=envelope.idprzepuszczony przezstr.maketrans({"/": "_", "\\": "_", ":": "_", "<": "", ">": ""}).YYYY/MMpochodzą zts(nagłówekDate), nie z daty pobrania.- Append-only twardo:
save_emlrobidest.exists()→FileExistsError. Nie ma trybu nadpisania. Wołający decyduje, czy to błąd, czy „już mam" (bulk-import liczy to jakoskipped). - Format pliku: surowe bajty wiadomości, zapisane przez
msg.as_bytes(policy=email.policy.compat32)— czyli bez normalizacji, z oryginalnymi 8-bitowymi bajtami w nagłówkach włącznie.
Stan na dziś (SOLARIA, odczyt lokalny): jedyny podkatalog to gmail/, roczniki
1970 (epoch fallback) oraz 2002–2026. Katalogu fastmail/ nie ma —
źródło fastmail jest greenfieldem, zero plików i zero wierszy w DB.
1.3 Tabela envelope — schemat i klucze dedup
services/kb-postgres/init/001_envelope.sql:
CREATE TABLE envelope (
id TEXT PRIMARY KEY, -- goły Message-ID (bez < >), lub sha256-<32hex>
source TEXT NOT NULL, -- fastmail | gmail | paperless
ts TIMESTAMPTZ NOT NULL, -- data wiadomości, UTC
geo JSONB, -- null dla maili
raw_ref TEXT NOT NULL, -- ścieżka względna do .eml
entities JSONB NOT NULL DEFAULT '[]'
);
CREATE INDEX envelope_source_idx ON envelope (source);
CREATE INDEX envelope_ts_idx ON envelope (ts);
Kontrakt zamrożony, addytywny (packages/kb-mail/src/kb_mail/envelope.py,
kb/subsystems/kb-mail-pillar.md §3). Kolumn UID/folder/etykieta nie ma
i nie było w planie.
Klucz dedup to id, czyli goły Message-ID. Wyprowadza go
gmail_bulk_import._message_id (importer.py:63):
mid = _sanitize(str(msg.get("Message-ID", ""))).strip().strip("<>")
return mid or ("sha256-" + hashlib.sha256(msg.as_bytes()).hexdigest()[:32])
Cała idempotencja stoi na INSERT ... ON CONFLICT (id) DO NOTHING
(kb_mail.db.insert_envelope, importer._insert_batch). Na żywej bazie:
9 kopert ma id z prefiksem sha256- (maile bez Message-ID), reszta to
prawdziwe Message-ID; średnia długość id = 60 znaków.
entities to lista otwartych obiektów tagowanych type. Dziś w użyciu trzy:
type |
Kto pisze | Zawartość |
|---|---|---|
attachment |
gmail-bulk-import | filename, content_type, size, sha256 |
headers |
gmail-header-backfill | from, to, cc, delivered_to, subject, date_raw |
threading |
mail-body-ingest | in_reply_to, references[] |
Oba appendy (headers, threading) są idempotentne przez
WHERE NOT EXISTS (SELECT 1 FROM jsonb_array_elements(entities) e WHERE e->>'type'='…').
1.4 Stan żywej bazy i archiwum (zmierzone 2026-08-06)
| Miara | Wartość | Skąd |
|---|---|---|
envelope source='gmail' |
225 030 | SELECT source, count(*) … GROUP BY source |
envelope source='paperless' |
191 | j.w. |
envelope source='fastmail' |
0 | j.w. |
max(ts) dla gmail |
2026-06-19 19:13:39+02 | j.w. |
document_chunk (model bge-m3) |
389 012, w tym 201 849 bez wektora (newsletter) | GROUP BY model |
Rozmiar document_chunk |
2 971 MB | pg_total_relation_size |
Rozmiar envelope |
264 MB | j.w. |
Rozmiar bazy kb |
3 248 MB | pg_database_size |
PIHA /home wolne |
136 GB (66% zajęte) | df -h |
| PIHA RAM | 8 GB, available 2,4 GB |
free -m |
| Archiwum gmail 2024 / 2025 / 2026 | 13 207 / 13 575 / 6 213 plików | find … -name '*.eml' | wc -l |
| Ostatni rocznik archiwum | 2026/06 (861 plików) |
ls |
Kluczowa liczba tego reconu: korpus urywa się 2026-06-19, dziś jest 2026-08-06 — dziura ma 48 dni. Przy zmierzonym tempie ~37 maili/dobę (13 575 w 2025; 6 213 przez ~170 dni 2026) to ~1 800 kopert, których w KB nie ma i nigdy nie będzie, dopóki ktoś nie zrobi drugiego Takeoutu albo nie postawi przyrostówki. Dziura rośnie o kolejny dzień każdego dnia.
1.5 Skąd source='gmail' i czym różni się ścieżka fastmail
source jest wpisany na sztywno w kod importera — Envelope(..., source="gmail", ...)
(importer.py:255) i save_eml(archive_root, envelope_id, "gmail", ts, raw)
(importer.py:248). Nie ma parametru CLI, nie ma detekcji. Tak samo
mail_body_ingest.fetch_envelopes (ingest.py:372) ma zaszyte
WHERE source = 'gmail'.
Ścieżka fastmail nie istnieje w żadnej postaci — ani kodu, ani wierszy, ani
katalogu w archiwum. Jedyne, co jest, to zarezerwowane miejsce w kontrakcie
(komentarz -- fastmail | gmail | ... w DDL i w dataclassie). Różnice, które
realnie wystąpią, gdy się ją zbuduje:
- Brak Takeoutu — nie ma bulk-importu do wykonania; fastmail zaczyna od pustego zbioru i albo bierzemy tylko nowe maile, albo robimy jednorazowy pełny zaciąg IMAP-em (Decyzja (e) w §4).
- Inny układ folderów — Gmail ma jeden worek
All Mail; Fastmail ma klasyczne IMAP-owe foldery i trzeba je wyliczyć (§2.3). - Ta sama przestrzeń id — i to jest pułapka, patrz §2.4.
2. Czego brakuje do przyrostówki
2.1 Klient IMAP — nie ma go nigdzie
Grep po całym repo (imaplib|aioimaplib|imapclient|IMAP|JMAP) daje wyłącznie
dokumenty: kb/subsystems/kb-mail-pillar.md, kb/subsystems/kb-overview.md,
§10 planu fazy mailowej, kilka session-logów. Zero linii kodu.
packages/kb-mail ma dokładnie pięć modułów — archive, chunking, db,
envelope, text — i dwie zależności (asyncpg, structlog). Nie ma warstwy
transportu i nigdy nie było.
Katalogi jobs/fastmail-poller/ i jobs/gmail-imap-poller/, zapowiadane w
kb-overview.md jako etapy 3–4, nie istnieją.
Co jest gotowe do reużycia i jest tego sporo:
| Element | Gdzie | Uwaga |
|---|---|---|
save_eml |
kb_mail/archive.py |
append-only, FileExistsError = „już mam" |
insert_envelope |
kb_mail/db.py |
ON CONFLICT (id) DO NOTHING |
parse_headers + parse_headers_fallback |
gmail_header_backfill/backfill.py:67,131 |
typed parse + compat32, przetestowane na 8-bitowych nagłówkach |
_message_id, _parse_date, _parse_attachments |
gmail_bulk_import/importer.py:63,78,97 |
wyprowadzenie id/ts/manifestu |
sanitize_surrogates, strip_nul |
kb_mail/text.py |
oba obowiązkowe przed zapisem do jsonb/text |
| Cały pipeline body | mail_body_ingest/ingest.py |
wymaga jednej zmiany, §3.2 |
embed_batch_resilient, check_ollama_health |
kb_retrieval/embed.py:168,256 |
retry + bisekcja + probe backendu |
imaplib jest w stdlib Pythona 3.11 (PIHA ma 3.11.2) — przyrostówka nie musi
dokładać żadnej zależności, jeśli zaakceptujemy synchroniczny klient
(uzasadnienie przy Decyzji (a)).
2.2 Model stanu synca — nie ma go, i to jest główna nowa rzecz do zaprojektowania
W bazie są trzy tabele: envelope, document_chunk, document_summary
(sprawdzone \dt). Nic nie przechowuje ani UIDVALIDITY, ani UIDNEXT, ani
last-seen-UID, ani daty ostatniego pollu.
Trzy warianty, uporządkowane:
(A) Nowa tabela mail_sync_state w kb-postgres — migracja 005:
CREATE TABLE mail_sync_state (
account TEXT NOT NULL, -- 'gmail' | 'fastmail' (== envelope.source)
folder TEXT NOT NULL, -- nazwa IMAP-owa, jak zwrócona przez LIST
uidvalidity BIGINT NOT NULL,
last_uid BIGINT NOT NULL, -- najwyższy UID przetworzony do końca
last_sync_ts TIMESTAMPTZ NOT NULL,
PRIMARY KEY (account, folder)
);
(B) Plik JSON w /opt/homelab/state/ — zgodne z konwencją runtime-state
z CLAUDE.md, ale rozjeżdża stan z danymi: przy odtworzeniu bazy z backupu plik
zostaje w przyszłości i przyrostówka cicho przeskakuje maile.
(C) Wyprowadzanie z max(envelope.ts) — bez stanu. Kuszące i złe:
ts to nagłówek Date nadawcy, nie moment dostarczenia. Mail z przekręconym
zegarem albo opóźniony w dostarczeniu wpadnie poniżej znacznika i zniknie na
zawsze. Znamy to z własnego korpusu — 2 559 kopert ma ts = epoch 1970.
Rekomendacja: (A). Stan synca to dane, nie konfiguracja — należy do tej
samej jednostki backupu co koperty, którym odpowiada. Kosztuje jedną migrację
(services/kb-postgres/init/005_mail_sync_state.sql), a płaci za to
najważniejszą właściwością: stan i dane odtwarzają się razem albo wcale.
Semantyka pętli (standardowa i jedyna poprawna dla IMAP):
SELECTfolderu → serwer zwracaUIDVALIDITYiUIDNEXT.- Jeśli
UIDVALIDITY≠ zapamiętany → cała numeracja UID jest unieważniona; zapamiętanelast_uidnic nie znaczy. Reakcja: pełne przemiecenie folderu (UID SEARCH ALL) i oparcie się wyłącznie na dedupie po Message-ID, po czym zapis nowegouidvalidity. To zdarza się rzadko, ale gdy się zdarzy i nie obsłużymy — tracimy maile bez śladu. - W przeciwnym razie
UID FETCH {last_uid+1}:*. - Zapis
last_uiddopiero po tym, jak koperta i plik.emlsą trwale zapisane — nigdy przed. Przerwanie w środku = powtórka partii przy następnym ticku, którą dedup wyciszy.
Uwaga na wyścig: UID FETCH n:* zawsze zwraca co najmniej jedną wiadomość
(serwer zwraca ostatnią, gdy przedział jest pusty) — trzeba odfiltrować UID-y
<= last_uid po stronie klienta, inaczej licznik „nowych maili" nigdy nie
spadnie do zera i obserwowalność z §3.4 kłamie.
2.3 Mapowanie folderów
Gmail. Model etykietowy: jedna wiadomość, wiele etykiet, każda etykieta
widoczna jako osobny folder IMAP. Synchronizowanie kilku folderów oznacza
pobranie tej samej wiadomości wielokrotnie. Właściwy wybór to jeden folder
\All ([Gmail]/All Mail), który zawiera wszystko poza Spamem i Koszem —
i to jest dokładnie ta sama populacja co Takeout „All Mail", na której stoi
obecny korpus (plan §Decyzja 4: „Takeout »All Mail« nie zawiera folderu Spam").
Przyrostówka na \All jest więc ciągła merytorycznie z tym, co już jest
w bazie.
Nazwy folderów Gmaila są lokalizowane (przy polskim UI [Gmail]/Wszystkie), więc
nie wolno ich zaszywać — trzeba wybrać folder po atrybucie SPECIAL-USE
\All z odpowiedzi LIST. To samo dotyczy \Sent/\Trash.
Fastmail. Model klasyczny: INBOX, Archive, Sent, Drafts, Trash,
Spam + foldery użytkownika. Nie ma jednego worka odpowiadającego Gmailowemu
\All [do weryfikacji na żywo — czy konto wystawia wirtualny folder
obejmujący całość]. Zakres trzeba więc podać jawnie jako listę; rozsądny
domyślny zestaw to INBOX + Archive + Sent (Decyzja (e)).
Konsekwencja projektowa: konfiguracja folderów musi być per konto listą, a
nie pojedynczą nazwą, bo Gmail chce jednego wpisu, a Fastmail trzech. Tabela
z §2.2 jest już kluczowana (account, folder), więc to obsługuje.
Czego nie zapisujemy: etykiet Gmaila. entities przyjęłoby
{"type":"labels", …} bez migracji, ale nic w retrievalu ich dziś nie czyta,
a X-GM-LABELS wymaga rozszerzenia X-GM-EXT-1, które przywiązuje kod do
Google — wprost wbrew zasadzie „protokół, nie provider"
(kb-overview.md §61). Odkładam.
2.4 Dedup nowych vs istniejące — działa, z jednym ostrym rogiem
Trzy warstwy, wszystkie już w kodzie:
- Archiwum:
save_eml→FileExistsError= ten.emljuż leży. - Koperty:
ON CONFLICT (id) DO NOTHINGpo Message-ID. - Chunki:
ON CONFLICT (envelope_id, chunk_index, model) DO NOTHING- pre-fetch kluczy.
Dla ponownego pobrania tej samej wiadomości z tego samego konta to jest komplet i nic nie trzeba dokładać. Ale:
Znalezisko — kolizja Message-ID między kontami.
envelope.idto goły Message-ID, globalnie unikalny w całej tabeli, bez prefiksu źródła (inaczej niż paperless, który używapaperless:N). Wiadomość obecna i w Gmailu, i w Fastmailu (ta sama lista dyskusyjna, przekierowanie, CC na oba adresy) ma ten sam Message-ID. Kto pierwszy wstawi, ten ustalasource; drugi jest cicho pominięty przezON CONFLICT DO NOTHING. Jednocześniesave_emlzapisze dwa pliki.eml(wgmail/…ifastmail/…, bo ścieżka zawierasource), araw_refbędzie wskazywał tylko na jeden z nich.
To nie jest korupcja danych i nie blokuje niczego — treść trafia do indeksu raz, co jest zachowaniem pożądanym. Ale ma dwa mierzalne skutki, o których trzeba wiedzieć zanim ktoś zacznie się dziwić liczbom:
- statystyki „nowe maile fastmail" będą systematycznie zaniżone o część wspólną,
- filtrowanie po
sourcew retrievalu (dziś:DEFAULT_SUMMARYLESS_SOURCES) przypisze taki mail do konta, które akurat wygrało wyścig.
Rekomendacja: zostawić zachowanie, dodać licznik. Job niech raportuje
envelopes_conflict_other_source (konflikt na id, którego istniejący wiersz ma
inny source) — jedno zapytanie przy pominiętym wstawieniu, zero zmiany
semantyki, a zjawisko przestaje być niewidzialne. Zmiana klucza na
(source, message_id) jest odrzucona: łamie zamrożony kontrakt koperty,
wymagałaby przepisania 225 030 istniejących id i wszystkich raw_ref, a kupuje
duplikaty w indeksie — czyli dokładnie to, przed czym dedup chroni.
2.5 Czego jeszcze brakuje, a nie widać na pierwszy rzut oka
(i) Nowe koperty muszą mieć headers od razu. mail_body_ingest.build_prefix
(ingest.py:344) buduje prefiks chunka Temat: … | Od: … | Data: …
z entities[type=headers], a przy ich braku podstawia "(brak tematu)" i "?"
— cicho, bez błędu. Historyczne koperty mają headers tylko dlatego, że
przejechał po nich osobny backfill. Gdyby poller wstawiał koperty tak jak
bulk-import (sam manifest załączników), każdy nowy mail dostałby bezużyteczny
prefiks i nikt by tego nie zauważył poza spadkiem jakości retrievalu.
Poller musi wstawiać headers i attachment w jednym entities przy
INSERCIE, wołając gmail_header_backfill.parse_headers z jego fallbackiem.
(ii) mail_body_ingest nie umie o źródle innym niż gmail.
fetch_envelopes ma zaszyte WHERE source = 'gmail'. Potrzebny parametr
--source (lub --sources przyjmujące listę) — zmiana jednolinijkowa
w zapytaniu plus flaga CLI.
(iii) fastmail jest niewidoczny w domyślnym trybie wyszukiwania.
packages/kb-retrieval/src/kb_retrieval/retrieval.py:43:
DEFAULT_SUMMARYLESS_SOURCES = ("gmail",)
kb-query nie nadpisuje tej wartości (services/kb-query/app/main.py:128
nie przekazuje summaryless_sources). Ponieważ maile nie mają streszczeń, do
wyniku wchodzą wyłącznie przez gałąź „summaryless" trybu hybrid — a ta
filtruje WHERE e.source = ANY($2). Koperty fastmail wpadłyby do bazy,
zembedowały się i nie pojawiły w żadnym wyniku /search. Poprawka to jedno
słowo (("gmail", "fastmail")), ale bez niej cała gałąź fastmail jest niema.
To jest najbardziej „cicha" pułapka w całym przedsięwzięciu.
(iv) Pre-fetch kluczy chunków nie skaluje się do trybu cyklicznego.
fetch_existing_chunk_keys (ingest.py:387) robi
SELECT envelope_id, chunk_index FROM document_chunk WHERE model = $1
bez ograniczenia — dziś 389 012 wierszy. Zmierzone na żywo: ~1,0 s po
stronie serwera, ~26 MB na drucie (średnia długość id = 60 B), plus set 389 tys.
krotek w Pythonie (rzędu 60–90 MB RSS) — na PIHA, gdzie free pokazuje 2,4 GB
available. Dla jednorazowego plastra 50k to nic; dla joba odpalanego co godzinę
to marnotrawstwo rosnące razem z korpusem. Poprawka: zawęzić pre-fetch do kopert
z bieżącego zbioru roboczego (WHERE model = $1 AND envelope_id = ANY($2)).
Nieblokujące — ON CONFLICT i tak stanowi drugą linię obrony — ale należy
zrobić to razem z (ii), bo dotyka tej samej funkcji.
3. Proponowana architektura
3.1 Gdzie biegnie fetcher: PIHA, i to nie jest bliska decyzja
| Kryterium | PIHA | SOLARIA |
|---|---|---|
| Dostępność | 24/7 | wyłączana ~16 h/dobę z założenia (kb/decisions/architektura-2026-07-28.md) |
| Archiwum kanoniczne | tak | lustro do backfillu, może dryfować |
| kb-postgres | lokalnie | przez Tailscale |
| GPU / Ollama | nie | tak |
Precedens (kb-ingest.timer) |
tak | — |
Przyrostówka jest zadaniem sieciowo-, nie obliczeniowo-zależnym: pobrać ~37 maili, zapisać ~4,6 MB, wstawić 37 wierszy. Umieszczenie jej na hoście wyłączanym na noc oznaczałoby, że stan skrzynki jest odczytywany tylko wtedy, gdy ktoś akurat włączył desktop — i że archiwum kanoniczne żyje po drugiej stronie sieci od procesu, który je zapisuje. Decyzja architektoniczna repo mówi o SOLARII wprost: „nic wymagającego 24/7 nie może tu mieszkać".
Embedding zostaje na SOLARII, ale tylko jako wywołanie HTTP. Zweryfikowane
na żywo z PIHA (2026-08-06 13:56): getent hosts solaria →
100.100.231.104, GET http://solaria:11434/api/tags → HTTP 200 w 8,2 ms,
RTT ICMP 1,26 ms. Przy dobowym przyroście rzędu 60 chunków (37 maili × 1,73
chunka/mail, zmierzone: 389 012 / 225 030) i ~8–18 ms/chunk to poniżej sekundy
pracy GPU na dobę. Argument z planu §Decyzja 8 przeciwko embedowaniu z PIHA
dotyczył 271 tys. chunków backfillu i nie przenosi się na tę skalę.
Wniosek: rsync archiwum PIHA→SOLARIA nie jest przyrostówce do niczego potrzebny. Lustro na SOLARII pozostaje artefaktem backfillu; niech dryfuje albo zniknie.
3.2 Jak nowe koperty wchodzą w istniejący tor — dwa etapy, nie jeden
Odrzucam „tryb --since" jako mechanizm spinający. --since filtruje po
envelope.ts, czyli po dacie z nagłówka nadawcy. Mail dostarczony dziś z datą
sprzed tygodnia wypadnie poza okno i nie zostanie nigdy zchunkowany — ta sama
wada, co wariant (C) w §2.2, tylko przesunięta o jeden krok dalej w potoku.
--since zostaje tym, czym był: narzędziem do etapowania ręcznych runów.
Właściwym spoiwem jest kolejka wynikająca z danych: koperta bez chunków jest elementem kolejki. Zapytanie:
SELECT e.id FROM envelope e
WHERE e.source = ANY($1)
AND NOT EXISTS (SELECT 1 FROM document_chunk c WHERE c.envelope_id = e.id)
Jest samonaprawiające się (przerwany run, koperta pominięta przy padniętej
Ollamie, mail wstawiony z datą wsteczną — wszystko wraca do kolejki samo)
i nie wymaga żadnego dodatkowego stanu. To wprost wzorzec, którym kb-ingest
liczy kb_ingest_embed_backlog.
Stąd dwa niezależne kroki, celowo rozdzielone po linii „potrzebuje GPU":
kb-mail-sync.timer (PIHA, co godzinę, bez GPU)
└─ jobs/mail-imap-sync
IMAP LOGIN → SELECT folder → UID FETCH last_uid+1:*
→ save_eml() (append-only, FileExistsError = skip)
→ insert_envelope() (entities = [headers…, attachment…])
→ UPDATE mail_sync_state.last_uid
→ .prom
kb-ingest.timer (PIHA, istniejący, bramkowany probe'em Ollamy)
└─ + nowy etap: mail_body_ingest.run(sources=[...], only_unchunked=True)
→ chunk → embed (http://solaria:11434) → document_chunk + threading
Zaletą tego podziału jest to, że pobieranie nigdy nie czeka na SOLARIĘ. Maile lądują w archiwum i w kopertach co godzinę niezależnie od tego, czy desktop jest włączony; indeksowanie dogania, gdy GPU jest dostępne — dokładnie tak, jak dziś działa paperless. Nowy kod ogranicza się do klienta IMAP; reszta to wpięcie istniejących funkcji.
3.3 Harmonogram — i pułapka odziedziczona po kb-ingest.timer
Wzorzec jednostek: jobs/documents-ingest/systemd/ — .service (Type=oneshot,
User=oskar, EnvironmentFile=/opt/homelab/kb/.env) + .timer
(Persistent=true) + cienki *-run.sh, którego jedynym zadaniem jest
przekierowanie do /opt/homelab/logs/<job>/run-YYYYMMDD.log (nigdy sam
journal — lekcja z runu, który stracił 4999 wierszy do zamkniętego tmuxa).
Proponowany takt: OnCalendar=hourly dla kb-mail-sync. Uzasadnienie:
przy ~37 mailach na dobę godzinny tick pobiera 1–2 wiadomości, czyli jest
praktycznie darmowy, a jednocześnie utrzymuje świeżość KB w granicach godziny —
co ma znaczenie dla przyszłego dyspozytora (§5). Persistent=true nadgania po
reboocie PIHA.
Znalezisko —
kb-ingest.timerchodzi o 03:30, kiedy SOLARIA prawie na pewno śpi. Dowód z żywego systemu (PIHA, 2026-08-06):NEXT Fri 2026-08-07 03:30, LAST Thu 2026-08-06 03:30:03oraz zawartość/opt/homelab/state/node-exporter/kb-ingest.prom:kb_ingest_embed_skipped 1,kb_ingest_embed_backlog 0. Dzisiejszy tick pominął oba etapy embedujące, bo probe Ollamy nie odpowiedział. Dziś to nieszkodliwe: paperless nie generuje nowych chunków, więc backlog stoi na zerze iKbEmbedBacklogGrowing(próg:> 0przezfor: 72h) nigdy nie ma czego mierzyć.Dopięcie etapu mailowego do tego samego ticku zmienia to jakościowo. Wpłynie ~60 nowych chunków na dobę, embed będzie pomijany każdej nocy, backlog zacznie rosnąć monotonicznie i po 72 h alert zapali się na stałe — nie sygnalizując żadnej awarii, tylko rozjazd harmonogramu z dobowym cyklem SOLARII. Alert, który świeci zawsze, przestaje być alertem.
Do rozstrzygnięcia razem z Decyzją (d). Najtańsza poprawka: przesunąć etap
mailowy (albo cały kb-ingest) na godzinę, o której SOLARIA realnie pracuje —
w chwili tego reconu jest włączona od ~10:40, uptime 3 h 12 min. Alternatywa
bez zgadywania: uruchamiać etap indeksujący częściej (np. co 2 h) i pozwolić,
by probe Ollamy sam wybrał okno, w którym GPU odpowiada. Ta druga opcja jest
odporna na zmianę nawyków operatora i nie wymaga zgadywania, o której SOLARIA
wstaje — rekomenduję ją.
3.4 Obserwowalność — istniejący tor, bez nowych bytów
Tor jest gotowy i sprawdzony: job pisze plik .prom atomowo (tmp + rename) do
/opt/homelab/state/node-exporter/, node_exporter@PIHA go zbiera przez
textfile collector (mount /:/host:ro już to pokrywa), reguły idą do
services/fleet-prometheus/rules/, a dostawę do Telegrama robi brain-watchdog
odpytujący /api/v1/alerts. Bez Alertmanagera — tak jak kb-ingest.yml
i liveness.yml.
Proponowane metryki w /opt/homelab/state/node-exporter/kb-mail-sync.prom:
| Metryka | Typ | Znaczenie |
|---|---|---|
kb_mail_sync_last_run_timestamp |
gauge | ostatni tick (sukces lub nie) |
kb_mail_sync_last_success_timestamp |
gauge | ostatni tick bez twardego błędu |
kb_mail_sync_last_exit_code |
gauge | kod wyjścia |
kb_mail_sync_envelopes_inserted{account} |
gauge | nowe koperty w tym ticku |
kb_mail_sync_envelopes_skipped_dup{account} |
gauge | dedup po Message-ID |
kb_mail_sync_conflict_other_source{account} |
gauge | kolizja międzykontowa z §2.4 |
kb_mail_sync_last_message_ts{account} |
gauge | max(envelope.ts) dla konta |
kb_mail_sync_uidvalidity_resets{account} |
counter | ile razy serwer unieważnił numerację |
Reguły alertowe — i tu jest realna subtelność:
KbMailSyncStale:time() - kb_mail_sync_last_success_timestamp > 21600(6 h = 6 nieudanych godzinnych ticków), severity critical. To jest właściwy alert.- „Zero nowych maili przez X dni" odradzam jako alert. Zero nowych maili jest
legalnym stanem skrzynki — urlop, weekend, przeniesienie ruchu na drugie konto.
Alert na ciszę zapali się przy zdrowym systemie i zostanie wyciszony, po czym
przestanie działać wtedy, gdy będzie potrzebny. Ten sam skutek daje sygnał,
który nie ma fałszywych trafień:
kb_mail_sync_last_success_timestampodpowiada na pytanie „czy poller w ogóle działa", i to niezależnie od tego, czy cokolwiek przyszło. - Jeśli mimo to operator chce ostrzeżenie o „martwej skrzynce", właściwą metryką
jest
kb_mail_sync_last_message_ts(data najnowszego maila, nie licznik ticku) z progiem per konto i severitywarning— gmail jest kontem śmieciowym, więc dla niego kilkudniowa cisza to anomalia, a dla fastmaila niekoniecznie. Sugerowany próg gmail: 7 dni. Do rozstrzygnięcia po miesiącu obserwacji, nie z góry.
4. Decyzje operatora
(a) Gmail: IMAP + hasło aplikacji czy Gmail API / OAuth2
Rekomendacja: IMAP + hasło aplikacji.
- Zasada z
kb-overview.md§61 mówi wprost: „Protokół, nie provider. Ingest pisany przeciw standardom (JMAP, IMAP, WebDAV), nie przeciw firmie → przenośność".kb-mail-pillar.md§2 powtarza to dla Gmaila: „adapter IMAP (protokół, nie Gmail API → przenośność)". Ta decyzja jest w repo już podjęta; recon jej nie zmienia, tylko potwierdza, że nic nowego nie każe jej rewidować. - Jeden adapter obsłuży oba konta. Gmail API dałby drugi, nieprzenośny tor kodu dla konta, które sam operator opisuje jako śmieciowe (loginy, 2FA, newslettery) — najgorszy możliwy stosunek nakładu do wartości.
- OAuth2 wymaga projektu w Google Cloud, ekranu zgody, cyklu odświeżania tokenu
i obsługi jego wygaśnięcia w jobie bezobsługowym. Hasło aplikacji to jeden
ciąg znaków w pliku
.env, który już istnieje i już trzyma inne sekrety. - [do weryfikacji na żywo] Hasła aplikacji wymagają włączonego 2FA na koncie i bywają wyłączane politykami Workspace; przy koncie prywatnym powinny być dostępne. Warto sprawdzić przy okazji, czy Google nie ogłosiło wygaszenia tej metody — mój stan wiedzy sięga maja 2026 i nie jest wiarygodnym źródłem dla polityki dostawcy z sierpnia 2026. Gdyby okazało się, że hasła aplikacji odpadły, OAuth2 staje się przymusem, nie wyborem — reszta architektury się nie zmienia, wymianie podlega wyłącznie sposób uwierzytelnienia w kliencie.
- [do weryfikacji na żywo] Gmail limituje dobowy transfer IMAP (rzędu kilku GB). Przy ~1 800 zaległych maili (~225 MB) i ~4,6 MB/dobę bieżąco nie ma to znaczenia, ale gdyby ktoś kiedyś chciał zaciągnąć IMAP-em pełną historię — miałby.
(b) Fastmail: IMAP + hasło aplikacji
Rekomendacja: tak, IMAP + hasło aplikacji — i tym samym zamykamy otwartą decyzję „unifikacja adaptera".
To nie jest formalność, bo stoi w sprzeczności z tym, co repo dziś zapisuje:
kb-mail-pillar.md §2 i §7 oraz §10 planu fazy mailowej przewidują dla
Fastmaila JMAP (jobs/fastmail-poller), a kb-mail-pillar.md §9 trzyma to
jako decyzję otwartą: „jeden wspólny IMAP dla Fastmail + Gmail (mniej kodu) vs
JMAP dla Fastmail + IMAP dla Gmail (JMAP bogatszy)?". Zlecenie tego reconu
mówi „IMAP (gmail + fastmail)", czyli rozstrzyga tę decyzję na rzecz unifikacji.
Recon się z tym zgadza:
- JMAP daje synchronizację po
state— elegancką i niepotrzebną przy dwóch ticku na godzinę i 37 mailach na dobę. UIDVALIDITY/UIDNEXT rozwiązuje ten sam problem, a jest jedyną rzeczą, którą trzeba i tak zaimplementować dla Gmaila. - Jeden adapter = jeden zestaw testów, jedna klasa błędów, jedna ścieżka hardeningu 8-bitowych nagłówków. Dwa protokoły to dwa razy tyle powierzchni przy identycznym wyniku w bazie.
- Fastmail wystawia pełny IMAP i hasła aplikacji z ograniczonym zakresem (można wydać poświadczenie tylko-IMAP, bez dostępu do panelu) — [do weryfikacji na żywo], ale jeśli tak, jest to ściślejsze uprawnienie niż token JMAP.
- JMAP nie jest zamknięty na zawsze: koperta i archiwum są protokołowo obojętne, więc wymiana transportu w przyszłości nie dotyka danych.
Konsekwencja dokumentacyjna: po zatwierdzeniu trzeba poprawić
kb-mail-pillar.md §2/§7/§9 i §10 planu fazy mailowej, żeby nie zostawiać
w KB dwóch sprzecznych zapisów. Nazwy jobów z tamtych dokumentów
(jobs/fastmail-poller + jobs/gmail-imap-poller) tracą sens przy jednym
adapterze — proponuję jeden jobs/mail-imap-sync sparametryzowany kontem.
(c) Sekrety — gdzie trzymać hasła
Sprostowanie założenia. Zlecenie mówi, że
POSTGRES_PASSWORDleży dziś plaintextem wservices/*/service.yaml. Tak nie jest.service.yamlwymienia wyłącznie nazwy zmiennych (runtime.env_vars: [POSTGRES_PASSWORD]—services/kb-postgres/service.yaml:22), plikienv.examplezawierają placeholdery (POSTGRES_PASSWORD=change-me-strong-password), a.gitignoreblokuje.envi*.env. Skan wszystkich śledzonych plików YAML pod kątem wartości wyglądających na sekrety (≥12 znaków, po odsianiu placeholderów) zwrócił zero trafień. Wzorzec repo jest poprawny i to jego należy użyć, a nie zastępować.
Rekomendacja: rozszerzyć istniejący /opt/homelab/kb/.env na PIHA.
Zweryfikowane na żywo: plik istnieje, ma uprawnienia -rw------- root root,
262 bajty, i trzyma już KB_DSN, PAPERLESS_API_TOKEN oraz
ANTHROPIC_API_KEY. kb-ingest.service czyta go przez
EnvironmentFile=/opt/homelab/kb/.env, mimo że sam biegnie jako User=oskar —
systemd wczytuje EnvironmentFile jako root, przed zrzuceniem uprawnień.
Efekt: sekret jest wstrzykiwany do procesu, ale nie jest czytelny dla
użytkownika oskar w spoczynku. To jest lepsza własność, niż dałby plik
600 oskar:oskar, i warto ją zachować świadomie, a nie przypadkiem.
Dokładamy cztery klucze:
MAIL_GMAIL_USER=…
MAIL_GMAIL_APP_PASSWORD=…
MAIL_FASTMAIL_USER=…
MAIL_FASTMAIL_APP_PASSWORD=…
Repo dostaje wyłącznie jobs/mail-imap-sync/env.example z placeholderami —
dokładnie jak services/*/env.example.
Zasady wykonawcze, wszystkie wyprowadzone z tego, co już się w tym repo wydarzyło:
- Nigdy w argv.
--dsn <hasło>trafia dopsi do historii powłoki. Istniejące joby dopuszczają--dsn, ale honorują teżKB_DSNz env — nowy job niech przyjmuje poświadczenia tylko ze środowiska, bez odpowiednika--password. - Nigdy do transkryptu. W repo są dwa udokumentowane przypadki rotacji
po wycieku do transkryptu sesji: token Paperless (
docs/sessions/2026-07-15.md§Krok 3) i klucz Anthropic (docs/sessions/2026-07-21.md, incydent 3). To nie jest hipotetyczne ryzyko — to jedyny sposób, w jaki sekrety w tym homelabie dotąd wyciekały. Instalacja hasła musi się odbyć bez wypisywania go na ekran. - Hasła aplikacji, nie hasła kont — odwoływalne pojedynczo, bez dostępu do ustawień konta i bez omijania 2FA.
- Bez własnego szyfrowania. SOPS/age/vault to nowy byt operacyjny (klucze, dystrybucja, odtwarzanie po awarii) dla jednego pliku na jednym hoście. Nieproporcjonalne, i wprost sprzeczne z „użyj istniejących mechanizmów".
Odrzucone: sekrety w hosts/<node>/runtime/… (śledzone w Gicie);
docker secret (wymaga Swarma, którego repo świadomie nie używa —
patrz konwencja mem_limit w CLAUDE.md); osobny plik per job (rozdrabnia to,
co już jest scentralizowane i poprawnie uprawnione).
(d) Host schedulera
Rekomendacja: PIHA — pełne uzasadnienie w §3.1 (24/7, archiwum kanoniczne,
lokalny kb-postgres, istniejący precedens kb-ingest.timer, decyzja
architektoniczna zakazująca stawiania na SOLARII czegokolwiek, co wymaga 24/7).
Do rozstrzygnięcia razem z tym: takt indeksowania wobec dobowego cyklu
SOLARII (§3.3). Rekomendacja: pobieranie co godzinę (kb-mail-sync, bez GPU),
indeksowanie częściej niż raz na dobę i bramkowane probe'em Ollamy, żeby
któryś tick trafił w okno pracy desktopu. Bez tego backlog embedów rośnie
każdej nocy i KbEmbedBacklogGrowing zapala się na stałe.
(e) Zakres folderów per konto
Rekomendacja:
| Konto | Foldery | Uzasadnienie |
|---|---|---|
| gmail | wyłącznie \All (SPECIAL-USE, nie nazwa) |
jedna wiadomość = jeden fetch mimo wielu etykiet; ta sama populacja co Takeout, na którym stoi 225 030 istniejących kopert → ciągłość korpusu |
| fastmail | INBOX + Archive + Sent |
brak odpowiednika \All; te trzy pokrywają korespondencję prowadzoną i zarchiwizowaną |
Wyłączone z obu kont: Spam, Trash, Drafts. Spam i Trash to zdefiniowany
szum, a ich włączenie zmieniłoby populację względem tego, co już jest w bazie
(plan §Decyzja 4 odnotowuje, że Takeout „All Mail" nie zawiera Spamu). Drafty
nie są korespondencją — nie mają stabilnego Message-ID i mutują.
Zakres musi być konfigurowalny listą per konto (MAIL_<ACCOUNT>_FOLDERS),
bo tabela stanu jest kluczowana (account, folder). Rozszerzenie o kolejny
folder jest wtedy zmianą konfiguracji, nie kodu, a dedup po Message-ID
gwarantuje, że dołożenie folderu z częściowo pokrywającą się zawartością niczego
nie zduplikuje.
Osobne pytanie, którego nie rozstrzygam za operatora: historia Fastmaila.
Konto ma zawartość sprzed dziś, a przyrostówka domyślnie zaczyna od
„od teraz" (last_uid = UIDNEXT-1 przy pierwszym SELECT). Dwie opcje:
- (e1) Tylko nowe. Pierwszy tick zapisuje
last_uidi nie pobiera nic wstecz. Najprostsze, natychmiastowe, historia zostaje poza KB. - (e2) Pełny zaciąg. Pierwszy przebieg z
UID SEARCH ALLściąga całą zawartość wybranych folderów. Koszt zależy od rozmiaru skrzynki, którego nie znam — to jest dokładnie ta „decyzja otwarta: sizing", którąkb-mail-pillar.md§9 trzyma niezamkniętą od czerwca.
Rekomendacja: (e2), ale dopiero po pomiarze. Historia Fastmaila to
prawdopodobnie ta „sensowna poczta", o którą operatorowi chodziło bardziej niż
o gmailowe newslettery — a mechanizm jest ten sam co dla przyrostu, więc nie
kosztuje osobnego kodu. Warunek: najpierw jedno bezpieczne zapytanie
(SELECT folderu + STATUS (MESSAGES)), które poda liczbę wiadomości, i
dopiero na tej liczbie decyzja. Wykonalne w minutę po postawieniu klienta —
nie ma sensu zgadywać teraz.
(f) Nowa decyzja, której zlecenie nie wymieniało: model stanu synca
Wypływa z §2.2 i wymaga zgody, bo dokłada migrację 005 do bazy, o której
faza mailowa deklarowała „zero migracji".
Rekomendacja: tabela mail_sync_state w kb-postgres (wariant A), nie plik
w /opt/homelab/state/. Powód rozstrzygający: stan synca i dane, którym
odpowiada, muszą się odtwarzać razem. Plik stanu przeżywający restore bazy
sprawia, że przyrostówka cicho przeskakuje wszystko między odtworzonym
stanem a bieżącym last_uid — awaria bez objawów, wykrywalna dopiero przy
zauważeniu brakujących maili miesiące później.
(g) Nowa decyzja: fastmail w domyślnym trybie wyszukiwania
Z §2.5 (iii). DEFAULT_SUMMARYLESS_SOURCES = ("gmail",) sprawia, że koperty
fastmail byłyby niewidoczne w /search mimo poprawnego zembedowania.
Rekomendacja: rozszerzyć do ("gmail", "fastmail") w tym samym commicie,
który wprowadza źródło fastmail — nie później. Rozdzielenie tych zmian tworzy
okno, w którym system wygląda na działający, a wyszukiwarka po cichu gubi całe
źródło. Zmiana jest jednowierszowa i nie ma wpływu na paperless (idzie kaskadą)
ani na inwariant startowy kb-query (sprawdza model embeddera, nie źródła).
5. Zależności z reconem multiagentowym i planem dyspozytora
Sprawdzony dokument: docs/architecture/RECON-multiagent-2026-07-27.md
już nie istnieje pod tą ścieżką — migracja OKF przeniosła go commitem
00de810 do kb/subsystems/recon-multiagent.md (type: subsystem).
Analogicznie recony-audyty poszły commitem 9f77a72 do kb/audits/
(type: audit, wymagane pole as_of) — dlatego ten dokument leży
w kb/audits/, nie w docs/architecture/.
Wprost: planu dyspozytora w tym reconie nie ma. Dokument dotyczy w całości
subsystemu A (control-plane, node-agenty, MQTT, event pipeline, topologia).
kb-query pojawia się w nim wyłącznie jako wiersz inwentarza („kb-query | piha",
linia 129) i jako rozjazd topologii (linia 463: topology.yaml pomija
kb-query, choć hosts/piha go ma). Fraza „dyspozytor" nie występuje.
Dyspozytor jest zdefiniowany gdzie indziej —
kb/decisions/architektura-2026-07-28.md: „B — do-the-work: dyspozytor zadań
(agent) + nogi KB / Home Assistant / homelab-ops. Osobny wysiłek, osobny
projekt", z PIHA jako „przyszłym domem dyspozytora subsystemu B".
Zależności twardych (blokujących) nie ma w żadną stronę. Przyrostówka pisze
do envelope/document_chunk; kb-query czyta. Kontrakt między nimi to schemat
bazy, który się nie zmienia. Są natomiast cztery realne styki, które warto
mieć zapisane:
- Wybór hosta jest zgodny z docelową architekturą. Dyspozytor ma zamieszkać na PIHA; przyrostówka też tam trafia. Świeżość korpusu i jego konsument będą w tym samym miejscu, bez nowych przeskoków sieciowych.
- Świeżość jest warunkiem sensowności dyspozytora. Agent odpowiadający na „co pisał X w zeszłym tygodniu" na korpusie urwanym 2026-06-19 zwróci pewną siebie i nieprawdziwą odpowiedź. Przyrostówka jest tym, co zamienia KB z archiwum w źródło bieżące — i to jest jej główny związek z subsystemem B.
- Wiki-kompilat jest zablokowany wprost.
kb/phases/kb-m5-faza3.md:620: „Pełna wiki po fazie mailowej (przyrostówka) — wcześniej kompilat byłby fotografią przeszłości". To jedyna znaleziona jawna zależność „X czeka na przyrostówkę" w całym KB. - Nowy timer powiększa „shadow set" z otwartego pytania nr 5 tamtego reconu.
Recon multiagentowy wymienia
kb-ingestwśród bytów instalowanych poza GitOps-owym wykrywaniem dryfu („shadow-deploy family: stability-agent, agent-system, frigate, kb-ingest, HA config push") i stawia decyzję: co jest w zakresie detekcji dryfu.kb-mail-sync.timerdołoży się do tej samej listy. Nie blokuje to niczego i nie jest powodem do zmiany planu — ale przy rozstrzyganiu pytania 5 lista będzie o jedną pozycję dłuższa i lepiej, żeby trafiła tam świadomie niż przez przeoczenie.
Dla kb-query „API bez UI" przyrostówka nie zmienia nic po stronie
kontraktu HTTP: /search przyjmuje q i mode, a źródła są wewnętrznym
szczegółem retrievalu. Jedyny punkt styku to Decyzja (g) — bez niej nowe
źródło nie pojawi się w odpowiedziach ani przez UI, ani przez API.
6. Podsumowanie i zakres pracy
Stan wyjściowy. Korpus mailowy jest kompletny i zaindeksowany — do 2026-06-19. Od 48 dni nie przyrasta, i nie przyrośnie, bo w repo nie ma ani jednej linii kodu rozmawiającej z serwerem pocztowym. Przy zmierzonym tempie ~37 maili/dobę poza KB jest już ~1 800 wiadomości, a licznik bije dalej.
Co trzeba zbudować (szacunek: 2 sesje, w tym testy):
| # | Element | Rodzaj |
|---|---|---|
| 1 | jobs/mail-imap-sync — klient IMAP (stdlib imaplib), pętla UIDVALIDITY/UIDNEXT, wybór folderu po SPECIAL-USE |
nowy kod |
| 2 | Migracja 005_mail_sync_state.sql |
nowy plik |
| 3 | Wstawianie kopert z entities = [headers, attachment] w jednym kroku (reuse parse_headers + parse_headers_fallback) |
reuse |
| 4 | --source / --sources w mail_body_ingest.fetch_envelopes + tryb „koperty bez chunków" |
2 małe zmiany |
| 5 | Zawężenie fetch_existing_chunk_keys do zbioru roboczego |
1 mała zmiana |
| 6 | DEFAULT_SUMMARYLESS_SOURCES += "fastmail" |
1 wiersz |
| 7 | Licznik envelopes_conflict_other_source |
1 mała zmiana |
| 8 | systemd/kb-mail-sync.{service,timer} + kb-mail-sync-run.sh |
wzorzec 1:1 z documents-ingest/systemd/ |
| 9 | Metryki .prom + reguła KbMailSyncStale w services/fleet-prometheus/rules/ |
wzorzec 1:1 z kb-ingest.yml |
| 10 | env.example + rozszerzenie /opt/homelab/kb/.env na PIHA |
placeholdery w repo, wartości poza |
Punkty 4–7 to cztery drobne zmiany w istniejącym kodzie; cały właściwy nowy kod to pozycja 1. Reszta pipeline'u — archiwum, koperta, parse z hardeningiem 8-bitowym, quote-strip, chunker, batch embed z breakerem i bisekcją, tor Prometheus — jest zbudowana, przetestowana i przećwiczona na 225 tysiącach wiadomości.
Trzy rzeczy, które łatwo przeoczyć, a każda kosztuje cicho:
- Koperty bez
entities[type=headers]dostają prefiksTemat: (brak tematu) | Od: ?— bez błędu, tylko z gorszym retrievalem (§2.5 i). fastmailnieujęty wDEFAULT_SUMMARYLESS_SOURCESznika z wyników mimo poprawnego zembedowania — bez błędu (§2.5 iii).- Etap mailowy dopięty do ticku 03:30 zapali
KbEmbedBacklogGrowingna stałe, bo o tej porze SOLARIA śpi — potwierdzone dziś odczytemkb_ingest_embed_skipped 1(§3.3).
Do decyzji operatora przed startem: (a) Gmail IMAP+app password,
(b) Fastmail IMAP+app password — co domyka otwartą od czerwca decyzję
„unifikacja adaptera" i wymaga korekty zapisów o JMAP w
kb-mail-pillar.md i §10 planu, (c) sekrety w istniejącym
/opt/homelab/kb/.env (założenie zlecenia o plaintextowych hasłach w repo
jest nieprawdziwe — obecny wzorzec jest poprawny), (d) PIHA + korekta taktu
indeksowania, (e) \All dla Gmaila i INBOX+Archive+Sent dla Fastmaila,
plus osobno historia Fastmaila po pomiarze rozmiaru skrzynki, (f) stan synca
jako tabela w bazie, (g) fastmail w domyślnym trybie wyszukiwania.