Domkniecie Kroku 7. Realizuje Decyzje (d) reconu (host schedulera + korekta
kadencji indeksowania) i doklada dokumentacje wg konwencji OKF.
Scheduler (NIEAKTYWOWANY — wlacza operator):
- jobs/mail-imap-sync/systemd/{service,timer,run.sh} — wzorzec 1:1 z kb-ingest,
OnCalendar=hourly, Persistent=true, log do pliku (nigdy sam journal).
- hosts/piha/jobs.yaml — deklaracja jednostek host-level na PIHA. Nowy plik, bo
services.yaml jest dla kontenerow (supervisor dopasowuje jego wpisy do world-state
i wpis niekontenerowy dryfowalby wiecznie jako missing_service). Nic tego pliku
nie czyta — istnieje po to, zeby "shadow-deploy family" z otwartego pytania 5
reconu multiagentowego byla spisana, a nie tylko na nodzie.
Takt indeksowania (Decyzja (d), recon §3.3):
- kb-ingest.timer: 03:30 raz na dobe -> co 2 h. O 03:30 SOLARIA prawie na pewno spi
(potwierdzone odczytem kb_ingest_embed_skipped 1 z 2026-08-06), a tick dostaje
teraz etap mailowy: ~60 nowych chunkow na dobe pomijanych kazdej nocy sprawiloby,
ze backlog rosnie monotonicznie i KbEmbedBacklogGrowing zapala sie NA STALE.
Co 2 h zamiast stalej godziny — probe Ollamy sam wybiera okno, wiec ktorys tick
w nie trafi niezaleznie od nawykow operatora.
- cyclic_ingest: etap mailowy (mail_body_ingest --only-unchunked), import miekki,
wiec venv bez tego pakietu pomija etap zamiast wywracac wrapper. Predykat bledu
JEST luzniejszy niz wlasne main() tamtego joba i to jedyne takie miejsce w tym
wrapperze: pojedynczy trwale nieparsowalny mail nie moze zamrozic
last_success_timestamp i zapalic KbIngestStale na zawsze. Bledy per-mail sa
publikowane jako kb_ingest_mail_parse_errors, nie chowane.
Obserwowalnosc: KbMailSyncStale (6 h bez udanego ticku). Alert na cisze w skrzynce
ODRZUCONY (decyzja operatora, zgodna z reconem §3.4) — zero nowych maili to legalny
stan skrzynki, a alert zapalajacy sie na zdrowym systemie zostaje wyciszony
i przestaje dzialac wtedy, gdy jest potrzebny.
Dokumentacja:
- kb/services/job-mail-imap-sync.md (OKF), kb/runbooks/mail-sync-run.md — 9 krokow
pierwszego uruchomienia, w tym checklista 4 punktow [do weryfikacji na zywo]
z reconu (polityki dostawcow — do sprawdzenia, nie do zgadniecia) oraz pomiar
STATUS (MESSAGES) na Fastmailu, na ktorym zapada ODLOZONA decyzja o historii.
- kb-mail-pillar.md: KOREKTA JMAP -> IMAP dla Fastmaila jako decyzja 2026-08-06;
stary zapis zostaje jako historia z data. Zamkniete "unifikacja adaptera"
i "sizing Gmaila"; otwarte zostaje "sizing Fastmaila" — celowo, bo rozstrzyga
je pomiar, nie dyskusja.
- kb-m5-faza-mailowa.md: Krok 7 IN PROGRESS + tabela zakresu wdrozonego,
kb-m5-faza3.md: korekta harmonogramu i sekwencji wrappera,
pkg-kb-mail.md: rozpisany ze stubu, kb-postgres.md: lista migracji + 005.
Testy: 642 passed (calosc kb-mail, kb-retrieval i jobs). systemd-analyze verify
na timerze przechodzi, OnCalendar=0/2:00:00 normalizuje sie do co 2 h.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Eval na pelnym korpusie 2026-08-06 (187 025 zembedowanych chunkow mailowych
w HNSW) dal PASS: kryterium 1 (regresja paperless) bez degradacji zadnego
istniejacego hitu we flat ani w hybrid, mailowe hit@3 5/5. Koszt hybrydy to
jedno dodatkowe zapytanie SQL na wyszukiwanie. Surowe wyniki:
eval-http-2026-08-06.json / eval-direct-2026-08-06.json w ~/kb/mail/ingest-logs
na PIHA (niecommitowane, artefakt runu).
- app/main.py: Query("cascade") -> Query("hybrid"); pattern bez zmian, wiec
jawne ?mode=cascade i ?mode=flat dzialaja dokladnie jak dotad.
- app/static/app.js: przy odznaczonym "tryb flat (debug)" UI nie wysyla juz
parametru mode w ogole -- dziedziczy default API. Default zdefiniowany
w jednym miejscu (serwer), nie zduplikowany w JS.
- testy: nowa klasa TestSearchEndpointModeDefault (TestClient bez lifespan,
fake pool/router) sprawdza kontrakt HTTP -- brak mode => tor hybrid
(weryfikowany po obecnosci koperty gmail osiagalnej wylacznie galezia
hybrid, nie po samej etykiecie), jawne mode=flat / mode=cascade => stare
tory, nieznany mode => 422. Frontend: buildSearchUrl pomija mode gdy brak.
- docs: kb/services/kb-query.md (tabela trybow + endpoint + przyklad
odpowiedzi + opis przelacznika w UI), env.example/service.yaml (komentarze
SUMMARY_MODEL; default mode nie jest konfigurowalny przez env),
kb/phases/kb-m5-faza-mailowa.md (DoD (d) SPELNIONE 2026-08-06 + wzmianki
w Kroku 3, Wyniku bramki, decyzjach Etapu B i tabeli planu).
Weryfikacja: pytest services/kb-query -> 46 passed; node --test
tests/frontend/app.test.js -> 6/6; docker build OK + smoke run (uvicorn
startuje, bez KB_DSN swiadomie konczy sie RuntimeError z env.example).
Deploy NIE wykonany -- operator wdraza z mastera na PIHA po mergu.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Batching /api/embed juz istnial (Krok 1 fazy mailowej, batch 64). Recon przed
Etapem B wykazal w torze backfillu blad blokujacy i dwie luki.
BUG (blokujacy dla Etapu B): flush_embed_buffer lapal wylacznie
aiohttp.ClientError, a wyczerpanie ClientTimeout(total=...) rzuca goly
builtins.TimeoutError, ktory NIE jest jego podklasa (zweryfikowane empirycznie
na aiohttp 3.14.3). Zawieszona Ollama — czyli jej udokumentowany failure mode,
"przyjmuje polaczenie i milczy" — wywalala caly run nieobsluzonym wyjatkiem,
bez breakera i bez flushu threadingu. Na plastrze 50k = utrata zarobionej pracy.
Klasy przejsciowe nazwane teraz jawnie w TRANSIENT_EMBED_ERRORS.
kb-retrieval:
- embed_batch(timeout_s=...) — bound per zadanie, skalowalny z batch size
- embed_batch_resilient() — retry z backoffem wykladniczym, a po ich wyczerpaniu
probe /api/tags rozstrzyga: backend zywy -> bisekcja izolujaca trujacy chunk
(jeden zly tekst kosztowal caly batch 64, bo /api/embed jest all-or-nothing);
backend martwy -> natychmiastowe gave_up bez bisekcji, ktora spalilaby 2n-1
zadan i opoznila breaker. EmbeddingDimensionError nigdy nie jest retry'owane.
- failed_indices wyprowadzane z wyniku, nie akumulowane per span — przy gave_up
w srodku bisekcji porzucone poddrzewo nigdy nie dochodzi do liscia.
mail-body-ingest:
- breaker liczy give-upy (backend padl), nie dowolne nieudane batche; porazka
czesciowa przy zywym backendzie nie przesuwa licznika, bo te chunki i tak
zlapie kolejny run przez idempotencje
- wiersze zembedowane w umierajacym batchu sa commitowane przed abortem
- parametryzacja: --batch-size/--embed-retries/--embed-backoff/--embed-timeout,
kazdy z odpowiednikiem env MAIL_INGEST_*; bledna wartosc env = glosny SystemExit
- metryka embed_ms_per_chunk (porownywalna miedzy runami, w odroznieniu od
sredniej per batch) + embed_requests_total/embed_calls jako sygnal zdrowia
mail-body-ingest-bench: nowy entry point, sweep batch size na realnych chunkach.
Read-only (SELECT + inferencja, zero sciezki zapisu), warmup przed pomiarem, ten
sam zbior chunkow dla kazdego rozmiaru. Czyni liczby z planu §1.4 odtwarzalnymi.
Fallback SOLARIA->PIHA dla backfillu SWIADOMIE nie powstaje (potwierdzone przez
operatora): 271k chunkow x 790 ms CPU ~ 60 h na 8 GB PIHA dzielonym z HA i
Paperlessem. Wlasciwa odpowiedzia na martwy backend jest exit 2 i wznowienie
plastra. Tor online (kb-query -> embed_router) zachowuje fallback — rozdzial
torow udokumentowany w docstringu embed.py i w kb/services/.
Testy: 117 zielonych (62 job + 22 klient embed + reszta pakietow), w tym
regresja na TimeoutError, bisekcja, ograniczony koszt przy martwym backendzie
i porazka czesciowa nieprzesuwajaca breakera. Bez uruchamiania backfillu.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>