Memoria e sincronizzazione

La memoria è un plugin di architettura integrato di oHarness (memory), non un server MCP. Registra gli strumenti di memoria, l'indice nel prompt di sistema, il richiamo per messaggio, l'estrazione opzionale, la pagina Memory e un sincronizzatore in background, tutto tramite lo stesso PluginHost dei plugin di modello, strumenti e audit. L'estensione della pagina resta ridotta: descrittori registrati più renderer gestiti dall'host (attualmente memory), non JavaScript di interfaccia arbitrario né un SDK in iframe.

Dove vivono le note

Una nota per file Markdown, con frontmatter 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]].
AmbitoDirectoryContiene
project<project>/MEMORY/Decisioni, vincoli e riferimenti che si spostano e si esportano con il progetto
user~/.oharness/memory/user/Chi è l'utente e come preferisce lavorare, in ogni progetto

Ogni progetto registrato contiene .oharness/project.json. Il suo id stabile mantiene collegati le sessioni lato Home, lo stato di sincronizzazione e le statistiche di richiamo quando la cartella viene rinominata o esportata. Un repository git usa inizialmente il proprio commit radice come id; le altre directory ricevono un id di progetto casuale. OHARNESS_HOME sposta le note utente e lo stato locale della macchina, ma non sposta mai la directory MEMORY/ del progetto.

memory.dir (o il più vecchio memoryDir) nella configurazione sostituisce la directory del progetto, relativa alla radice del progetto. Quando non è possibile creare la directory utente, le note di progetto restano disponibili in <project>/MEMORY; diventano non disponibili solo le note a livello utente e lo stato della memoria lato Home, e un avviso lo segnala.

Tipi

TipoAmbito predefinitoCosa registra
userutenteIl ruolo dell'utente, le sue competenze e preferenze di lavoro
feedbackprogettoUna correzione, o un approccio che l'utente ha confermato
projectprogettoUna decisione e la sua ragione, un vincolo, un lavoro in corso non deducibile dal codice
referenceprogettoDove si trovano le informazioni esterne

Solo le note user vanno per impostazione predefinita nell'ambito utente. Il feedback va per impostazione predefinita nel progetto, perché la maggior parte delle correzioni riguarda il modo di lavorare su una specifica codebase; una correzione che vale chiaramente ovunque viene messa di proposito nell'ambito utente.

Le note scritte prima che esistessero i tipi non ne hanno e continuano a funzionare. type fa parte dell'hash del contenuto di una nota solo quando è presente, così le note esistenti mantengono gli hash già registrati nelle loro basi di sincronizzazione.

Scrivere le note

Il modello scrive con remember, legge con memory ed elimina con forget. remember del plugin di memoria passa senza chiedere in ogni modalità di permesso tranne plan, che lo rifiuta come qualsiasi altra scrittura. Una regola ask o deny che nomina remember si applica comunque, forget chiede comunque, e uno strumento di un altro plugin chiamato remember non riceve questa esenzione. I tag vengono ripuliti, divisi su virgole e spazi e deduplicati prima che la nota venga salvata. La sezione Memory del prompt di sistema dice quando vale la pena scrivere una nota e quando no. remember restituisce le note esistenti più correlate a ciò che è appena stato scritto, segnala un probabile duplicato ed elenca i link che puntano a note non ancora esistenti. Tutto ciò che somiglia a una credenziale viene rifiutato. Alle note si chiede di portare tag per le altre parole che qualcuno potrebbe usare: sinonimi, e il termine nell'altra lingua in uso, dato che il richiamo confronta parole e non significati.

Le note si collegano tra loro nel corpo come [[note-id]]. I link vengono ricavati dai corpi ogni volta che le note vengono caricate; nulla di essi viene salvato a parte, e il protocollo di sincronizzazione non sa che esistono. Un link viene risolto prima nell'ambito del progetto, poi in quello utente. Un link a una nota mancante viene mantenuto: di solito indica una nota che qualcuno intendeva scrivere.

Estrazione automatica

Disattivata per impostazione predefinita, perché ogni passaggio è una chiamata al modello sul credito dell'utente. Con memory.autoExtract.onStop, la fine di un'esecuzione passa al modello piccolo la conversazione dall'ultimo passaggio, una volta accumulati da allora almeno minUserTurns messaggi reali dell'utente. Con onCompaction (o onStop), la parte del tratto che la compattazione sta per scartare e che nessun passaggio ha ancora letto viene consegnata per prima, per quanto breve, perché è l'ultima occasione per leggerla. Una sessione ripresa inizia a contare dal punto in cui è stata ripresa, invece di estrarre di nuovo tutta la sua cronologia. Dai sub-agenti non si estrae mai.

