Compare commits
2 commits
f43c83f218
...
a14909d921
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a14909d921 | ||
|
|
f6fcf6b97a |
291
docs/sessions/2026-08-06.md
Normal file
291
docs/sessions/2026-08-06.md
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
---
|
||||
okf: "0.1"
|
||||
type: session-log
|
||||
visibility: private
|
||||
status: active
|
||||
updated: 2026-08-06
|
||||
links: []
|
||||
---
|
||||
|
||||
# Session log 2026-08-06
|
||||
|
||||
## Session 13:20
|
||||
|
||||
Produkcyjna weryfikacja pierwszego cyklu safe-cleanup na LUSTRO (R1/R2/R3, deploy
|
||||
2026-08-05) oraz pierwszy pełny cykl HITL: approval → dispatch → wykonanie na nodzie →
|
||||
`action_result` → `completed`. Sesja supervised, checkpointy zatwierdzane przez operatora,
|
||||
approvale wykonywane wyłącznie przez operatora. Incydent źródłowy:
|
||||
`docs/incidents/2026-07-30-ollama-solaria-vanish.md`.
|
||||
|
||||
### Commits
|
||||
|
||||
Ta sesja **nie wprowadziła żadnych commitów poza niniejszym logiem** — zero zmian w kodzie
|
||||
i konfiguracji repo. Cała praca to recon read-only + kontrolowane zapisy runtime na
|
||||
LUSTRO i VPS (kanarki testowe, plik akcji), wszystkie sprzątnięte lub udokumentowane niżej.
|
||||
|
||||
Poprzedni log sesji: `67aa092 docs: session 2026-08-05 22:47` — potwierdzony na
|
||||
`origin/master` (ahead/behind 0/0) na starcie sesji.
|
||||
|
||||
### Files changed
|
||||
|
||||
Brak — poza tym plikiem.
|
||||
|
||||
---
|
||||
|
||||
## KROK 1 — safe-cleanup na LUSTRO: obie gałęzie potwierdzone produkcyjnie
|
||||
|
||||
### Stan wyjściowy
|
||||
|
||||
Marker `/opt/homelab/state/last-docker-cleanup` = `1785925612` (2026-08-05 12:26:52 CEST),
|
||||
**sprzed deployu** (obraz node-agenta utworzony 22:42:05 CEST). Bramka
|
||||
`_cleanup_rate_ok()` (`CLEANUP_INTERVAL_SECS = 86_400`) trzymała pierwszy cykl nowego kodu
|
||||
do 12:26:52 dnia 2026-08-06. Brak linii cleanup w logach do tego momentu był więc
|
||||
zachowaniem poprawnym, nie awarią.
|
||||
|
||||
Kod w runtime zweryfikowany: `md5(/app/src/node_agent.py)` == `md5(repo HEAD)` =
|
||||
`c9ac64e10b42b3e0ed9e4c168579bfaa`, brak wykonywalnego `containers.prune()`,
|
||||
`NODE_TYPE=sd_card`.
|
||||
|
||||
Pułapka interpretacyjna: logi kontenera node-agent są w **UTC**, host w CEST. Pozorna
|
||||
6-godzinna dziura w logach to nocny `halt` (root cron `30 23 * * * /usr/sbin/halt`,
|
||||
boot 06:30) — LUSTRO ma duty cycle jak SOLARIA, zgodnie z `inventory/topology.yaml`
|
||||
(`duty_cycle: nightly`).
|
||||
|
||||
### Test w oknie prune
|
||||
|
||||
Do okna przygotowano trzy kontenery `exited` pokrywające obie gałęzie filtra:
|
||||
|
||||
| Kontener | Polityka / label | Oczekiwane | Wynik |
|
||||
|---|---|---|---|
|
||||
| `prune-disposable` | `restart=no`, brak compose | usunięty | ✅ usunięty |
|
||||
| `prune-canary` | `restart=unless-stopped`, brak compose | zachowany | ✅ przeżył |
|
||||
| `node-exporter` (realny serwis) | `restart=always`, brak compose | zachowany | ✅ przeżył |
|
||||
|
||||
Log z okna (12:27:49 CEST / 10:27:49 UTC):
|
||||
|
||||
```
|
||||
INFO - Pruned dangling images (0 MB reclaimed)
|
||||
WARNING - Removed 1 disposable stopped container(s): prune-disposable (kept 2 managed)
|
||||
```
|
||||
|
||||
Marker zaktualizowany na `1786012069`. `kept 2` = `prune-canary` + `node-exporter`.
|
||||
Wszystkie kontenery produkcyjne nietknięte.
|
||||
|
||||
**Werdykt:** obie gałęzie filtra działają na produkcji — gałąź ochronna (kontener zatrzymany
|
||||
przez operatora z polityką restartu przeżywa prune; dokładny scenariusz incydentu ollamy)
|
||||
oraz gałąź usuwania (filtr nie jest no-opem, faktycznie kasuje jednorazowe resztki).
|
||||
|
||||
### Korekta wniosku z 2026-08-05
|
||||
|
||||
Wczorajszy test kanarka na SOLARII (przeżył 151 s) **nie dowodził działania filtra** —
|
||||
SOLARIA ma `NODE_TYPE=lte_node` (mitygacja M1), gdzie `run_safe_cleanup()` kończy się
|
||||
`return` przed jakimkolwiek prune. Ten test potwierdził M1, nie R1. Dowód dla R1 powstał
|
||||
dopiero dziś na LUSTRO.
|
||||
|
||||
### Stan cleanupu na flocie
|
||||
|
||||
| Node | NODE_TYPE | Cleanup |
|
||||
|---|---|---|
|
||||
| PIHA | `sd_card` | ✅ działa (2026-08-05 16:27 CEST: `kept 0 managed`) |
|
||||
| LUSTRO | `sd_card` | ✅ działa (2026-08-06 12:27:49, jw.) |
|
||||
| SOLARIA | `lte_node` (M1) | wyłączony w całości, brak markera |
|
||||
| VPS | `lte_node` (M1) | wyłączony w całości, brak markera |
|
||||
|
||||
M1 nadal zdjęte do zrobienia na SOLARII i VPS — do tego czasu te nody nie sprzątają
|
||||
Dockera wcale.
|
||||
|
||||
---
|
||||
|
||||
## KROK 2 — dlaczego crash-loop watchtowera nie generował akcji
|
||||
|
||||
Eventy płynęły poprawnie: `evt-lustro-<ts>-containers_not_running-watchtower.json` co ~60 s,
|
||||
11 317 plików w `events/lustro/` na VPS (node-agent rsyncuje z `--remove-source-files`,
|
||||
stąd pusty katalog lokalny). To nie był problem transportu ani emisji.
|
||||
|
||||
**Przyczyna:** `supervisor.reconcile()` iteruje wyłącznie po `desired_state["services"]`
|
||||
ładowanym z `hosts/<node>/services.yaml` (`supervisor.py:380`). `hosts/lustro/services.yaml`
|
||||
deklaruje `node-agent`, `node-exporter`, `piper-tts` — watchtowera tam nie ma. Brak wpisu
|
||||
w desired state ⇒ brak driftu ⇒ brak rekomendacji. Ponad 11 tys. eventów dead-enduje.
|
||||
Observer natomiast **zna** `lustro/watchtower` (incydent `inc-1786007596-lustro-watchtower`,
|
||||
status `unhealthy`) — rozjazd dotyczy wyłącznie supervisora.
|
||||
|
||||
Wykluczone: `shadow_mode` (dotyczy tylko `HA_DIAG_SHADOW_MODE`, ścieżka HA-diag),
|
||||
`duty_cycle` (tłumi wyłącznie liveness node'a), progi/cooldown (`containers_not_running`
|
||||
jest w `CONTAINER_RESTART_TRIGGERS`, dedup po stabilnym ID).
|
||||
|
||||
Konsekwencja druga: akcja wstawiona ręcznie do `pending/` dla serwisu spoza desired state
|
||||
żyje jeden cykl supervisora. `_cancel_resolved_pending_actions()` (`supervisor.py:560`)
|
||||
skasował ją po 15 s z powodem `service_removed_from_desired_state`. Kasowanie dotyczy
|
||||
**wyłącznie `pending/`** — `approved/` i `running/` są z założenia nietykalne. Dlatego
|
||||
akcja ręczna dla watchtowera trafiła ostatecznie prosto do `approved/`.
|
||||
|
||||
---
|
||||
|
||||
## KROK 3 — dwa pełne cykle HITL
|
||||
|
||||
Wszystkie znaczniki UTC (CEST = +2). Pętla executora: 10 s. Pętla node-agenta: 60 s.
|
||||
|
||||
### Cykl 1 — `node-exporter` (ścieżka w pełni organiczna)
|
||||
|
||||
| Etap | Timestamp | Δ |
|
||||
|---|---|---|
|
||||
| `docker stop node-exporter` (trigger) | 10:22:49 | — |
|
||||
| event → observer → incydent `inc-1786011821-lustro-node-exporter` | ~10:23:41 | +52 s |
|
||||
| supervisor: `Generated recommendation` → `pending/` | 10:24:00.66 | +71 s |
|
||||
| approval operatora (`mv` → `approved/`) | ~10:38:5x | — |
|
||||
| executor: `Executing action` → `running/` | 10:39:00.650 | ≤10 s |
|
||||
| executor: `Dispatched … to node-agent on lustro` | 10:39:00.657 | +7 ms |
|
||||
| node-agent: rsync-pull + bramki + `docker restart` | 10:39:23.15 | +22,5 s |
|
||||
| event `action_result` (`success: true`) | 10:39:23.293 | +0,14 s |
|
||||
| executor: `completed` | 10:39:30.752 | +7,5 s |
|
||||
|
||||
**Approval → completed: 30,1 s.**
|
||||
|
||||
### Cykl 2 — `pi-watchtower-1` (akcja utworzona ręcznie, zatwierdzona przez operatora)
|
||||
|
||||
| Etap | Timestamp | Δ |
|
||||
|---|---|---|
|
||||
| operator zapisuje akcję do `approved/` | 11:08:38 | — |
|
||||
| executor: `Executing action` → `running/` | 11:08:40.829 | +2,8 s |
|
||||
| executor: `Dispatched … (container=pi-watchtower-1)` | 11:08:40.838 | +9 ms |
|
||||
| node-agent: `Restarted container 'pi-watchtower-1'` | 11:08:51.002 | +10,2 s |
|
||||
| event `action_result` (`success: true`) | 11:08:51.002 | — |
|
||||
| executor: `completed` | 11:09:00.909 | +9,9 s |
|
||||
|
||||
**Approval → completed: 20,1 s.** Watchtower wrócił do crash-loopa — zgodnie z założeniem;
|
||||
sukcesem było przejście pipeline'u i poprawny `action_result`, nie uzdrowienie kontenera.
|
||||
|
||||
### Bramki agenta
|
||||
|
||||
- **Whitelista typu** i **node scoping** — przeszły; logują się tylko przy odrzuceniu,
|
||||
więc dowodem przejścia jest sama egzekucja.
|
||||
- **Self-restart guard** — nie dotyczył (cel ≠ `node-agent`).
|
||||
- **Idempotencja** — zadziałała na żywo i wielokrotnie:
|
||||
`Action … already processed — skipping (idempotency)`, markery
|
||||
`/opt/homelab/state/processed-actions/<action_id>.done`.
|
||||
|
||||
### Obserwacja uboczna: regeneracja i auto-cancel
|
||||
|
||||
O 10:39:48 (18 s po udanym restarcie) supervisor **wygenerował ponownie** akcję dla
|
||||
node-exportera, bo world state jeszcze pokazywał `unhealthy` (opóźnienie observera).
|
||||
O 10:40:49 sam ją skasował (`drift_resolved_auto`). Podwójnego restartu nie było, ale
|
||||
istnieje ~60-sekundowe okno, w którym po udanej remediacji potrafi powstać duplikat.
|
||||
|
||||
---
|
||||
|
||||
## KROK 4a — rekomendacja ws. poluzowania bramek HITL
|
||||
|
||||
**Rekomendacja: jeszcze nie, ale wąskie poluzowanie jest obronialne po trzech warunkach.**
|
||||
|
||||
Za:
|
||||
- Pipeline przeszedł end-to-end dwukrotnie, w tym raz w pełni organicznie (event →
|
||||
observer → supervisor → approval → executor → node-agent → wynik).
|
||||
- Czas maszynowy to 20–30 s; wąskim gardłem jest wyłącznie człowiek (dziś ~15 i ~30 min).
|
||||
- `container_restart` jest tanie i odwracalne, wykonanie jest scoped do node'a, whitelisty
|
||||
jednego typu akcji i guardu self-restartu; egzekutor nigdy nie wchodzi na node po SSH.
|
||||
- Kolejka sama się czyści: `drift_resolved_auto` kasuje akcje, które przestały być
|
||||
potrzebne, więc opóźniony approval nie powoduje zbędnego restartu.
|
||||
- Idempotencja obroniła się w warunkach bojowych (patrz defekt dispatch niżej).
|
||||
|
||||
Przeciw:
|
||||
- Próbka: 2 wykonania, 1 node, 1 typ akcji, obie ścieżki udane. **Ani razu nie zaobserwowano
|
||||
ścieżki porażki** (`success: false`), timeoutu akcji w `running/`, ani odrzucenia przez
|
||||
bramkę node/whitelisty. Dowód dotyczy szczęśliwej ścieżki.
|
||||
- Restart nie leczy przyczyn źródłowych. Watchtower ma 1000+ restartów dziennie — automat
|
||||
restartowałby go w kółko, maskując problem. Bez budżetu restartów (np. max 3/24 h na
|
||||
serwis, potem eskalacja do `alert_only`) auto-remediacja produkuje pętlę zamiast naprawy.
|
||||
- Otwarty defekt dispatch (niżej) w trybie automatycznym oznacza, że jedynym zabezpieczeniem
|
||||
przed powtórnym wykonaniem jest marker idempotencji per `action_id`. Wystarczy nowy
|
||||
`action_id` na ten sam objaw, by restart poszedł ponownie.
|
||||
- Okno duplikatu (~60 s) po udanej remediacji — dziś skasowane w porę, ale to kwestia
|
||||
wyścigu, nie gwarancji.
|
||||
- `_get_container_name()` po cichu zwraca nazwę serwisu, gdy brak `services/<svc>/docker-compose.yml`.
|
||||
Dla watchtowera dałoby to `watchtower` zamiast `pi-watchtower-1` — akcja wygenerowana
|
||||
organicznie zakończyłaby się `failed`. W trybie automatycznym to stały szum porażek.
|
||||
|
||||
Warunki wstępne do poluzowania:
|
||||
1. Naprawa wycieku plików dispatch (niżej) — inaczej automat stoi na jednej bramce.
|
||||
2. Budżet restartów per serwis + eskalacja do `alert_only` po jego wyczerpaniu.
|
||||
3. Poluzowanie tylko dla `container_restart` i tylko dla serwisów obecnych w desired state;
|
||||
`redeploy` i `disk_cleanup` zostają w pełnym HITL.
|
||||
|
||||
---
|
||||
|
||||
## KROK 4b — root cause crash-loopa watchtowera
|
||||
|
||||
Log kontenera, każde uruchomienie:
|
||||
|
||||
```
|
||||
level=error msg="Error response from daemon: client version 1.25 is too old.
|
||||
Minimum supported API version is 1.40, please upgrade your client to a newer version"
|
||||
```
|
||||
|
||||
`pi-watchtower-1` (`containrrr/watchtower`, obraz `c352868a1654`, kontener utworzony
|
||||
2025-04-15) rozmawia z socketem Dockera przez API 1.25. Demon na LUSTRO wymaga minimum
|
||||
1.40 i odrzuca połączenie, watchtower kończy się `exit 1`, `restart=always` uruchamia go
|
||||
ponownie — cykl ~60 s. Licznik restartów kasuje się przy nocnym `halt`/boot, stąd
|
||||
„973 restarty" to dorobek jednego dnia pracy, a nie narastająca awaria.
|
||||
|
||||
Co by go naprawiło (do backlogu, **nie wykonane w tej sesji**):
|
||||
1. `docker pull containrrr/watchtower:latest` + recreate — aktualne wydania negocjują
|
||||
nowsze API. Najprostsze.
|
||||
2. Obejście: `DOCKER_API_VERSION=1.41` w env kontenera.
|
||||
3. **Preferowane:** usunąć watchtowera z LUSTRO. To relikt spoza GitOps, a automatyczne
|
||||
podmienianie obrazów na edge'owym Pi kłóci się z modelem repo jako źródła prawdy.
|
||||
Jeśli ma zostać — dopisać go do `hosts/lustro/services.yaml`, bo dopiero wtedy stanie
|
||||
się widoczny dla supervisora.
|
||||
|
||||
Efekt uboczny do rozważenia niezależnie: watchtower generuje ~1440 eventów/dobę, które
|
||||
nigdzie nie prowadzą, i jest głównym powodem, dla którego `events/lustro/` ma 11 tys. plików.
|
||||
|
||||
---
|
||||
|
||||
## Follow-upy
|
||||
|
||||
1. **Wyciek plików dispatch (nowy defekt, potwierdzony).** Executor tworzy
|
||||
`actions/dispatch/<node>/` z uprawnieniami **755** (`aerbot:aerbot`), a rsync-pull leci
|
||||
jako `oskar` (grupa `aerbot`) — brak prawa zapisu w katalogu, więc
|
||||
`--remove-source-files` nie kasuje źródła. Dla porównania `dispatch/piha` ma 775.
|
||||
Dodatkowo rsync zwraca wtedy kod 23, który node-agent traktuje jako benign
|
||||
(`returncode not in (0, 23, 24)`) → **cicha porażka, zero ostrzeżeń**. Skutek: LUSTRO
|
||||
re-pulluje te same akcje co 60 s i odbija się od bramki idempotencji — w nieskończoność.
|
||||
Docstring `pull_dispatched_actions()` twierdzi, że plik jest kasowany po pobraniu; nie jest.
|
||||
Fix: `mkdir(mode=0o775)` w executorze + osobna obsługa rc=23 przy `--remove-source-files`.
|
||||
Do czasu naprawy na LUSTRO trwa zombie re-pull dwóch plików co 60 s.
|
||||
2. `_get_container_name()` — cichy fallback na nazwę serwisu przy braku
|
||||
`services/<svc>/docker-compose.yml`; produkuje akcje celujące w nieistniejące kontenery.
|
||||
3. Okno ~60 s, w którym po udanej remediacji powstaje duplikat akcji (opóźnienie observera).
|
||||
4. Root cause watchtowera — patrz KROK 4b.
|
||||
5. Zdjęcie M1 (`NODE_TYPE=lte_node`) na SOLARII i VPS — do tego czasu zero cleanupu Dockera
|
||||
na obu nodach.
|
||||
6. 17 zwietrzałych akcji w `pending/` z czerwca i lipca (16× `alert-*`, `redeploy-vps-gokapi`)
|
||||
— nikt ich nie zamyka, zaśmiecają kolejkę operatora.
|
||||
7. `events/lustro/` — 11 tys. plików, rosnące głównie przez watchtowera.
|
||||
|
||||
## Pominięte / niepewne
|
||||
|
||||
- Bramki node-scoping i whitelisty typu potwierdzone **tylko pośrednio** (przez udaną
|
||||
egzekucję), bez testu negatywnego.
|
||||
- Ścieżka porażki (`action_result` z `success: false`) oraz timeout akcji w `running/`
|
||||
nie zostały przetestowane.
|
||||
- Gałąź prune obrazów wykonała się na zerze — `0 MB reclaimed` przy 0 dangling images
|
||||
przed i po. Potwierdza, że kod się wykonuje, nie że potrafi cokolwiek odzyskać.
|
||||
- Wycieknięte pliki dispatch na VPS usunięte ręcznie przez operatora na koniec sesji.
|
||||
Sam defekt (uprawnienia 755 + połknięty rc=23) pozostaje — wyciek wróci przy następnej
|
||||
akcji dispatchowanej na LUSTRO.
|
||||
- Cykl HITL sprawdzony wyłącznie na LUSTRO. PIHA (jedyny inny node z aktywnym dispatch)
|
||||
nie był testowany.
|
||||
|
||||
## Sprzątanie
|
||||
|
||||
- `prune-canary` — usunięty po weryfikacji (12:29:25).
|
||||
- `prune-disposable` — usunięty przez sam cleanup, zgodnie z zamysłem testu.
|
||||
- Obraz `alpine` (ściągnięty na potrzeby kanarków) — usunięty.
|
||||
- `node-exporter` — działa, podniesiony **przez pipeline HITL**, nie ręcznie.
|
||||
- `actions/dispatch/lustro/` na VPS — opróżniony przez operatora (zombie re-pull ustał).
|
||||
- LUSTRO na koniec: `node-agent` (healthy), `node-exporter` (up), `piper-tts` (up),
|
||||
`pi-watchtower-1` (restarting — bez zmian, świadomie).
|
||||
|
||||
### Narrative
|
||||
|
||||
> _user-provided summary_
|
||||
|
|
@ -63,32 +63,86 @@ queries:
|
|||
baseline_top1_dist: 0.6210
|
||||
note: "Poprawny brak w pilocie; separacja od trafień wyraźna."
|
||||
|
||||
- id: "N2"
|
||||
text: "piaskownica plastikowa"
|
||||
# --- historia kontrolki borderline: N2 "piaskownica plastikowa" (WYCOFANA 2026-08-06) ---
|
||||
# Nie kasować: to zapis decyzji, nie komentarz do kodu. Rolę kontrolki borderline przejmuje
|
||||
# N3 poniżej, a samo zapytanie o piaskownicę żyje dalej jako pozytywne M5 w `mail_queries`.
|
||||
#
|
||||
# Pilot (kb-m5-eval-retrieval-pilot.md): kind negative_control_borderline, baseline_top1_dist
|
||||
# 0.5533, no_answer_threshold 0.50. Poprawnie na granicy "brak" -- semantycznie sąsiednie
|
||||
# dokumenty wspólnoty mieszkaniowej, nie odpowiedź na zapytanie.
|
||||
# 2026-07-23 (faza mailowa, bramka po Etapie A): po dolaniu chunków mailowych top-1 =
|
||||
# newsletter szkoły narciarskiej (Rossignol, rozmiary nart 155-181 cm, dist 0.5298) --
|
||||
# zweryfikowana treść pokazała kolizję semantyczną krótkich, liczbowych tekstów w
|
||||
# przestrzeni wektorowej, NIE realny mail o piaskownicy; korpus mailowy Etapu A takiego
|
||||
# trafienia nie zawierał (próbne zapytanie "piaskownica plac zabaw wspólnota" dało wtedy
|
||||
# dist 0.5585, miss -- odrzucone jako wpis mail_queries, ówczesne "M5"). Próg obniżono
|
||||
# wtedy z 0.55 do 0.50 właśnie z powodu tej znanej kolizji, żeby bramka nie płonęła co
|
||||
# uruchomienie na nie-problemie.
|
||||
# 2026-08-06 (zamknięcie Etapu B, pełny korpus mailowy w HNSW) -- ZWROT AKCJI: kontrolka
|
||||
# unieważniona przez wzrost korpusu, w dwóch niezależnych miejscach naraz:
|
||||
# a) "piaskownica plastikowa" spadła do 0.4924 (flat i hybrid, cascade 0.5533) na mail
|
||||
# przedszkolny "Materiały plastyczne" -- znowu kolizja leksykalna (plastikowa /
|
||||
# plastyczne), ale tym razem PONIŻEJ progu 0.50, więc kryterium 3 FAIL przy PASS
|
||||
# kryteriów 1/2/4 -- czyli bramka płonęła dokładnie na tym, przed czym próg miał
|
||||
# chronić;
|
||||
# b) odrzucone w Etapie A "piaskownica plac zabaw wspólnota" ma dziś realne odpowiedzi:
|
||||
# maile administracji wspólnoty (holc.waw.pl, 2023) "WM Targowa 2A - zabawy na terenie
|
||||
# wspólnoty" (dist 0.4562) i "WM Targowa 2A - wymiana piasku" (dist 0.4597,
|
||||
# envelope 6ab218df-ef21-dc78-d589-34f69a97aae6@holc.waw.pl).
|
||||
# Wniosek: to NIE regresja retrievalu -- korpus urósł o treść, której w Etapie A nie było,
|
||||
# a kontrolka negatywna z definicji traci ważność w chwili, gdy odpowiedź na nią wpada do
|
||||
# korpusu. Stąd przekwalifikowanie na pozytywne M5 zamiast łatania progu w dół.
|
||||
|
||||
# --- dobór następcy (kandydaci sprawdzeni na żywym kb-query 2026-08-06, --gate-n 10) ---
|
||||
# Wymóg: temat sąsiadujący z korpusem (żeby kontrolka była borderline, nie oczywista), ale
|
||||
# bez odpowiednika w Paperless ANI w mailach, dist > 0.50 we WSZYSTKICH torach.
|
||||
# Uwaga metodologiczna z tego doboru: korpus mailowy jest po Etapie B znacznie szerszy niż
|
||||
# "wspólnota / przedszkole / FLL / polisy / telco / narty" -- zawiera też archiwum zawodowe,
|
||||
# newslettery, oferty pracy, Allegro, rezerwacje hoteli, PIT, najem, judo. Odrzucone jako
|
||||
# kontrolki, bo korpus MA na nie realną odpowiedź (dist w nawiasie = min po torach):
|
||||
# kominiarski/wentylacja (0.4277, paperless:105), hotel nad morzem (0.4166), PIT-37 (0.3994),
|
||||
# judo dla dzieci (0.3469), najem mieszkania (0.3699), prawo jazdy kat. B (0.3606),
|
||||
# opony zimowe (0.4098), gwarancja AGD (0.4498), karta rowerowa (0.4340).
|
||||
# Kandydaci, którzy przeszli (flat / cascade / hybrid):
|
||||
# A. "sterylizacja kota cennik kliniki weterynaryjnej" 0.5257 / 0.5799 / 0.5257 <- AKTYWNY
|
||||
# B. "karta wędkarska pozwolenie na połów ryb" 0.5203 / 0.5223 / 0.5203
|
||||
# (odrzucony: cascade tylko 0.0223 nad progiem -- za ciasno na trwałą bramkę)
|
||||
# C. "pasieka ul pszczeli zbiór miodu" 0.5985 / 0.6318 / 0.5985
|
||||
# (odrzucony jako aktywny: za łatwy, powiela rolę N "przepis na sernik"; trzymany jako
|
||||
# zapasowy, gdyby korpus kiedyś unieważnił A tak jak unieważnił N2)
|
||||
- id: "N3"
|
||||
text: "sterylizacja kota cennik kliniki weterynaryjnej"
|
||||
kind: negative_control_borderline
|
||||
expected_envelope: null
|
||||
baseline_top1_dist: 0.5533
|
||||
baseline_top1_dist: 0.5257
|
||||
no_answer_threshold: 0.50
|
||||
note: >
|
||||
Poprawnie na granicy "brak" w pilocie -- semantycznie sąsiednie dokumenty wspólnoty
|
||||
mieszkaniowej, nie odpowiedź na zapytanie. Faza mailowa, bramka po Etapie A
|
||||
(2026-07-23): po dolaniu chunków mailowych top-1 = newsletter szkoły narciarskiej
|
||||
(Rossignol, rozmiary nart 155-181cm, dist 0.5298) -- zweryfikowana treść pokazuje, że to
|
||||
kolizja semantyczna krótkich, liczbowych tekstów w przestrzeni wektorowej, NIE realny
|
||||
mail o piaskownicy -- korpus mailowy takiego trafienia nie zawiera (próbne zapytanie
|
||||
"piaskownica plac zabaw wspólnota" dało dist 0.5585, miss; odrzucone jako mail_queries
|
||||
wpis, patrz historia sesji). Próg tej kontroli obniżony do 0.50 (z 0.55) właśnie z powodu
|
||||
tej znanej kolizji, żeby bramka nie płonęła co uruchomienie na nie-problemie.
|
||||
Następca N2 (2026-08-06, patrz historia powyżej). Temat celowo sąsiedni wobec korpusu --
|
||||
usługa domowa z językiem cennika/rozliczenia, czyli rejestr, w którym korpus jest gęsty --
|
||||
ale zwierząt domowych nie ma w nim ani w Paperless, ani w mailach. Baseline zmierzony na
|
||||
żywym kb-query (192.168.31.5:8230, --transport http, --gate-n 10), nie w pilocie: flat
|
||||
0.5257 / cascade 0.5799 / hybrid 0.5257. Top-1 we flat/hybrid to newsletter zakupowy z
|
||||
ofertami -- brak realnej odpowiedzi, czyli kontrolka działa. Próg 0.50 przeniesiony z N2
|
||||
bez zmiany (margines ~0.026 nad progiem; przy kolejnym dużym doładowaniu korpusu należy
|
||||
go zweryfikować, a nie obniżać próg -- lekcja z N2).
|
||||
|
||||
# Faza mailowa (kb/phases/kb-m5-faza-mailowa.md, §8, Krok 5) -- bramka jakościowa dla
|
||||
# treści mailowej wprowadzonej w Etapie A (ostatnie 12 miesięcy, plan §7 Krok 4). Wypełniona
|
||||
# przez operatora 2026-07-23 (5 zapytań "wiem że to mam w mailach z ostatniego roku"; M5
|
||||
# odrzucone po weryfikacji, patrz N2 powyżej i historia sesji). expected_envelope celowo null
|
||||
# dla wszystkich -- operator dostarczył treść zapytania, nie surowy Message-ID; kind: mail_hit
|
||||
# ma inną semantykę hit@3 niż `queries:` powyżej: hit iff top-3 hybrid zawiera wynik z gałęzi
|
||||
# odrzucone wtedy po weryfikacji, patrz historia N2 powyżej -- i PRZYWRÓCONE 2026-08-06 po
|
||||
# Etapie B, gdy odpowiedź na nie realnie znalazła się w korpusie). expected_envelope jest null
|
||||
# dla M1-M4 -- operator dostarczył treść zapytania, nie surowy Message-ID; kind: mail_hit ma
|
||||
# inną semantykę hit@3 niż `queries:` powyżej: hit iff top-3 hybrid zawiera wynik z gałęzi
|
||||
# mailowej (envelope.source w summaryless_sources, dziś gmail) z dist < 0.45
|
||||
# (retrieval_eval.py::mail_hit_at_3) -- identity-match na expected_envelope nie ma tu
|
||||
# zastosowania, bo nie ma czego dopasować.
|
||||
# zastosowania, bo dla M1-M4 nie ma czego dopasować.
|
||||
#
|
||||
# WYJĄTEK (2026-08-06): M5 ma znane, zweryfikowane curl-em expected_envelope. Uwaga przy
|
||||
# czytaniu raportu -- retrieval_eval.py::summarize_query_result dla kind: mail_hit NIE używa
|
||||
# expected_envelope (idzie ścieżką mail_hit_at_3, source-match), więc ten identyfikator ma
|
||||
# dziś wartość wyłącznie dokumentacyjną/audytową: mówi, który mail uznaliśmy za poprawną
|
||||
# odpowiedź, ale bramka go nie sprawdza. Jeśli kiedyś ma być egzekwowany, to zmiana w
|
||||
# retrieval_eval.py (np. identity-match gdy expected_envelope != null), nie w tym pliku.
|
||||
#
|
||||
# Format wpisu (identyczny co do pól z `queries:` powyżej, minus baseline_top1_dist -- nie było
|
||||
# pilota mailowego przed tą bramką):
|
||||
|
|
@ -118,3 +172,18 @@ mail_queries:
|
|||
kind: mail_hit
|
||||
expected_envelope: null
|
||||
note: "Wątki organizacyjne FLL 25-26."
|
||||
- id: "M5"
|
||||
text: "wymiana piasku w piaskownicy na placu zabaw wspólnoty"
|
||||
kind: mail_hit
|
||||
expected_envelope: "6ab218df-ef21-dc78-d589-34f69a97aae6@holc.waw.pl"
|
||||
note: >
|
||||
Dawna kontrolka negatywna N2, przekwalifikowana 2026-08-06 po Etapie B (pełna historia w
|
||||
komentarzu w `queries:` powyżej). Odpowiedź: mail administracji wspólnoty "WM Targowa 2A
|
||||
- wymiana piasku" (k.szrajner@holc.waw.pl, 2023-06-16, wymiana piasku w piaskownicy na
|
||||
placu zabaw osiedla 19.06). Sformułowanie to naturalniejsza wariacja operatorskiego
|
||||
"piaskownica plac zabaw wspólnota": samo "piaskownica plac zabaw wspólnota" daje dziś
|
||||
top-1 0.4562 / top-2 0.4597 -- oba NAD progiem HIT_THRESHOLD 0.45, więc mail_hit_at_3
|
||||
liczyłoby to jako miss mimo poprawnych i zweryfikowanych trafień w top-2. Wariant z
|
||||
"wymianą piasku" trafia expected_envelope na pozycji 1 z dist 0.2858 (flat i hybrid),
|
||||
z zapasem wobec progu. To zarazem jedyne mail_query z niepustym expected_envelope --
|
||||
patrz WYJĄTEK w komentarzu nagłówkowym tej sekcji (bramka go nie egzekwuje).
|
||||
|
|
|
|||
|
|
@ -217,6 +217,11 @@ def summarize_query_result(result: dict, envelope_sources: Optional[dict[str, st
|
|||
if query["kind"] == "mail_hit":
|
||||
# plan §8 Krok 5: no expected_envelope (operator gave query text, not a Message-ID) --
|
||||
# graded on mail_hit_at_3's source-match semantics instead of hit_at_3's identity match.
|
||||
# Since 2026-08-06 one mail query (M5, the requalified N2 control) *does* carry an
|
||||
# expected_envelope, verified by hand against the live index. It stays documentation
|
||||
# only: this branch deliberately keeps grading every mail_hit by source-match, so the
|
||||
# id is never silently half-enforced. Enforcing it would be a change here (identity
|
||||
# match when expected_envelope is not None), not in queries.yaml.
|
||||
flat_hit3 = None # flat never reaches summaryless (gmail) chunks -- not a meaningful axis
|
||||
hybrid_hit3 = mail_hit_at_3(hybrid_chunks, envelope_sources or {})
|
||||
else:
|
||||
|
|
|
|||
Loading…
Reference in a new issue