7 settembre 2026

hermes-memory-pgvector v0.5: Una rinomina dell'importazione che rompe la compatibilità, e le correzioni dietro di essa

Di Andrea Borghi
hermes-memory-pgvector v0.5: Una rinomina dell'importazione che rompe la compatibilità, e le correzioni dietro di essa

Sei settimane dopo che v0.4.2 ha reso il plugin installabile con pip, è uscita hermes-memory-pgvector v0.5.0. È la prima release della linea 0.x con una modifica davvero incompatibile, e il motivo è un nome che non avrei dovuto prendere in primo luogo.

Il conflitto

Il pacchetto di importazione si chiamava pgvector. Quel nome è di pgvector-python. Installate entrambi nello stesso virtualenv e quello arrivato per ultimo vince il nome di livello superiore — a quel punto lo shim di discovery di questo plugin poteva importare del tutto il modulo sbagliato.

La modalità di guasto è ciò che rende il salto di versione quasi maggiore: il loader tratta un import errato come "plugin assente" e ripiega sulla memoria integrata con una sola riga di log. Non va in crash nulla. Il parco si limita a smettere silenziosamente di condividere la memoria, e ve ne accorgete settimane dopo, quando il richiamo torna vuoto.

Quindi il pacchetto di importazione ora è hermes_pgvector. Tre cose che non sono cambiate, perché sono namespace separati e solo uno di essi si è spostato:

  • la distribuzione — ancora hermes-memory-pgvector
  • la CLI — ancora hermes-pgvector
  • il provider hermes — ancora pgvector (memory.provider: pgvector)

Aggiornamento

v0.5.0 dichiara l'entry point hermes_agent.memory_providers, quindi su un host abbastanza nuovo da risolvere i plugin in quel modo, una semplice installazione pip è ora sufficiente e lo shim non serve più. Una directory di shim ha ancora la precedenza sull'entry point, ed è proprio per questo che una vecchia va rimossa:

pip install -U hermes-memory-pgvector

# preferito: rimuovi lo shim, lascia che l'entry point lo risolva
hermes-pgvector install --remove

# host più vecchio che scansiona solo le directory dei plugin:
hermes-pgvector install --force

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

Controllate i job pianificati prima di allontanarvi. Qualsiasi invocazione python -m pgvector ... ora è hermes-pgvector ..., e fallisce in silenzio — il backfill notturno semplicemente si ferma, e le righe scritte durante un'interruzione dell'embed restano non ricercabili:

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

La parte che non mi aspettavo

