Compare commits

..

33 commits

Author SHA1 Message Date
oskar 9128530589 fix(kb): 13 pozostalych odwolan do sciezek sprzed migracji
Weryfikacja 0-odwolan z 01db57a liczyla tylko podzbior prefiksow — poza nim
zostalo 13 wskaznikow w plikach niemarkdownowych i w prozie dokumentow:

  docs/kb/modules/05-faza3-plan.md   -> kb/phases/kb-m5-faza3.md (2x systemd)
  docs/kb/modules/05-faza4-plan.md   -> kb/phases/kb-m5-faza4.md (kb-query app.js)
  docs/kb/modules/DECYZJE-*.md       -> kb/decisions/kb-dokumenty-otwarte.md
  docs/backlog.md (npm panel admina) -> kb/decisions/backlog-aktywne.md
  docs/backlog/ (uid/gid floty)      -> kb/decisions/backlog-uid-gid-flota.md
  docs/kb/modules/0X-*.md            -> kb/phases/kb-m*.md
  docs/incidents/2026-07-30-*.md     -> kb/incidents/ (3x node-agent)
  docs/architecture/RECON-multi*.md  -> kb/subsystems/recon-multiagent.md
  jobs/deploy-runner/README.md       -> kb/services/job-deploy-runner.md

Wyjatek zamierzony: `docs/kb/modules/05-faza3-pilot-streszczen.md` w §11 planu
fazy 3 to nazwa artefaktu, ktory nigdy nie powstal — przepiety na docelowa
konwencje (kb/phases/kb-m5-faza3-pilot-streszczen.md), zeby przyszly plik
wyladowal w nowym drzewie, a nie w skasowanym katalogu.

Weryfikacja: skan po 67 sciezkach zmigrowanych w tej galezi (git grep -F na
kazdej) = 0 trafien poza docs/sessions (logi historyczne, celowo nietkniete);
0 martwych linkow markdown na 190 plikach; check_okf.py 190/190 ZGODNE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:02:12 +02:00
oskar 6d52452a3d chore(kb): usuniecie docs/questions.md
Stub z 2026-04-15 (65 linii) z pierwszego reconu, gdy homelab byl jednym
RPi5. Wszystkie "Unknown / needs clarification" dawno odpowiedziane przez
kb/subsystems/fleet-inventory.md i hosts/<node>/capabilities.yaml.

Kasowany, a nie migrowany, bo niesie publiczne IPv4/IPv6 i Tailscale IP
VPS-a przy zerowej wartosci merytorycznej — migracja oznaczalaby
przeniesienie tych danych do KB bez zadnego zysku.

Zero odwolan w repo. Tresc pozostaje w historii gita.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar 00a5d62c89 fix(kb): README-wskazniki dla services i hosts + wyjatek ken-legacy
Naprawa kontraktu CLAUDE.md §Service Structure (opcja b). Migracja do KB
zabrala README z katalogow serwisow i hostow, przez co 0/26 katalogow
services/ spelnialo wymagany layout. Wskazniki przywracaja nawigacje,
nie duplikujac tresci.

31 wskaznikow, jednolity format, dokladnie 5 linii:

    # <nazwa>

    <jedno zdanie opisu>

    Dokumentacja: [kb/...](../../kb/...)

Opis nie jest pisany od zera — wyciagany z kb-doca: pierwsze pelne zdanie
pierwszego akapitu (sklejane z zawinietych linii, ciete tylko tam, gdzie
backticki i nawiasy sa zbilansowane), a dla node'ow czlon tytulu H1 po
myslniku. Dla ha-mcp opis z H1, bo pierwszy akapit zaczyna sie od markera
statusu. Wiodace markery "**Status: ...**" sa zdejmowane.

26 x services/<svc>/README.md, 5 x hosts/<node>/README.md.

WYJATEK services/home-assistant/config/ken-legacy/README.md: pelne
ostrzezenie "historical archive, do not deploy" przywrocone doslownie
z historii (odzyskane z drzewa sprzed migracji) + link do kb-doca.
Ostrzezenie musi stac tam, gdzie chroni — w katalogu archiwum, nie tylko
w KB. Odwolanie do services/home-assistant/DESIGN.md przepiete na
kb/decisions/ha-configs-as-code.md + kb/incidents/2026-07-22-ha-dwie-instancje.md.

check_okf.py: POINTER_GLOBS + is_pointer() wykluczaja wskazniki ze scope'u
lintu. Wskazniki celowo NIE maja frontmattera OKF — to nawigacja, nie
dokumenty KB. Wykluczenie zapisane wprost, zeby poszerzenie SCOPE nie
zaczelo ich nagle walidowac.

Bez wskaznikow: hosts/chelsty-ha/ i hosts/lustro/ — nie maja dokumentow
w kb/nodes/ (luka odnotowana juz w reconie etapu 1). Utworzenie ich
wymagaloby napisania nowej dokumentacji, czyli wyjscia poza konwersje.

Lint: 190/190 ZGODNE. Weryfikacja 822 plikow: 0 martwych linkow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar 4658089e21 fix(kb): przepiecie wszystkich odwolan wewnetrznych po migracji
126 plikow (md, yaml, sh, py) odwolywalo sie do sciezek sprzed migracji.

  15  markdown-linkow [..](..) -> policzona sciezka WZGLEDNA wobec pliku
      odsylajacego (wczesniej czesc z nich byla repo-root-relative i nie
      rozwiazywala sie z katalogu, w ktorym lezala)
 200  odwolan tekstowych (backticki, proza, yaml, importy w kodzie)
      -> nowa sciezka repo-root-relative, zgodnie z konwencja repo
   5  linkow rodzenstwa (gole nazwy plikow, np. "](DEPLOY.md)") — dzialaly
      tylko w starym katalogu; przeliczone recznie

Objete m.in.: CLAUDE.md (scripts/onboard/README.md -> kb/runbooks/
node-onboarding-tool.md, docs/backlog.md -> kb/phases/backlog.md),
README.md, .claude/skills/, 20 session logow, kod jobow.

Ostatnie 5 odwolan pochodzi z tresci wciagnietej rebasem z origin/master
(session log 2026-07-31, override node-agenta na SOLARII, dwie pozycje
backlogu) — wskazywaly na docs/incidents/, docs/kb/modules/ i
services/narty27/README.md sprzed migracji.

Dodany wzajemny link miedzy kb/services/control-plane.md (stub kodu)
a kb/subsystems/control-plane.md (opis, deprecated) — dwa dokumenty o tym
samym systemie, latwe do pomylenia.

