2026年9月7日

hermes-memory-pgvector v0.5: 壊れるインポート名の変更と、その背後にある修正

Andrea Borghi による
hermes-memory-pgvector v0.5: 壊れるインポート名の変更と、その背後にある修正

v0.4.2 でプラグインが pip インストール可能になってから 6 週間後、hermes-memory-pgvector v0.5.0 がリリースされました。0.x 系で本当に破壊的な変更を含む最初のリリースであり、その理由は、そもそも最初から採用すべきではなかった名前にあります。

衝突

インポートパッケージ名は pgvector でした。その名前は pgvector-python が所有しています。同じ virtualenv に両方を入れると、最後に入ったほうがトップレベル名を奪い、その時点でこのプラグインの discovery shim はまったく別のモジュールを読み込んでしまう可能性がありました。

この失敗モードこそが、これを少し大きめのバージョンアップに値するものにしています。ローダーは不正な import を「プラグイン不在」とみなし、単一のログ行だけを残して組み込みメモリへフォールバックします。クラッシュはしません。単に全体でメモリ共有が静かに止まり、数週間後に想起結果が空で戻ってきたときにはじめて気づくのです。

そのため、インポートパッケージ名は現在 hermes_pgvector です。変わっていないものは 3 つあります。別々の名前空間であり、移動したのはそのうち 1 つだけだからです。

  • ディストリビューション — 依然として hermes-memory-pgvector
  • CLI — 依然として hermes-pgvector
  • hermes provider — 依然として pgvector (memory.provider: pgvector)

アップグレード

v0.5.0 では hermes_agent.memory_providers の entry point を宣言しているため、そのような方法でプラグインを解決できる十分新しいホストでは、通常の pip install だけで足り、shim は不要になりました。とはいえ shim ディレクトリ は entry point より優先されるため、古いものは削除しなければなりません。

pip install -U hermes-memory-pgvector

# 推奨: shim を削除して、entry point に解決させる
hermes-pgvector install --remove

# プラグインディレクトリしかスキャンしない古いホスト:
hermes-pgvector install --force

hermes memory status        # 期待値: Provider: pgvector; Status: available

離れる前にスケジュール済みジョブを確認してください。 python -m pgvector ... の呼び出しはすべて hermes-pgvector ... に変更されており、しかも静かに失敗します。つまり、夜間のバックフィルはただ停止し、埋め込み障害中に書き込まれた行は検索不能のまま残ります。

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

想定していなかった部分

追跡対象 24 ファイルすべてを対象にしたフルコードベースのレビューで、28 件の確定事項が見つかりました。痛かったのは境界ケースではなく、このリポジトリ内で設定契約が完全に食い違っているものです。どれも正しく設定されているように見せながら、制御を静かに反転させていました。

  • allowed_themes は文字列として宣言され、リストとして消費されていました。 文字列の allow-list は 1 文字ずつ反復されるため、すべての theme が membership test に失敗し、全体が default にルーティングされていました。アイデンティティのガバナンスは、まったく逆のことをしながら設定済みに見えていました。
  • Boolean トグルは false を無視していました。 embed_on_writesync_turnshybrid_searchbulk_sync_on_init は schema 上は文字列の "true"/"false" として宣言されているのに、単純な truthiness で読み取られていました。bool("false")True です。
  • 埋め込みタイムアウトは config から一切流し込まれていませんでした。 どの呼び出し元もハードコードされた 10s を使っていました。6–17s で応答する endpoint に対しては、多数の write が timeout し、NULL embeddings で着地していました。しかも各試行が必要な latency より 低く 上限設定されていたため、retry は助けになりませんでした。現在は経路ごとに分割されています: embed_timeout (10s, agent thread) と embed_write_timeout (30s, background writer)。
  • replace() は 0 行しか更新していませんでした。 一括 UPDATE が UNIQUE(agent_identity, target, content) に衝突して UniqueViolation を投げるため、2 件以上に一致する replace は何も行わず、その間に組み込みツールの state だけが先へ進んでいました。
  • すべての conversation turn が 2 回書き込まれていました。 sync_turnon_session_end の両方が同じ turn を記録しており、conversations には unique constraint がありません。
  • 死んだ database は無音でした。 worker の failure は debug にしか記録されず、_healthy は再チェックされなかったため、Postgres の restart により、その session の durable write はすべて何の合図もなく破棄されました。

