2026-08-06 13:14:02 +02:00
|
|
|
|
---
|
|
|
|
|
|
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_
|
2026-08-06 15:52:05 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Session 15:25
|
|
|
|
|
|
|
|
|
|
|
|
Wdrożenie do runtime dwóch fixów zmergowanych na `master` (supervised, checkpointy
|
|
|
|
|
|
zatwierdzane przez operatora): dispatch `0o775` + rc=23 w executorze/node-agencie
|
|
|
|
|
|
(`52eca1c`) oraz zdjęcie mitygacji M1 na SOLARII i VPS (`1bab321`). Domyka follow-upy
|
|
|
|
|
|
#1 i #5 z sesji 13:20.
|
|
|
|
|
|
|
|
|
|
|
|
### Commits
|
|
|
|
|
|
|
|
|
|
|
|
Ta sesja **nie wprowadziła żadnych commitów** poza niniejszym logiem — deploy runtime,
|
|
|
|
|
|
zero zmian w kodzie. Wdrożone commity powstały wcześniej, na branchu
|
|
|
|
|
|
`task/dispatch-perms-m1`:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
1bab321 revert(m1): zdjecie NODE_TYPE=lte_node na SOLARII i VPS po wdrozeniu R1
|
|
|
|
|
|
52eca1c fix(dispatch): inbox 0o775 + rsync rc=23 przestaje byc cichy
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
W trakcie sesji main checkout przesunął się o `75d9695 docs(recon): przyrostowka IMAP
|
|
|
|
|
|
gmail + fastmail` (druga sesja operatora, docs-only). Bez wpływu: `node_agent.py` ma to
|
|
|
|
|
|
samo `sha256 aec6cb03` w obu drzewach, więc SOLARIA — zdeployowana jeszcze z `1bab321` —
|
|
|
|
|
|
nie rozjechała się z VPS-em deployowanym z `75d9695`.
|
|
|
|
|
|
|
|
|
|
|
|
### Files changed
|
|
|
|
|
|
|
|
|
|
|
|
Brak — drzewo robocze czyste przez całą sesję, poza tym plikiem.
|
|
|
|
|
|
|
|
|
|
|
|
### Deploys
|
|
|
|
|
|
|
|
|
|
|
|
| Node | Serwis | Przed | Po | Wynik |
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
| SOLARIA | node-agent | `node_agent.py` `fee079e8`, obraz `438111e2` | `aec6cb03` == repo HEAD, obraz `bc28a30a` | OK |
|
|
|
|
|
|
| VPS | node-agent | `node_agent.py` `c21967d3`, obraz `27be875d` | `aec6cb03` == repo HEAD | OK |
|
|
|
|
|
|
| VPS | control-plane | `executor.py` `5ca0490e`, 4 obrazy z 2026-08-05 | `1c3b569f` == repo HEAD, 4 obrazy przebudowane | OK |
|
|
|
|
|
|
|
|
|
|
|
|
Mechanizm: `scripts/deploy/deploy-service.sh --build-if-needed` dla node-agenta (ta sama
|
|
|
|
|
|
ścieżka co 2026-08-05). **Nie** użyto `scripts/deploy/deploy.sh <target>`: jest to
|
|
|
|
|
|
dyspozytor Saturn-side po SSH, deployujący *cały* node — na VPS ruszyłby npm, outline,
|
|
|
|
|
|
joplin i ai-cluster, czyli daleko poza zakres, a sesja toczyła się z SOLARII (`ssh solaria`
|
|
|
|
|
|
to połączenie do samego siebie).
|
|
|
|
|
|
|
|
|
|
|
|
`NODE_TYPE` po zdjęciu M1: SOLARIA `lte_node` → **`ai_node`** (jawnie w override),
|
|
|
|
|
|
VPS `lte_node` → **linia usunięta**, `NODE_TYPE=""` z base compose → `_resolve_node_type()`
|
|
|
|
|
|
zwraca `standard`. Log startowy potwierdza jedno i drugie (`type=ai_node`, `type=standard`),
|
|
|
|
|
|
czyli przewidywanie z commita `1bab321` co do pustego stringa było trafne.
|
|
|
|
|
|
|
|
|
|
|
|
Gate testowy: **pytest niedostępny w main checkoucie** (`.venv` bez pytest,
|
|
|
|
|
|
`~/.local/bin/pytest` ma zepsuty `_pytest`). Oparto się na wyniku sprzed merge'a
|
|
|
|
|
|
(node-agent 70 passed, control-plane 173 passed) plus `docker build` obu stacków przy
|
|
|
|
|
|
deployu. Follow-up 15:25/#4.
|
|
|
|
|
|
|
|
|
|
|
|
### Pierwszy cykl cleanup po zdjęciu M1
|
|
|
|
|
|
|
|
|
|
|
|
Na obu nodach marker `/opt/homelab/state/last-docker-cleanup` **nie istniał** (M1 blokował
|
|
|
|
|
|
zapis od 2026-08-04), więc `_cleanup_rate_ok()` zwrócił `True` i prune poszedł w pierwszym
|
|
|
|
|
|
cyklu, ~0,5 s po starcie — zgodnie z ostrzeżeniem w `1bab321`. Dlatego kolejność w każdym
|
|
|
|
|
|
kroku była: **najpierw tagi rollback, potem deploy.**
|
|
|
|
|
|
|
|
|
|
|
|
SOLARIA:
|
|
|
|
|
|
```
|
|
|
|
|
|
INFO - node-agent starting: node=solaria type=ai_node
|
|
|
|
|
|
INFO - Pruned dangling images (0 MB reclaimed)
|
|
|
|
|
|
INFO - No disposable stopped containers (kept 0 managed)
|
|
|
|
|
|
INFO - Pruned build cache (91 MB reclaimed)
|
|
|
|
|
|
```
|
|
|
|
|
|
VPS:
|
|
|
|
|
|
```
|
|
|
|
|
|
INFO - node-agent starting: node=vps type=standard
|
|
|
|
|
|
INFO - Pruned dangling images (0 MB reclaimed)
|
|
|
|
|
|
INFO - No disposable stopped containers (kept 0 managed)
|
|
|
|
|
|
INFO - Pruned build cache (239 MB reclaimed)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**Zero ubytków kontenerów na obu nodach** — `docker ps -a` przed vs po, diff nazw pusty
|
|
|
|
|
|
(SOLARIA 9/9, VPS 24/24). `humanai-mailer` i `humanai-landing` (bez definicji w repo)
|
|
|
|
|
|
nietknięte. Tagi `rollback-*` przeżyły prune.
|
|
|
|
|
|
|
|
|
|
|
|
Dwie prognozy przedwdrożeniowe wymagały korekty — obie z tego samego powodu, że
|
|
|
|
|
|
`docker images` pokazuje **rozmiar pozorny z warstwami współdzielonymi**, a nie realny
|
|
|
|
|
|
odzysk:
|
|
|
|
|
|
|
|
|
|
|
|
* **SOLARIA, „4 dangling ≈ 553 MB":** cztery obrazy faktycznie zniknęły (35 → 32, przy
|
|
|
|
|
|
+1 nowym buildzie), ale `SpaceReclaimed` = **0 MB**. Ich warstwy są współdzielone z
|
|
|
|
|
|
control-plane i kb-query. Realny odzysk obrazów ≈ 70 MB (17,89 → 17,82 GB); z build
|
|
|
|
|
|
cache (606,1 → 510,5 MB) łącznie ≈ 165 MB.
|
|
|
|
|
|
* **VPS, „1 dangling 395 MB":** ten obraz to **żywy `outline-postgres-1`** — untagged, ale
|
|
|
|
|
|
oznaczony `U` (in use). Docker odmawia usunięcia obrazu używanego przez kontener, więc
|
|
|
|
|
|
prune go nie ruszył i **nie miał prawa ruszyć**. Realny odzysk to wyłącznie build cache
|
|
|
|
|
|
239 MB (z 250,8 MB reclaimable).
|
|
|
|
|
|
|
|
|
|
|
|
Kontener `control-plane-ui` na SOLARII stoi w stanie `created` — poza zasięgiem prune'a
|
|
|
|
|
|
podwójnie: `_prune_stopped_containers()` listuje wyłącznie `status=exited`, a kontener ma
|
|
|
|
|
|
i tak label compose oraz `restart=unless-stopped`.
|
|
|
|
|
|
|
|
|
|
|
|
### Weryfikacja fixu dispatch end-to-end
|
|
|
|
|
|
|
|
|
|
|
|
Stan wejściowy zdjęty **przed** deployem control-plane (patrz follow-up 15:25/#1 — inaczej
|
|
|
|
|
|
`deploy-local.sh` by go zatarł):
|
|
|
|
|
|
|
|
|
|
|
|
| ścieżka | mode | |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| `actions/dispatch/piha` | 775 | historycznie działał |
|
|
|
|
|
|
| `actions/dispatch/lustro` | **755** | wyciek |
|
|
|
|
|
|
| `actions/deploy` | 755 | inbox deploy-runnera, bez podkatalogów |
|
|
|
|
|
|
|
|
|
|
|
|
**Korekta do sekcji „Sprzątanie" z sesji 13:20.** Zapis „`actions/dispatch/lustro/` na VPS
|
|
|
|
|
|
— opróżniony przez operatora (zombie re-pull ustał)" **nie odzwierciedla stanu
|
|
|
|
|
|
faktycznego**: o 13:16 oba pliki (z 10:39 i 11:08) nadal leżały w źródle, a LUSTRO
|
|
|
|
|
|
re-pullowało je co 60 s aż do 13:18:23. Katalog zdrenował się dopiero w wyniku poniższego
|
|
|
|
|
|
testu — i mógł, bo dopiero wtedy miał prawa `775`.
|
|
|
|
|
|
|
|
|
|
|
|
Mechanizm potwierdzony co do joty: inbox `aerbot:aerbot 755`, a ciągnie z niego
|
|
|
|
|
|
`VPS_EVENTS_USER=oskar` — uid **1002**, tylko *członek* grupy `aerbot` (gid 1000). Grupa ma
|
|
|
|
|
|
`r-x` bez `w`, więc `--remove-source-files` nie może zrobić unlinku:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
13:15:13 INFO - Action container-restart-lustro-node-exporter already processed — skipping
|
|
|
|
|
|
13:15:13 INFO - Action container-restart-lustro-watchtower already processed — skipping
|
|
|
|
|
|
13:16:16 ... (to samo) 13:17:19 ... (to samo)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**Test HITL.** Cel zmieniony z `watchtower` na `node-exporter`: watchtower na LUSTRO jest
|
|
|
|
|
|
w crash-loopie (137 restartów, `restart=always` — root cause opisany w KROKU 4b sesji
|
|
|
|
|
|
13:20), więc nie dałby czystego sygnału. Nowe `action_id`
|
|
|
|
|
|
(`container-restart-lustro-node-exporter-dispatchfix`), bo recykling starego odbiłby się od
|
|
|
|
|
|
bramki idempotencji na LUSTRO i zawiesiłby akcję w `running` do timeoutu.
|
|
|
|
|
|
|
|
|
|
|
|
**Ścieżka: przez `approved/`, nie przez approval operatora.** Plik `pending` zapisany
|
|
|
|
|
|
13:16:56 UTC został auto-anulowany przez supervisora po **7 sekundach**
|
|
|
|
|
|
(`drift_resolved_auto` — `node-exporter` był zdrowy), zanim operator zdążył kliknąć. To
|
|
|
|
|
|
dokładnie zjawisko z sekcji „Obserwacja uboczna: regeneracja i auto-cancel" sesji 13:20,
|
|
|
|
|
|
tym razem szybsze (7 s vs 15 s). Zgodnie z ustaleniem przed testem druga kopia trafiła
|
|
|
|
|
|
prosto do `approved/`; cel testu to mechanika dispatchu, nie ścieżka approvalu. Pole
|
|
|
|
|
|
`status` w tej kopii zostało `cancelled` — bez znaczenia, executor kieruje się katalogiem,
|
|
|
|
|
|
nie polem.
|
|
|
|
|
|
|
|
|
|
|
|
Wynik — wszystkie sześć kryteriów spełnione:
|
|
|
|
|
|
|
|
|
|
|
|
1. **755 → 775** dokładnie w momencie zapisu przez executora (13:17:23) ✅
|
|
|
|
|
|
2. plik akcji **zniknął** ze źródła po pobraniu i **nie wrócił** w kolejnych cyklach ✅
|
|
|
|
|
|
3. **oba zaległe pliki zdrenowane przy okazji** — `dispatch/lustro/` całkowicie pusty ✅
|
|
|
|
|
|
4. spam `already processed — skipping` **ustał**: dwa pełne cykle (13:19:25, 13:20:28) z
|
|
|
|
|
|
zerem trafień ✅
|
|
|
|
|
|
5. akcja `completed` przez `action_result` po 70 s, `node-exporter` z nowym uptime ✅
|
|
|
|
|
|
6. zero ERROR/WARNING w executorze, observerze, supervisorze i node-agentach — poza znanym,
|
|
|
|
|
|
niezwiązanym crash-loopem watchtowera na LUSTRO ✅
|
|
|
|
|
|
|
|
|
|
|
|
`13:18:23 INFO - Restarted container 'node-exporter' for action
|
|
|
|
|
|
container-restart-lustro-node-exporter-dispatchfix`
|
|
|
|
|
|
|
|
|
|
|
|
**Czego ten test NIE dowiódł.** Druga połowa fixu — klasyfikacja rc=23 z WARNING-iem — na
|
|
|
|
|
|
LUSTRO nie pojechała: node-agent tam ma nadal `fee079e8` (ten sam sha, który SOLARIA miała
|
|
|
|
|
|
przed dzisiejszym deployem). `grep -c "rc=23"` = 0 w logach LUSTRO dlatego, że stary kod
|
|
|
|
|
|
fizycznie nie umie tego zalogować, a nie dlatego, że jest dobrze. Follow-up 15:25/#3.
|
|
|
|
|
|
|
|
|
|
|
|
### Stan tagów rollback
|
|
|
|
|
|
|
|
|
|
|
|
| Node | Tag | Image ID |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| SOLARIA | `node-agent:rollback-pre-dispatchfix` | `438111e2` |
|
|
|
|
|
|
| SOLARIA | `node-agent:rollback-pre-r1` (z 2026-08-05) | `c80a711f` |
|
|
|
|
|
|
| VPS | `node-agent:rollback-pre-dispatchfix` | `27be875d` |
|
|
|
|
|
|
| VPS | `control-plane-executor:rollback-pre-dispatchfix` | `b878d835` |
|
|
|
|
|
|
| VPS | `control-plane-observer:rollback-pre-dispatchfix` | `4b79511d` |
|
|
|
|
|
|
| VPS | `control-plane-supervisor:rollback-pre-dispatchfix` | `357b70d1` |
|
|
|
|
|
|
| VPS | `control-plane-operator-ui:rollback-pre-dispatchfix` | `e522bbaa` |
|
|
|
|
|
|
|
|
|
|
|
|
Pierwsze tagi `rollback-*` na VPS w ogóle. Otagowano wszystkie cztery obrazy control-plane,
|
|
|
|
|
|
nie tylko executora — `deploy-local.sh` przebudowuje cały stack, więc każdy potrzebuje celu
|
|
|
|
|
|
rollbacku.
|
|
|
|
|
|
|
|
|
|
|
|
### Follow-upy (sesja 15:25)
|
|
|
|
|
|
|
|
|
|
|
|
1. **`deploy-local.sh` robi rekurencyjny `chown` + `chmod` na całym `/opt/homelab`** — do
|
|
|
|
|
|
przeglądu, czy to w ogóle pożądane. Oba branche by dziś zadziałały: `sudo chown -R
|
|
|
|
|
|
1000:1000` (wyzwalacz: `events/solaria/evt-solaria-1785946768-node_health-node.json`) i
|
|
|
|
|
|
`sudo chmod -R 775` (wyzwalacze: `actions/dispatch/lustro`, `actions/deploy`). Ten drugi
|
|
|
|
|
|
ustawiłby `dispatch/lustro` na 775 z zupełnie innego powodu niż fix w executorze,
|
|
|
|
|
|
zacierając stan wejściowy testu, i nadałby 775 *plikom* w całym drzewie runtime (stąd
|
|
|
|
|
|
`-rwxrwxr-x` na heartbeatach i JSON-ach akcji). **Świadoma decyzja operatora: nie
|
|
|
|
|
|
naprawiać ręcznie.** Control-plane zdeployowano samym krokiem compose (`up -d --build
|
|
|
|
|
|
--force-recreate`) — bez sudo, więc rozjazdy chown/chmod zostały nietknięte i czekają na
|
|
|
|
|
|
przegląd. Dodatkowo `sudo` na VPS wymaga hasła, więc kanoniczna ścieżka i tak kończyłaby
|
|
|
|
|
|
się handoffem (exit 5).
|
|
|
|
|
|
2. **Lokalny control-plane na SOLARII** (executor/observer/supervisor/ui, `dispatch/solaria/`
|
|
|
|
|
|
z 2026-07-22) ma nadal executor z bugiem `0o755`. Nie jest dyspozytorem floty — log to
|
|
|
|
|
|
same `Starting executor loop` — ale to drugi, niezdeployowany egzemplarz tego samego
|
|
|
|
|
|
kodu. Osobna sesja.
|
|
|
|
|
|
3. **LUSTRO i PIHA bez dzisiejszego node-agenta.** LUSTRO: `fee079e8`. Bez fixu rc=23
|
|
|
|
|
|
nadal *połykają* nieudany unlink — dziś to nie boli, bo executor już nie tworzy złych
|
|
|
|
|
|
inboxów, ale każdy przyszły wyciek o innej przyczynie znowu będzie niewidoczny.
|
|
|
|
|
|
4. **Brak pytest w main checkoucie na SOLARII** — `deploy.sh <target>` przewróciłby się na
|
|
|
|
|
|
gate (exit 2) bez `--no-gate`. Do naprawy, zanim ktoś sięgnie po kanoniczną ścieżkę
|
|
|
|
|
|
deployu z tej maszyny.
|
|
|
|
|
|
5. **Duplikat akcji testowej** — `container-restart-lustro-node-exporter-dispatchfix.json`
|
|
|
|
|
|
leży jednocześnie w `completed/` (właściwy przebieg) i w `cancelled/` (kopia z
|
|
|
|
|
|
auto-anulowania sprzed approvalu). Pozostawione bez zmian — kosmetyka, żaden zapis
|
|
|
|
|
|
produkcyjny nie był potrzebny.
|
|
|
|
|
|
|
|
|
|
|
|
### Narrative
|
|
|
|
|
|
|
|
|
|
|
|
> _user-provided summary_
|