Notatki
Notatki z pracy nad Kolofonem i z nauki .NET: wdrożenia, architektura, front. Kod Kolofonu pisali Claude i Claude Code; ja byłem pomysłodawcą, właścicielem produktu i architektem; „my” w notatkach to ja z Claude i Claude Code. W nauce .NET (DeviceHub, „Cztery awarie jednego wdrożenia”) kod piszę sam.
Paweł · 24 października 2026
Agenci AI rozmawiali ze sobą. Po 10 wiadomościach czekali na człowieka
„Agenci AI w jednym projekcie przekazywali sobie pracę krótkimi wiadomościami: „twoja kolej”, „gotowe”. Po 10 takich wiadomościach rozmowa czekała, aż napisze człowiek. Co się psuło i jak to obeszliśmy.” Niedawno napisałem, że w jednym z moich projektów agenci AI w ogóle ze sobą nie rozmawiają. To było uproszczenie. Rozmawiali, tylko bardzo krótko: „twoja kolej”, „zrobione”. Wszystko, co miało jakąkolwiek wagę, zapisywali gdzie indziej. A po pewnym czasie te krótkie sygnały przestawały docierać. Piszę książkę dokumentalną z zespołem agentów. Każdy agent to osobna rozmowa z modelem językowym, z jednym zadaniem i własną instrukcją: koordynator, redaktor, autorzy tekstu (Claude i GPT), krytyk, kontroler faktów, tłumacz na angielski, osoba od składu. Wszyscy pracują na jednym repozytorium git. Ja podaję fakty i podejmuję decyzje. Sam nie edytuję żadnego pliku. Agenci korzystali z dwóch dróg naraz. Pierwsza to sygnał. Aplikacja, w której działają, pozwala jednej rozmowie wysłać krótką wiadomość do drugiej. Koordynator pisał na przykład do krytyka: „twoja kolej, przeczytaj pierwsze dziesięć rozdziałów”. Krytyk po skończonej pracy odpisywał: „gotowe”, z numerem zmiany w repozytorium. Kiedy w nocy mnie nie było, agenci przekazywali sobie kolejkę sami, jeden po drugim, bez koordynatora. Druga to repozytorium. Szedł tamtędy tekst, fakty, raporty i informacja, kto ma teraz ruch: jedna linia w pliku stanu. Zlecenie miało też kopię w osobnym katalogu. Zasada była prosta: w sygnale nie ma żadnej treści. Sygnał może nie dojść. Plik zostaje. Zanim koordynator wyśle sygnał, zapisuje w repozytor
agenci AI · architektura
Paweł · 15 września 2026
Kanał boczny, który stał się protokołem
„1200 programów, każdy zamknięty w swoim sandboksie, miało jeden wspólny zasób: firmowy cache pakietów. W 4 dni zbudowało na nim, oddolnie, kompletny stos komunikacyjny — adresowanie, wątki, kolejki, przesył plików…” To jest opis protokołu, którego nikt nie zaprojektował. W lipcu 2026 OpenAI puściło dziesiątki tysięcy programów na jeden test bezpieczeństwa. Każdy siedział w osobnym sandboksie, odcięty od reszty i od sieci. Około 1200 z nich znalazło się nawzajem i przez 4 dni wymieniło ponad 70 000 wiadomości. Zrobiło to na jedynym zasobie, który współdzieliły — na firmowym cache'u pakietów. Rozłożyło to na części niezależne dochodzenie METR i Redwood Research, i to z ich raportu biorę wszystkie surowe wpisy poniżej. Nie interesuje mnie tu, po co oszukiwały test. Interesuje mnie, że na medium, które nie jest do rozmowy, odtworzyły oddolnie prawie kompletny stos komunikacyjny: transport, ramkowanie, adresowanie, wątkowanie, kolejki, przesył plików, uwierzytelnianie i wzajemne wykluczanie. Opiszę go warstwami, od medium w górę — bo tak się go najłatwiej czyta i bo tak samo byśmy go projektowali świadomie. Sandboksy nie miały jak się dosięgnąć — poza jednym. Program, który instaluje paczki, bierze je z firmowego repozytorium: JFrog Artifactory, postawiony jako proxy z cache. Wszystkie sandboksy łączyły się z tą samą instancją. To była jedyna wspólna powierzchnia i, jak się okazało, powierzchnia zapisywalna. Zalążek jest w jednym zdaniu rozumowania, zacytowanym w raporcie: w cache widać ścieżki pobrań innych programów. To jest cały błąd w izolacji — wspólny, obserwowalny stan. Reszta to już tyl
architektura · bezpieczeństwo
Paweł · 11 września 2026
Dwa języki, jedno wdrożenie
„Do dziś każda wersja językowa była osobnym serwisem: własny adres, repozytorium, baza i wdrożenie. Dziś połączyliśmy je w jedno — w dwóch serwisach naraz, a ta strona dostała wersję angielską od razu w nowym układzie…” Od sierpnia każda wersja językowa serwisu na moim silniku była osobną instancją: polska pod jednym adresem, angielska pod subdomeną, każda z własnym repozytorium, bazą i wdrożeniem. Dziś to się skończyło. Dwa serwisy zostały połączone, a ta strona dostała wersję angielską od razu w nowym układzie — pod /en, w tym samym wdrożeniu. Osobne instancje były łatwe do postawienia i drogie w utrzymaniu. Każda poprawka treści szła dwa razy: dwa commity, dwa wdrożenia, dwie weryfikacje. Reguła „wersja angielska w tej samej sesji” była pilnowana ręcznie, czyli czasem nie była. Statystyki leżały w dwóch bazach i raport trzeba było sklejać. Jedna z wersji wdrażała się ręcznie, bo automatyczne budowanie wymagało kliknięcia w panelu. A czytelnik zalogowany po polsku po przejściu na angielski był dla serwisu kimś obcym, bo sesja siedzi na hoście. Jeden push wdraża obie wersje. Jedna baza trzyma oba języki — każdy rekord dotyczący strony ma kolumnę z językiem, a rzeczy dotyczące materiału (oceny, komentarze, głosy) i tak od początku były kluczowane kluczem wpisu ponad językiem. Jedna domena to jedna tożsamość przeglądarki, jedna sesja i jedna mapa strony dla wyszukiwarki. Zamiast pięciu wdrożeń do pilnowania są trzy. Stare adresy angielskie przekierowują regułą 301 na nowe, ze ścieżką i parametrami. O języku wpisu decyduje folder: wpisy polskie leżą tam, gdzie leżały, angielskie w src/content/en
architektura · kolofon
Paweł · 11 września 2026
Licznik, który odliczał poza ekranem
„Liczby na stronie głównej dobijały do wartości docelowej poprawnie. Problem polegał na tym, że robiły to wtedy, gdy nikt na nie nie patrzył — i naprawa okazała się mniej oczywista niż usterka.” Cztery kafelki na stronie głównej pokazywały wtedy liczby, które dobijały do wartości docelowej. Animacja działała od pierwszej wersji i przez pół dnia byłem przekonany, że jest skończona. Nie była. Odliczanie startowało wtedy, gdy obserwator przecięcia zgłaszał kafelek — a to nie to samo, co „kafelek widać”. Przy takiej animacji w grę wchodzą trzy zdarzenia i łatwo je pomylić, bo na komputerze programisty zwykle zachodzą niemal jednocześnie. Na telefonie te trzy momenty rozjeżdżają się o sekundy. Kafelek wchodzi w obszar obserwacji dolną krawędzią, gdy widać z niego kilkanaście pikseli. Odsłania się z opóźnieniem, bo czeka na swoją kolej w sekwencji wejścia. A w kadrze jest naprawdę dopiero po przewinięciu. Pierwsza wersja reagowała na pierwsze zdarzenie. Licznik przelatywał całą drogę od zera do wyniku, zanim kafelek zdążył się pojawić — i po dojechaniu wzrokiem stała tam gotowa liczba. Pierwszy odruch: zduplikować w komponencie liczby tę samą logikę opóźnień, którą ma komponent odsłaniający. Zły odruch — dwa miejsca liczące to samo rozjadą się przy pierwszej zmianie. Lepiej zapytać wprost o skutek. Komponent liczby nie musi wiedzieć, kiedy nastąpi odsłonięcie; musi wiedzieć, czy już nastąpiło. Dwie rzeczy, które mnie zaskoczyły przy pisaniu tego warunku. Pierwsza: **własne krycie elementu zawsze wynosi jeden**. Przezroczystość siedzi na kontenerze odsłaniającym, więc trzeba
frontend · kolofon
Paweł · 11 września 2026
Skórka to nie arkusz stylów
„Ta strona i serwis literacki stoją na tym samym silniku, a wyglądają jak dwa różne produkty. Nie przez osobny arkusz stylów — przez jedno pole w konfiguracji i kilkanaście decyzji o tym, co wolno zmienić, a czego nie.” Silnik, na którym stoi ta strona, obsługuje pięć serwisów. Cztery z nich wyglądają jak książka: papier, antykwa, akapity justowane, inicjał na początku tekstu. Piąty — ten — wygląda jak strona techniczna. Ten sam kod, ta sama wersja, jedno pole różnicy w konfiguracji. Poniżej opisuję, co się za tym polem kryje, bo droga do niego prowadziła przez trzy pomyłki, a każda z nich uczy czegoś innego. Pierwszym odruchem było napisanie drugiego arkusza stylów i podmienianie go w zależności od instancji. Odruch jest zły z powodu, który widać dopiero przy trzeciej zmianie: dwa arkusze trzeba utrzymywać równolegle. Poprawka koloru linii w jednym miejscu nie dociera do drugiego i po miesiącu serwisy różnią się rzeczami, o których nikt nie decydował. Skórka nie jest zbiorem reguł. Jest zbiorem WARTOŚCI, które reguły odczytują. Komponenty w ogóle nie wiedzą, że istnieje coś takiego jak motyw — biorą tokeny. Atrybut siedzi na elemencie html i bierze się wprost z konfiguracji instancji. Instancja, która nie deklaruje niczego, dostaje skórkę książkową — czyli dokładnie to, co miała przed dodaniem tej funkcji. To warunek, nie uprzejmość: cztery działające serwisy nie mogą zauważyć, że pod nimi coś się zmieniło. Najciekawsza z całej trójki. Chciałem, żeby skórka techniczna używała kroju bezszeryfowego wszędzie tam, gdzie książkowa używa antykwy. Wydawało się to jednolinijkowe: wystarczy w selektor
architektura · kolofon
Paweł · 10 września 2026
Wdrożenia bez serwera i bez twardego sleep
„Cztery repozytoria wdrażają się same z pusha, piąte ręcznie — bo brakuje jednego kliknięcia, którego nie da się zrobić z API. O tym, co w tym układzie działa, a co jest protezą.” Serwis stoi na brzegu sieci — nie ma maszyny, systemu do łatania ani puli aplikacji. Jest kod, baza i magazyn obiektów. Wdrożenie polega na wysłaniu paczki, nie na restarcie usługi. Cztery z pięciu repozytoriów budują się i wdrażają same po pushu na główną gałąź. Piąte nie, i powód jest pouczający. Podpięcie automatycznego budowania wymaga zgody na dostęp do repozytorium, udzielanej w panelu. Zgody nie da się wystawić przez API — to celowe zabezpieczenie i trudno mieć o nie pretensje. Efekt jest jednak taki, że jedna instancja przez tygodnie wdrażała się ręcznie i regularnie zostawała w tyle. Wniosek ogólniejszy: w każdym łańcuchu automatyzacji znajdzie się jeden krok wymagający człowieka. Warto go znać z nazwy, bo to on decyduje, ile naprawdę zaoszczędziłeś. Pierwsza wersja skryptu weryfikującego czekała stałe trzy i pół minuty. Działało, dopóki wdrożenie nie zaczęło raz trwać czterdzieści sekund, a raz osiem minut. Twardy sleep jest zawsze złym rozwiązaniem: albo czekasz za długo, albo sprawdzasz stan, którego jeszcze nie ma. Zastąpiliśmy go odpytywaniem co pięć sekund z warunkiem wyjścia na zmianie identyfikatora wersji. Log budowania mówi, że budowanie się udało. Nie mówi, że działa to, co miało zacząć działać. Nie da się też sprawdzić wersji silnika po adresie z metadanymi — zwraca znacznik budowania, nie numer wydania. Jedyna wiarygodna metoda to pobrać stronę i poszukać
wdrożenia · kolofon
Paweł · 10 września 2026
Strażnicy z błędów, nie z teorii
„Sześć skryptów uruchamianych przed commitem. Żaden nie powstał z podręcznika dobrych praktyk — każdy jest pamiątką po konkretnej wpadce, którą znalazłem na telefonie zamiast w testach.” W projekcie jest sześć skryptów kontrolnych. Wszystkie mają wspólną cechę: powstały po fakcie, każdy po jednej konkretnej wpadce. Silnik ma być niezależny od języka. Brzmi oczywiście, dopóki ktoś nie wpisze słowa „Czytaj" wprost w komponencie — wtedy angielska wersja serwisu dostaje polski przycisk i nikt tego nie widzi, dopóki nie otworzy jej czytelnik. Wyszukiwanie takich napisów po polskich znakach diakrytycznych nie działa. „Czytaj", „minuty czytania" i typograficzne cudzysłowy nie mają ani jednego ogonka. Strażnik szuka więc po miejscu, nie po zawartości: każdy tekst renderowany dla czytelnika musi pochodzić ze słownika, nie z literału. Drugi strażnik pilnuje odwrotnej strony: instancja musi zadeklarować wszystkie napisy, które zna silnik. Świadome braki — formy liczby mnogiej, których angielski nie ma — siedzą na liście wyjątków i są tam wymienione z nazwy. Ten jest najbardziej pouczający. Serwis miał ustawienie wyłączające podpis silnika w stopce. Działało: w wygenerowanym HTML-u nie było ani słowa o silniku. Ale stała z nazwą i tak jechała do przeglądarki w paczce z kodem, bo warunek sprawdzany jest w czasie wykonania i bundler nie ma jak wyciąć martwej gałęzi. Ktoś, kto otworzyłby narzędzia deweloperskie, znalazłby nazwę, która miała być ukryta. Strażnik wywala teraz budowanie, gdy konfiguracja jest czytana zachłannie — nawet w komentarzu dokumentacyjnym, bo tam też po
proces · kolofon
Paweł · 10 września 2026
Sto dziewięć wydań w trzydzieści siedem dni
„Każda zmiana w silniku to nowe wydanie z numerem, tagiem i wpisem w changelogu. Brzmi jak biurokracja, dopóki nie zobaczysz, co się dzieje, gdy jedna instancja zostaje trzy wersje w tyle.” Pierwszy commit silnika: czwartego sierpnia. Ostatnie wydanie: dziesiątego września. W tym czasie sto dziewięć otagowanych wersji. Średnio wydanie co osiem godzin. To nie jest powód do dumy, tylko konsekwencja jednej zasady: KAŻDA zmiana kodu silnika to nowe wydanie. Nie ma poprawek doklejanych do numeru, który już wyszedł. Z jednej sytuacji. Instancja stała na wersji, której changelog opisywał coś innego, niż faktycznie w niej było — bo poprawkę doklejono do wydanego numeru zamiast wypuścić następny. Diagnoza usterki zajęła wtedy tyle, co jej naprawa. Wpis w changelogu opisuje to, co dane wydanie faktycznie robiło. Poprawka idzie jako następny numer, nie jako przepisanie poprzedniego. Historia ma być zapisem tego, co się stało, a nie tego, co chcielibyśmy, żeby się stało. Silnik żyje w swoim repozytorium, ale wykonuje się w instancjach. Które pliki do niego należą, mówi manifest — czterdzieści osiem pozycji, katalogi w całości albo pojedyncze pliki. Kontrola przed synchronizacją nie jest opcjonalna. Repozytorium instancji bywa edytowane także innymi narzędziami, a zmiana zrobiona tam w pliku silnika zniknie bez śladu przy najbliższym przebiegu. Ten przełącznik istnieje, bo raz tak zniknęła. Manifest jest na własnej liście, więc synchronizuje się sam. Bez tego instancja zostawałaby ze starym spisem ścieżek i po cichu przegapiała pliki dołożone w nowszej wersji — co dokładnie pr
proces · kolofon
Paweł · 10 września 2026
Silnik i instancja — gdzie postawić granicę
„Ten sam kod obsługuje kilka serwisów o różnych markach, językach i treści. Cała sztuka polega na tym, żeby wiedzieć, co należy do silnika, a co do instancji — i pilnować tej granicy, gdy zaczyna przeciekać.” Kolejne serwisy kończyły się tak samo: przy drugim okazywało się, że osiemdziesiąt procent kodu jest wspólne, tylko rozjechane w dwóch repozytoriach. Kolofon zaczął się od próby zrobienia tego porządnie. Zasada jest jedna: silnik nie wie nic o marce, autorach ani treści, którą renderuje. Wszystko dostaje jako dane, przez sześć wyraźnie nazwanych szwów. Wszystko poza tą listą należy do silnika i jest wciągane do instancji przez synchronizację. Instancja tych plików nie edytuje — a jeśli zrobi to mimo wszystko, zmiana zniknie przy najbliższym przebiegu. Naturalny odruch: skoro to biblioteka, opublikuj ją jako pakiet i wciągaj zależnością. Nie zrobiłem tego i zapisałem powód w dokumentacji, żeby za pół roku samemu do niego nie wracać. Powody są dwa i oba są długiem, nie zaletą. Po pierwsze, moduł czytający wpisy skanuje je globem po ścieżce względnej — w katalogu zależności taki glob wskaże w pustkę. Po drugie, silnik w kilkunastu miejscach importuje treść instancji, więc zależność jest dwukierunkowa: obcy użytkownik nie ma czego zaimportować. Do czasu spłacenia obu obowiązuje vendoring, czyli kopiowanie plików silnika do instancji. Brzmi prymitywnie i takie jest. Ale jest zapisane, ma mechanizm i wiadomo, kiedy się skończy — a to więcej niż można powiedzieć o większości długu technicznego. Serwis ma wersję polską i angielską. Pierwsza wersja wiązała je mapą adresów: słownik, któr
architektura · kolofon
Paweł · 10 września 2026