All'estrattore vengono mostrate le note esistenti più correlate alla conversazione. Le poche più rilevanti arrivano con il corpo completo, entro un budget di dimensione, e solo quelle possono essere aggiornate; le altre sono elencate per riepilogo, per poterle collegare. Un aggiornamento sostituisce il corpo, quindi la versione sostituita viene prima salvata in ~/.oharness/memory-backups/. La sua risposta è trattata come non attendibile: gli aggiornamenti a id che non gli sono stati mostrati vengono scartati, i link a id non mostrati perdono le parentesi, le credenziali vengono oscurate, e le nuove note di uno stesso batch si riferiscono l'una all'altra come [[new:N]] finché non hanno un id. La configurazione di un repository può disattivare l'estrazione; solo la configurazione dell'utente può attivarla.

L'estrazione termina dopo che l'esecuzione che l'ha avviata è finita, quindi le note salvate vengono annunciate all'inizio del messaggio successivo: “saved N memories from this conversation”, in ogni interfaccia.

Richiamo

Due percorsi portano le note davanti al modello:

  1. L'indice. All'inizio di ogni esecuzione, una riga per nota, dalla più recentemente aggiornata, fino a memory.indexChars caratteri. Le altre vengono contate, non perse; lo strumento memory le cerca tutte.
  2. Richiamo per messaggio. Ogni messaggio dell'utente viene confrontato con ogni nota, e le corrispondenze migliori vengono allegate a quel messaggio dentro <recalled-memory>, con le note a cui rimandano elencate per riepilogo. Le interfacce mostrano “recalled N memories”. Non viene allegato nulla quando nulla supera la soglia, che è il caso più comune. Senza alcuna nota, l'indice è sostituito da due righe che dicono quando scriverne una, invece delle istruzioni complete.

Il punteggio è lessicale e calcolato in locale: le parole vengono ridotte alla radice, il testo CJK viene diviso in coppie sovrapposte, e le corrispondenze nel riepilogo e nei tag pesano più di quelle nel corpo. Una nota deve corrispondere nel riepilogo o nei tag per essere allegata. Una nota allegata una volta non viene allegata di nuovo finché un messaggio che la contiene resta nella trascrizione, anche dopo che la sessione viene ripresa o l'app riavviata; gli id vengono riletti dai metadati dei messaggi. La compattazione libera le note i cui messaggi ha riassunto. Le note allegate stanno in un blocco a sé del messaggio dell'utente, registrato nei metadati del messaggio, così trascrizioni, titoli delle sessioni e cronologia web mostrano le parole dell'utente. I sub-agenti non fanno richiamo.

Un errore del richiamo non impedisce mai l'invio di un messaggio.

Limiti

ImpostazionePredefinitoSignificato
memory.maxNotes5000Per ambito. Oltre questa soglia i file modificati meno di recente non vengono caricati e un avviso dice quanti sono; le nuove note vengono rifiutate finché alcune non vengono rimosse
memory.maxNoteBytes131072Dimensione del corpo; anche il server di sincronizzazione applica un proprio limite
memory.indexChars4000Caratteri di riepiloghi nel prompt di sistema
memory.recall.enabledtrueRichiamo per messaggio
memory.recall.perTurn3Note allegate a un messaggio
memory.recall.budgetChars2000Caratteri che quelle note possono occupare
memory.recall.minScore1.2Soglia per allegare
memory.autoExtract.onCompaction / onStopfalseEstrazione automatica
memory.autoExtract.minUserTurns3Le conversazioni più brevi vengono saltate

Quante volte ogni nota è stata richiamata viene conservato per macchina in ~/.oharness/memory-usage/, fuori dalle note, così il richiamo non provoca mai un upload di sincronizzazione. Influisce sull'ordinamento e non viene mai sincronizzato.

Sincronizzazione cloud

  • Le note funzionano sempre in locale, senza accesso e offline.
  • L'accesso attiva la sincronizzazione automatica per impostazione predefinita. La sincronizzazione va in entrambe le direzioni: le modifiche locali salgono, e quelle fatte su altri dispositivi scendono da sole.
  • La pagina Memory offre 关闭云同步 e 立即同步. L'interruttore copre entrambi gli ambiti. Lo stato disattivato persiste tra riavvii e nuovi accessi, e blocca le richieste cloud automatiche e manuali finché non viene riattivato esplicitamente. Gli strumenti locali e la modifica continuano a funzionare.
  • Disattivare la sincronizzazione annulla le richieste in corso. I dati già accettati da Cloud non possono essere ritirati; disattivare la sincronizzazione non elimina le copie nel cloud.
  • Ogni ambito logico di progetto/utente è vincolato al primo account con cui si è sincronizzato e alla sua origine Cloud. Cambiare account o origine mette in pausa la sincronizzazione. La pagina non offre alcuna riassegnazione insicura con un clic.

