7 de setembro de 2026

hermes-memory-pgvector v0.5: Uma renomeação de importação que quebra compatibilidade, e as correções por trás dela

Por Andrea Borghi
hermes-memory-pgvector v0.5: Uma renomeação de importação que quebra compatibilidade, e as correções por trás dela

Seis semanas depois de a v0.4.2 ter tornado o plugin instalável via pip, saiu o hermes-memory-pgvector v0.5.0. É a primeira versão na linha 0.x com uma alteração genuinamente incompatível, e a razão é um nome que eu não devia ter escolhido logo à partida.

O conflito

O pacote de importação chamava-se pgvector. Esse nome pertence ao pgvector-python. Instale ambos no mesmo virtualenv e o que tiver sido instalado por último ganha o nome de topo — momento em que o shim de descoberta deste plugin podia importar o módulo errado por completo.

O modo de falha é o que torna isto digno de um salto de versão maior: o carregador trata uma importação inválida como "plugin ausente" e recorre à memória incorporada numa única linha de registo. Nada falha. A frota apenas deixa de partilhar memória em silêncio, e só semanas depois se descobre, quando a recuperação volta vazia.

Assim, o pacote de importação passa agora a ser hermes_pgvector. Três coisas que não mudaram, porque são namespaces separados e só um deles se deslocou:

  • a distribuição — continua hermes-memory-pgvector
  • a CLI — continua hermes-pgvector
  • o provider hermes — continua pgvector (memory.provider: pgvector)

Atualização

A v0.5.0 declara o entry point hermes_agent.memory_providers, pelo que, num host suficientemente recente para resolver plugins dessa forma, uma simples instalação via pip passa a ser suficiente e o shim deixa de ser necessário. Um diretório de shim continua a ter precedência sobre o entry point, e é precisamente por isso que um obsoleto tem de sair:

pip install -U hermes-memory-pgvector

# preferido: remover o shim, deixar o entry point resolvê-lo
hermes-pgvector install --remove

# host mais antigo que só procura em diretórios de plugins:
hermes-pgvector install --force

hermes memory status        # expect: Provider: pgvector; Status: available

Verifique os seus trabalhos agendados antes de se afastar. Qualquer invocação python -m pgvector ... passa agora a ser hermes-pgvector ..., e falha em silêncio — o preenchimento noturno simplesmente pára, e as linhas escritas durante uma indisponibilidade de embed continuam impossíveis de pesquisar:

sudo grep -rl 'python -m pgvector' /etc/systemd/system/ /etc/cron.d/

A parte que eu não esperava

Uma revisão completa de toda a base de código, nos 24 ficheiros rastreados, encontrou 28 resultados confirmados. Os que doeram não foram casos-limite — foram contratos de configuração que se contradiziam inteiramente dentro deste repositório, cada um invertendo silenciosamente um controlo enquanto parecia estar corretamente configurado:

  • allowed_themes era declarado como string e consumido como lista. Uma allow-list em string era iterada carácter a carácter, pelo que todos os temas falhavam o teste de pertença e toda a frota era encaminhada para default. A governação de identidade parecia configurada enquanto fazia precisamente o oposto.
  • Os alternadores booleanos ignoravam false. embed_on_write, sync_turns, hybrid_search e bulk_sync_on_init são declarados pelo schema como as strings "true"/"false", e depois lidos com truthiness simples. bool("false") é True.
  • Os timeouts de embed nunca eram ligados a partir da configuração. Todos os chamadores usavam um valor fixo de 10s. Perante um endpoint a responder em 6–17s, uma grande parte das escritas esgotava o tempo e ficava com embeddings NULL — e as tentativas repetidas não podiam ajudar, porque cada tentativa estava limitada abaixo da latência de que precisava. Agora estão divididos por caminho: embed_timeout (10s, thread do agente) e embed_write_timeout (30s, writer em background).
  • replace() atualizava zero linhas. Um UPDATE em massa colidia com UNIQUE(agent_identity, target, content) e lançava UniqueViolation, pelo que um replace que correspondesse a duas ou mais entradas não fazia nada enquanto o estado da ferramenta incorporada avançava.
  • Cada turno da conversa era escrito duas vezes. sync_turn e on_session_end capturavam ambos os mesmos turnos, e conversations não tem restrição única.
  • Uma base de dados em falha ficava silenciosa. As falhas do worker eram registadas em debug e _healthy nunca era reavaliado, pelo que um restart do Postgres descartava todas as escritas duráveis da sessão sem qualquer sinal.

