7 सितंबर 2026

hermes-memory-pgvector v0.5: एक ब्रेकिंग इम्पोर्ट रिनेम, और उसके पीछे के सुधार

Andrea Borghi द्वारा
hermes-memory-pgvector v0.5: एक ब्रेकिंग इम्पोर्ट रिनेम, और उसके पीछे के सुधार

v0.4.2 के बाद, जिसने प्लगइन को pip-installable बनाया था, छह हफ्ते बाद hermes-memory-pgvector v0.5.0 जारी हो गया है। यह 0.x लाइन का पहला रिलीज़ है जिसमें सचमुच एक breaking change है, और इसकी वजह एक ऐसा नाम है जिसे मुझे पहली ही बार में नहीं लेना चाहिए था।

टकराव

इम्पोर्ट पैकेज का नाम pgvector था। यह नाम pgvector-python का है। दोनों को एक ही virtualenv में install करें, और जो भी आख़िर में land करे वही top-level name जीत लेता है — और उसी समय इस प्लगइन का discovery shim पूरी तरह गलत module import कर सकता है।

यही failure mode इसे एक major-ish bump के लायक बनाता है: loader एक खराब import को "plugin absent" मानता है और एक ही log line पर built-in memory पर fallback कर जाता है। कुछ भी crash नहीं होता। पूरा fleet बस चुपचाप memory share करना बंद कर देता है, और आपको हफ्तों बाद पता चलता है जब recall खाली वापस आता है।

इसलिए अब इम्पोर्ट पैकेज hermes_pgvector है। तीन चीज़ें जो बदली नहीं हैं, क्योंकि वे अलग namespaces हैं और उनमें से केवल एक ही move हुई है:

  • distribution — अभी भी hermes-memory-pgvector
  • CLI — अभी भी hermes-pgvector
  • hermes provider — अभी भी pgvector (memory.provider: pgvector)

अपग्रेड करना

v0.5.0 hermes_agent.memory_providers entry point घोषित करता है, इसलिए जिस host पर plugins को इस तरह resolve किया जा सके, वहाँ साधारण pip install अब पर्याप्त है और shim की ज़रूरत नहीं रहती। हालांकि shim directory अभी भी entry point पर precedence लेता है, और यही कारण है कि पुराने shim को हटाना ज़रूरी है:

pip install -U hermes-memory-pgvector

# पसंदीदा तरीका: shim हटाएँ, entry point को इसे resolve करने दें
hermes-pgvector install --remove

# पुराना host जो केवल plugin directories scan करता है:
hermes-pgvector install --force

hermes memory status        # अपेक्षित: Provider: pgvector; Status: available

बाहर जाने से पहले अपने scheduled jobs जाँच लें। अब कोई भी python -m pgvector ... invocation hermes-pgvector ... है, और यह चुपचाप fail करता है — nightly backfill बस रुक जाता है, और embed outage के दौरान लिखी गई rows search नहीं की जा सकतीं:

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

वह हिस्सा जिसकी मुझे उम्मीद नहीं थी

सभी 24 tracked files की full-codebase review में 28 confirmed findings निकले। जो सबसे चुभने वाले थे वे edge cases नहीं थे — वे config contracts थे जो इस repo के भीतर ही एक-दूसरे से पूरी तरह उलट थे, और हर एक सही तरीके से configured दिखते हुए भी किसी control को चुपचाप उल्टा कर रहा था:

  • allowed_themes को string के रूप में घोषित किया गया था और list की तरह consume किया गया। String allow-list को character by character iterate किया गया, इसलिए हर theme membership test में fail हो गया और पूरा fleet default पर route कर दिया गया। Identity governance configured दिखती रही जबकि वह उसका बिल्कुल उल्टा कर रही थी।
  • Boolean toggles ने false को अनदेखा किया। embed_on_write, sync_turns, hybrid_search और bulk_sync_on_init schema में strings "true"/"false" के रूप में घोषित हैं, फिर उन्हें plain truthiness से read किया जाता है। bool("false") True होता है।
  • Embed timeouts config से कभी pass ही नहीं किए गए। हर caller hardcoded 10s ले रहा था। 6–17s में जवाब देने वाले endpoint के सामने, writes का बड़ा हिस्सा timeout हो गया और NULL embeddings के साथ land हुआ — और retries मदद नहीं कर सके, क्योंकि हर attempt उस latency से नीचे cap था जिसकी उसे ज़रूरत थी। अब path के हिसाब से split है: embed_timeout (10s, agent thread) और embed_write_timeout (30s, background writer)।
  • replace() ने zero rows update कीं। एक bulk UPDATE UNIQUE(agent_identity, target, content) से टकराया और UniqueViolation फेंक दी, इसलिए दो या अधिक entries से match करने वाला replace कुछ नहीं करता था जबकि built-in tool की state आगे बढ़ जाती थी।
  • हर conversation turn दो बार लिखा गया। sync_turn और on_session_end दोनों वही turns capture करते थे, और conversations पर कोई unique constraint नहीं है।
  • एक dead database चुप था। Worker failures debug पर log होते थे और _healthy को कभी re-probe नहीं किया गया, इसलिए Postgres restart ने session के हर durable write को बिना किसी signal के discard कर दिया।