La pagina gestisce ~/.oharness/plugins.json (OHARNESS_HOME lo sostituisce), sotto runtimePlugins[id=memory].config.syncTargets[sha256(stableScopeKey)]:

{ "userId": "verified-account-label", "origin": "https://your-cloud.example", "enabled": false }

Non mettere chiavi R2 o configurazione di Supabase in questo file. L'URL di Cloud resta in cloud.json; le credenziali R2 esistono solo nell'ambiente del deployment di sincronizzazione. Per disattivare l'intero plugin (pagina, strumenti, prompt, richiamo e lavoro in background), aggiungi { "id": "memory", "enabled": false } a runtimePlugins; l'effetto si ha alla successiva creazione dell'Harness. La registrazione del progetto crea comunque le directory standard vuote MEMORY/ e ARTIFACTS/. Per chi incorpora il motore, harness.memory è l'archivio del progetto e harness.memoryUserService quello dell'utente; entrambi sono assenti quando il plugin è disattivato. AgentOptions.memory resta un'API di incorporamento di livello più basso e ottiene il richiamo senza un plugin host.

Quando sincronizza

Un solo coordinatore per processo pianifica ogni directory di note, per quanti progetti abbiano aperti un server web locale o l'app desktop. Ogni directory ha un solo servizio di sincronizzazione nel processo, così le note dell'utente si sincronizzano una volta sola e non una volta per ogni progetto aperto.

  • Una scrittura locale viene caricata circa tre secondi dopo; una raffica di scritture è un solo passaggio.
  • Altrimenti una directory viene controllata ogni minuto, rallentando fino a ogni cinque minuti finché nulla cambia da nessuna delle due parti.
  • Le tue note vengono sempre controllate. Le note di un progetto vengono controllate solo mentre il progetto è in uso (un messaggio inviato al suo interno, o la pagina Memory aperta) e per i trenta minuti successivi.
  • Il processo si limita a circa venti richieste al minuto, lasciando spazio sotto il limite per account del servizio di sincronizzazione agli altri dispositivi.
  • Un 429 mette in pausa ogni directory fino all'orario indicato nel suo Retry-After, e la pagina dice che la sincronizzazione è stata troppo frequente e verrà ritentata, invece di dare la colpa alla configurazione.

立即同步 viene sempre eseguito subito.

Cosa mostra la pagina

L'interruttore, “ultima sincronizzazione” e gli errori coprono entrambi gli ambiti: la sincronizzazione risulta attiva solo quando lo è per entrambi, l'ora mostrata è la più vecchia delle due, e ogni errore indica se riguarda le note di progetto o le tue. La pagina mostra anche cosa ha portato l'ultimo pull (aggiunte, modifiche, rimozioni) e, con un piano gratuito, i limiti del piano.

Riordinare le note

L'elenco 待整理 della pagina è una revisione eseguita su questa macchina, senza un modello:

RisultatoSignificatoCosa puoi fare
Stesso argomentoDa due a sei note in uno stesso ambito i cui riepiloghi e tag si sovrappongono in gran parteUnire, o ignorare
Link rottoUn [[id]] che non nomina alcuna notaAprire la nota, o ignorare
File mancanteUna nota di progetto nomina un percorso relativo che non è nel progettoAprire, o ignorare
InutilizzataNé richiamata né modificata per memory.consolidate.unusedDays (90)Aprire, o ignorare
Troppo lungaUn corpo oltre gli 8.000 caratteriAprire, o ignorare

Un rifiuto viene conservato in locale rispetto alle versioni attuali delle note, quindi modificarne una fa ricomparire il risultato.

L'unione apre un editor con una bozza: quella del modello piccolo (生成合并建议) o tutti gli originali concatenati (手动合并). Applicarla mantiene l'id della nota più vecchia, elimina le altre, fa puntare ogni link [[deleted]] di quell'ambito alla nota mantenuta ed esegue il backup di ogni nota che modifica. Un'unione pianificata a partire da note che nel frattempo sono cambiate viene rifiutata.

