| 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 |
| `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) |
| `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 |
| `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 `kb/services/ollama-piha.md` |
**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