Memória e sincronização

A memória é um plugin de arquitetura embutido do oHarness (memory), e não um servidor MCP. Ele registra as ferramentas de memória, o índice no prompt de sistema, o recall por mensagem, a extração opcional, a página Memory e um sincronizador em segundo plano, tudo pelo mesmo PluginHost usado pelos plugins de modelo, de ferramenta e de auditoria. A extensão de página continua pequena: descritores registrados mais renderizadores do host (hoje, memory), e não JavaScript arbitrário de frontend ou um SDK de iframe.

Onde ficam as notas

Uma nota por arquivo Markdown, com frontmatter em 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]].
EscopoDiretórioGuarda
project<project>/MEMORY/Decisões, restrições e referências que se movem e são exportadas com o projeto
user~/.oharness/memory/user/Quem é o usuário e como ele gosta de trabalhar, em todos os projetos

Todo projeto registrado tem um .oharness/project.json. O id estável dele mantém vinculados as sessões do lado da Home, o estado de sincronização e as estatísticas de recall quando a pasta é renomeada ou exportada. Um repositório git usa inicialmente o commit raiz como esse id; outros diretórios recebem um id de projeto aleatório. OHARNESS_HOME move as notas do usuário e o estado local da máquina, mas nunca muda de lugar o diretório MEMORY/ do projeto.

memory.dir (ou o antigo memoryDir) na configuração substitui o diretório do projeto, relativo à raiz do projeto. Quando o diretório do usuário não pode ser criado, as notas do projeto continuam disponíveis em <project>/MEMORY; só as notas do usuário e o estado da memória do lado da Home ficam indisponíveis, e um aviso informa isso.

Tipos

TipoEscopo padrãoO que registra
useruserA função, a experiência e as preferências de trabalho do usuário
feedbackprojectUma correção, ou uma abordagem que o usuário confirmou
projectprojectUma decisão e o motivo dela, uma restrição, um trabalho em andamento que não se deduz do código
referenceprojectOnde fica uma informação externa

Só as notas user vão por padrão para o escopo do usuário. O feedback vai por padrão para o projeto, porque a maioria das correções é sobre como se trabalha em uma base de código; uma que claramente vale em todo lugar é colocada no escopo do usuário de propósito.

Notas escritas antes da existência dos tipos não têm nenhum e continuam funcionando. type só entra no hash de conteúdo de uma nota quando está presente, então as notas existentes mantêm os hashes que suas bases de sincronização já guardam.

Escrevendo notas

O modelo escreve com remember, lê com memory e exclui com forget. O remember do plugin de memória passa sem perguntar em todos os modos de permissão, exceto plan, que o recusa como qualquer outra escrita. Uma regra ask ou deny que cite remember continua valendo, forget continua perguntando, e a ferramenta remember de outro plugin não recebe essa exceção. As tags são aparadas, divididas por vírgulas e espaços e deduplicadas antes de a nota ser salva. A seção Memory do prompt de sistema diz quando vale a pena escrever uma nota e quando não vale. remember retorna as notas existentes mais relacionadas ao que acabou de ser escrito, sinaliza uma provável duplicata e lista os links que apontam para notas que ainda não existem. Qualquer coisa que pareça uma credencial é recusada. As notas devem levar tags com as outras palavras que alguém poderia usar: sinônimos e o termo no outro idioma em uso, já que o recall compara palavras, não significado.

As notas se ligam umas às outras no corpo como [[note-id]]. Os links são derivados dos corpos sempre que as notas são carregadas; nada sobre eles é armazenado à parte, e o protocolo de sincronização nem sabe que existem. Um link é resolvido primeiro no escopo do projeto e depois no do usuário. Um link para uma nota inexistente é mantido: em geral ele indica uma nota que alguém pretendia escrever.

Extração automática

Desligada por padrão, porque cada passada é uma chamada de modelo paga com o crédito do próprio usuário. Com memory.autoExtract.onStop, o fim de uma execução entrega ao modelo pequeno a conversa desde a última passada, assim que pelo menos minUserTurns mensagens reais do usuário tiverem se acumulado desde então. Com onCompaction (ou onStop), a parte do trecho que a compactação está prestes a descartar e que nenhuma passada leu ainda é entregue primeiro, por mais curta que seja, já que é a última chance de lê-la. Uma sessão retomada começa a contar de onde foi retomada, em vez de extrair todo o histórico de novo. Nunca se extrai nada de subagentes.

