7 septembre 2026

hermes-memory-pgvector v0.5 : un renommage d’import cassant, et les correctifs qui l’accompagnent

Par Andrea Borghi
hermes-memory-pgvector v0.5 : un renommage d’import cassant, et les correctifs qui l’accompagnent

Six semaines après que v0.4.2 a rendu le plugin installable via pip, hermes-memory-pgvector v0.5.0 est disponible. C’est la première version de la ligne 0.x avec un changement réellement cassant, et la raison tient à un nom que je n’aurais pas dû prendre dès le départ.

La collision

Le package d’import s’appelait pgvector. Ce nom appartient à pgvector-python. Installez les deux dans le même virtualenv et celui qui arrive en dernier remporte le nom de niveau supérieur — à ce moment-là, le shim de découverte de ce plugin pouvait importer complètement le mauvais module.

Le mode d’échec est précisément ce qui en fait une mise à niveau presque majeure : le chargeur traite un mauvais import comme « plugin absent » et retombe sur la mémoire intégrée avec une seule ligne de journal. Rien ne plante. La flotte cesse simplement de partager la mémoire, en silence, et vous l’apprenez des semaines plus tard quand le rappel revient vide.

Le package d’import est donc désormais hermes_pgvector. Trois choses qui n’ont pas changé, parce qu’elles relèvent d’espaces de noms séparés et qu’une seule d’entre elles a bougé :

  • la distribution — toujours hermes-memory-pgvector
  • la CLI — toujours hermes-pgvector
  • le provider hermes — toujours pgvector (memory.provider: pgvector)

Mise à niveau

v0.5.0 déclare le point d’entrée hermes_agent.memory_providers, donc sur un hôte suffisamment récent pour résoudre les plugins de cette manière, une simple installation pip suffit désormais et le shim n’est plus nécessaire. Un répertoire shim a toujours la priorité sur le point d’entrée, ce qui explique précisément pourquoi un ancien doit disparaître :

pip install -U hermes-memory-pgvector

# recommandé : supprimez le shim, laissez le point d’entrée le résoudre
hermes-pgvector install --remove

# hôte plus ancien qui ne parcourt que les répertoires de plugins :
hermes-pgvector install --force

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

Vérifiez vos tâches planifiées avant de partir. Toute invocation python -m pgvector ... est désormais hermes-pgvector ..., et elle échoue silencieusement — le remplissage nocturne s’arrête tout simplement, et les lignes écrites pendant une panne d’embeddings restent impossibles à rechercher :

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

La partie que je n’attendais pas

Un examen complet du codebase sur l’ensemble des 24 fichiers suivis a révélé 28 constats confirmés. Ceux qui ont fait mal n’étaient pas des cas limites — c’étaient des contrats de configuration qui se contredisaient entièrement à l’intérieur de ce dépôt, chacun inversant silencieusement une commande tout en ayant l’air correctement configuré :

  • allowed_themes était déclaré comme une chaîne et consommé comme une liste. Une allow-list sous forme de chaîne était parcourue caractère par caractère, si bien que tous les thèmes échouaient au test d’appartenance et que toute la flotte était redirigée vers default. La gouvernance d’identité semblait configurée tout en faisant exactement l’inverse.
  • Les bascules booléennes ignoraient false. embed_on_write, sync_turns, hybrid_search et bulk_sync_on_init sont déclarés par le schéma comme les chaînes "true"/"false", puis lus avec une simple vérité de valeur. bool("false") vaut True.
  • Les délais d’embedding n’étaient jamais transmis depuis la configuration. Chaque appelant utilisait un 10s codé en dur. Face à un endpoint répondant en 6–17s, une grande partie des écritures expirait et aboutissait avec des embeddings NULL — et les nouvelles tentatives ne pouvaient rien y faire, car chaque essai était plafonné en dessous de la latence nécessaire. Désormais, séparation par chemin : embed_timeout (10s, thread de l’agent) et embed_write_timeout (30s, writer en arrière-plan).
  • replace() mettait à jour zéro ligne. Une mise à jour en masse entrait en collision avec UNIQUE(agent_identity, target, content) et levait UniqueViolation, donc un replace correspondant à deux entrées ou plus ne faisait rien pendant que l’état de l’outil intégré avançait.
  • Chaque tour de conversation était écrit deux fois. sync_turn et on_session_end capturaient tous deux les mêmes tours, et conversations n’a aucune contrainte d’unicité.
  • Une base de données en panne était silencieuse. Les échecs des workers étaient consignés au niveau debug et _healthy n’était jamais revérifié, si bien qu’un redémarrage de Postgres supprimait chaque écriture durable de la session sans aucun signal.