Weryfikacja na 790 plikach: 0 odwolan do starych sciezek,
0 martwych linkow markdown. Lint OKF: 190/190 plikow ZGODNE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar 89be9b9780 feat(kb): SPLIT backlog.md (72 KB) -> 19 dokumentow + cienki indeks
Rozstrzygniecie 1. Monolit mieszal cztery typy OKF. Rozbity PER TYP po
granicy sekcji `##`:

  10 x decision  — pozycje backlogu (w tym backlog-aktywne 28 KB
                   i backlog-zamkniete 12 KB, ktore zostaja calosciami)
   7 x incident  — bugi/awarie dotad wtopione w backlog: cutover HA ken,
                   checkpoint observera, ha-diag-agent node=unknown,
                   deploy-local ghost-kontenery, paperless-worker config,
                   deploy-node nie przebudowuje obrazu, ollama bez sterownika
   2 x phase     — HA configs-as-code, monitoring floty Prometheus

kb/phases/backlog.md zostaje jako cienki indeks (type: phase, status: active):
oryginalna preambula + wygenerowany spis linkow do wszystkich 19 elementow.
23 przychodzace odwolania zostaja przepiete na ta sciezke w grupie 7.

NIE rozbijano po `###` (38 pozycji w "Aktywne" + 13 w "Zamkniete" = 51
plikow). Rozstrzygniecie mowi "rozbij per typ", a nie per pozycja;
rozdrobnienie do 51 plikow rozerwaloby czytelnosc backlogu.

Kontrola: preambula + 19 sekcji == oryginal z HEAD (multizbior niepustych
linii). Tresc pozycji nietknieta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar 9f77a723e8 feat(kb): 5 audytow/reconow -> kb/audits/ (type: audit, as_of)
czujniki-2026-07-30 (node-agent vs stability-agent)
lustro-shipping-2026-07-16 (event=dead prom=up, 1507 mismatchy)
prometheus-cutover-2026-07-06 (recon starego toru livenesci)
piha-slim-2026-07-02 (audyt odchudzania PIHA)
vps-stacki-2026-07-27 (audyt niezarzadzanych stackow na VPS)

ODSTEPSTWO OD RECONU — swiadome. Recon typowal te 5 plikow jako SPLIT
(audit+decision / audit+incident / audit+phase). Rozstrzygniecie 2 wprowadza
typ `audit` z polem as_of i mapuje kazdy z nich na JEDNA sciezke
kb/audits/<obszar>-<data>.md. Audyt jest spojna migawka stanu z konkretna
data — rozbicie go na "ustalenia" i "rekomendacje" rozerwaloby ten kontekst
i wymagaloby redakcji tresci, czego etap 2 zabrania. Zostaja w calosci.

Efekt: 29 SPLIT-ow z reconu realizowane jako 24 (10 service+runbook,
14 wielotypowych), 5 zamienionych na caloscowe dokumenty type: audit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar dbee9bd470 feat(kb): SPLIT kb-00-overview -> subsystem + decision
kb/subsystems/kb-overview.md — architektura 4 warstwy x 4 filary, zasady
  przekrojowe, kolejnosc projektow, stan per zrodlo, konwencje katalogow
kb/decisions/kb-log-decyzji.md — sekcja "Decyzje — zamkniete vs otwarte"

Tresc sekcji nietknieta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar a1590f85ed feat(kb): SPLIT stability-agent-rollout -> runbook + subsystem
kb/runbooks/stability-agent-rollout.md — Deployment, Verification, Troubleshooting
kb/subsystems/stability-agent-architektura.md — Architecture Summary +
  "Why UI only showed CHELSTY"

Tresc sekcji nietknieta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar aa1e7fce1b feat(kb): SPLIT vps-control-plane -> subsystem (deprecated) + runbook
kb/subsystems/control-plane.md — status: deprecated,
  superseded_by: "przepisany tor redeploy, commity da151fc/79bfe8c 2026-08-03"
kb/runbooks/control-plane-deploy-recovery.md — Deployment + Recovery

Rozstrzygniecie 3: dokument NIE jest odswiezany, tylko oznaczony jako
nieaktualny. Ostatnia zmiana tresci 2026-05-27, czyli przed przepisaniem
toru redeployu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar 9756338133 feat(kb): SPLIT lifecycle -> subsystem + runbook
kb/subsystems/service-lifecycle.md (visibility private wg rozstrzygniecia 6)
kb/runbooks/service-operational-recovery.md — Operational Recovery

Tresc sekcji nietknieta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar 28b1224001 feat(kb): SPLIT deployment -> subsystem + incident
kb/subsystems/deployment.md — konwencje deployu
kb/incidents/deploy-sh-vps-niszczy-control-plane.md — sekcja
  "ZNANY BUG — deploy.sh vps niszczy control-plane (2026-06-25)"

UWAGA: "Recovery Workflow" to ### zagniezdzone w "Staged Deployment
Framework". Split mechaniczny tnie wylacznie po ##, a wyciagniecie tego
fragmentu wymagaloby przebudowy tresci — zostaje w dokumencie glownym.
Do rozwazenia jako osobny runbook w etapie redakcyjnym.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar 8a1abd5750 feat(kb): SPLIT chelsty-runtime -> node + runbook
kb/nodes/chelsty-infra.md — runtime layout, SLZB-06U, ograniczenia sieciowe,
  lokalizacja configu Z2M, chelsty-ha bez node-agenta, backup sets
kb/runbooks/chelsty-deploy-recovery.md — Deployment Flow + Recovery Procedures

Tresc sekcji nietknieta; kontrola multizbioru linii == oryginal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar ccb738cddf feat(kb): SPLIT documents-ingest (30 KB) -> service + phase + runbook
kb/services/job-documents-ingest.md — opis jobu i mechanizmow
  (candidate selection, matching, consume/, idempotency, dry-run)
kb/phases/kb-m5-documents-ingest-fazy.md — faza 2, faza 2 krok 6,
  faza 3 krok 4, faza 3 krok 5 (4 sekcje fazowe wtopione w README)
kb/runbooks/documents-ingest-run.md — Usage, Verifying in Paperless, Tests

Najwiekszy README w repo. Tresc sekcji nietknieta; kontrola multizbioru
linii == oryginal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar 5439c00def feat(kb): SPLIT home-assistant/DESIGN.md -> decision + incident
kb/decisions/ha-configs-as-code.md — decyzje projektowe HA configs-as-code
kb/incidents/2026-07-22-ha-dwie-instancje.md — sekcja "Incident log":
dwie instancje HA sterujace domem rownolegle po migracji

Incydent byl dotad wtopiony w dokument decyzyjny; teraz jest adresowalny
jako osobny wpis type: incident. Tresc nietknieta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar 280ed48d9a feat(kb): SPLIT deploy-runner -> service + decision + runbook
kb/services/job-deploy-runner.md (How it works now)
kb/decisions/deploy-runner-uzasadnienie.md (What was broken)
kb/runbooks/deploy-runner-install.md (Install per node, Operating it, Tests)

Tresc sekcji nietknieta; kontrola multizbioru linii == oryginal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar 05eaf31613 feat(kb): SPLIT paperless -> service + decision + runbook
kb/services/paperless.md (Stack, OIDC, Storage i backup, RAM na PIHA)
kb/decisions/paperless-split-ocr.md (split OCR serwis@PIHA + worker@SOLARIA)
kb/runbooks/paperless-cutover.md (cutover checklist)

