Synchronizacja pamięci bez zasady „ostatni zapis wygrywa”
· 4 min czytania
Dwie maszyny, jeden zestaw notatek, jedna z nich edytowana, gdy druga spała. Każdy system synchronizacji musi na to odpowiedzieć, a większość odpowiada porównaniem znaczników czasu, które po cichu odrzuca jedną stronę. To w porządku w przypadku szkicu, który możesz napisać od nowa. Nie jest w porządku w przypadku pliku, który agent czyta przed każdą turą, bo to, co zostaje odrzucone, to linia, którą napisałeś, żeby coś się więcej nie powtórzyło.
Dlatego synchronizacja pamięci w OHarness w ogóle nie ma ścieżki „ostatni zapis wygrywa”. Jest uzgadnianie trójstronne, a tam, gdzie ono nie może rozstrzygnąć, jest konflikt, który czeka na ciebie.
Najpierw lokalnie, a jeśli chcesz — tylko lokalnie
Notatki znajdują się w <project>/.oharness/memory/*.md. Działają bez logowania, działają offline i są zwykłym Markdownem, który możesz edytować w dowolnym edytorze. Nic w chmurze nie jest nośne: synchronizacja to funkcja nałożona na pliki, które bez niej są już kompletne.
Zalogowanie się włącza automatyczną synchronizację, wykrywane w ciągu około piętnastu sekund, gdy harness działa. Strona Pamięć pozwala ją wyłączyć i zsynchronizować od razu. Wyłączenie przetrwa restart procesu i ponowne zalogowanie oraz blokuje żądania automatyczne i ręczne, dopóki ponownie jej nie włączysz — przełącznik, który po cichu wraca do poprzedniego stanu przy następnym uruchomieniu, nie jest przełącznikiem.
Jedno uczciwe ograniczenie, podane w dokumentacji, a nie odkrywane później: wyłączenie anuluje trwające żądania, ale danych, które chmura już przyjęła, nie da się wycofać. Wyłączenie zatrzymuje następne wysyłanie; nie usuwa tego, co już tam jest.
Trójstronnie, nie dwustronnie
Uzgadnianie potrzebuje trzech stanów, a nie dwóch: tego, co jest tutaj, tego, co jest tam, i tego, na co obie strony ostatnio się zgodziły. Ten ostatni to stan bazowy, przechowywany w ~/.oharness/memory-sync/, i to on stanowi różnicę między „te się różnią” a „ten się zmienił”.
Dalej przypadki są niczym niezwykłym i o to chodzi:
- bez zmian po obu stronach — pominięte, bez odczytu, bez zapisu
- zmienione po jednej stronie — skopiowane na drugą
- zmienione po obu stronach — konflikt, pokazany i pozostawiony tobie
Usunięcia są nagrobkami (tombstones), a nie brakiem, bo brakującego pliku nie da się odróżnić od pliku, który jeszcze nie dotarł, a traktowanie drugiego jak pierwszego to sposób, w jaki system synchronizacji usuwa twoją pracę przy wolnej sieci.
Po stronie chmury R2 przechowuje niezmienne rewizje w Markdownie oraz manifest, który jest podmieniany atomowo:
users/<server-verified-user-id>/projects/<project-id>/memory/
manifest.json
objects/<note-id>/<revision-id>.md
Zapisy manifestu używają compare-and-swap opartego na ETag, więc równoczesna nieaktualna zmiana jest odrzucana, a nie stosowana. Rewizje są niezmienne, więc przegrana strona konfliktu nadal jest istniejącym obiektem. A gdy manifest się nie zmienił, pobierany jest tylko manifest — typowy przypadek, maszyna działająca całe popołudnie bez niczego do zrobienia, kosztuje jeden mały odczyt.
Tożsamością projektu jest korzeń Git (root commit), dzięki czemu ten sam projekt w dwóch klonach pod dwiema różnymi ścieżkami jest jednym projektem. Projekty spoza Git korzystają awaryjnie z id opartego na ścieżce bezwzględnej i dlatego nie są automatycznie mapowane między urządzeniami. To realne ograniczenie, a nie drobna niedoskonałość do zamaskowania: alternatywą jest heurystyka, która od czasu do czasu scala dwa niepowiązane katalogi, a ta awaria nie ma dobrej wersji.
Każdy lokalny katalog pamięci jest przypięty do pierwszego konta i pierwszego źródła w chmurze, które go zsynchronizowały. Zmiana któregokolwiek z nich wstrzymuje synchronizację i prosi o użycie osobnego katalogu. Celowo nie ma ponownego przypisania jednym kliknięciem, bo wersja tej operacji na jedno kliknięcie to ta, która wysyła notatki jednej osoby na konto innej osoby.
Czym to nie jest
Dwie rzeczy, które warto powiedzieć bez ogródek.
To nie jest szyfrowanie end-to-end. R2 jest prywatny, a wszystko idzie przez TLS, ale chmura może odczytać treść notatek. Jeśli to zły kompromis dla tego, co zapisujesz, wyłącz synchronizację i trzymaj pliki lokalnie — to wspierana konfiguracja, a nie okrojona.
Synchronizowane są tylko notatki pamięci. Nie logi sesji, nie pliki, nie dane uwierzytelniające i nie ślad audytowy uprawnień — ten ostatni zostaje na maszynie, która podjęła decyzje, i jest usuwany razem ze swoją sesją.
Są też limity, i to takie, jakie ma pierwsza wersja: 500 wpisów manifestu na projekt, łącznie z nagrobkami, 128 KiB na treść notatki, 2 MiB na zserializowaną migawkę. Stare rewizje są zachowywane; odśmiecanie rewizji i nagrobków nie zostało jeszcze zaimplementowane. Usunięte notatki i przegrane wersje konfliktów są archiwizowane w ~/.oharness/memory-backups/ i można je przywrócić przez edytor, choć przeglądarki kopii zapasowych jeszcze nie ma.
Dlaczego wtyczka
Pamięć to wbudowana wtyczka architektury, a nie serwer MCP. Rejestruje swoje narzędzie, swój indeks w prompcie systemowym dla każdej tury, swoją stronę i swój synchronizator działający w tle przez ten sam PluginHost, przez który przechodzą wtyczki modeli, narzędzi i audytu — a { "id": "memory", "enabled": false } w plugins.json usuwa wszystkie cztery.
To test, który system wtyczek albo zdaje, albo oblewa. Jeśli funkcje dostarczane w pudełku docierają do środowiska uruchomieniowego prywatnymi drzwiami, drzwi publiczne są dekoracją, a każdy, kto pisze pod nie, przekonuje się o tym powoli. Rozszerzenie stron pozostaje małe z tego samego powodu: zarejestrowane deskryptory i renderery należące do hosta, a nie dowolny JavaScript frontendu ani SDK oparte na iframe. Powierzchnia, która może wszystko, to powierzchnia, której nikt potem nie może zmienić.
OHarness to harness agenta open source na licencji MIT. Dokumentacja pamięci i synchronizacji w pełni opisuje reguły konfliktów, układ przechowywania i granice konfiguracji, a własny serwer synchronizacji wyjaśnia, jak uruchomić całość na własnej infrastrukturze.