C’est l’histoire honnête de cette version : le renommage fait les gros titres, et les bugs de configuration sont la raison pour laquelle ce renommage valait la peine d’être fait maintenant plutôt qu’en 1.0.

Le bucketing d’identité est désormais imposé des deux côtés

whatsapp-dm, le nouveau external-group et _bench n’étaient que des puits côté écriture. La normalisation d’identité retirait les données personnelles de l’identité, mais les corps des messages vivent toujours dans content et rien ne les filtrait à la lecture — donc n’importe quel thème pouvait faire entrer du contenu DM dans son contexte via scope='all'. Désormais, scope='all' exclut ces puits et le fait d’en nommer un explicitement est rejeté. Un agent qui est un bucket conserve un accès complet à ses propres lignes, et le rappel ordinaire entre thèmes reste inchangé.

Un point mérite d’être lu attentivement : la barrière exclut par nom de bucket, et le bucketing se fait au moment de l’écriture. Les lignes ne sont jamais réécrites rétroactivement — c’est volontaire, car sinon les lignes historiques deviendraient impossibles à rappeler. Les lignes écrites avant que leur clé ne soit bucketée conservent l’identité brute. hermes-pgvector remap --old <raw> --new whatsapp-dm prévisualise le déplacement ; ajoutez --execute pour l’effectuer réellement.

Validation

100 tests en mode skip, 145 sur un Postgres 16 en direct avec pgvector 0.8.6 et les quatre migrations appliquées. Aucun changement de schéma et aucune nouvelle migration dans cette version.

Mise à jour — v0.5.1

v0.5.1 est arrivée une heure plus tard, et c’est la seule version de cette ligne sur laquelle vous ne devez pas temporiser : un seul memory remove supprimait toutes les entrées miroir d’un thème, pas une seule.

L’opération remove de l’outil intégré transporte sa cible dans old_text et laisse content vide, et l’hôte la transmet via les métadonnées plutôt que via le contenu. _worker passait old_text=item.content, qui était donc toujours "" — ainsi store.remove construisait content LIKE '%%', et cela correspond à toutes les lignes. Un remove effaçait tout le miroir pour cette paire (agent_identity, target). Le store intégré n’a jamais été affecté ; uniquement le miroir pgvector. J’ai vérifié le déploiement de référence pour détecter des dommages et je n’en ai trouvé aucun — l’historique de chaque thème est continu, ce qui vous apprend surtout à quel point les suppressions sont rares en pratique.

C’est corrigé à deux niveaux, car un seul ne suffit pas face à une suppression silencieuse à portée complète : _worker lit désormais extra["old_text"] et refuse un remove sans cible exploitable, et store.remove() rejette d’emblée un motif vide, de sorte que la suppression destructive n’est plus atteignable par omission depuis un appelant quelconque. remove() supprime désormais au plus une ligne, comme le comportement intégré.

La même version a corrigé le problème de contenu vide qui se trouvait en dessous. Rien ne rejetait un add/replace vide, si bien qu’une ligne pouvait exister et que embed() ne pourrait jamais la traiter — EmbeddingError("empty input") est inconditionnel pour un texte vide. Le backfill nocturne le retentait alors à chaque exécution, maintenant failed au-dessus de zéro et rendant remaining == 0 inatteignable, ce qui détruit le seul signal qu’un opérateur surveille réellement : une ligne bloquée de façon permanente devient indiscernable d’un nouveau vrai échec. Les écritures vides sont désormais ignorées (remove excepté), et les lignes impossibles à vectoriser sont exclues du balayage et signalées séparément comme unembeddable.

Deux détails plus petits valent d’être nommés, car ils étaient tous deux invisibles. trim() de Postgres enlève uniquement les espaces, donc une ligne ne contenant qu’un saut de ligne ou une tabulation passait encore et échouait à jamais — chaque site utilise désormais content ~ '\S'. Et ces prédicats étaient de simples f-strings, où \S est une séquence d’échappement invalide : quatre DeprecationWarnings aujourd’hui, SyntaxWarning à partir de 3.12+, et sous -W error le module échoue purement et simplement à l’import — ce qui ferait retomber silencieusement hermes-agent sur la mémoire intégrée.

Validation : 118 tests en mode skip, 164 sur un Postgres 16 en direct avec pgvector 0.8.6, les quatre migrations appliquées. Deux passes de revue — la première a trouvé le bug de perte de données, la seconde a trouvé la séquence d’échappement cassée dans ce correctif.

Obtenir la version

pip install hermes-memory-pgvector

Source, notes de mise à niveau et référence complète de configuration sur GitHub :

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