Tresc sekcji nietknieta; kontrola multizbioru linii == oryginal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar 96d5c814f2 feat(kb): SPLIT nextcloud -> service + decision + runbook
kb/services/nextcloud.md (Stack, WebDAV dla ingestu, Storage i backup)
kb/decisions/nextcloud-host-piha.md (decyzja: host = PIHA)
kb/runbooks/nextcloud-cutover.md (OIDC przez Forgejo + cutover checklist)

Tresc sekcji nietknieta; kontrola multizbioru linii == oryginal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar d2401ef566 feat(kb): SPLIT gokapi -> service + decision + runbook
kb/services/gokapi.md (Stack, Backup, Rejestracja w repo)
kb/decisions/gokapi-storage-e2e-siec.md (storage lokalny zamiast S3,
szyfrowanie E2E, bind tylko na Tailscale)
kb/runbooks/gokapi-cutover.md (cutover checklist)

Tresc sekcji nietknieta; kontrola multizbioru linii == oryginal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar 0a8a668b05 feat(kb): SPLIT fleet-prometheus -> service + decision + runbook
kb/services/fleet-prometheus.md (Stack, Placement & exposure, Data, Next steps)
kb/decisions/fleet-prometheus-osobny-od-prom.md ("Why separate from the home prom")
kb/runbooks/fleet-prometheus-deploy.md (Configuration, Verify)

Wzajemne links. Tresc sekcji nietknieta; kontrola: multizbior niepustych
linii czesci == oryginal z HEAD.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar 6b85c7ef68 feat(kb): SPLIT service+runbook — 10 serwisow -> 20 dokumentow
Wzorzec mechaniczny: sekcje deploy/verify/install/testy wycinane do
kb/runbooks/<serwis>-*.md, reszta zostaje dokumentem type: service.
Wzajemne `links` w obie strony. Tresc sekcji nietknieta — przenoszone
doslownie, dodany wylacznie naglowek H1 nowego runbooka.

kb-query, paperless-worker, planner-agent, ha-diag-agent, ollama-piha,
narty27, home-assistant, ha-mcp, job-gmail-header-backfill, job-mail-body-ingest.

Weryfikacja: dla kazdego pliku multizbior niepustych linii
(main + runbook) == oryginal z HEAD. Zero zgubionych, zero dodanych.

Recon szacowal 13 splitow service+runbook; faktycznie 2-typowych jest 10,
pozostale 5 (paperless, nextcloud, gokapi, fleet-prometheus, deploy-runner)
sa 3-typowe i ida osobno jako splity wielotypowe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00
oskar 28df42cbee feat(kb): stuby dla katalogow z kodem bez README (8 plikow)
Rozstrzygniecie 5: katalogi z kodem, ktore nie mialy README, dostaja stub
(frontmatter + stub: true + opis jednozdaniowy). NIE pisana pelna dokumentacja.

kb/services/: control-plane, node-agent, brain-watchdog, node-exporter,
job-gmail-bulk-import, pkg-kb-mail, pkg-kb-retrieval.

Kazdy stub podaje zrodlo opisu (service.yaml / docstring / CLAUDE.md /
pyproject.toml). Dla gmail-bulk-import i kb-retrieval zrodla brak — stub
mowi o tym wprost zamiast zmyslac opis.

Dodatkowo kb/subsystems/repo-operating-contract.md — wskaznik na CLAUDE.md
(rozstrzygniecie 4). CLAUDE.md zostaje w korzeniu jako zywa konfiguracja
narzedzia; kb-doc niesie pole contradicts: brak katalogow services/joplin,
services/outline, services/ai-cluster deklarowanych w sekcji
"Repo-managed services on VPS". Kontekst PR2 feat/vps-service-migration,
swiadomie nienaprawiane.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:04 +02:00
oskar ea9c6bbf7f feat(kb): przenosiny type=audit do kb/audits/ (2 pliki, bez SPLIT)
Nowy typ `audit` (rozstrzygniecie 2) — migawka stanu z pola `as_of`,
nie opis stanu biezacego.

monitoring-coverage-2026-07-14, ha-automatyzacje-2026-07-23.
Pozostale 5 audytow/reconow jest wielotypowych — wychodza w grupie SPLIT-ow.

git mv + frontmatter, tresc nietknieta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:04 +02:00
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
oskar 454a8f18f3 feat(kb): przenosiny type=runbook do kb/runbooks/ (5 plikow, bez SPLIT)
ollama-solaria-cutover, node-onboarding-tool (ze scripts/onboard/README.md),
ha-diag-agent-deploy, npm-api (ze scripts/npm/README.md),
node-onboarding (public).

UWAGA: scripts/onboard/README.md jest linkowany z CLAUDE.md — odwolanie
naprawiane w grupie 7.

git mv + frontmatter, tresc nietknieta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:04 +02:00
oskar f577e46281 feat(kb): przenosiny type=incident do kb/incidents/ (1 plik)
Jedyny jawny incident-doc w repo. Pozostale incydenty siedza wtopione
w docs/backlog.md, services/home-assistant/DESIGN.md i lustro-shipping-recon
— wychodza w grupie SPLIT-ow.

git mv + frontmatter, tresc nietknieta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:04 +02:00
oskar b162aadf55 feat(kb): przenosiny type=decision do kb/decisions/ (5 plikow, bez SPLIT)
architektura-2026-07-28, ai-cluster-legacy (public), sso-forgejo-oidc,
kb-dokumenty-otwarte (status: planned — decyzje jeszcze niepodjete),
tech-debt-legacy (deprecated, przejete przez backlog).

git mv + frontmatter, tresc nietknieta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:04 +02:00
oskar 00de8107ea feat(kb): przenosiny type=subsystem do kb/subsystems/ (18 plikow, bez SPLIT)
public (wzorce/schematy, bez IP/portow/sciezek hostow): observer,
capability-model, event-system, standards, agent-operating-procedures,
service-model, action-approval-model.

private: recon-multiagent, fleet-inventory, fleet-inventory-verify,
kb-mail-pillar, kb-documents-pillar, topology, agent-system.

deprecated (martwe stuby z 2026-04-15) — visibility private wg
rozstrzygniecia 6: access-model, core-stack, legacy-services-list, networking.

git mv + frontmatter, tresc nietknieta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:04 +02:00
oskar a1a57b04bb feat(kb): przenosiny type=service do kb/services/ (13 plikow, bez SPLIT)
Serwisy o jednorodnej tresci (bez wydzielonej sekcji deploy/runbook):
kb-postgres, stability-agent, llm-gateway, vikunja, zigbee2mqtt, npm,
forgejo, ollama, ir-ac-ha-integration, chelsty-stability-agent.

Deprecated: mosquitto (NOT DEPLOYED/LEGACY), home-assistant-ken-legacy
(archiwum importu), joplin (stub z 2026-04-15, brak katalogu services/joplin/).

