---
okf: "0.1"
type: phase
visibility: private
status: active
updated: 2026-07-30
links: []
---

# 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ę S1–S4 (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ł „1–2 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_chunk` — **bez 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_URL`→`EMBED_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.2–5.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_for` → `TimeoutError`) 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.md` → `docs/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.2–5.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).