O extrator recebe as notas existentes mais relacionadas à conversa. As poucas mais relevantes vêm com o corpo completo, dentro de um limite de tamanho, e só elas podem ser atualizadas; as demais são listadas pelo resumo, para criar links. Uma atualização substitui o corpo, então a versão substituída é copiada antes para ~/.oharness/memory-backups/. A resposta do extrator é tratada como não confiável: atualizações para ids que não foram mostrados a ele são descartadas, links para ids que não foram mostrados perdem os colchetes, credenciais são ocultadas, e notas novas em um mesmo lote se referem umas às outras como [[new:N]] até receberem ids. A configuração de um repositório pode desligar a extração; só a configuração do próprio usuário pode ligá-la.

A extração termina depois que a execução que a disparou acabou, então as notas que ela salvou são anunciadas no início da mensagem seguinte: "saved N memories from this conversation", em todas as interfaces.

Recall

Dois caminhos colocam notas diante do modelo:

  1. O índice. No início de cada execução, uma linha por nota, das atualizadas mais recentemente para as mais antigas, até memory.indexChars caracteres. As demais são contadas, não perdidas; a ferramenta memory busca em todas.
  2. Recall por mensagem. Cada mensagem do usuário recebe uma pontuação contra todas as notas, e as melhores correspondências são anexadas a essa mensagem dentro de <recalled-memory>, com as notas para as quais elas apontam listadas pelo resumo. As interfaces mostram "recalled N memories". Nada é anexado quando nada passa do limiar, que é o caso mais comum. Sem nenhuma nota, o índice é substituído por duas linhas dizendo quando escrever uma, em vez da orientação completa.

A pontuação é lexical e roda localmente: as palavras passam por stemming, o texto CJK é dividido em pares sobrepostos, e correspondências no resumo e nas tags pesam mais do que no corpo. Uma nota precisa corresponder no resumo ou nas tags para ser anexada. Uma nota anexada uma vez não é anexada de novo enquanto uma mensagem que a carrega continuar na transcrição, inclusive depois de a sessão ser retomada ou de o app reiniciar; os ids são lidos de volta dos metadados das mensagens. A compactação libera as notas cujas mensagens ela resumiu. As notas anexadas ficam em um bloco próprio da mensagem do usuário, registrado nos metadados da mensagem, então as transcrições, os títulos de sessão e o histórico web mostram as palavras do próprio usuário. Subagentes não fazem recall.

Uma falha no recall nunca impede o envio de uma mensagem.

Limites

OpçãoPadrãoSignificado
memory.maxNotes5000Por escopo. Acima disso, os arquivos modificados há mais tempo não são carregados e um aviso diz quantos; novas notas são recusadas até que algumas sejam removidas
memory.maxNoteBytes131072Tamanho do corpo; o servidor de sincronização também aplica o próprio limite
memory.indexChars4000Caracteres de resumos no prompt de sistema
memory.recall.enabledtrueRecall por mensagem
memory.recall.perTurn3Notas anexadas a uma mensagem
memory.recall.budgetChars2000Caracteres que essas notas podem ocupar
memory.recall.minScore1.2Limiar para anexar
memory.autoExtract.onCompaction / onStopfalseExtração automática
memory.autoExtract.minUserTurns3Conversas mais curtas são ignoradas

Quantas vezes cada nota foi recuperada é registrado por máquina em ~/.oharness/memory-usage/, fora das notas, para que o recall nunca provoque um upload de sincronização. Isso ajusta levemente a classificação e nunca é sincronizado.