git mv + frontmatter, tresc nietknieta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:04 +02:00
oskar 8568526f39 feat(kb): przenosiny type=node do kb/nodes/ (6 plikow)
hosts/{piha,saturn,solaria,vps}/README.md -> kb/nodes/<node>.md
docs/hetzner-vps.md -> kb/nodes/hetzner-vps.md (deprecated, stub z 2026-04-15)
docs/hardware.md    -> kb/nodes/legacy-hardware.md (deprecated)

git mv + frontmatter, tresc nietknieta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:04 +02:00
oskar 510fe0b600 feat(kb): frontmatter OKF dla 39 session logow
Session logi zostaja w docs/sessions/ (decyzja z etapu 1). Dodany wylacznie
blok frontmattera: type: session-log, visibility: private, status: active,
updated = data ostatniego commita pliku.

Tresc nietknieta — kazdy plik to +9/-0 linii.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:04 +02:00
oskar 77a048b234 feat(kb): walidator OKF v0.1 — scripts/kb/check_okf.py
Zaadaptowany z ~/narty-2027/saalbach-kb/check_okf.py. Tamten sprawdzal
wylacznie obecnosc frontmattera i niepuste `type`. Tutaj dochodza reguly
tego repo: okf przypiete do "0.1", type/visibility/status z zamknietych
list, updated/as_of jako YYYY-MM-DD, as_of wymagane wylacznie dla
type: audit, superseded_by wymagane wylacznie dla status: deprecated,
stub jako bool, links rozwiazywalne wzgledem katalogu dokumentu.

Zakres walidacji: kb/ + docs/sessions/. Reszta repo (CLAUDE.md, README.md,
.claude/skills/) lezy poza baza wiedzy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:04 +02:00
oskar 17640526ef feat(mail-body-ingest): circuit breaker na martwy backend embed przed Etapem B
Tolerancja pojedynczego nieudanego batcha jest słuszna, przeżycie martwej Ollamy
już nie. Parse archiwum jest jednowątkowy i wyprzedza GPU, więc przy Etapie B
(~212k kopert bez --since) padnięta Ollama przemieliłaby resztę korpusu z
prędkością parse'u, oznaczając każdy chunk jako chunks_errors — bez ani jednego
zapisu, ale kosztem ~2 h przebiegu do powtórzenia. Znany tryb awarii
Ollama@SOLARIA jest totalny (zniknięcie kontenera / network-detach, 4 incydenty,
§1.4/§7), nie częściowy, więc próg z kolejnych porażek trafia w niego od razu.

--max-embed-failures N (domyślnie 5, 0 wyłącza) → EmbedBackendUnavailableError
i exit 2, odrębny od exit 1 (który pełny korpus osiąga legalnie na pojedynczych
parse_errors — §1.5). Licznik zeruje się po udanym batchu, więc kryterium jest
"kolejnych", nie "łącznie". Przy abortcie dopychane są zaległe wpisy
entities[type=threading]: nie zależą od Ollamy, są idempotentne, a ich odtworzenie
oznaczałoby ponowny odczyt tych samych 27 GB. Nowy licznik embed_batch_failures
jest wyłącznie diagnostyczny — równania bilansu bez zmian.

Plan §9: dopisane decyzje operatora do Etapu B (plastry po 50k, breaker, pominięty
dry-run całości, hybrid default poza zakresem) + nota jak czytać exit 1 vs exit 2.

Testy: 4 nowe (trip po N kolejnych, reset po sukcesie, 0 wyłącza, flush threadingu
przy abortcie); 55 passed mail-body-ingest, 25 passed kb-retrieval. Smoke:
--limit 5 dry-run na żywym kb-postgres@PIHA — bilans domknięty, zero zapisów,
zero wywołań Ollamy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:53:04 +02:00
oskar 7c40d1dc32 docs: plan fazy mailowej — oznaczenie stanu kroków w §12
Tabela §12 wyglądała jak lista TODO mimo nagłówka „Kroki 0-4 WYKONANE" — to
wprowadzało w błąd przy planowaniu kolejnych tasków (założono zbędną pracę nad
batchingiem, zrobionym już w 51998fd). Każdy wiersz dostaje kolumnę Stan
(WYKONANE / OTWARTE) i dowód: commit, plik lub weryfikacja na żywej bazie.

Stan ustalony z dowodów w repo/gicie, nie z nagłówka:
- Kroki 0-3: commity 348ce10, 51998fd, ad0ef40, a640cf1 + istniejące pliki/testy
- Krok 4: §7 + potwierdzenie na żywej bazie (33 871 chunków gmail = Etap A)
- Krok 5: §8 PASS, commity 56f64e9 / bce635c / 71eb264
- Kroki 6 i 7: OTWARTE — brak commitów, brak artefaktów, wolumen bazy to nadal
  wyłącznie Etap A

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:53:04 +02:00
18 changed files with 222 additions and 34 deletions

View file

@ -4,7 +4,7 @@ services:
- NODE_NAME=vps
- CHECK_INTERVAL=60
# TEMPORARY mitigation (M1) for the unfiltered-prune incident
# (docs/incidents/2026-07-30-ollama-solaria-vanish.md §7). node-agent runs
# (kb/incidents/2026-07-30-ollama-solaria-vanish.md §7). node-agent runs
# `docker container prune()` with NO filters every CHECK_INTERVAL, and the
# Docker API removes EVERY non-running container regardless of restart
# policy or compose labels — this already destroyed ollama@solaria. On VPS

View file

@ -1,5 +1,5 @@
# homelab-deploy-runner.service — host-side executor for control-plane `redeploy`
# actions (docs/architecture/RECON-multiagent-2026-07-27.md D14/D15).
# actions (kb/subsystems/recon-multiagent.md D14/D15).
#
# Host-level and NOT a container, deliberately: it runs `docker compose` the way
# a human deploy does, so relative bind mounts and the compose project name

View file

@ -1,4 +1,4 @@
# kb-ingest.service — module 5 phase 3 step 5 (docs/kb/modules/05-faza3-plan.md §7.1).
# kb-ingest.service — module 5 phase 3 step 5 (kb/phases/kb-m5-faza3.md §7.1).
# First systemd unit in this repo: deliberately host-level, not a container — the job
# needs simultaneous LAN (Paperless), local DB (kb-postgres@PIHA), and Tailscale (Ollama@
# SOLARIA) access; containerizing it buys nothing here.

View file

@ -1,4 +1,4 @@
# kb-ingest.timer — module 5 phase 3 step 5 (docs/kb/modules/05-faza3-plan.md §7.1).
# kb-ingest.timer — module 5 phase 3 step 5 (kb/phases/kb-m5-faza3.md §7.1).
# Daily at 03:30, not hourly: the plan fixes this schedule explicitly (low-traffic window,
# and the underlying jobs are full-corpus re-scans each tick — cheap at 186 documents
# today, but daily keeps headroom as the corpus grows). Persistent=true catches up after a

View file

