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_themesera 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 paradefault. A governação de identidade parecia configurada enquanto fazia precisamente o oposto.- Os alternadores booleanos ignoravam
false.embed_on_write,sync_turns,hybrid_searchebulk_sync_on_initsã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) eembed_write_timeout(30s, writer em background). replace()atualizava zero linhas. Um UPDATE em massa colidia comUNIQUE(agent_identity, target, content)e lançavaUniqueViolation, 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_turneon_session_endcapturavam ambos os mesmos turnos, econversationsnão tem restrição única. - Uma base de dados em falha ficava silenciosa. As falhas do worker eram registadas em
debuge_healthynunca 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:
