homelab-codex-ws/kb/phases/kb-m5-faza4-fallback-dedup.md
oskar 0858e25c81 feat(kb): przenosiny type=phase do kb/phases/ (14 plikow, bez SPLIT)
Plany faz KB (modul 5 fazy 2/3/4/mailowa, moduly 0/2/3/4, documents-ingest,
raport fallback-dedup, eval retrieval-pilot) oraz prometheus-cutover-etap2,
subsystem-a-naprawa, okit-cloudflare.

git mv + frontmatter, tresc nietknieta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:04 +02:00

13 KiB
Raw Blame History

okf type visibility status updated links
0.1 phase private active 2026-07-30

Raport dedup: fallback embed SOLARIA→PIHA — e7625cd (master) vs 3d4ee38 (task/kb-f4-fallback)

Data: 2026-07-30 · Worktree: task/kb-fallback-dedup · Status: salvage wykonany 2026-07-30 — operator zatwierdził pełną listę S1S4 (sekcja „Rekomendacja zbiorcza"); zrealizowane na tym branchu: S1 (cherry-pick --transport http + README), S2 (session log 2026-07-27 z dopiskiem redakcyjnym), S3 (testy T1/T2/T3 w test_embed_router.py; T4 pominięty zgodnie z raportem), S4 (wynik kalibracji w override + README ollama-piha). Weryfikacja: pytest kb-query 42/42 PASS; retrieval_eval.py --transport http przepuszczony end-to-end przeciwko stubowi /search (raport + werdykt bramki generują się poprawnie; kryterium 4 na stubie nie przechodzi wyłącznie dlatego, że mail_queries w queries.yaml mają expected_envelope: null — placeholdery, artefakt danych, nie kodu).

Dwie równoległe sesje zaimplementowały ten sam krok planu (moduł 5 faza 4, §2 decyzja 2 / §5):

  • e7625cd (2026-07-29, ZMERGOWANY do master, wdrożony na PIHA): app/embed_router.py
    • test_embed_router.py + services/ollama-piha + README z testami A/B/C.
  • 3d4ee38 (2026-07-27, origin/task/kb-f4-fallback, NIEZMERGOWANY): app/fallback.py
    • test_fallback.py + własny services/ollama-piha + retrieval_eval.py --transport http
    • session log z kalibracją live.

Master = źródło prawdy. 3d4ee38 nie będzie mergowany — ten raport klasyfikuje jego deltę i rekomenduje, co uratować.


1. Porównanie maszyn stanów (embed_router.py vs fallback.py)

Werdykt: embed_router.py na masterze pokrywa 100% zachowań fallback.py i dodaje kilka istotnych rzeczy ponad to. Nic nie zgubił.

Zachowanie (wymóg planu §2 D2 / zadania) fallback.py (branch) embed_router.py (master)
Health-check cache 30 s (probe GET /api/tags po wygaśnięciu) SolCircuitBreaker, TTL 30 s TTL 30 s (env EMBED_HEALTH_TTL_S)
Timeout probe'a 0.5 s (stała) 1.5 s (env; udokumentowane: absorbuje jitter Tailscale, spec zadania mówił „12 s")
Twardy timeout 3 s na nodze SOLARIA ClientTimeout(total=3) przez nowy param embed_chunk(timeout_s=...) asyncio.wait_for(3 s) wokół niezmienionego embed_chunkbez ruszania pakietu kb-retrieval; obejmuje też parsowanie odpowiedzi
One-shot switch (fail mid-embed → ten sam request z PIHA, breaker „down" na okno TTL) + łapie dodatkowo ValueError (brak embedding w odpowiedzi) i loguje circuit open for 30s
Noga PIHA bez twardego timeoutu (cold-load, ostatnia deska) (celowe, udokumentowane w docstringu)
Powrót na SOLARIĘ ≤ 30 s po wygaśnięciu TTL
Obsługa 503 (oba backendy padnięte → 503, nie gołe 500) ⚠️ częściowa: tylko aiohttp.ClientError → 503 w main.py; TimeoutError z nogi PIHA wyleciałby jako gołe 500 dedykowany EmbedBackendError → 503; osobno pokryty przypadek „fallback nieskonfigurowany"
Inwariant embed-model (query model == document_chunk.model) startowy DB-check + jedna stała embed_model na obie nogi (dowód strukturalny w teście) to samo plus leniwa weryfikacja per-backend przy pierwszym użyciu: /api/tags backendu musi listować EMBED_MODEL, inaczej ModelMismatchError → głośne 500 (nigdy cichy embed w złej przestrzeni wektorowej)
/healthz dzieli cache z /search sol_status sol_status + dodatkowo fallback_status (up/down/unconfigured)
embed_backend w odpowiedzi /search (tylko sol_status) (potrzebne do debugowania jakości per backend)
Praca bez fallbacku (env nieustawiony) placeholder http://localhost:11434 (fail-closed, ale mylący sol_status) jawny tryb EMBED_FALLBACK_URL unset → zachowanie sprzed Kroku 2 (503)
Parametryzacja (TTL/timeouty) stałe modułowe env vars z defaultami
Logging decyzji routingu (kb-query.embed, backend=…, elapsed_ms)

Różnice czysto kosmetyczne: semantyka wygaśnięcia dokładnie na granicy TTL identyczna (obie strony: t == TTL → wygasłe); branch trzymał breaker jako dataclass z injektowalnym zegarem, master trzyma to samo wewnątrz EmbedRouter (też injektowalny zegar).


2. Pełna delta 3d4ee38 (git diff origin/master...origin/task/kb-f4-fallback) + klasyfikacja

Plik Rozmiar delty Klasyfikacja Rekomendacja Uzasadnienie
services/kb-query/app/fallback.py +113 duplikat funkcji porzuć embed_router.py = nadzbiór (tabela §1)
services/kb-query/app/main.py ±32 duplikat porzuć master ma własną, pełniejszą integrację (mapowanie 503/500, env-schemat EMBED_PRIMARY/FALLBACK_URL)
services/kb-query/app/search.py ±36 duplikat porzuć identyczny pomysł (embed raz → *_retrieve z gotowym wektorem); master dodaje embed_backend
services/kb-query/tests/test_fallback.py +221 duplikat + 3 unikalne scenariusze adaptuj luki testowe do przeniesienia do test_embed_router.py — patrz §3
services/kb-query/tests/test_search.py ±38 duplikat porzuć master zaadaptował te same testy przez _FakeRouter (czystsze — nie dubluje maszyny stanów w testach searcha)
services/kb-query/{README,env.example,service.yaml,docker-compose.yml} ~±58 duplikat porzuć inny (porzucony) schemat env OLLAMA_PIHA_URL; master bogatszy (testy A/B/C, rename OLLAMA_URLEMBED_PRIMARY_URL)
packages/kb-retrieval/src/kb_retrieval/embed.py (timeout_s w embed_chunk) +16 duplikat funkcji porzuć master osiąga twardy timeout przez asyncio.wait_for bez zmiany współdzielonego pakietu — mniejsza powierzchnia zmian, ten sam efekt
jobs/documents-ingest/eval/retrieval_eval.py (--transport {direct,http} + --base-url) +109 unikalna wartość cherry-pick plan §2 decyzja 6 / §9 (bramka HTTP-equivalence) — na masterze w ogóle nie istnieje; e7625cd nie tknął tego pliku, patch aplikuje się czysto; kod woła tylko GET /search i czyta envelope_id/dist/source — w pełni zgodny z odpowiedzią mastera
jobs/documents-ingest/README.md +6 unikalna wartość cherry-pick dokumentacja powyższego, idzie w parze
docs/sessions/2026-07-27-kb-f4-fallback.md +189 unikalna wartość adaptuj jedyny zapis: (1) znalezisko osieroconego natywnego ollama.service na PIHA + jego wyłączenie 2026-07-27 i backlog odinstalowania, (2) kalibracja live ollama-piha (GO: peak ~983 MiB, ~4.25.3 s/embed), (3) metodologia i wyniki bramki §9 (HTTP-equivalence 0 rozbieżności; sol-down Δ~3e-4), (4) rsync-deploy → dirty working tree na PIHA. Wciągnąć z dopiskiem redakcyjnym, że zmergowana implementacja to inny kod (e7625cd) i wyniki bramki wymagają powtórki
services/ollama-piha/* (5 plików) +155 duplikat porzuć wersja mastera lepsza: named volume ollama_piha_models (uzasadnienie uid-pattern PIHA), healthcheck sprawdza obecność bge-m3, bind tylko 127.0.0.1+LAN
hosts/piha/runtime/ollama-piha/docker-compose.override.yml +13 duplikat + 1 unikalny fakt adaptuj (mikro) ten sam mem_limit: 2560m; ale komentarz brancha zawiera potwierdzony pomiar (peak ~983 MiB), a master wciąż mówi „Confirm/trim after live calibration" — dopisać wynik kalibracji do komentarza override'u i/lub sekcji „Calibration" w services/ollama-piha/README.md
hosts/piha/services.yaml ±26 duplikat porzuć master ma własny wpis ollama-piha + soft-dependency kb-query; drobna różnica (offline_required: true na branchu vs false na masterze) — master źródłem prawdy

3. Pokrycie testowe — test_embed_router.py (13 testów) vs test_fallback.py (16 testów)

Scenariusze wspólne (pokryte po obu stronach): embed na SOLARIA gdy up; cache werdyktu w TTL (bez re-probe); down z probe'a → PIHA bez próby embedu na SOLARIA; down cache'owany (kolejne requesty omijają SOLARIĘ bez probe'a); one-shot same-request switch (wariant connection-error); powrót na SOLARIĘ ≤30 s (test C — po wygaśnięciu TTL); oba backendy padnięte → wyjątek; obie nogi z identycznym embed_model (na masterze mocniej: weryfikacja /api/tags per backend).

Master ma ponadto testy, których branch nie miał: ModelMismatchError (fallback bez bge-m3 → głośny błąd, nie cichy embed; primary bez bge-m3 → failover), brak fallbacku skonfigurowanego → EmbedBackendError, fallback_status (up/unconfigured), sufiks :latest spełnia gołą nazwę modelu.

Luki mastera względem test_fallback.py (kandydaci na salvage — pozycja „adaptuj" z §2):

# Scenariusz z test_fallback.py Stan na masterze Waga
T1 One-shot switch przy timeoutcie mid-embed (nie tylko connection-error) — na masterze to inna ścieżka kodu (asyncio.wait_forTimeoutError) i jest nieprzetestowana brak wysoka — to główny scenariusz produkcyjny („SOLARIA wisi", nie „SOLARIA odrzuca")
T2 Po one-shot switchu breaker zostaje „down": kolejny request w tym samym oknie TTL idzie prosto na PIHA bez probe'a i bez próby SOLARIA brak (master testuje cache „down" tylko z probe'a, nie z mid-embed failure) średnia
T3 Noga PIHA bez twardego timeoutu (strukturalna asercja) brak (własność tylko udokumentowana) niska — opcjonalnie
T4 Wygaśnięcie cache dokładnie na granicy TTL (t == 30 s) brak (master testuje 31 s) niska — semantyka i tak identyczna, można pominąć

4. Rekomendacja zbiorcza (lista do zatwierdzenia)

  1. S1 — cherry-pick: retrieval_eval.py --transport http --base-url + akapit w jobs/documents-ingest/README.md (plan §2 D6/§9; aplikuje się czysto, zero zależności od porzuconego kodu brancha).
  2. S2 — adaptuj: docs/sessions/2026-07-27-kb-f4-fallback.mddocs/sessions/ z dopiskiem redakcyjnym na górze (implementacja z tej sesji porzucona na rzecz e7625cd; fakty operacyjne — ollama.service, kalibracja, metodologia bramki — pozostają w mocy).
  3. S3 — adaptuj: luki testowe T1 + T2 (T3 opcjonalnie) do test_embed_router.py.
  4. S4 — adaptuj (mikro): wynik kalibracji 2026-07-27 (peak ~983 MiB, ~4.25.3 s, werdykt GO) do komentarza hosts/piha/runtime/ollama-piha/docker-compose.override.yml i sekcji Calibration w services/ollama-piha/README.md — pomiar dotyczył kontenera ollama-piha (ta sama konfiguracja: obraz, OLLAMA_KEEP_ALIVE=0, mem_limit 2560m), więc przenosi się na wersję mastera; różni się tylko storage (bind vs named volume), co nie wpływa na RAM/latencję.
  5. Porzuć całą resztę delty (kolumna „porzuć" w §2).

5. Follow-up (po salvage)

  • (a) Sprzątanie brancha (wykona Oskar): skasować origin/task/kb-f4-fallback (3d4ee38) i worktree ~/homelab-codex-ws-kb-f4-fallback po zakończeniu salvage.
  • (b) Powtórka testu sol-down na żywym masterze: kalibracja i bramka z 2026-07-27 dotyczyły innego kodu (fallback.py, env OLLAMA_PIHA_URL) — na wdrożonym e7625cd trzeba przejść testy A/B/C z services/kb-query/README.md oraz bramkę retrieval_eval.py --transport http (po S1): HTTP-equivalence przy SOLARIA-up (identyczne dist) i sol-down (Δ≤epsilon, kolejność top-k identyczna; baseline z 27.07: Δ~3e-4). Symulacja wg README: EMBED_PRIMARY_URL=http://192.0.2.1:11434 (TEST-NET), nie dotykając SOLARII.
  • (c) Weryfikacja stanu PIHA po podwójnym deployu (znalezisko z session loga): sesja 27.07 wdrożyła kod brancha przez rsync do ~/homelab-codex-ws na PIHA (dirty working tree), a ollama-piha wystartował tam z bind mountem /opt/homelab/data/ollama-piha; master definiuje named volume ollama_piha_models. Sprawdzić: (1) working tree na PIHA czysty po merge'u e7625cd i kontenery faktycznie zbudowane z kodu mastera, (2) który storage żywy kontener naprawdę montuje; jeśli named volume — czy bge-m3 jest w nim spullowany, a stary katalog bind (/opt/homelab/data/ollama-piha, ~1.2 GB modelu) do skasowania, (3) .env kb-query używa EMBED_FALLBACK_URL (schemat mastera), nie martwego OLLAMA_PIHA_URL — szybki test: /healthz musi zwracać fallback_status: "up", nie "unconfigured".
  • (d) Backlog z 27.07 (przetrwa tylko dzięki S2): natywny, osierocony ollama.service na PIHA wyłączony (systemctl disable --now) 2026-07-27 — odinstalować binarkę/unit po ~2 tygodniach ciszy (≈ 2026-08-10).