Una revisione completa del codebase di tutti i 24 file tracciati ha portato alla luce 28 riscontri confermati. Quelli che hanno fatto male non erano casi limite — erano contratti di configurazione che si contraddicevano del tutto all'interno di questo repository, ciascuno invertendo silenziosamente un controllo pur sembrando configurato correttamente:

  • allowed_themes era dichiarato come stringa e consumato come lista. Una allow-list stringa veniva iterata carattere per carattere, quindi ogni tema falliva il test di appartenenza e l'intero parco veniva instradato a default. La governance dell'identità sembrava configurata mentre faceva l'esatto contrario.
  • Gli switch booleani ignoravano false. embed_on_write, sync_turns, hybrid_search e bulk_sync_on_init sono dichiarati dallo schema come le stringhe "true"/"false", poi letti con la semplice verità booleana. bool("false") è True.
  • I timeout degli embed non venivano mai passati dalla configurazione. Ogni chiamante usava un 10s codificato in modo fisso. Contro un endpoint che risponde in 6–17s, una larga parte delle scritture andava in timeout e finiva con embedding NULL — e i retry non potevano aiutare, perché ogni tentativo era limitato al di sotto della latenza di cui aveva bisogno. Ora è separato per percorso: embed_timeout (10s, thread dell'agente) e embed_write_timeout (30s, writer in background).
  • replace() aggiornava zero righe. Un UPDATE di massa collideva con UNIQUE(agent_identity, target, content) e sollevava UniqueViolation, quindi una replace che corrispondeva a due o più voci non faceva nulla mentre lo stato dello strumento integrato andava avanti.
  • Ogni turno di conversazione veniva scritto due volte. sync_turn e on_session_end catturavano entrambi gli stessi turni, e conversations non ha alcun vincolo univoco.
  • Un database morto era silenzioso. I guasti del worker venivano registrati a debug e _healthy non veniva mai ricontrollato, quindi un riavvio di Postgres scartava ogni scrittura persistente per la sessione senza alcun segnale.

Questa è la storia onesta di questa release: la rinomina è il titolo, e i bug di configurazione sono il motivo per cui la rinomina valeva la pena farla ora invece che alla 1.0.

Il bucketing dell'identità ora è applicato su entrambi i lati

whatsapp-dm, il nuovo external-group e _bench erano solo sink lato scrittura. La normalizzazione dell'identità rimuoveva i dati personali dall'identità, ma i corpi dei messaggi restano in content e nulla li filtrava in lettura — quindi qualsiasi tema poteva portare contenuti DM nel proprio contesto tramite scope='all'. Ora scope='all' esclude quei sink e nominare uno di essi esplicitamente viene rifiutato. Un agente che è un bucket mantiene pieno accesso alle proprie righe, e il richiamo ordinario tra temi resta invariato.

Vale la pena leggere con attenzione una precisazione: il gate esclude per nome del bucket, e il bucketing avviene al momento della scrittura. Le righe non vengono mai riscritte retroattivamente — è voluto, perché altrimenti le righe storiche diventerebbero irrecuperabili. Le righe scritte prima che la loro chiave fosse bucketed conservano l'identità grezza. hermes-pgvector remap --old <raw> --new whatsapp-dm mostra in anteprima lo spostamento; aggiungete --execute per eseguirlo davvero.

Validazione

100 test in modalità skip, 145 contro Postgres 16 live con pgvector 0.8.6 e tutte e quattro le migrazioni applicate. Nessuna modifica allo schema e nessuna nuova migrazione in questa release.

Aggiornamento — v0.5.1

v0.5.1 è arrivata un'ora dopo, ed è la release di questa linea su cui non dovreste indugiare: una singola memory remove cancellava ogni voce specchiata per un tema, non una sola.

L'operazione remove dello strumento integrato trasporta il proprio target in old_text e lascia content vuoto, e l'host la inoltra tramite i metadati anziché tramite il contenuto. _worker stava passando old_text=item.content, che quindi era sempre "" — così store.remove costruiva content LIKE '%%', e ciò corrisponde a ogni riga. Una rimozione cancellava l'intero mirror per quella coppia (agent_identity, target). Lo store integrato non è mai stato coinvolto; solo il mirror pgvector. Ho controllato il deployment di riferimento per verificare eventuali danni e non ne ho trovati — la cronologia di ogni tema è continua, il che vi dice soprattutto quanto siano rare le rimozioni nella pratica.

La correzione è su due livelli, perché uno solo non basta per una cancellazione silenziosa su tutto l'ambito: _worker ora legge extra["old_text"] e rifiuta una remove senza un target utilizzabile, e store.remove() rifiuta del tutto un pattern vuoto, quindi la cancellazione distruttiva è irraggiungibile per omissione da qualsiasi chiamante. remove() inoltre ora elimina al massimo una riga, in linea con lo strumento integrato.

La stessa release ha corretto anche il problema di contenuto vuoto dietro a questo. Nulla rifiutava un add/replace vuoto, quindi poteva esistere una riga che embed() non potrà mai processare — EmbeddingError("empty input") è incondizionato per il testo vuoto. Il backfill notturno la ritentava poi a ogni esecuzione, mantenendo failed sopra zero e rendendo irraggiungibile remaining == 0, il che distrugge l'unico segnale che un operatore osserva davvero: una riga bloccata per sempre diventa indistinguibile da un nuovo guasto autentico. Le scritture vuote ora vengono saltate (remove escluso), e le righe non embeddabili vengono escluse dalla scansione e riportate separatamente come unembeddable.

Due cose minori che vale la pena nominare, perché entrambe erano invisibili. trim() di Postgres rimuove solo gli spazi, quindi una riga contenente solo un newline o una tabulazione passava comunque e falliva per sempre — ora tutti i siti usano content ~ '\S'. E quei predicati erano semplici f-string, dove \S è una escape non valida: quattro DeprecationWarning oggi, SyntaxWarning su 3.12+, e con -W error il modulo fallisce direttamente l'importazione — il che farebbe sì che il loader di hermes-agent ripieghi silenziosamente sulla memoria integrata.

Validazione: 118 test in modalità skip, 164 contro Postgres 16 live con pgvector 0.8.6, tutte e quattro le migrazioni applicate. Due passaggi di revisione — il primo ha trovato il bug di perdita dati, il secondo ha trovato la sequenza di escape rotta in quella correzione.

Scaricalo

pip install hermes-memory-pgvector

Source, note di aggiornamento e riferimento completo alla configurazione su GitHub:

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