Con la sincronizzazione cloud attiva, l'ambito viene prima sincronizzato e l'unione viene confermata nel cloud prima che su questa macchina (commitFirst), così una modifica da un altro dispositivo nel frattempo fa rifiutare l'unione invece di trasformarsi in un conflitto. Il corpo unito deve rientrare nel limite per nota del servizio di sincronizzazione. Con la sincronizzazione disattivata, l'unione è una normale modifica locale.

L'unione in background è attiva per impostazione predefinita solo per i piani a pagamento ("auto": "subscribers"): viene eseguita quando l'ultima risposta del servizio di sincronizzazione ha indicato che l'account, o un team di cui fa parte, ha un piano a pagamento, e resta disattivata per gli account gratuiti e ogni volta che il piano è sconosciuto (sincronizzazione cloud disattivata, senza accesso). true o false sostituiscono questo comportamento:

{ "memory": { "consolidate": { "auto": false, "cooldownHours": 24, "maxMerges": 3 } } }

Quando è attiva, un passaggio parte due minuti dopo l'avvio, al massimo una volta per periodo di attesa, e unisce fino a maxMerges gruppi di note sullo stesso argomento con il modello piccolo, seguendo lo stesso percorso della pagina. Non agisce mai sugli altri risultati. La configurazione di un progetto può disattivarla ma non attivarla.

Backup e ripristino

Una copia viene scritta in ~/.oharness/memory-backups/ prima che una nota venga sostituita o rimossa senza che qualcuno abbia scelto quella versione: l'eliminazione dalla pagina, forget, la scelta della versione cloud in un conflitto, un pull automatico che modifica o rimuove una nota, una riscrittura dell'estrattore e un ripristino che sostituisce una versione più recente. La pagina Memory elenca le cinquanta più recenti con quando e perché, e ripristina qualsiasi di esse nella directory da cui proviene; la nota viene poi sincronizzata come qualsiasi altra modifica locale. Anche i backup scritti prima che venissero registrati i motivi sono elencati e ripristinabili.

Archiviazione e riconciliazione

Il server di sincronizzazione espone gli endpoint autenticati /v1/memory/identity, /snapshot e /commit. Non memorizza righe di database. R2 contiene revisioni Markdown immutabili più un manifest JSON sostituito in modo atomico:

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

Le implementazioni che usano un altro sistema di identità o di archiviazione possono farlo; le forme esatte delle richieste, l'algoritmo di hash, il comportamento CAS e i codici di errore sono specificati in Protocollo di sincronizzazione.

La base locale (~/.oharness/memory-sync/) consente una riconciliazione a tre vie: le note invariate vengono saltate e le modifiche da un solo lato vengono copiate. Accanto a essa il client conserva una copia di ogni nota com'era all'ultima sincronizzazione, così una nota modificata da entrambi i lati viene unita invece di essere semplicemente segnalata:

  • il riepilogo e il tipo vengono dal lato che li ha modificati;
  • i tag vengono combinati come insiemi, mantenendo ciò che un lato ha aggiunto e scartando ciò che un lato ha rimosso;
  • i corpi vengono uniti per riga, e le modifiche distanti tra loro vengono combinate;
  • una modifica contro un'eliminazione mantiene la modifica, perché eliminare di nuovo è un clic, mentre una modifica persa non torna più.

Prima di un'unione viene eseguito il backup di entrambe le versioni. Ciò che non si può combinare (due riepiloghi o tipi diversi, modifiche alla stessa riga o a righe vicine, un'unione troppo grande per il piano, o una nota senza copia della sua ultima versione sincronizzata) diventa un conflitto. Un conflitto blocca solo la propria nota: tutto il resto continua a essere caricato e scaricato. La pagina Memory mostra l'ultima versione sincronizzata, quella di questa macchina e quella del cloud, e permette di mantenere quella di questa macchina, prendere quella del cloud, mantenerle entrambe (quella del cloud diventa una nuova nota accanto) o unirle a mano; tutto ciò che viene messo da parte finisce in backup, da qualunque lato provenga. Non esiste una sovrascrittura last-write-wins. Le eliminazioni sono tombstone, e le scritture del manifest usano il compare-and-swap con ETag.

Le modifiche in batch che nessuno revisiona nota per nota (il consolidamento) vengono scritte prima nel cloud: ogni nota coinvolta deve essere completamente sincronizzata, il commit porta la versione del manifest che è stata controllata, e solo quando il servizio lo accetta il batch viene scritto in locale. Una modifica da un altro dispositivo nel frattempo viene rifiutata dal servizio invece di diventare un conflitto.

