7. September 2026

hermes-memory-pgvector v0.5: Eine einschneidende Import-Umbenennung und die dahinterliegenden Korrekturen

Von Andrea Borghi
hermes-memory-pgvector v0.5: Eine einschneidende Import-Umbenennung und die dahinterliegenden Korrekturen

Sechs Wochen nachdem v0.4.2 das Plugin per pip installierbar gemacht hat, ist hermes-memory-pgvector v0.5.0 da. Es ist die erste Veröffentlichung in der 0.x-Reihe mit einer tatsächlich einschneidenden Änderung, und der Grund ist ein Name, den ich von Anfang an nicht hätte verwenden sollen.

Der Konflikt

Das Importpaket hieß pgvector. Dieser Name gehört pgvector-python. Installiert man beide in dieselbe virtuelle Umgebung, gewinnt die zuletzt geladene Variante den Top-Level-Namen — und an diesem Punkt konnte der Discovery-Shim dieses Plugins das falsche Modul vollständig importieren.

Der Fehlmodus macht den größeren Versionssprung gerechtfertigt: Der Loader behandelt einen fehlerhaften Import als „Plugin fehlt“ und fällt mit einer einzigen Logzeile auf die eingebaute Memory-Funktion zurück. Nichts stürzt ab. Die Flotte hört nur stillschweigend auf, Memory zu teilen, und Wochen später merkt man erst, dass der Abruf leer zurückkommt.

Das Importpaket heißt daher jetzt hermes_pgvector. Drei Dinge haben sich nicht geändert, weil es separate Namensräume sind und nur einer davon umgezogen ist:

  • die Distribution — weiterhin hermes-memory-pgvector
  • die CLI — weiterhin hermes-pgvector
  • der hermes provider — weiterhin pgvector (memory.provider: pgvector)

Upgrade

v0.5.0 deklariert den Entry Point hermes_agent.memory_providers, sodass auf einem Host, der Plugins auf diese Weise auflösen kann, ein einfaches pip install jetzt ausreicht und der Shim nicht mehr benötigt wird. Ein Shim-Verzeichnis hat jedoch weiterhin Vorrang vor dem Entry Point, und genau deshalb muss ein veralteter Shim entfernt werden:

pip install -U hermes-memory-pgvector

# bevorzugt: den Shim entfernen und die Auflösung dem Entry Point überlassen
hermes-pgvector install --remove

# älterer Host, der nur Plugin-Verzeichnisse scannt:
hermes-pgvector install --force

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

Prüfe deine geplanten Jobs, bevor du weggehst. Jede python -m pgvector ...-Invocation ist jetzt hermes-pgvector ..., und sie scheitert stillschweigend — der nächtliche Backfill stoppt einfach, und Zeilen, die während eines Embed-Ausfalls geschrieben wurden, bleiben nicht durchsuchbar:

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

Der Teil, den ich nicht erwartet hatte

Eine vollständige Codebasis-Prüfung aller 24 verfolgten Dateien brachte 28 bestätigte Befunde zutage. Die schmerzhaften waren keine Randfälle — es waren Config-Verträge, die sich innerhalb dieses Repos vollständig widersprachen und jeweils stillschweigend eine Steuerung umkehrten, obwohl sie korrekt konfiguriert aussahen:

  • allowed_themes war als String deklariert und als Liste verwendet. Eine String-Allowlist wurde Zeichen für Zeichen iteriert, sodass jedes Theme den Membership-Test nicht bestand und die gesamte Flotte auf default umgeleitet wurde. Identity Governance sah konfiguriert aus, während sie genau das Gegenteil tat.
  • Boolesche Schalter ignorierten false. embed_on_write, sync_turns, hybrid_search und bulk_sync_on_init werden vom Schema als die Strings "true"/"false" deklariert und dann mit einfacher Wahrheitswertprüfung gelesen. bool("false") ist True.
  • Embed-Timeouts wurden nie aus der Config übernommen. Jeder Aufrufer verwendete hart kodiert 10s. Gegen einen Endpunkt, der in 6–17s antwortet, liefen große Teile der Writes in ein Timeout und landeten mit NULL-Embeddings — und Wiederholungen konnten nicht helfen, weil jeder Versuch unterhalb der benötigten Latenz gedeckelt war. Jetzt nach Pfad getrennt: embed_timeout (10s, Agent-Thread) und embed_write_timeout (30s, Background-Writer).
  • replace() aktualisierte null Zeilen. Ein Bulk-UPDATE kollidierte mit UNIQUE(agent_identity, target, content) und löste UniqueViolation aus, sodass ein Replace, der zwei oder mehr Einträge traf, nichts tat, während der Zustand des eingebauten Tools weiterlief.
  • Jede Gesprächsrunde wurde zweimal geschrieben. sync_turn und on_session_end erfassten beide dieselben Turns, und conversations hat keinen Unique-Constraint.
  • Eine tote Datenbank war still. Worker-Fehler wurden auf debug protokolliert und _healthy wurde nie erneut geprüft, sodass ein Postgres-Neustart jeden dauerhaften Write für die Sitzung ohne jedes Signal verwarf.

