Pular para o conteúdo
nandomoreira.dev

Memória para agentes: um backbone em quatro camadas

#claude-code#memória#agentes

Ajustes de leitura
Fundo
Tamanho do texto
Entrelinha
Largura

Em 1º de junho eu salvei duas memórias no meu Claude Code. Uma dizia que a barra do meu Niri é o Noctalia, e não a Waybar. A outra, que o gerenciador de versões da máquina é o mise, e que ele nunca deve procurar nvm, pyenv ou asdf. Parecem bobas, e existem porque o agente, sem elas, presumia o contrário: toda sessão nova começava do zero, e do zero o palpite padrão para um desktop Linux é Waybar e nvm.

Esse é o problema inteiro da memória para agente: cada sessão começa sem saber nada, e a solução óbvia (jogar tudo no contexto) custa caro e piora conforme a memória cresce.

Antes: dois sistemas e nenhum dono

Até maio eu tinha duas memórias convivendo. Uma era a memória automática do próprio Claude Code, guardada por projeto. A outra era um plugin chamado amnesia, com comandos /amnesia:*, que eu usava para fatos de clientes e subprojetos. Cada uma tinha seu formato e seu caminho, e nenhuma sabia da outra.

Em 19 de maio eu reescrevi isso como um backbone só, com um formato só (frontmatter YAML e corpo em Markdown, com links [[nome]] entre memórias) e quatro camadas.

As quatro camadas

Camada Onde fica Quando carrega O que guarda
Self ~/.claude/USER.md e SOUL.md Sempre Quem eu sou, como o agente deve soar, preferências
Workspace <workspace>/.claude/WORKSPACE.md Sempre, se existir Regras comuns a vários projetos do mesmo workspace
Entity <workspace>/.memory/entities/<id>.md Só quando pedida Fatos de um cliente ou subprojeto específico
Project <projeto>/.claude/MEMORY.md Sempre, dentro do projeto Decisões do repositório atual

A decisão que importa é a terceira linha: Entity é preguiçosa de propósito. Um workspace pode ter dezenas de entidades, e carregar todas em toda sessão infla o contexto base para responder perguntas que quase nunca são sobre elas. Então ela só entra quando alguém a menciona.

Preguiça tem um risco, e eu escrevi a regra pensando nele: memória preguiçosa vira “preguiçosa e nunca carregada”. A regra 14 do framework obriga o agente a chamar /memory recall <id> antes de responder sempre que eu citar uma entidade que não está nas camadas já carregadas. Se o nome não bate, ele devolve sugestões parecidas e me pergunta qual era, em vez de adivinhar.

O resto da interface é manual e pequena: save (que pergunta a camada antes de gravar), recall, list, audit e verify.

A migração que um portão segurou

Migrar memória sem perder nada era o requisito que eu não queria negociar. O migrate-memory.sh roda em três fases (backup, consolidação da memória automática, conversão das entidades do amnesia), tem --dry-run e grava um marcador para não rodar duas vezes.

Um detalhe me custou mais do que parecia. O Claude Code guarda a memória automática numa pasta com o caminho do projeto codificado num hash, e decodificar esse hash de volta para o caminho é ambíguo. A saída foi fazer o caminho contrário: percorrer ~/dev, codificar cada caminho candidato do mesmo jeito e ver qual batia.

A remoção do plugin antigo ficou atrás de um portão: o --finalize roda o audit e se recusa a continuar se ainda existir alguém chamando /amnesia. Na primeira tentativa o audit reclamou de cinco coisas, e nenhuma era real. USER.md e SOUL.md têm nome de arquivo diferente do campo name (de propósito, são arquivos para gente ler), e as skills novas citavam /amnesia em texto, justamente para dizer “não use /amnesia”. O regex não distingue chamada de aviso. Ajustei a lista de exceções, o audit ficou limpo e o --finalize rodou. O plugin saiu de vez em 18 de junho.

O que quebrou

No mesmo 18 de junho achei um bug que comia memória em silêncio. O carregador das camadas tirava o frontmatter contando linhas ---. Só que --- também é a linha horizontal do Markdown, então uma memória que usasse esse separador no corpo era cortada ali, e o agente lia metade do arquivo achando que era o arquivo inteiro. A correção foi tirar o frontmatter só quando o arquivo abre com --- na linha 1, com um teste de regressão em cima.

O pior veio depois, num dia comum. Eu queria mover uma linha de lugar dentro do índice de memória e fiz isso com grep -v "$LINHA" seguido de um redirecionamento de volta para o arquivo. A linha começava com - [, como todo item de lista Markdown, e o grep daqui leu aquilo como opção e morreu. O passo seguinte escrevia no arquivo original a partir de uma saída vazia, e o MEMORY.md ficou com zero bytes (o shell trunca o destino antes de rodar o comando, então nem dá tempo de se arrepender).

Hoje a regra é escrever num arquivo temporário, conferir que ele não está vazio e só então mover por cima. E -- antes de qualquer padrão que veio de uma variável.

Memória que mente

O último problema é o mais sutil: memória certa no dia em que foi escrita, e errada seis meses depois. Uma memória de projeto que cita src/lib/qualquer-coisa.ts continua afirmando isso com toda a confiança depois que o arquivo foi renomeado.

O /memory verify existe para isso. Ele lê as memórias da camada Project, pega tudo o que está entre crases (arquivos, funções, flags) e confere se ainda existe no repositório. O que sumiu vira relatório de drift, e eu decido se a memória morre ou se atualiza.

Ficou um sistema pequeno: quatro camadas, cinco comandos e uma regra que obriga o agente a lembrar quando eu cito alguém.

/clear