Sincronização na nuvem

  • As notas sempre funcionam localmente, sem login e offline.
  • Entrar na conta ativa a sincronização automática por padrão. A sincronização vai nos dois sentidos: alterações locais sobem, e alterações feitas em outros dispositivos descem sozinhas.
  • A página Memory oferece 关闭云同步 e 立即同步. O interruptor cobre os dois escopos. Desligado, ele persiste entre reinicializações e novos logins, e bloqueia requisições à nuvem automáticas e manuais até ser reativado explicitamente. As ferramentas locais e a edição continuam funcionando.
  • Desligar a sincronização cancela as requisições em andamento. Dados já aceitos pelo Cloud não podem ser retirados; desligar a sincronização não exclui as cópias na nuvem.
  • Cada escopo lógico de projeto/usuário fica fixado na primeira conta que o sincronizou e na origem do Cloud. Mudar de conta ou de origem pausa a sincronização. A página não oferece uma reatribuição insegura com um clique.

A página mantém ~/.oharness/plugins.json (OHARNESS_HOME substitui esse caminho), em runtimePlugins[id=memory].config.syncTargets[sha256(stableScopeKey)]:

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

Não coloque chaves do R2 nem configuração do Supabase nesse arquivo. A URL do Cloud continua em cloud.json; as credenciais do R2 existem apenas no ambiente da implantação de sincronização. Para desativar o plugin inteiro (página, ferramentas, prompts, recall e trabalho em segundo plano), adicione { "id": "memory", "enabled": false } a runtimePlugins; isso vale a partir da próxima criação do Harness. O registro de projetos continua criando os diretórios padrão vazios MEMORY/ e ARTIFACTS/. Para quem embute o harness, harness.memory é o armazenamento do projeto e harness.memoryUserService o do usuário; os dois ficam ausentes quando o plugin está desativado. AgentOptions.memory continua sendo uma API de embutimento de nível mais baixo e recebe recall sem um host de plugins.

Quando sincroniza

Um coordenador por processo agenda todos os diretórios de notas, não importa quantos projetos um servidor web local ou o app para desktop tenha abertos. Cada diretório tem um único serviço de sincronização no processo, então as notas do próprio usuário são sincronizadas uma vez, e não uma vez por projeto aberto.

  • Uma escrita local é enviada cerca de três segundos depois; uma rajada de escritas vira uma única passada.
  • Fora isso, um diretório é verificado a cada minuto, espaçando para a cada cinco minutos enquanto nada muda em nenhum dos lados.
  • Suas próprias notas são sempre verificadas. As notas de um projeto só são verificadas enquanto o projeto está em uso (uma mensagem enviada nele, ou a página Memory aberta) e por trinta minutos depois disso.
  • O processo se limita a cerca de vinte requisições por minuto, deixando margem sob o limite por conta do serviço de sincronização para outros dispositivos.
  • Um 429 pausa todos os diretórios até o horário indicado no Retry-After, e a página diz que a sincronização foi frequente demais e será tentada de novo, em vez de culpar a configuração.

立即同步 sempre roda na hora.

O que a página mostra

O interruptor, a "última sincronização" e os erros cobrem os dois escopos: a sincronização só conta como ligada quando está ligada nos dois, o horário mostrado é o mais antigo dos dois, e cada erro diz se é sobre as notas do projeto ou as suas. A página também mostra o que o último pull trouxe (adicionado, alterado, removido) e, em um plano gratuito, os limites do plano.

Organizando as notas

A lista 待整理 da página é uma revisão feita nesta máquina, sem modelo:

AchadoSignificadoO que você pode fazer
Mesmo assuntoDe duas a seis notas em um escopo cujos resumos e tags se sobrepõem em grande parteMesclar, ou dispensar
Link quebradoUm [[id]] que não aponta para nenhuma notaAbrir a nota, ou dispensar
Arquivo ausenteUma nota de projeto cita um caminho relativo que não está no projetoAbrir, ou dispensar
Sem usoNão foi recuperada nem editada há memory.consolidate.unusedDays (90)Abrir, ou dispensar
Longa demaisUm corpo com mais de 8.000 caracteresAbrir, ou dispensar

Uma dispensa é guardada localmente associada às versões atuais das notas, então editar uma delas traz o achado de volta.

Mesclar abre um editor com um rascunho: o do modelo pequeno (生成合并建议) ou todos os originais juntos (手动合并). Aplicá-lo mantém o id da nota mais antiga, exclui as outras, aponta todo link [[deleted]] naquele escopo para a nota mantida e faz backup de todas as notas que altera. Uma mesclagem planejada a partir de notas que mudaram desde então é recusada.