これが、このリリースの正直な話です。rename が見出しであり、config バグこそが、1.0 ではなく今 rename をやる価値があった理由です。

アイデンティティの bucketing は、現在は両側で強制されています

新しい external-groupwhatsapp-dm、そして _bench は write-side の sink にしかなっていませんでした。アイデンティティの正規化は identity から PII を取り除いていましたが、message body は依然として content にあり、read 時点では何もフィルタされていませんでした。そのため、どの theme も scope='all' を通じて DM content を context に取り込めたのです。現在は scope='all' がそれらの sink を除外し、明示的に 1 つ名前を挙げることも拒否されます。bucket である agent は自分自身の rows への完全なアクセスを保持し、通常のテーマ間 recall は変わりません。

慎重に読む価値のある注意点が 1 つあります。gate は bucket の name によって除外し、bucketing は write 時に行われます。rows は後から遡って書き換えられることはありません。それは意図的です。なぜなら、そうしなければ履歴 rows が再想起不能になってしまうからです。key が bucket 化される前に書かれた rows は、raw identity を保持します。hermes-pgvector remap --old <raw> --new whatsapp-dm で移行をプレビューでき、実際に実行するには --execute を追加します。

検証

skip-mode で 100 tests、pgvector 0.8.6 とすべての 4 migrations を適用した live Postgres 16 に対して 145 tests。今回の release では schema changes も新しい migrations もありません。

更新 — v0.5.1

v0.5.1 は 1 時間後にリリースされました。この系統で、先送りにすべきではない唯一の release です。たった 1 回の memory remove で、1 つではなくテーマのミラーされたエントリがすべて削除されていました。

組み込みツールの remove op は対象を old_text に持ち、content は空のままにします。ホストはそれを content ではなく metadata 経由で渡します。ところが _workerold_text=item.content を渡していたため、常に "" になっていました。すると store.removecontent LIKE '%%' を組み立て、それは すべて の row に一致します。1 回の remove で、その (agent_identity, target) ペアのミラー全体が消えていました。組み込み store は一切影響を受けず、影響を受けたのは pgvector mirror だけです。reference deployment の被害を確認したところ、損傷は見つかりませんでした。各 theme の履歴は途切れなく続いており、それは主に、実運用で remove がどれほどまれかを示しています。

これは 2 層で修正されています。無音の full-scope delete に対しては 1 層では不十分だからです。_worker は現在 extra["old_text"] を読み、使える対象のない remove を拒否します。さらに store.remove() は空の pattern を明示的に拒否するため、削除はどの呼び出し元からも省略によって到達できません。remove() はまた、組み込み実装に合わせて最大 1 行しか削除しなくなりました。

同じ release では、その背後にあった空の content 問題も修正されました。空の add/replace は何も拒否しなかったため、embed() が決して処理できない row が存在し得ました。blank text に対しては EmbeddingError("empty input") が無条件で発生します。夜間の backfill はその row を毎回再試行し、failed を 0 より上に固定し、remaining == 0 を到達不能にしていました。これは operator が実際に見るたった 1 つの signal を壊します。つまり、永久に詰まった row は新たな本物の failure と区別できなくなるのです。空の write は現在スキップされ(remove は除外)、埋め込み不能の row は sweep から除外され、unembeddable として別個に報告されます。

どちらも見えなかったので、名前を挙げておく価値のある小さな点が 2 つあります。Postgres の trim() は空白だけを取り除くため、改行やタブだけを持つ row はそのまま通過して、やはり永久に失敗していました。現在はすべての site で content ~ '\S' を使っています。そしてそれらの predicate は通常の f-string だったため、\S は無効な escape でした。今日の時点では 4 件の DeprecationWarning、3.12+ では SyntaxWarning、さらに -W error では module の import が完全に失敗します。そうなると hermes-agent の loader は静かに built-in memory にフォールバックしてしまうでしょう。

検証: skip-mode で 118 tests、pgvector 0.8.6 とすべての 4 migrations を適用した live Postgres 16 に対して 164 tests。2 回の review pass があり、1 回目でデータ損失バグが見つかり、2 回目でその修正に含まれていた壊れた escape sequence が見つかりました。

入手する

pip install hermes-memory-pgvector

ソース、アップグレードノート、完全な config reference は GitHub にあります:

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