Le richieste elencano gli hash delle note presenti su questa macchina (have); il server non legge né restituisce quei corpi, quindi una sincronizzazione in cui è cambiato poco trasferisce poco. I commit vengono suddivisi secondo la quota di modifiche annunciata dal server. I tombstone più vecchi della finestra del server vengono rimossi dal manifest; una macchina che torna dopo quel periodo e trova non più elencata una nota che aveva visto sincronizzata e invariata la elimina, mentre una nota che ha modificato nel frattempo diventa un conflitto. Due id di nota che differiscono solo per maiuscole e minuscole vengono rifiutati, perché i file system di macOS e Windows non possono contenerli entrambi.

La sincronizzazione ritenta gli errori transitori dopo un'attesa di 60 secondi; la sincronizzazione manuale può ritentare subito. I conflitti attendono l'utente. La pagina esegue il backup delle note eliminate, e delle versioni locali perdenti nei conflitti, sotto ~/.oharness/memory-backups/; forget fa lo stesso. Le modifiche dirette al file system restano supportate, ma una scrittura esterna concorrente durante un'unione non è protetta dal lock di scrittura locale al processo; tieni un solo processo in esecuzione per directory.

R2 è privato e usa TLS, ma questa non è crittografia end-to-end: Cloud può leggere il contenuto delle note. Si sincronizzano solo le note, non i log delle sessioni, i file o le credenziali.

Quote e piani

Il server gestisce le proprie quote e le annuncia in ogni risposta; i client non ne conservano una copia. Valori predefiniti: 5000 note attive per progetto e per utente, 500 modifiche per commit, 128 KiB per corpo, 16 MiB di corpi per risposta, 90 giorni per tombstone. Una quota abbassata blocca solo la crescita, quindi un utente che la supera può sempre eliminare. Vedi Gestire il tuo server di sincronizzazione per le variabili.

Gli account gratuiti sono soggetti in aggiunta a limiti più stretti: un numero di note per progetto e per utente, e una dimensione per nota, definiti nel file src/memory-entitlements.json del servizio di sincronizzazione. Gli account a pagamento non hanno limiti di piano, solo la quota sopra. Il piano viene letto dal claim ent che Cloud scrive nell'access token: un piano a pagamento dell'utente stesso, o di un team di cui fa parte, conta come a pagamento. È limitata solo la sincronizzazione cloud; le note sulla macchina no, e un account gratuito oltre il limite mantiene tutte le note locali e smette di caricarne di nuove, con un messaggio che spiega il perché.

Eliminare un account

Eliminare un account sul sito web cancella tutto ciò che ha sincronizzato: i feed delle sessioni e la memoria in entrambi gli ambiti. Il gateway chiama POST /v1/admin/erase del servizio di sincronizzazione con una chiave che solo lui possiede (OHARNESS_SYNC_ADMIN_KEY su entrambi), prima che l'account stesso venga eliminato, e ancora una volta dopo. Il servizio di sincronizzazione scrive prima un marcatore per l'account, e da quel momento rifiuta le sue richieste di feed e di memoria, così un dispositivo ancora connesso non può caricare di nuovo i dati. Le note e le sessioni sulle tue macchine non vengono toccate.

Eliminare un team fa lo stesso per la cronologia delle sessioni condivisa del team: il gateway cancella feed/team-<id>/ (la cancellazione {team} del servizio di sincronizzazione) dopo aver annullato l'abbonamento del team e prima di rimuoverne le righe, poi ancora una volta. Da quel momento il servizio di sincronizzazione rifiuta i feed di quel team, anche per un token che lo elenca ancora. I team non hanno memoria.

Aggiornamento

Aggiorna il server di sincronizzazione prima dei client. Un nuovo client ripiega sul vecchio protocollo con un vecchio server per le note di progetto senza tipo. Non può sincronizzare note a livello utente, né note con un tipo, con un vecchio server. Un vecchio client che riceve dal cloud una nota con tipo smette di sincronizzare con un errore di checksum finché non viene aggiornato.

Verifica e deployment

Esegui npm run test:memory, npm run test:sync-memory, npm run test:plugins, npm run typecheck e npm run build. script/memory-ui-demo.ts avvia un'interfaccia isolata e senza accesso sulla porta 4319 con una home temporanea; non legge mai le credenziali dell'utente. Una configurazione R2 mancante produce un errore di sincronizzazione visibile senza compromettere la memoria locale. Riavvia il server di sviluppo locale dopo aver modificato i sorgenti.