Com a sincronização na nuvem ligada, o escopo é sincronizado primeiro e a mesclagem é confirmada na nuvem antes desta máquina (commitFirst), então uma edição feita em outro dispositivo nesse meio-tempo recusa a mesclagem em vez de virar um conflito. O corpo mesclado precisa caber no limite por nota do serviço de sincronização. Com a sincronização desligada, a mesclagem é uma edição local comum.

A mesclagem em segundo plano vem ligada por padrão apenas para planos pagos ("auto": "subscribers"): ela roda quando a última resposta do serviço de sincronização disse que a conta, ou qualquer equipe da qual ela faz parte, está em um plano pago, e fica desligada para contas gratuitas e sempre que o plano é desconhecido (sincronização na nuvem desligada, sem login). true ou false substituem esse comportamento:

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

Quando ligada, uma passada começa dois minutos após a inicialização, no máximo uma vez por período de espera, e mescla até maxMerges grupos de notas do mesmo assunto com o modelo pequeno, pelo mesmo caminho da página. Ela nunca age sobre os outros achados. A configuração de um projeto pode desligá-la, mas não ligá-la.

Backups e restauração

Uma cópia é gravada em ~/.oharness/memory-backups/ antes que qualquer nota seja substituída ou removida sem que alguém tenha escolhido aquela versão: exclusão na página, forget, escolher o lado da nuvem em um conflito, um pull automático que altera ou remove uma nota, uma reescrita do extrator e uma restauração que substitui uma versão mais nova. A página Memory lista as cinquenta mais recentes, com quando e por quê, e restaura qualquer uma delas no diretório de origem; a nota então é sincronizada como qualquer outra alteração local. Backups gravados antes de os motivos serem registrados também são listados e podem ser restaurados.

Armazenamento e reconciliação

O servidor de sincronização expõe /v1/memory/identity, /snapshot e /commit autenticados. Ele não armazena linhas em banco de dados. O R2 guarda revisões imutáveis em Markdown, mais um manifesto JSON substituído de forma atômica:

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

Implementações podem usar outro sistema de identidade ou de armazenamento; os formatos exatos das requisições, o algoritmo de hash, o comportamento do CAS e os códigos de erro estão especificados em Protocolo de sincronização.

A base local (~/.oharness/memory-sync/) permite a reconciliação em três vias: notas inalteradas são ignoradas e alterações de um só lado são copiadas. Ao lado dela, o cliente guarda uma cópia de cada nota como estava na última sincronização, então uma nota alterada nos dois lados é mesclada em vez de apenas sinalizada:

  • o resumo e o tipo vêm do lado que os alterou;
  • as tags são combinadas como conjuntos, mantendo o que qualquer lado adicionou e descartando o que qualquer lado removeu;
  • os corpos são mesclados por linha, e edições distantes umas das outras são combinadas;
  • uma edição contra uma exclusão mantém a edição, já que excluir de novo é um clique e uma edição perdida não volta.

As duas versões recebem backup antes de uma mesclagem. O que não pode ser combinado (dois resumos ou tipos diferentes, edições na mesma linha ou em linhas vizinhas, uma mesclagem grande demais para o plano, ou uma nota sem cópia da última versão sincronizada) vira um conflito. Um conflito segura apenas a própria nota: todo o resto continua subindo e descendo. A página Memory mostra a última versão sincronizada, a desta máquina e a da nuvem, e oferece manter a desta máquina, ficar com a da nuvem, manter as duas (a da nuvem vira uma nota nova ao lado) ou mesclar à mão; o que for deixado de lado recebe backup, de qualquer lado que tenha vindo. Não existe sobrescrita do tipo "última escrita vence". Exclusões são tombstones, e as gravações do manifesto usam compare-and-swap com ETag.

Alterações em lote que ninguém revisa nota por nota (consolidação) são gravadas primeiro na nuvem: todas as notas envolvidas precisam estar totalmente sincronizadas, o commit leva a versão do manifesto que foi verificada, e só depois que o serviço o aceita o lote é gravado localmente. Uma alteração vinda de outro dispositivo nesse meio-tempo é recusada pelo serviço em vez de virar um conflito.