@ -43,9 +43,16 @@ stays in the DB (reversible: `UPDATE ... SET excluded_reason=NULL WHERE excluded
'newsletter'` + a re-embed run un-flags them later, per plan Decyzja 4).
Ollama-offline tolerance: a failed `embed_batch()` call is caught per-batch (`chunks_errors +=
len(batch)`), never aborting the run those chunks never enter the idempotency set, so a later
re-run naturally retries them. Only a wrong embedding dimension aborts the whole run
(`EmbeddingDimensionError`) never silently indexes a mismatched vector.
len(batch)`) those chunks never enter the idempotency set, so a later re-run naturally retries
them. Transient single-batch failures therefore cost nothing. What they must NOT do is let a run
survive a *dead* backend: with the archive parsed single-threaded ahead of the GPU, a full-corpus
run (Etap B, plan §9) would otherwise keep chewing through 200k+ mails at parse speed, marking
every chunk `chunks_errors`, and the whole multi-hour pass would have to be repeated. Hence the
circuit breaker: `--max-embed-failures` consecutive failed batches (default 5, `0` disables)
raise `EmbedBackendUnavailableError` and stop the run early with exit code 2, after flushing the
threading updates already earned. A single successful batch resets the counter. Only a wrong
embedding dimension is more severe (`EmbeddingDimensionError`, exit 1) never silently indexes
a mismatched vector.
"""
from __future__ import annotations
@ -81,6 +88,17 @@ _log = structlog.get_logger(__name__)
DEFAULT_ARCHIVE_ROOT = Path("/home/oskar/kb/mail/archive")
DEFAULT_BATCH_SIZE = 64
THREADING_UPDATE_BATCH_SIZE = 500
# Circuit breaker: consecutive failed embed batches that mean "the backend is down, not flaky".
# 5 x --batch-size chunks written off before stopping; Ollama@SOLARIA's known failure mode is
# total (container vanishes / network-detached), not partial, so this trips within seconds of it.
DEFAULT_MAX_EMBED_FAILURES = 5
EXIT_EMBED_BACKEND_UNAVAILABLE = 2
class EmbedBackendUnavailableError(RuntimeError):
"""Raised when `--max-embed-failures` consecutive embed batches failed — the run stops instead
of parsing the rest of the archive into `chunks_errors`. Nothing is corrupted: failed chunks
were never inserted, so a re-run picks them up through the ordinary idempotency path."""
_CHUNK_INSERT_SQL = """
INSERT INTO document_chunk (envelope_id, chunk_index, text, embedding, model, excluded_reason)
@ -374,6 +392,7 @@ def _new_stats() -> dict:
"chunks_errors": 0,
"quoted_chars_stripped_total": 0,
"embed_calls": 0,
"embed_batch_failures": 0, # diagnostic only — not part of the balance equations
"embed_seconds_total": 0.0,
"threading_updated": 0,
"threading_already_present": 0,
@ -390,6 +409,7 @@ async def run(
offset: Optional[int] = None,
apply: bool = False,
batch_size: int = DEFAULT_BATCH_SIZE,
max_embed_failures: int = DEFAULT_MAX_EMBED_FAILURES,
) -> dict:
"""Process one --limit/--offset (optionally --since-filtered) slice of `source='gmail'`
envelopes. dry-run (apply=False): parse + quote-strip + classify + chunk + count, zero
@ -413,8 +433,10 @@ async def run(
embed_buffer: list[tuple[str, int, str]] = []
threading_pending: list[tuple[str, str]] = []
consecutive_embed_failures = 0
async def flush_embed_buffer() -> None:
nonlocal consecutive_embed_failures
if not embed_buffer:
return
texts = [t for (_eid, _idx, t) in embed_buffer]
@ -425,9 +447,18 @@ async def run(
except aiohttp.ClientError:
_log.warning("skip.embed_batch_error", count=len(embed_buffer), exc_info=True)
stats["chunks_errors"] += len(embed_buffer)
stats["embed_batch_failures"] += 1
consecutive_embed_failures += 1
embed_buffer.clear()
if max_embed_failures and consecutive_embed_failures >= max_embed_failures:
raise EmbedBackendUnavailableError(
f"{consecutive_embed_failures} consecutive embed batches failed "
f"({ollama_url}) — stopping instead of parsing the rest of the archive "
f"into chunks_errors; re-run to retry the missed chunks"
)
return
consecutive_embed_failures = 0
stats["embed_calls"] += 1
stats["embed_seconds_total"] += elapsed
for (eid, idx, chunk), embedding in zip(embed_buffer, embeddings):
@ -540,6 +571,12 @@ async def run(
if apply:
await flush_embed_buffer()
await flush_threading()
except EmbedBackendUnavailableError:
# The threading appends already earned by the mails parsed so far are independent of
# Ollama and idempotent — flush them rather than making the next run re-derive them.
await flush_threading()
_log.error("embed_backend_unavailable_abort", ollama_url=ollama_url, **stats)
raise
finally:
if session is not None:
await session.close()
@ -586,6 +623,12 @@ def main() -> None:
help="Slice offset, ordered by envelope id (default: 0)")
parser.add_argument("--batch-size", type=int, default=DEFAULT_BATCH_SIZE,
help=f"Ollama /api/embed batch size (default: {DEFAULT_BATCH_SIZE})")
parser.add_argument("--max-embed-failures", type=int, default=DEFAULT_MAX_EMBED_FAILURES,
metavar="N",
help=f"Abort (exit {EXIT_EMBED_BACKEND_UNAVAILABLE}) after N consecutive "
f"failed embed batches — a dead Ollama must not turn a multi-hour run into "
f"200k chunks_errors. 0 disables the breaker. "
f"Default: {DEFAULT_MAX_EMBED_FAILURES}.")
parser.add_argument("--apply", action="store_true",
help="Actually call Ollama, insert chunks, and update threading entities. "
"Default is dry-run (parse + classify + chunk + count only).")
@ -610,11 +653,17 @@ def main() -> None:
offset=args.offset,
apply=args.apply,
batch_size=args.batch_size,
max_embed_failures=args.max_embed_failures,
)
)
except EmbeddingDimensionError as exc:
_log.error("dim_mismatch_abort", error=str(exc))
sys.exit(1)
except EmbedBackendUnavailableError as exc:
# Distinct exit code: unlike exit 1 (which a full-corpus run can legitimately reach on a
# handful of parse_errors), this one means "nothing more will succeed until Ollama is back".
_log.error("embed_backend_unavailable", error=str(exc))
sys.exit(EXIT_EMBED_BACKEND_UNAVAILABLE)
mode = "APPLY" if args.apply else "DRY-RUN"
avg_embed_ms = (

View file

@ -17,6 +17,7 @@ import pytest
from kb_retrieval.embed import EmbeddingDimensionError
from mail_body_ingest.ingest import (
_CHUNK_INSERT_SQL,
EmbedBackendUnavailableError,
_decode_jsonb,
_has_threading,
build_prefix,
@ -388,17 +389,26 @@ class _FakeTagsResponse:
class _FakeOllamaSession:
def __init__(self, dim=1024, fail_batches=False, health_up=True):
def __init__(self, dim=1024, fail_batches=False, health_up=True, fail_pattern=None):
self._dim = dim
self._fail_batches = fail_batches
self._health_up = health_up
# Per-request failure sequence (True = this batch fails); exhausting it falls back to
# `fail_batches`. Lets a test interleave failures and successes to exercise the breaker's
# "consecutive" semantics rather than a plain total.
self._fail_pattern = list(fail_pattern) if fail_pattern is not None else None
self.requests: list[dict] = []
self.closed = False
def _should_fail(self) -> bool:
if self._fail_pattern:
return self._fail_pattern.pop(0)
return self._fail_batches
def post(self, url, json):
assert url.endswith("/api/embed")
self.requests.append({"url": url, "json": json})
if self._fail_batches:
if self._should_fail():
return _FakeEmbedResponse({}, status=500)
n = len(json["input"])
return _FakeEmbedResponse({"embeddings": [[0.01] * self._dim for _ in range(n)]})
@ -558,6 +568,76 @@ class TestRun:
+ stats["chunks_conflict_skipped"] + stats["chunks_errors"]
)
def _write_n_mails(self, tmp_path, n: int) -> list:
rows = []
for i in range(n):
eid = f"m{i}@x"
_write_eml(tmp_path, f"gmail/2025/08/{eid}.eml", _plain_eml(
{"From": "a@b.com", "Subject": f"hi {i}", "Date": "Fri, 01 Aug 2025 10:00:00 +0000"},
f"hello there number {i}",
))
rows.append(_env_row(eid, f"hi {i}"))
return rows
async def test_breaker_aborts_after_consecutive_embed_failures(self, tmp_path, monkeypatch):
"""A dead backend must stop the run, not let it parse the rest of the archive into
chunks_errors (plan §9 Etap B: 200k+ mails behind a single-threaded parse)."""
conn = _FakeConn(envelopes=self._write_n_mails(tmp_path, 8))
ollama = _FakeOllamaSession(dim=1024, fail_batches=True)
self._patch(monkeypatch, conn, ollama)
with pytest.raises(EmbedBackendUnavailableError):
await run(dsn="postgresql://fake", archive_root=tmp_path, apply=True,
batch_size=1, max_embed_failures=5)
# Stopped at the 5th failed batch — mails 6-8 were never parsed, let alone embedded.
assert len(ollama.requests) == 5
assert conn.execute_calls == [] # nothing inserted: failed chunks stay retryable
async def test_breaker_counter_resets_on_successful_batch(self, tmp_path, monkeypatch):
""""Consecutive", not "total" — flaky batches interleaved with successes must not trip it."""
conn = _FakeConn(envelopes=self._write_n_mails(tmp_path, 5))
ollama = _FakeOllamaSession(dim=1024, fail_pattern=[True, True, False, True, True])
self._patch(monkeypatch, conn, ollama)
stats = await run(dsn="postgresql://fake", archive_root=tmp_path, apply=True,
batch_size=1, max_embed_failures=3)
assert len(ollama.requests) == 5 # ran to completion
assert stats["chunks_errors"] == 4
assert stats["chunks_inserted"] == 1
assert stats["embed_batch_failures"] == 4
assert stats["chunks_total"] == (
stats["chunks_inserted"] + stats["chunks_newsletter_flagged"] + stats["chunks_already_embedded"]
+ stats["chunks_conflict_skipped"] + stats["chunks_errors"]
)
async def test_breaker_disabled_with_zero(self, tmp_path, monkeypatch):
conn = _FakeConn(envelopes=self._write_n_mails(tmp_path, 6))
ollama = _FakeOllamaSession(dim=1024, fail_batches=True)
self._patch(monkeypatch, conn, ollama)
stats = await run(dsn="postgresql://fake", archive_root=tmp_path, apply=True,
batch_size=1, max_embed_failures=0)
assert len(ollama.requests) == 6
assert stats["chunks_errors"] == 6
async def test_breaker_abort_flushes_pending_threading(self, tmp_path, monkeypatch):
"""Threading appends are Ollama-independent and idempotent — the abort keeps them rather
than making the next run re-derive them from the same 27 GB read."""
conn = _FakeConn(envelopes=self._write_n_mails(tmp_path, 8))
ollama = _FakeOllamaSession(dim=1024, fail_batches=True)
self._patch(monkeypatch, conn, ollama)
with pytest.raises(EmbedBackendUnavailableError):
await run(dsn="postgresql://fake", archive_root=tmp_path, apply=True,
batch_size=1, max_embed_failures=5)
assert len(conn.executemany_calls) == 1
# The 5 mails processed before the breaker tripped, none of the 3 after it.
assert len(conn.executemany_calls[0][1]) == 5
async def test_dimension_mismatch_aborts(self, tmp_path, monkeypatch):
_write_eml(tmp_path, "gmail/2025/08/m1@x.eml", _plain_eml(
{"From": "a@b.com", "Subject": "hi", "Date": "Fri, 01 Aug 2025 10:00:00 +0000"}, "hello there"

View file

@ -598,6 +598,33 @@ mała zmiana w serwisie, nie w `packages/kb-retrieval`).
**Szacunek: 1 sesja (run w tle).**
### Decyzje operatora do Etapu B (2026-08-04) — przed runem
Recon przed Etapem B (mirror archiwum na SOLARII żyje: 225 057 plików / 27 GB; RTT
SOLARIA→PIHA 0,83 ms; PIHA 140 GB wolne, baza 397 MB; M1 — `NODE_TYPE=lte_node`
na node-agencie SOLARII — zdeployowane, więc kontener Ollamy nie zniknie po
zatrzymaniu) wykazał dwie rzeczy do rozstrzygnięcia. Decyzje:
1. **Run w plastrach po 50k** (`--limit 50000 --offset 0/50k/100k/150k/200k`),
log per plaster, `nice`/`ionice`. Powód: brak checkpointu (restart = ponowny
parse od początku listy, ~1 h) + nocne wyłączanie SOLARII. Plaster ≈ 2540 min.
Tempo kolejnych plastrów po obserwacji PIHA po pierwszym.
2. **Circuit breaker w jobie: TAK**`--max-embed-failures` (domyślnie 5),
abort z kodem wyjścia 2 po N kolejnych nieudanych batchach embed. Powód:
parse jest jednowątkowy i wyprzedza GPU, więc martwa Ollama (4 incydenty)
zamieniłaby 2-godzinny przebieg w 200k+ `chunks_errors` bez ani jednego
zapisu. Licznik zeruje się po udanym batchu.
3. **Dry-run całości pomijamy** — idempotencja i odwracalność flag newsletterowych
wystarczają; ewentualna kalibracja heurystyki na dekadzie 20102015 po fakcie,
na już zapisanych flagach.
4. Przełączenie domyślnego `mode` kb-query na `hybrid` (DoD (d)) — **poza zakresem
Etapu B**, osobny task po PASS regresji.
Uwaga do czytania wyników: na pełnym korpusie `exit 1` jest spodziewany
(pojedyncze `parse_errors` — §1.5 dokumentuje ~9 maili na fallbacku compat32).
Werdyktem jest bilans i liczniki w linii `summary`, nie kod wyjścia. `exit 2`
oznacza co innego: backend embed padł, trzeba wznowić plaster po naprawie Ollamy.
## 10. Krok 7 — IMAP/JMAP przyrostówka (zarys; szczegóły = osobny recon)
Zakotwiczone w kb-00 jako etapy 34 (`jobs/fastmail-poller`,
@ -630,16 +657,16 @@ Zakotwiczone w kb-00 jako etapy 34 (`jobs/fastmail-poller`,
## 12. Plan implementacji (kolejność = zależności)
| # | Krok | Zależy od | Szacunek |
|---|---|---|---|
| 0 | Chunker → `packages/kb-mail` | — | 0,5 sesji |
| 1 | `embed_batch` w kb-retrieval | — | 0,5 sesji |
| 2 | Job `mail-body-ingest` | 0, 1 | 2 sesje |
| 3 | Tryb hybrid (kb-retrieval + kb-query) | — (równolegle z 2) | 1 sesja |
| 4 | rsync + Etap A (12 mies.) + kalibracja | 2 | 1 sesja |
| 5 | Bramka jakościowa (eval mailowy + regresja) | 3, 4 + zapytania od operatora | 1 sesja |
| 6 | Etap B (pełne archiwum) + regresja + obserwacja PIHA | 5 = PASS | 1 sesja |
| 7 | Recon przyrostówki IMAP/JMAP | — (po 6) | 1 sesja (poza DoD fazy) |
| # | Krok | Zależy od | Szacunek | Stan | Dowód (2026-08-04) |
|---|---|---|---|---|---|
| 0 | Chunker → `packages/kb-mail` | — | 0,5 sesji | **WYKONANE** | `348ce10`; `packages/kb-mail/src/kb_mail/chunking.py` + `tests/test_chunking.py` |
| 1 | `embed_batch` w kb-retrieval | — | 0,5 sesji | **WYKONANE** | `51998fd`; `kb_retrieval/embed.py:61` (`embed_batch`) + `tests/test_embed.py` |
| 2 | Job `mail-body-ingest` | 0, 1 | 2 sesje | **WYKONANE** | `ad0ef40` (job), `a95524c` (README), `fc5c698` (fix html_to_text); `jobs/mail-body-ingest/` + `tests/test_ingest.py` |
| 3 | Tryb hybrid (kb-retrieval + kb-query) | — (równolegle z 2) | 1 sesja | **WYKONANE** | `a640cf1`; `kb_retrieval/retrieval.py:112` (`hybrid_retrieve`), `:195` (`hybrid_query`), `kb-query/app/main.py:119` (`mode` pattern). Uwaga: domyślny `mode` to nadal `cascade` — przełączenie to follow-up z §8, nie część Kroku 3 |
| 4 | rsync + Etap A (12 mies.) + kalibracja | 2 | 1 sesja | **WYKONANE** | §7 „Wynik Etapu A" (run na żywo 2026-07-23); potwierdzone na żywej bazie 2026-08-04: `document_chunk` gmail = 33 871 (6 398 z embeddingiem + 27 473 `newsletter`) — zgodne co do sztuki z tabelą §7 |
| 5 | Bramka jakościowa (eval mailowy + regresja) | 3, 4 + zapytania od operatora | 1 sesja | **WYKONANE** (PASS) | §8 „Wynik bramki"; `56f64e9` (eval + queries.yaml dla hybrid), `bce635c` (`mail_hit@3`, próg N2, werdykt PASS), `71eb264` (`--transport http`) |
| 6 | Etap B (pełne archiwum) + regresja + obserwacja PIHA | 5 = PASS | 1 sesja | **OTWARTE** | Brak commitu, brak sekcji z wynikiem w tym dokumencie; żywa baza pokazuje wyłącznie wolumen Etapu A (33 871 chunków gmail vs oczekiwane ~496k), więc run bez `--since` nie był wykonany |
| 7 | Recon przyrostówki IMAP/JMAP | — (po 6) | 1 sesja (poza DoD fazy) | **OTWARTE** | Brak `jobs/fastmail-poller` / `jobs/gmail-imap-poller`, brak dokumentu reconu; IMAP/JMAP występuje wyłącznie jako zarys w §10 i w `kb-00-overview.md` |
**Kryterium ukończenia fazy mailowej:** (a) pełny korpus gmail zchunkowany
(bilans domknięty, `parse_errors` na poziomie pojedynczych sztuk jak

View file

@ -448,7 +448,7 @@ Próbka 25 dokumentów (stratyfikowana: polisy, faktury, umowy, urzędowe, FLL/s
- **kompletność faktów kluczowych** (kwoty, daty, strony, numery),
- **jakość tagów** (trafność + zgodność ze słownikiem).
Wynik do `docs/kb/modules/05-faza3-pilot-streszczen.md`: tabela per dokument + wnioski.
Wynik do `kb/phases/kb-m5-faza3-pilot-streszczen.md`: tabela per dokument + wnioski.
**Kryterium „lokalny wystarcza na skalę mailową"**: mediana wierności = 2 (zero tolerancji
dla przekręconych kwot — to trafia do wiki) i kompletność ≥ 80% punktów API. Jeśli lokalny
nie daje rady → decyzja o skali mailowej rozważa API z polityką eskalacji fazy 5 (koszt

View file

@ -20,6 +20,11 @@ mail-body-ingest --dsn postgresql://kb:<pw>@piha:5433/kb --archive-root /home/os
# Etap A pilot — last 12 months only (plan Decyzja 9):
mail-body-ingest --dsn ... --since 2025-07-01 --apply > mail-ingest-etapA.log 2>&1
# Etap B — full archive in 50k slices (plan §9; ORDER BY id is stable, so slices are
# reproducible, and idempotency covers their boundaries):
nice -n 10 ionice -c2 -n7 mail-body-ingest --dsn ... --apply \
--limit 50000 --offset 0 > mail-ingest-etapB-0.log 2>&1
# Smoke-test slice:
mail-body-ingest --dsn ... --apply --limit 10
```
@ -34,11 +39,13 @@ pip install -e "jobs/mail-body-ingest[dev]"
cd jobs/mail-body-ingest && pytest
```
Pure unit tests (48), no DB/Ollama — `run()` is tested by monkeypatching `asyncpg.connect`
Pure unit tests (55), no DB/Ollama — `run()` is tested by monkeypatching `asyncpg.connect`
and `aiohttp.ClientSession` with in-memory fakes, `.eml` bytes written to `tmp_path`. Covers:
quote-strip (EN/PL/Outlook markers, bare `>` lines), HTML->text (style/script/blockquote/
gmail_quote skipping), newsletter classification, threading extraction, prefix building,
body extraction (plain-preferred, HTML fallback, attachment-only), the typed/compat32 parse
fallback, stats balance, idempotency (second run inserts nothing new), newsletter chunks
never reaching Ollama, Ollama-offline batch isolation, and dimension-mismatch abort.
never reaching Ollama, Ollama-offline batch isolation, dimension-mismatch abort, and the
circuit breaker (trips on N consecutive failures, resets on a success, disabled by `0`,
flushes pending threading on abort).

View file

@ -103,7 +103,7 @@ so Paperless can read them. If the chown fails (e.g. the job isn't running as
root/uid 1000), a warning is logged but the run continues — the write itself
already succeeded; fix ownership/perms on `consume/` separately if needed.
PIHA's uid/gid convention across the fleet is tracked as its own tech-debt
item (see `docs/backlog/`), not solved here.
item (see `kb/decisions/backlog-uid-gid-flota.md`), not solved here.
## Idempotency — registry

View file

@ -82,14 +82,39 @@ Any non-zero `read_errors`/`parse_errors`/`missing_file`/`chunks_errors`/
`chunks_conflict_skipped`, or an unbalanced sum, makes the CLI exit 1 — same convention as
`gmail-header-backfill`/`documents-ingest`'s `chunk_embed`.
## Ollama-offline tolerance
**Reading exit 1 on a full-corpus run**: it is a "look at this", not "the run failed". Across
225k mails a handful of `parse_errors` is expected (plan §1.5 documents ~9 mails that need the
compat32 fallback), and any one of them alone trips exit 1. The verdict is the balance and the
counters in the `summary` line, not the exit code. Exit 2 is different — see below.
## Exit codes
| Code | Meaning |
|---|---|
| 0 | Balanced, zero errors |
| 1 | Balanced-but-imperfect (any `parse_errors`/`missing_file`/`read_errors`/`chunks_errors`/`chunks_conflict_skipped`), an unbalanced sum, or an embedding-dimension abort |
| 2 | `--max-embed-failures` consecutive embed batches failed — the embed backend is down; re-run once it is back |
## Ollama-offline tolerance and the circuit breaker
A failed `embed_batch()` call is caught per-batch (`aiohttp.ClientError` -> the whole batch,
up to `--batch-size` chunks, counts as `chunks_errors`; the run logs a warning and continues).
Those chunks never enter the idempotency set, so a later re-run retries them automatically —
no separate checkpointing needed. Only a wrong embedding dimension
(`EmbeddingDimensionError`) aborts the entire run, since that would otherwise silently index
a vector that doesn't match `document_chunk.embedding VECTOR(1024)`.
no separate checkpointing needed.
Tolerating a *flaky* backend is right; surviving a *dead* one is not. The archive is parsed
single-threaded ahead of the GPU, so on a full-corpus run (Etap B) a dead Ollama would let the
job chew through 200k+ mails at parse speed, mark every chunk `chunks_errors`, and throw away a
multi-hour pass. `--max-embed-failures` (default 5, `0` disables) therefore stops the run after
that many *consecutive* failed batches, with exit code 2; a single successful batch resets the
counter. Ollama@SOLARIA's known failure mode is total (container vanishes, network-detached —
4 incidents, plan §1.4/§7), so the breaker trips within seconds of it. On abort, pending
`entities[type=threading]` appends are flushed first: they don't depend on Ollama, they're
idempotent, and re-deriving them would mean re-reading the same 27 GB.
Only a wrong embedding dimension is more severe (`EmbeddingDimensionError`, exit 1) — it aborts
immediately, since that would otherwise silently index a vector that doesn't match
`document_chunk.embedding VECTOR(1024)`.
## Idempotency

View file

@ -12,7 +12,7 @@ links: []
> Dokument-master filaru dokumentow. Stoi pod `kb-00-overview.md`.
> Cel: kazda sesja / Claude Code startuje z pelnym kontekstem decyzji.
> Status: ARCHITEKTURA ZAMKNIETA (2026-07-01), implementacja modulowa czeka.
> Moduly implementacyjne: `docs/kb/modules/0X-*.md` — puszczane CC jeden po drugim.
> Moduly implementacyjne: `kb/phases/kb-m*.md` — puszczane CC jeden po drugim.
---

View file

@ -6,7 +6,7 @@ NPM_PIHA_PASS=CHANGEME
# VPS — public ingress. Panel admina osiagalny WYLACZNIE przez Tailscale mesh
# (http://100.95.58.48:81). NIGDY nie uzywac publicznego IP (135.181.153.108:81) —
# patrz docs/backlog.md, wpis o publicznej ekspozycji panelu admina.
# patrz kb/decisions/backlog-aktywne.md, wpis o publicznej ekspozycji panelu admina.
NPM_VPS_USER=CHANGEME
NPM_VPS_PASS=CHANGEME

View file

@ -81,7 +81,7 @@ services:
- /opt/homelab:/opt/homelab
# Read-only since the redeploy fix: the executor used to run
# scripts/deploy/deploy-node.sh out of this mount (it never worked — see
# jobs/deploy-runner/README.md). Deploys now happen on the node itself, so
# kb/services/job-deploy-runner.md). Deploys now happen on the node itself, so
# nothing here needs write access to the checkout.
- ../..:/repo:ro
- /var/run/docker.sock:/var/run/docker.sock

View file

@ -1,4 +1,4 @@
// kb-query frontend -- module 5 phase 4 (docs/kb/modules/05-faza4-plan.md §7). Vanilla JS, no
// kb-query frontend -- module 5 phase 4 (kb/phases/kb-m5-faza4.md §7). Vanilla JS, no
// build step (plan §2 decision 4): fetch()s /search, renders results grouped by envelope_id.
//
// Threshold rule (plan §7, task spec): dist < 0.45 green, 0.45-0.55 yellow (still rendered with

View file

@ -2,7 +2,7 @@
# docker-compose.yml and fill in real values. Never commit .env.
# LAN IP of the node Nextcloud lands on. Host decided: PIHA (see
# docs/kb/modules/DECYZJE-do-podjecia.md #1). The web port (8220) binds ONLY
# kb/decisions/kb-dokumenty-otwarte.md #1). The web port (8220) binds ONLY
# to this interface — never 0.0.0.0.
LAN_BIND_IP=192.168.31.5

View file

@ -638,7 +638,7 @@ class NodeAgent:
NEVER uses containers.prune(): the Docker API removes *every*
non-running container, ignoring RestartPolicy and compose labels. That
is what deleted `ollama` on SOLARIA 19 s after an operator stopped it
(docs/incidents/2026-07-30-ollama-solaria-vanish.md). No filter can fix
(kb/incidents/2026-07-30-ollama-solaria-vanish.md). No filter can fix
it either `until` filters on creation time, not stop time, so it
never protects a long-lived service.

View file

@ -1,5 +1,5 @@
"""Tests for NodeAgent Docker cleanup — regression cover for the 2026-07-30
unfiltered-prune incident (docs/incidents/2026-07-30-ollama-solaria-vanish.md).
unfiltered-prune incident (kb/incidents/2026-07-30-ollama-solaria-vanish.md).
`containers.prune()` removes EVERY non-running container, ignoring restart
policy and compose labels; that is what deleted `ollama` on SOLARIA 19 s after