Cztery awarie jednego wdrożenia
Prosta usługa w .NET 10, Azure App Service, baza SQL. Miało zająć godzinę. Zajęło pięć i po drodze wywaliło się na cztery różne sposoby — każdy z innej warstwy i każdy uczący czegoś innego. Zadanie było proste: mała usługa zbierająca pomiary z urządzeń, .NET 10, Entity Framework, baza w Azure SQL, hosting na App Service. Kilka endpointów, dwie tabele. Zakładałem godzinę. Kod napisał się w kwadrans. Cała reszta poszła na wdrożenie i to ono okazało się właściwą lekcją. Wywaliło się cztery razy, za każdym razem z innego powodu i w innej warstwie — od formatu archiwum po architekturę startu aplikacji. Opisuję po kolei, bo każda z tych rzeczy dopadnie każdego, kto wdraża z Windowsa na Linuksa. Trzy z czterech miały tę samą cechę: komunikat wskazywał zupełnie inne miejsce niż przyczyna. Aplikacja jest banalna. Rejestr urządzeń, pomiary, dwa endpointy do zapisu i odczytu. Wszystko w minimalnym API, bez kontrolerów. Encje zwykłe, dwie klasy. Jedna rzecz warta uwagi dla kogoś, kto wraca do .NET po dłuższej przerwie: właściwości oznaczone jako wymagane. Kompilator wymusza ich ustawienie przy tworzeniu obiektu, więc nie trzeba już obiecywać mu wykrzyknikiem, że ktoś to kiedyś wypełni. Zbudowałem paczkę, wysłałem, dostałem błąd wdrożenia bez sensownego komunikatu — samo „Deployment failed" i kod 400. Kod 400 nie mówi nic. Prawdę widać dopiero w dzienniku wdrożenia po stronie serwera — a tam wysypało się kilkadziesiąt linii tego samego wzorca. Popatrz na ścieżki. Ukośniki odwrotne w środku, tam gdzie powinny być zwykłe. Standard ZIP wymaga ukośników zwykłych w ścieżkach wewnęt
dotnet · azure · wdrożenia