इस release की ईमानदार कहानी यही है: rename headline है, और config bugs इस बात की वजह हैं कि rename अभी, 1.0 के बजाय, किया जाना चाहिए था।

Identity bucketing अब दोनों तरफ enforce होता है

whatsapp-dm, नया external-group, और _bench केवल write-side sinks थे। Identity normalization ने identity से PII हटा दिया था, लेकिन message bodies अभी भी content में रहते हैं और read पर कुछ भी उन्हें filter नहीं करता था — इसलिए कोई भी theme scope='all' के ज़रिए DM content को अपने context में खींच सकता था। अब scope='all' उन sinks को exclude करता है और किसी एक को नाम से चुनना rejected है। जो agent bucket है, उसे अपनी ही rows पर पूरा access मिलता है, और सामान्य cross-theme recall अपरिवर्तित है।

एक सावधानी जिसे ध्यान से पढ़ना चाहिए: gate bucket name के आधार पर exclude करता है, और bucketing write time पर होती है। Rows कभी retroactively rewrite नहीं होतीं — यह जानबूझकर है, क्योंकि वरना historical rows unrecallable हो जातीं। जिन rows के लिखे जाने से पहले उनकी key bucket नहीं की गई थी, वे raw identity रखती हैं। hermes-pgvector remap --old <raw> --new whatsapp-dm move का preview दिखाता है; actual perform करने के लिए --execute जोड़ें।

Validation

skip-mode में 100 tests, live Postgres 16 के साथ pgvector 0.8.6 और सभी चार migrations लागू होने पर 145 tests। इस release में कोई schema changes नहीं और कोई नई migrations नहीं।

Update — v0.5.1

v0.5.1 एक घंटे बाद आया, और यह इस line का वह release है जिस पर आपको रुकना नहीं चाहिए: एक memory remove ने पूरे theme के हर mirrored entry को delete कर दिया, सिर्फ एक को नहीं।

Built-in tool की remove op अपना target old_text में रखती है और content खाली छोड़ देती है, और host उसे content की बजाय metadata के माध्यम से forward करता है। _worker old_text=item.content pass कर रहा था, जो इसलिए हमेशा "" था — इसलिए store.remove ने content LIKE '%%' बनाया, और वह हर row से match करता है। एक remove ने उस (agent_identity, target) pair के लिए पूरे mirror को wipe कर दिया। Built-in store पर कभी असर नहीं पड़ा; सिर्फ pgvector mirror पर। मैंने damage के लिए reference deployment की जाँच की और कुछ नहीं मिला — हर theme का history लगातार है, जो ज़्यादातर यह बताता है कि practice में removes कितने rare हैं।

इसे दो layers पर fix किया गया है, क्योंकि silent full-scope delete के लिए एक काफी नहीं है: _worker अब extra["old_text"] पढ़ता है और बिना usable target वाले remove को refuse करता है, और store.remove() एक खाली pattern को सीधे reject करता है, इसलिए omission के ज़रिए destructive delete किसी भी caller से reach नहीं किया जा सकता। remove() अब built-in की तरह अधिकतम एक row ही delete करता है।

उसी release ने उसके पीछे छिपी empty-content समस्या को भी ठीक किया। किसी empty add/replace को reject नहीं किया जाता था, इसलिए एक ऐसी row हो सकती थी जिसे embed() कभी process नहीं कर सकता — blank text के लिए EmbeddingError("empty input") unconditional है। फिर nightly backfill हर run पर उसे retry करता था, जिससे failed zero से ऊपर pinned रहता था और remaining == 0 unreachable हो जाता था, जो उस एक signal को तोड़ देता था जिस पर operator वास्तव में नज़र रखता है: permanently stuck row एक नई असली failure से अलग नहीं दिखती थी। Empty writes अब skip की जाती हैं (remove exempt है), और un-embeddable rows sweep से exclude करके अलग से unembeddable के रूप में report की जाती हैं।

दो छोटी बातें जिनका नाम लेना ज़रूरी है, क्योंकि दोनों अदृश्य थीं। Postgres trim() सिर्फ spaces हटाता है, इसलिए सिर्फ newline या tab वाली row फिर भी slip कर जाती थी और हमेशा fail होती थी — अब हर site content ~ '\S' का उपयोग करता है। और वे predicates plain f-strings थे, जहाँ \S एक invalid escape है: आज चार DeprecationWarnings, 3.12+ पर SyntaxWarning, और -W error के तहत module बिल्कुल import नहीं होता — जिससे hermes-agent का loader चुपचाप built-in memory पर fallback कर जाता।

Validation: skip-mode में 118 tests, live Postgres 16 के साथ pgvector 0.8.6, सभी चार migrations लागू। दो review passes — पहले ने data-loss bug पकड़ा, दूसरे ने उस fix में broken escape sequence पकड़ा।

इसे प्राप्त करें

pip install hermes-memory-pgvector

Source, upgrade notes, और full config reference GitHub पर:

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