Das ist die ehrliche Geschichte dieser Veröffentlichung: Die Umbenennung ist die Schlagzeile, und die Config-Bugs sind der Grund, warum die Umbenennung jetzt sinnvoll war und nicht erst bei 1.0.

Identity-Bucketung wird jetzt auf beiden Seiten erzwungen

whatsapp-dm, das neue external-group und _bench waren nur Sinks auf der Write-Seite. Die Identity-Normalisierung entfernte PII aus der Identity, aber Nachrichtentexte leben weiterhin in content, und beim Lesen wurde nichts gefiltert — also konnte jedes Theme DM-Inhalte über scope='all' in seinen Kontext ziehen. Jetzt schließt scope='all' diese Sinks aus, und die explizite Benennung eines solchen Sinks wird abgelehnt. Ein Agent, der selbst ein Bucket ist, behält vollen Zugriff auf seine eigenen Zeilen, und das normale themenübergreifende Abrufen bleibt unverändert.

Ein wichtiger Hinweis: Das Gate schließt nach Bucket-Namen aus, und das Bucketing erfolgt zum Schreibzeitpunkt. Zeilen werden nie nachträglich umgeschrieben — das ist beabsichtigt, denn historische Zeilen würden sonst nicht mehr abrufbar sein. Zeilen, die geschrieben wurden, bevor ihr Schlüssel gebucktet war, behalten die rohe Identity. hermes-pgvector remap --old <raw> --new whatsapp-dm zeigt die Verschiebung in der Vorschau; füge --execute hinzu, um sie tatsächlich auszuführen.

Validierung

100 Tests im Skip-Modus, 145 gegen live Postgres 16 mit pgvector 0.8.6 und alle vier Migrationen angewendet. In dieser Veröffentlichung gibt es keine Schemaänderungen und keine neuen Migrationen.

Update — v0.5.1

v0.5.1 erschien eine Stunde später, und es ist die eine Veröffentlichung in dieser Reihe, auf der man nicht sitzen bleiben sollte: ein einzelnes memory remove löschte für ein Theme jeden gespiegelten Eintrag, nicht nur einen.

Der Remove-Op des eingebauten Tools trägt sein Ziel in old_text und lässt content leer, und der Host reicht ihn über Metadaten statt über Content weiter. _worker übergab old_text=item.content, was daher immer "" war — also baute store.remove content LIKE '%%', und das trifft jede Zeile. Ein Remove löschte den gesamten Mirror für dieses (agent_identity, target)-Paar. Der eingebaute Store war nie betroffen; nur der pgvector-Mirror. Ich habe die Referenzbereitstellung auf Schäden geprüft und keine gefunden — die Historie jedes Themes ist lückenlos, was vor allem zeigt, wie selten Removes in der Praxis sind.

Das ist auf zwei Ebenen behoben, denn eine reicht für ein stilles Vollbereichs-Löschen nicht aus: _worker liest jetzt extra["old_text"] und lehnt einen Remove ohne brauchbares Ziel ab, und store.remove() weist ein leeres Muster konsequent zurück, sodass das zerstörerische Löschen durch Unterlassung von jedem Aufrufer aus unerreichbar ist. remove() löscht außerdem jetzt höchstens eine Zeile und entspricht damit dem eingebauten Verhalten.

Die gleiche Veröffentlichung behob das Problem mit leerem Inhalt, das dahinterlag. Nichts wies ein leeres add/replace zurück, sodass eine Zeile existieren konnte, die embed() niemals verarbeiten kann — EmbeddingError("empty input") ist für leeren Text bedingungslos. Der nächtliche Backfill versuchte es dann bei jedem Lauf erneut, hielt failed über Null und machte remaining == 0 unerreichbar, was genau das eine Signal zerstört, auf das ein Operator tatsächlich schaut: Eine dauerhaft festhängende Zeile wird von einem neuen echten Fehler nicht mehr unterschieden. Leere Writes werden jetzt übersprungen (remove ausgenommen), und nicht einbettbare Zeilen werden aus dem Sweep ausgeschlossen und separat als unembeddable gemeldet.

Zwei kleinere Dinge sind erwähnenswert, weil beide unsichtbar waren. Postgres trim() entfernt nur Leerzeichen, also rutschte eine Zeile mit nur einem Zeilenumbruch oder Tab weiterhin durch und scheiterte weiterhin für immer — jede Stelle verwendet jetzt content ~ '\S'. Und diese Prädikate waren einfache f-Strings, in denen \S eine ungültige Escape-Sequenz ist: heute vier DeprecationWarnings, ab 3.12+ SyntaxWarning, und unter -W error schlägt das Modul beim Import vollständig fehl — was hermes-agent's Loader dazu bringen würde, stillschweigend auf die eingebaute Memory-Funktion zurückzufallen.

Validierung: 118 Tests im Skip-Modus, 164 gegen live Postgres 16 mit pgvector 0.8.6, alle vier Migrationen angewendet. Zwei Review-Durchläufe — der erste fand den Datenverlustfehler, der zweite die kaputte Escape-Sequenz in dieser Korrektur.

Hol es dir

pip install hermes-memory-pgvector

Quellcode, Upgrade-Hinweise und vollständige Config-Referenz auf GitHub:

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