As requisições listam os hashes das notas que esta máquina tem (have); o servidor não lê nem retorna esses corpos, então uma sincronização em que pouco mudou transfere pouco. Os commits são divididos de acordo com a cota de alterações que o servidor anuncia. Tombstones mais antigos que a janela do servidor são removidos do manifesto; uma máquina que volta depois disso encontra uma nota que viu sincronizada pela última vez, sem alterações, que não está mais listada, e a exclui, enquanto uma nota que ela editou nesse meio-tempo vira um conflito. Dois ids de nota que diferem apenas em maiúsculas e minúsculas são recusados, já que os sistemas de arquivos do macOS e do Windows não conseguem guardar os dois.

A sincronização tenta de novo após falhas transitórias, depois de uma espera de 60 segundos; a sincronização manual pode tentar de novo imediatamente. Os conflitos esperam pelo usuário. A página faz backup das notas excluídas, e das versões locais perdidas em conflitos, em ~/.oharness/memory-backups/; forget faz o mesmo. Edições diretas no sistema de arquivos continuam suportadas, mas uma escrita externa simultânea durante uma mesclagem não é protegida pelo lock de escrita local do processo; mantenha um único processo rodando por diretório.

O R2 é privado e usa TLS, mas isso não é criptografia de ponta a ponta: o Cloud pode ler o conteúdo das notas. Só as notas são sincronizadas, não os logs de sessão, arquivos ou credenciais.

Cotas e planos

O servidor define as próprias cotas e as anuncia em toda resposta; os clientes não guardam cópia. Padrões: 5000 notas ativas por projeto e por usuário, 500 alterações por commit, 128 KiB por corpo, 16 MiB de corpos por resposta, 90 dias por tombstone. Uma cota reduzida só bloqueia o crescimento, então um usuário acima dela sempre pode excluir. Veja Rodando seu próprio servidor de sincronização para as variáveis.

As contas gratuitas têm limites mais apertados por cima disso: um número de notas por projeto e por usuário, e um tamanho por nota, definidos em src/memory-entitlements.json do serviço de sincronização. As contas pagas não têm limite de plano, apenas a cota acima. O plano é lido da claim ent que o Cloud grava no token de acesso: um plano pago do próprio usuário, ou de qualquer equipe da qual ele faz parte, conta como pago. Só a sincronização na nuvem é limitada; as notas na máquina não são, e uma conta gratuita acima do limite mantém todas as notas locais e para de enviar as novas, com uma mensagem explicando o motivo.

Excluindo uma conta

Excluir uma conta no site apaga tudo o que ela sincronizou: feeds de sessão e memória nos dois escopos. O gateway chama POST /v1/admin/erase do serviço de sincronização com uma chave que só ele tem (OHARNESS_SYNC_ADMIN_KEY nos dois lados), antes de a conta em si ser excluída, e mais uma vez depois. O serviço de sincronização grava primeiro um marcador para a conta e, a partir daí, recusa as requisições de feed e de memória dela, para que um dispositivo ainda conectado não consiga enviar os dados de novo. As notas e sessões nas suas próprias máquinas não são tocadas.

Excluir uma equipe faz o mesmo com o histórico de sessões compartilhado da equipe: o gateway apaga feed/team-<id>/ (a exclusão {team} do serviço de sincronização) depois de cancelar a assinatura da equipe e antes de remover as linhas dela, e então mais uma vez. O serviço de sincronização recusa os feeds dessa equipe a partir daí, mesmo para um token que ainda a liste. Equipes não guardam memória.

Atualizando

Atualize o servidor de sincronização antes dos clientes. Um cliente novo recorre ao protocolo antigo contra um servidor antigo para notas de projeto que não têm tipo. Ele não consegue sincronizar notas do usuário, nem notas com tipo, com um servidor antigo. Um cliente antigo que recebe da nuvem uma nota com tipo para de sincronizar com um erro de checksum até ser atualizado.

Verificação e implantação

Rode 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 inicia uma UI isolada, sem login, na porta 4319, com um home temporário; ele nunca lê as credenciais do usuário. A falta de configuração do R2 produz um erro de sincronização visível sem quebrar a memória local. Reinicie o servidor de desenvolvimento local depois de alterar o código-fonte.