Pamięć i synchronizacja
Pamięć to wbudowana wtyczka architektury oHarness (memory), a nie serwer MCP. Rejestruje narzędzia pamięci, indeks w prompcie systemowym, przywoływanie przy każdej wiadomości, opcjonalną ekstrakcję, stronę Memory i synchronizację w tle — wszystko przez ten sam PluginHost co wtyczki modeli, narzędzi i audytu. Rozszerzenie strony pozostaje niewielkie: zarejestrowane deskryptory plus renderery należące do hosta (obecnie memory), a nie dowolny JavaScript interfejsu czy SDK dla iframe.
Gdzie leżą notatki
Jedna notatka na plik Markdown, z frontmatterem w JSON:
---
{"id":"pricing-borderless-3fa","summary":"Pricing cards use fills, not borders","type":"feedback","tags":["design"],"createdAt":1789000000000,"updatedAt":1789000000000}
---
Cards on the pricing page are separated by fill and whitespace.
**Why:** the outlines were the loudest thing on a page of numbers.
**How to apply:** keep the border declared but transparent. See [[pricing-layout-a91]].
| Zakres | Katalog | Zawiera |
|---|---|---|
project | <project>/MEMORY/ | Decyzje, ograniczenia i odniesienia, które przenoszą się i eksportują razem z projektem |
user | ~/.oharness/memory/user/ | Kim jest użytkownik i jak lubi pracować — w każdym projekcie |
Każdy zarejestrowany projekt ma .oharness/project.json. Jego stały identyfikator utrzymuje powiązanie sesji po stronie Home, stanu synchronizacji i statystyk przywoływania, gdy folder zostanie przemianowany lub wyeksportowany. Repozytorium git początkowo używa jako tego identyfikatora swojego pierwszego commita; inne katalogi dostają losowy identyfikator projektu. OHARNESS_HOME przenosi notatki użytkownika i stan lokalny maszyny, ale nigdy nie przenosi katalogu MEMORY/ projektu.
memory.dir (lub starsze memoryDir) w konfiguracji nadpisuje katalog projektu, względnie do jego katalogu głównego. Gdy nie da się utworzyć katalogu użytkownika, notatki projektu pozostają dostępne w <project>/MEMORY; niedostępne są tylko notatki na poziomie użytkownika i stan Memory po stronie Home, o czym informuje ostrzeżenie.
Typy
| Typ | Domyślny zakres | Co zapisuje |
|---|---|---|
user | użytkownik | Rolę użytkownika, jego wiedzę i preferencje pracy |
feedback | projekt | Poprawkę albo podejście, które użytkownik potwierdził |
project | projekt | Decyzję i jej powód, ograniczenie, trwającą pracę, której nie da się wywnioskować z kodu |
reference | projekt | Gdzie znajdują się informacje zewnętrzne |
Tylko notatki user domyślnie trafiają do zakresu użytkownika. Feedback domyślnie trafia do projektu, bo większość poprawek dotyczy tego, jak pracuje się z jedną bazą kodu; taką, która ewidentnie obowiązuje wszędzie, celowo umieszcza się w zakresie użytkownika.
Notatki zapisane, zanim pojawiły się typy, nie mają typu i nadal działają. type wchodzi do hasha treści notatki tylko wtedy, gdy jest obecne, więc istniejące notatki zachowują hashe, które mają już ich bazy synchronizacji.
Zapisywanie notatek
Model zapisuje przez remember, czyta przez memory i usuwa przez forget. remember z wtyczki pamięci przechodzi bez pytania w każdym trybie uprawnień z wyjątkiem plan, który odrzuca je jak każdy inny zapis. Reguła ask lub deny wskazująca remember nadal obowiązuje, forget nadal pyta, a narzędzie innej wtyczki nazwane remember nie dostaje takiego wyjątku. Tagi są przycinane, dzielone po przecinkach i spacjach oraz deduplikowane przed zapisaniem notatki. Sekcja Memory w prompcie systemowym mówi, kiedy notatkę warto zapisać, a kiedy nie. remember zwraca istniejące notatki najbardziej powiązane z tym, co właśnie zapisano, oznacza prawdopodobny duplikat i wymienia linki wskazujące na notatki, które jeszcze nie istnieją. Wszystko, co wygląda na dane uwierzytelniające, jest odrzucane. Notatki powinny mieć tagi dla innych słów, których ktoś mógłby użyć: synonimów i terminu w drugim używanym języku, bo przywoływanie dopasowuje słowa, a nie znaczenie.
Notatki linkują do siebie w treści jako [[note-id]]. Linki są wyprowadzane z treści przy każdym ładowaniu notatek; nic o nich nie jest przechowywane osobno, a protokół synchronizacji nie wie o ich istnieniu. Link rozwiązuje się najpierw w zakresie projektu, potem w zakresie użytkownika. Link do brakującej notatki jest zachowywany: zwykle nazywa notatkę, którą ktoś zamierzał napisać.
Automatyczna ekstrakcja
Domyślnie wyłączona, bo każdy przebieg to wywołanie modelu na środkach użytkownika. Z memory.autoExtract.onStop koniec uruchomienia przekazuje małemu modelowi rozmowę od ostatniego przebiegu, gdy od tego czasu zebrało się co najmniej minUserTurns prawdziwych wiadomości użytkownika. Z onCompaction (lub onStop) część odcinka, którą kompaktowanie zaraz usunie i której żaden przebieg jeszcze nie przeczytał, jest przekazywana najpierw, choćby była krótka, bo to ostatnia szansa, by ją przeczytać. Wznowiona sesja zaczyna liczyć od miejsca wznowienia, zamiast ponownie wyciągać całą historię. Z subagentów nigdy nie prowadzi się ekstrakcji.
Ekstraktor widzi istniejące notatki najbardziej powiązane z rozmową. Kilka najtrafniejszych dostaje pełną treść, w ramach budżetu rozmiaru, i tylko te mogą zostać zaktualizowane; reszta jest wymieniona przez streszczenie, do linkowania. Aktualizacja zastępuje treść, więc zastąpiona wersja jest najpierw zapisywana w kopii zapasowej w ~/.oharness/memory-backups/. Odpowiedź ekstraktora jest traktowana jako niezaufana: aktualizacje identyfikatorów, których mu nie pokazano, są odrzucane, linki do identyfikatorów, których mu nie pokazano, tracą nawiasy, dane uwierzytelniające są usuwane, a nowe notatki w jednej partii odwołują się do siebie jako [[new:N]], dopóki nie dostaną identyfikatorów. Konfiguracja repozytorium może wyłączyć ekstrakcję; włączyć ją może tylko własna konfiguracja użytkownika.
Ekstrakcja kończy się po zakończeniu uruchomienia, które ją wywołało, więc zapisane notatki są ogłaszane na początku następnej wiadomości: „saved N memories from this conversation”, w każdym interfejsie.
Przywoływanie
Dwie ścieżki podsuwają notatki modelowi:
- Indeks. Na początku każdego uruchomienia — jedna linia na notatkę, ostatnio aktualizowane najpierw, do
memory.indexCharsznaków. Reszta jest liczona, a nie gubiona; narzędziememoryprzeszukuje wszystkie. - Przywoływanie przy każdej wiadomości. Każda wiadomość użytkownika jest oceniana względem każdej notatki, a najlepsze dopasowania są dołączane do tej wiadomości wewnątrz
<recalled-memory>, z notatkami, do których linkują, wymienionymi przez streszczenie. Interfejsy pokazują „recalled N memories”. Nic nie jest dołączane, gdy nic nie przekracza progu — i tak jest zwykle. Gdy nie ma żadnych notatek, indeks zastępują dwie linie mówiące, kiedy jakąś zapisać, zamiast pełnych wskazówek.
Ocena jest leksykalna i działa lokalnie: słowa są sprowadzane do rdzenia, tekst CJK dzielony na nakładające się pary, a dopasowania w streszczeniu i tagach ważą więcej niż dopasowania w treści. Aby notatka została w ogóle dołączona, musi pasować w streszczeniu lub tagach. Notatka dołączona raz nie jest dołączana ponownie, dopóki wiadomość, która ją niesie, pozostaje w transkrypcji — także po wznowieniu sesji lub restarcie aplikacji; identyfikatory są odczytywane z metadanych wiadomości. Kompaktowanie zwalnia notatki, których wiadomości streściło. Dołączone notatki leżą we własnym bloku wiadomości użytkownika, zapisanym w metadanych wiadomości, więc transkrypcje, tytuły sesji i historia w interfejsie webowym pokazują własne słowa użytkownika. Subagenci nie przywołują.
Niepowodzenie przywoływania nigdy nie blokuje wysłania wiadomości.
Limity
| Ustawienie | Domyślnie | Znaczenie |
|---|---|---|
memory.maxNotes | 5000 | Na zakres. Powyżej tej liczby najdawniej modyfikowane pliki nie są ładowane, a ostrzeżenie mówi, ile ich jest; nowe notatki są odrzucane, dopóki części nie usuniesz |
memory.maxNoteBytes | 131072 | Rozmiar treści; serwer synchronizacji stosuje też własny limit |
memory.indexChars | 4000 | Znaki streszczeń w prompcie systemowym |
memory.recall.enabled | true | Przywoływanie przy każdej wiadomości |
memory.recall.perTurn | 3 | Notatki dołączane do jednej wiadomości |
memory.recall.budgetChars | 2000 | Znaki, jakie mogą zająć te notatki |
memory.recall.minScore | 1.2 | Próg dołączenia |
memory.autoExtract.onCompaction / onStop | false | Automatyczna ekstrakcja |
memory.autoExtract.minUserTurns | 3 | Krótsze rozmowy są pomijane |
To, jak często każda notatka była przywoływana, jest przechowywane osobno dla każdej maszyny w ~/.oharness/memory-usage/, poza notatkami, więc przywoływanie nigdy nie powoduje wysyłki synchronizacji. Lekko wpływa na ranking i nigdy nie jest synchronizowane.
Synchronizacja z chmurą
- Notatki zawsze działają lokalnie, bez logowania i offline.
- Zalogowanie domyślnie włącza automatyczną synchronizację. Synchronizacja działa w obie strony: lokalne zmiany idą w górę, a zmiany z innych urządzeń same spływają w dół.
- Strona Memory oferuje 关闭云同步 i 立即同步. Przełącznik obejmuje oba zakresy. Wyłączenie przetrwa restarty i ponowne logowanie i blokuje automatyczne oraz ręczne zapytania do chmury, dopóki nie zostanie jawnie włączone ponownie. Lokalne narzędzia i edycja nadal działają.
- Wyłączenie synchronizacji anuluje trwające zapytania. Danych już przyjętych przez Cloud nie da się wycofać; wyłączenie synchronizacji nie usuwa kopii w chmurze.
- Każdy logiczny zakres projektu/użytkownika jest przypięty do pierwszego konta i originu Cloud, które go synchronizowały. Zmiana konta lub originu wstrzymuje synchronizację. Strona nie oferuje niebezpiecznego przypisania jednym kliknięciem.
Strona utrzymuje ~/.oharness/plugins.json (OHARNESS_HOME to nadpisuje), w runtimePlugins[id=memory].config.syncTargets[sha256(stableScopeKey)]:
{ "userId": "verified-account-label", "origin": "https://your-cloud.example", "enabled": false }
Nie umieszczaj w tym pliku kluczy R2 ani konfiguracji Supabase. URL Cloud pozostaje w cloud.json; dane uwierzytelniające R2 istnieją tylko w środowisku wdrożenia synchronizacji. Aby wyłączyć całą wtyczkę (stronę, narzędzia, prompty, przywoływanie i pracę w tle), dodaj { "id": "memory", "enabled": false } do runtimePlugins; zadziała to przy następnym utworzeniu Harness. Rejestracja projektu nadal tworzy standardowe puste katalogi MEMORY/ i ARTIFACTS/. Dla osadzających: harness.memory to magazyn projektu, a harness.memoryUserService — magazyn użytkownika; oba są nieobecne, gdy wtyczka jest wyłączona. AgentOptions.memory pozostaje niskopoziomowym API osadzania i daje przywoływanie bez hosta wtyczek.
Kiedy synchronizuje
Jeden koordynator na proces planuje każdy katalog notatek, niezależnie od tego, ile projektów ma otwartych lokalny serwer webowy lub aplikacja desktopowa. Każdy katalog ma w procesie jedną usługę synchronizacji, więc własne notatki użytkownika synchronizują się raz, a nie raz na każdy otwarty projekt.
- Lokalny zapis jest wysyłany po około trzech sekundach; seria zapisów to jeden przebieg.
- W przeciwnym razie katalog jest sprawdzany co minutę, a gdy po żadnej stronie nic się nie zmienia — rzadziej, do co pięć minut.
- Twoje własne notatki są sprawdzane zawsze. Notatki projektu są sprawdzane tylko wtedy, gdy projekt jest w użyciu (wysłano w nim wiadomość lub otwarta jest strona Memory), i przez trzydzieści minut później.
- Proces ogranicza się do około dwudziestu zapytań na minutę, zostawiając miejsce pod limitem usługi synchronizacji na konto dla innych urządzeń.
429wstrzymuje każdy katalog do czasu z nagłówkaRetry-After, a strona informuje, że synchronizacja była zbyt częsta i zostanie ponowiona, zamiast winić konfigurację.
立即同步 zawsze uruchamia się od razu.
Co pokazuje strona
Przełącznik, „ostatnia synchronizacja” i błędy obejmują oba zakresy: synchronizacja liczy się jako włączona tylko wtedy, gdy jest włączona dla obu, pokazany czas to starszy z dwóch, a każdy błąd mówi, czy dotyczy notatek projektu, czy twoich własnych. Strona pokazuje też, co przyniosło ostatnie pobranie (dodane, zmienione, usunięte), a w bezpłatnym planie — jego limity.
Porządkowanie notatek
Lista 待整理 na stronie to przegląd wykonywany na tej maszynie, bez modelu:
| Wynik | Znaczenie | Co możesz zrobić |
|---|---|---|
| Ten sam temat | Od dwóch do sześciu notatek w jednym zakresie, których streszczenia i tagi w większości się pokrywają | Scal lub odrzuć |
| Zepsuty link | [[id]] nie wskazuje żadnej notatki | Otwórz notatkę lub odrzuć |
| Brakujący plik | Notatka projektu podaje ścieżkę względną, której nie ma w projekcie | Otwórz lub odrzuć |
| Nieużywana | Ani przywołana, ani edytowana przez memory.consolidate.unusedDays (90) | Otwórz lub odrzuć |
| Za długa | Treść ponad 8 000 znaków | Otwórz lub odrzuć |
Odrzucenie jest zapisywane lokalnie względem bieżących wersji notatek, więc edycja którejś z nich przywraca wynik.
Scalanie otwiera edytor z wersją roboczą: od małego modelu (生成合并建议) albo wszystkie oryginały połączone razem (手动合并). Zastosowanie zachowuje identyfikator najstarszej notatki, usuwa pozostałe, kieruje każdy link [[deleted]] w tym zakresie na zachowaną notatkę i robi kopię zapasową każdej notatki, którą zmienia. Scalenie zaplanowane z notatek, które od tego czasu się zmieniły, jest odrzucane.
Przy włączonej synchronizacji z chmurą zakres jest najpierw synchronizowany, a scalenie zatwierdzane w chmurze przed tą maszyną (commitFirst), więc edycja z innego urządzenia w międzyczasie odrzuca scalenie, zamiast zamieniać się w konflikt. Scalona treść musi zmieścić się w limicie usługi synchronizacji na notatkę. Przy wyłączonej synchronizacji scalenie to zwykła lokalna edycja.
Scalanie w tle jest domyślnie włączone tylko w płatnych planach ("auto": "subscribers"): działa, gdy ostatnia odpowiedź usługi synchronizacji mówiła, że konto albo którykolwiek zespół, do którego należy, ma płatny plan, i pozostaje wyłączone dla kont bezpłatnych oraz zawsze, gdy plan jest nieznany (synchronizacja z chmurą wyłączona, brak logowania). true lub false to nadpisuje:
{ "memory": { "consolidate": { "auto": false, "cooldownHours": 24, "maxMerges": 3 } } }
Gdy jest włączone, przebieg startuje dwie minuty po uruchomieniu, najwyżej raz na okres karencji, i scala do maxMerges grup notatek o tym samym temacie za pomocą małego modelu, tą samą ścieżką co strona. Nigdy nie działa na pozostałych wynikach. Konfiguracja projektu może to wyłączyć, ale nie włączyć.
Kopie zapasowe i przywracanie
Kopia jest zapisywana w ~/.oharness/memory-backups/, zanim jakakolwiek notatka zostanie zastąpiona lub usunięta bez tego, że ktoś wybrał tę wersję: usunięcie na stronie, forget, wybór strony chmury w konflikcie, automatyczne pobranie, które zmienia lub usuwa notatkę, przepisanie przez ekstraktor oraz przywrócenie zastępujące nowszą wersję. Strona Memory wymienia pięćdziesiąt najnowszych z informacją, kiedy i dlaczego, i przywraca każdą z nich do katalogu, z którego pochodzi; notatka synchronizuje się potem jak każda inna lokalna zmiana. Kopie zapasowe zapisane, zanim zaczęto zapisywać powody, też są wymieniane i można je przywrócić.
Przechowywanie i uzgadnianie
Serwer synchronizacji udostępnia uwierzytelnione /v1/memory/identity, /snapshot i /commit. Nie przechowuje wierszy w bazie danych. R2 trzyma niezmienne rewizje Markdown oraz atomowo podmieniany manifest JSON:
users/<server-verified-user-id>/projects/<project-id>/memory/
users/<server-verified-user-id>/memory/ user scope
manifest.json
objects/<note-id>/<revision-id>.md
Implementacje mogą korzystać z innego systemu tożsamości lub przechowywania; dokładne kształty zapytań, algorytm hashowania, zachowanie CAS i kody błędów opisuje Protokół synchronizacji.
Lokalna baza (~/.oharness/memory-sync/) umożliwia uzgadnianie trójstronne: niezmienione notatki są pomijane, a zmiany po jednej stronie kopiowane. Obok niej klient trzyma kopię każdej notatki w wersji z ostatniej synchronizacji, więc notatka zmieniona po obu stronach jest scalana, a nie tylko oznaczana:
- streszczenie i typ pochodzą od tej strony, która je zmieniła;
- tagi są łączone jako zbiory — z zachowaniem tego, co dodała którakolwiek strona, i usunięciem tego, co którakolwiek strona usunęła;
- treści są scalane linia po linii, a edycje w różnych miejscach są łączone;
- edycja kontra usunięcie zachowuje edycję, bo ponowne usunięcie to jedno kliknięcie, a utracona edycja przepada.
Przed scaleniem obie wersje trafiają do kopii zapasowej. To, czego nie da się połączyć (dwa różne streszczenia lub typy, edycje tych samych lub sąsiednich linii, scalenie zbyt duże dla planu albo notatka bez kopii wersji z ostatniej synchronizacji), staje się konfliktem. Konflikt wstrzymuje tylko własną notatkę: wszystko inne nadal się wysyła i pobiera. Strona Memory pokazuje wersję z ostatniej synchronizacji, wersję tej maszyny i wersję z chmury, i oferuje zachowanie wersji tej maszyny, przyjęcie wersji z chmury, zachowanie obu (wersja z chmury staje się nową notatką obok) albo ręczne scalenie; to, co zostaje odłożone, trafia do kopii zapasowej, niezależnie od strony, z której pochodzi. Nie ma nadpisywania typu „wygrywa ostatni zapis”. Usunięcia to nagrobki (tombstones), a zapisy manifestu używają compare-and-swap na ETag.
Zmiany zbiorcze, których nikt nie przegląda notatka po notatce (konsolidacja), są zapisywane najpierw w chmurze: każda zaangażowana notatka musi być w pełni zsynchronizowana, commit niesie sprawdzoną wersję manifestu, a partia jest zapisywana lokalnie dopiero, gdy usługa ją przyjmie. Zmiana z innego urządzenia w międzyczasie jest odrzucana przez usługę, zamiast stać się konfliktem.
Zapytania wymieniają hashe notatek, które ma ta maszyna (have); serwer ani nie czyta, ani nie zwraca tych treści, więc synchronizacja, przy której niewiele się zmieniło, przesyła niewiele. Commity są dzielone według limitu zmian, który ogłasza serwer. Nagrobki starsze niż okno serwera są usuwane z manifestu; maszyna, która wraca po tym czasie, widzi, że notatki, którą ostatnio widziała jako zsynchronizowaną i niezmienioną, nie ma już na liście, i usuwa ją, a notatka, którą w międzyczasie edytowała, staje się konfliktem. Dwa identyfikatory notatek różniące się tylko wielkością liter są odrzucane, bo systemy plików macOS i Windows nie pomieszczą obu.
Synchronizacja ponawia przejściowe błędy po 60-sekundowej karencji; ręczna synchronizacja może ponowić od razu. Konflikty czekają na użytkownika. Strona robi kopie zapasowe usuniętych notatek oraz przegrywających lokalnych wersji konfliktów w ~/.oharness/memory-backups/; forget robi to samo. Bezpośrednie edycje w systemie plików są nadal obsługiwane, ale równoczesny zewnętrzny zapis w trakcie scalania nie jest chroniony przez lokalną dla procesu blokadę zapisu; uruchamiaj jeden proces na katalog.
R2 jest prywatne i używa TLS, ale to nie jest szyfrowanie end-to-end: Cloud może czytać treść notatek. Synchronizowane są tylko notatki, a nie logi sesji, pliki ani dane uwierzytelniające.
Limity i plany
Serwer zarządza swoimi limitami i ogłasza je w każdej odpowiedzi; klienci nie trzymają ich kopii. Wartości domyślne: 5000 aktywnych notatek na projekt i na użytkownika, 500 zmian na commit, 128 KiB na treść, 16 MiB treści na odpowiedź, 90 dni na nagrobek. Obniżony limit blokuje tylko wzrost, więc użytkownik powyżej niego zawsze może usuwać. Zmienne opisuje Własny serwer synchronizacji.
Konta bezpłatne mają dodatkowo ostrzejsze limity: liczbę notatek na projekt i na użytkownika oraz rozmiar notatki, zdefiniowane w src/memory-entitlements.json usługi synchronizacji. Konta płatne nie mają limitu planu, tylko powyższy limit serwera. Plan jest odczytywany z claimu ent, który Cloud wpisuje do tokenu dostępu: płatny plan własny użytkownika lub któregokolwiek zespołu, do którego należy, liczy się jako płatny. Ograniczona jest tylko synchronizacja z chmurą; notatki na maszynie nie są, a konto bezpłatne powyżej limitu zachowuje każdą lokalną notatkę i przestaje wysyłać nowe, z komunikatem wyjaśniającym dlaczego.
Usuwanie konta
Usunięcie konta na stronie kasuje wszystko, co zsynchronizowało: strumienie sesji i pamięć w obu zakresach. Bramka wywołuje POST /v1/admin/erase usługi synchronizacji z kluczem, który tylko ona ma (OHARNESS_SYNC_ADMIN_KEY po obu stronach), zanim samo konto zostanie usunięte, i jeszcze raz potem. Usługa synchronizacji najpierw zapisuje znacznik dla konta i od tej chwili odrzuca jego zapytania o strumienie i pamięć, więc urządzenie, które wciąż jest zalogowane, nie może ponownie wysłać danych. Notatki i sesje na twoich własnych maszynach pozostają nietknięte.
Usunięcie zespołu robi to samo ze wspólną historią sesji zespołu: bramka kasuje feed/team-<id>/ (kasowanie {team} w usłudze synchronizacji) po anulowaniu subskrypcji zespołu i przed usunięciem jego wierszy, a potem jeszcze raz. Usługa synchronizacji od tej chwili odrzuca strumienie tego zespołu, nawet dla tokenu, który wciąż go wymienia. Zespoły nie mają pamięci.
Aktualizacja
Zaktualizuj serwer synchronizacji przed klientami. Nowy klient przy starym serwerze wraca do starego protokołu dla notatek projektu bez typu. Nie może synchronizować ze starym serwerem notatek na poziomie użytkownika ani notatek z typem. Stary klient, który dostanie z chmury notatkę z typem, przestaje się synchronizować z błędem sumy kontrolnej, dopóki nie zostanie zaktualizowany.
Weryfikacja i wdrożenie
Uruchom npm run test:memory, npm run test:sync-memory, npm run test:plugins, npm run typecheck i npm run build. script/memory-ui-demo.ts uruchamia odizolowany, niezalogowany interfejs na porcie 4319 z tymczasowym katalogiem domowym; nigdy nie czyta danych uwierzytelniających użytkownika. Brak konfiguracji R2 daje widoczny błąd synchronizacji bez psucia lokalnej pamięci. Po zmianach w kodzie źródłowym uruchom ponownie lokalny serwer deweloperski.