Essa é a história honesta desta versão: a renomeação é a manchete, e os bugs de configuração são a razão pela qual a renomeação valeu a pena agora e não apenas na 1.0.

A segmentação de identidade é agora aplicada dos dois lados

whatsapp-dm, o novo external-group, e _bench eram apenas destinos do lado da escrita. A normalização de identidade removia PII da identity, mas os corpos das mensagens continuavam em content e nada os filtrava na leitura — por isso qualquer tema podia puxar conteúdo de DM para o seu contexto via scope='all'. Agora scope='all' exclui esses destinos e nomeá-los explicitamente é rejeitado. Um agente que é um bucket mantém acesso total às suas próprias linhas, e a recuperação normal entre temas permanece inalterada.

Há uma ressalva que vale a pena ler com atenção: a gate exclui por nome do bucket, e o bucket é definido no momento da escrita. As linhas nunca são reescritas retroativamente — isso é deliberado, porque, caso contrário, os dados históricos deixariam de poder ser recuperados. As linhas escritas antes de a sua chave ter sido colocada num bucket mantêm a identidade bruta. hermes-pgvector remap --old <raw> --new whatsapp-dm pré-visualiza a mudança; adicione --execute para a efetuar de facto.

Validação

100 testes em modo skip, 145 contra Postgres 16 em produção com pgvector 0.8.6 e todas as quatro migrações aplicadas. Sem alterações de schema e sem novas migrações nesta versão.

Atualização — v0.5.1

A v0.5.1 chegou uma hora depois, e é a versão desta linha que não deve ignorar: um único memory remove apagava todas as entradas espelhadas de um tema, e não apenas uma.

A operação remove da ferramenta incorporada transporta o seu alvo em old_text e deixa content vazio, e o host encaminha isso através de metadados em vez de conteúdo. _worker estava a passar old_text=item.content, que por isso era sempre "" — logo, store.remove construía content LIKE '%%', e isso corresponde a todas as linhas. Um remove apagava todo o espelho para esse par (agent_identity, target). O armazenamento incorporado nunca foi afetado; apenas o espelho pgvector. Verifiquei a implementação de referência para danos e não encontrei nenhum — o histórico de cada tema está contínuo, o que diz sobretudo quão raros são os removes na prática.

Está corrigido em duas camadas, porque uma não chega para uma eliminação silenciosa de âmbito total: _worker agora lê extra["old_text"] e recusa um remove sem alvo utilizável, e store.remove() rejeita de imediato um padrão vazio, pelo que a eliminação destrutiva fica inacessível por omissão de qualquer chamador. remove() também passa agora a eliminar no máximo uma linha, à semelhança do comportamento incorporado.

A mesma versão corrigiu também o problema de conteúdo vazio que estava por trás disto. Nada rejeitava um add/replace vazio, pelo que podia existir uma linha que embed() nunca consegue processar — EmbeddingError("empty input") é incondicional para texto em branco. O preenchimento noturno então voltava a tentar em cada execução, mantendo failed acima de zero e tornando remaining == 0 inalcançável, o que destrói o único sinal que um operador realmente observa: uma linha permanentemente bloqueada torna-se indistinguível de uma nova falha genuína. As escritas vazias passam agora a ser ignoradas (remove é exceção), e as linhas que não podem ser embeddadas são excluídas da varredura e reportadas separadamente como unembeddable.

Duas coisas mais pequenas que vale a pena nomear, porque ambas eram invisíveis. trim() no Postgres remove apenas espaços, portanto uma linha com apenas uma nova linha ou um tab ainda passava e falhava para sempre — agora todos os sítios usam content ~ '\S'. E esses predicados eram f-strings simples, onde \S é uma sequência de escape inválida: quatro DeprecationWarnings hoje, SyntaxWarning no 3.12+, e com -W error o módulo falha logo a importação — o que faria o loader do hermes-agent recuar silenciosamente para a memória incorporada.

Validação: 118 testes em modo skip, 164 contra Postgres 16 em produção com pgvector 0.8.6, com todas as quatro migrações aplicadas. Duas passagens de revisão — a primeira encontrou o bug de perda de dados, a segunda encontrou a sequência de escape quebrada nessa correção.

Obtenha-o

pip install hermes-memory-pgvector

Código-fonte, notas de atualização e referência completa de configuração no GitHub:

👉 github.com/andreab67/hermes-memory-pgvector