hermes-memory-pgvector 0.5.1__tar.gz → 0.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/PKG-INFO +23 -12
  2. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/README.md +22 -11
  3. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_memory_pgvector.egg-info/PKG-INFO +23 -12
  4. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_pgvector/plugin.yaml +1 -1
  5. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/pyproject.toml +1 -1
  6. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/tests/test_empty_content.py +14 -5
  7. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/LICENSE +0 -0
  8. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_memory_pgvector.egg-info/SOURCES.txt +0 -0
  9. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_memory_pgvector.egg-info/dependency_links.txt +0 -0
  10. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_memory_pgvector.egg-info/entry_points.txt +0 -0
  11. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_memory_pgvector.egg-info/requires.txt +0 -0
  12. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_memory_pgvector.egg-info/top_level.txt +0 -0
  13. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_pgvector/__init__.py +0 -0
  14. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_pgvector/__main__.py +0 -0
  15. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_pgvector/embed.py +0 -0
  16. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_pgvector/identity.py +0 -0
  17. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_pgvector/migrations/001_schema.sql +0 -0
  18. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_pgvector/migrations/002_agent_attribution.sql +0 -0
  19. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_pgvector/migrations/003_hybrid_search_fts.sql +0 -0
  20. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_pgvector/migrations/004_runtime_grants.sql +0 -0
  21. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_pgvector/store.py +0 -0
  22. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/hermes_pgvector/writer.py +0 -0
  23. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/setup.cfg +0 -0
  24. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/tests/test_async_writer.py +0 -0
  25. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/tests/test_config_coercion.py +0 -0
  26. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/tests/test_embed_timeouts.py +0 -0
  27. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/tests/test_exclude_identities_live.py +0 -0
  28. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/tests/test_hybrid_search.py +0 -0
  29. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/tests/test_identity.py +0 -0
  30. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/tests/test_install_shim.py +0 -0
  31. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/tests/test_read_side_gate.py +0 -0
  32. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/tests/test_save_config_merge.py +0 -0
  33. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/tests/test_session_switch_contract.py +0 -0
  34. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/tests/test_smoke.py +0 -0
  35. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/tests/test_store_v04.py +0 -0
  36. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/tests/test_system_prompt_block.py +0 -0
  37. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/tests/test_tool_args_hardening.py +0 -0
  38. {hermes_memory_pgvector-0.5.1 → hermes_memory_pgvector-0.5.2}/tests/test_turn_dedup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hermes-memory-pgvector
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: Postgres + pgvector memory provider plugin for hermes-agent. Multi-agent storage layer with per-minion themes, identity governance, agent attribution + delegation provenance, async writer, no LLM in the memory hot path.
5
5
  Author: Andrea Borghi
6
6
  License: BSD-3-Clause
@@ -125,17 +125,6 @@ Still a storage-layer feature: **no LLM, no entity graph, no new tables or colum
125
125
 
126
126
  - **Dependency floor raised**: `psycopg[binary]>=3.3.5` (upstream bugfix release, 2026-08-31: prepared-statement invalidation on `ALTER`/`DISCARD`, DataError fixes for malformed COPY/jsonb data, client-encoding aliases). No code changes.
127
127
 
128
- ## New in v0.5.1 — `memory remove` no longer wipes a whole theme
129
-
130
- Patch release, but **upgrade promptly**: it fixes a data-loss bug. No schema changes, no migrations, no API changes.
131
-
132
- - **`remove` deleted every mirrored entry for a theme, not one.** `_worker` passed `old_text=item.content` — but the built-in tool's remove op carries its target in `old_text` and leaves `content` empty, and the host forwards `old_text` via *metadata*. So `item.content` was always `""`, `store.remove` built `content LIKE '%%'`, and that matches every row: a single `memory remove` deleted the entire mirror for that `(agent_identity, target)`. Verified against Postgres — `DELETE … WHERE c LIKE '%%'` removes all rows. `_worker` now reads `extra["old_text"]`, and `store.remove()` **refuses an empty pattern outright**, so no caller can reach that delete by omission. `remove()` also now deletes **at most one row** (lowest id), matching both the built-in tool — which requires a unique match and errors on ambiguity — and this class's own `replace()`. (The built-in store was never affected; only the pgvector mirror. No loss occurred on the reference deployment: every theme's history is continuous.)
133
-
134
- Found in production: one `memory_entries` row sat with a NULL embedding and **zero-length content**, arrived through the built-in tool's `replace` path. Two defects met there.
135
-
136
- - **Nothing rejected empty content on write.** `on_memory_write` filtered on `target` and `action` but never on content, so an `add`/`replace` carrying nothing created a row that can never be embedded — `embed()` raises `EmbeddingError("empty input")` unconditionally for empty or whitespace text. Such writes are now ignored (`remove` is exempt: it legitimately arrives with empty content and targets the row via `old_text`).
137
- - **`backfill_null_embeddings` retried it forever.** The sweep selected `WHERE embedding IS NULL` with no content filter, so every nightly run re-fetched the row, called `embed()`, failed, and moved on — permanently pinning `failed` above zero and making `remaining == 0` unreachable. That is the damaging half: it destroys the one signal an operator watches, because you can no longer distinguish a permanently-stuck row from a new genuine failure. Un-embeddable rows are now **skipped and reported separately** as `unembeddable` (skipping them silently would be equally misleading), so `remaining` can actually reach zero again.
138
-
139
128
  ## New in v0.5.0 — import rename (BREAKING), read-side identity gate, config-contract fixes
140
129
 
141
130
  > ### Breaking upgrade — read before installing
@@ -200,6 +189,28 @@ Correctness release from a full-codebase review. **No schema changes and no new
200
189
  - **`save_config` stops deleting your settings.** It replaced the whole `plugins.pgvector` block with schema-declared keys, silently dropping hand-edited ones that are read at runtime (`identity_aliases`, `embed_write_backoff`). It merges now.
201
190
  - **Fail-soft hardening** (invariant #4): `sync_turn` is wrapped, config casts are guarded, and the recall tools coerce non-string `query`/`scope`/`target` instead of raising `AttributeError` out of the hook. `hermes-pgvector install --remove` now fails closed and requires `--force` on a directory that isn't a generated shim, instead of deleting it outright.
202
191
 
192
+ ## New in v0.5.1 — `memory remove` no longer wipes a whole theme
193
+
194
+ Patch release, but **upgrade promptly**: it fixes a data-loss bug. No schema changes, no migrations, no API changes.
195
+
196
+ - **`remove` deleted every mirrored entry for a theme, not one.** `_worker` passed `old_text=item.content` — but the built-in tool's remove op carries its target in `old_text` and leaves `content` empty, and the host forwards `old_text` via *metadata*. So `item.content` was always `""`, `store.remove` built `content LIKE '%%'`, and that matches every row: a single `memory remove` deleted the entire mirror for that `(agent_identity, target)`. Verified against Postgres — `DELETE … WHERE c LIKE '%%'` removes all rows. `_worker` now reads `extra["old_text"]`, and `store.remove()` **refuses an empty pattern outright**, so no caller can reach that delete by omission. `remove()` also now deletes **at most one row** (lowest id), matching both the built-in tool — which requires a unique match and errors on ambiguity — and this class's own `replace()`. (The built-in store was never affected; only the pgvector mirror. No loss occurred on the reference deployment: every theme's history is continuous.)
197
+
198
+ Found in production: one `memory_entries` row sat with a NULL embedding and **zero-length content**, arrived through the built-in tool's `replace` path. Two defects met there.
199
+
200
+ - **Nothing rejected empty content on write.** `on_memory_write` filtered on `target` and `action` but never on content, so an `add`/`replace` carrying nothing created a row that can never be embedded — `embed()` raises `EmbeddingError("empty input")` unconditionally for empty or whitespace text. Such writes are now ignored (`remove` is exempt: it legitimately arrives with empty content and targets the row via `old_text`).
201
+ - **`backfill_null_embeddings` retried it forever.** The sweep selected `WHERE embedding IS NULL` with no content filter, so every nightly run re-fetched the row, called `embed()`, failed, and moved on — permanently pinning `failed` above zero and making `remaining == 0` unreachable. That is the damaging half: it destroys the one signal an operator watches, because you can no longer distinguish a permanently-stuck row from a new genuine failure. Un-embeddable rows are now **skipped and reported separately** as `unembeddable` (skipping them silently would be equally misleading), so `remaining` can actually reach zero again.
202
+
203
+ ## New in v0.5.2 — documentation only
204
+
205
+ **No code changes.** `git diff v0.5.1..v0.5.2` touches only `README.md` and one test file; nothing under `hermes_pgvector/` differs, so the installed behaviour is byte-for-byte identical to v0.5.1. There is no reason to redeploy for this release.
206
+
207
+ It exists because PyPI renders a project's README **frozen at upload time**: two fixes that landed after v0.5.1 shipped were visible on GitHub but not on the package page.
208
+
209
+ - **The v0.5.1 release notes were out of order.** The section sat before v0.5.0 instead of after it, so the newest release was buried mid-list. These sections run oldest-to-newest.
210
+ - A test asserted a `failed` count against a dry-run baseline that is hardcoded to `0`, making the comparison a no-op. Asserted directly now, with the reasoning recorded rather than the misleading framing.
211
+
212
+ If you are on v0.5.1 you already have every fix in this release. If you are on **v0.5.0 or earlier, upgrade** — v0.5.1 fixed a data-loss bug where a single `memory remove` deleted a whole theme's mirrored memory.
213
+
203
214
  ## Multi-agent / per-minion themes
204
215
 
205
216
  Each systemd-run minion sets one header on its OpenAI client; everything else flows automatically:
@@ -92,17 +92,6 @@ Still a storage-layer feature: **no LLM, no entity graph, no new tables or colum
92
92
 
93
93
  - **Dependency floor raised**: `psycopg[binary]>=3.3.5` (upstream bugfix release, 2026-08-31: prepared-statement invalidation on `ALTER`/`DISCARD`, DataError fixes for malformed COPY/jsonb data, client-encoding aliases). No code changes.
94
94
 
95
- ## New in v0.5.1 — `memory remove` no longer wipes a whole theme
96
-
97
- Patch release, but **upgrade promptly**: it fixes a data-loss bug. No schema changes, no migrations, no API changes.
98
-
99
- - **`remove` deleted every mirrored entry for a theme, not one.** `_worker` passed `old_text=item.content` — but the built-in tool's remove op carries its target in `old_text` and leaves `content` empty, and the host forwards `old_text` via *metadata*. So `item.content` was always `""`, `store.remove` built `content LIKE '%%'`, and that matches every row: a single `memory remove` deleted the entire mirror for that `(agent_identity, target)`. Verified against Postgres — `DELETE … WHERE c LIKE '%%'` removes all rows. `_worker` now reads `extra["old_text"]`, and `store.remove()` **refuses an empty pattern outright**, so no caller can reach that delete by omission. `remove()` also now deletes **at most one row** (lowest id), matching both the built-in tool — which requires a unique match and errors on ambiguity — and this class's own `replace()`. (The built-in store was never affected; only the pgvector mirror. No loss occurred on the reference deployment: every theme's history is continuous.)
100
-
101
- Found in production: one `memory_entries` row sat with a NULL embedding and **zero-length content**, arrived through the built-in tool's `replace` path. Two defects met there.
102
-
103
- - **Nothing rejected empty content on write.** `on_memory_write` filtered on `target` and `action` but never on content, so an `add`/`replace` carrying nothing created a row that can never be embedded — `embed()` raises `EmbeddingError("empty input")` unconditionally for empty or whitespace text. Such writes are now ignored (`remove` is exempt: it legitimately arrives with empty content and targets the row via `old_text`).
104
- - **`backfill_null_embeddings` retried it forever.** The sweep selected `WHERE embedding IS NULL` with no content filter, so every nightly run re-fetched the row, called `embed()`, failed, and moved on — permanently pinning `failed` above zero and making `remaining == 0` unreachable. That is the damaging half: it destroys the one signal an operator watches, because you can no longer distinguish a permanently-stuck row from a new genuine failure. Un-embeddable rows are now **skipped and reported separately** as `unembeddable` (skipping them silently would be equally misleading), so `remaining` can actually reach zero again.
105
-
106
95
  ## New in v0.5.0 — import rename (BREAKING), read-side identity gate, config-contract fixes
107
96
 
108
97
  > ### Breaking upgrade — read before installing
@@ -167,6 +156,28 @@ Correctness release from a full-codebase review. **No schema changes and no new
167
156
  - **`save_config` stops deleting your settings.** It replaced the whole `plugins.pgvector` block with schema-declared keys, silently dropping hand-edited ones that are read at runtime (`identity_aliases`, `embed_write_backoff`). It merges now.
168
157
  - **Fail-soft hardening** (invariant #4): `sync_turn` is wrapped, config casts are guarded, and the recall tools coerce non-string `query`/`scope`/`target` instead of raising `AttributeError` out of the hook. `hermes-pgvector install --remove` now fails closed and requires `--force` on a directory that isn't a generated shim, instead of deleting it outright.
169
158
 
159
+ ## New in v0.5.1 — `memory remove` no longer wipes a whole theme
160
+
161
+ Patch release, but **upgrade promptly**: it fixes a data-loss bug. No schema changes, no migrations, no API changes.
162
+
163
+ - **`remove` deleted every mirrored entry for a theme, not one.** `_worker` passed `old_text=item.content` — but the built-in tool's remove op carries its target in `old_text` and leaves `content` empty, and the host forwards `old_text` via *metadata*. So `item.content` was always `""`, `store.remove` built `content LIKE '%%'`, and that matches every row: a single `memory remove` deleted the entire mirror for that `(agent_identity, target)`. Verified against Postgres — `DELETE … WHERE c LIKE '%%'` removes all rows. `_worker` now reads `extra["old_text"]`, and `store.remove()` **refuses an empty pattern outright**, so no caller can reach that delete by omission. `remove()` also now deletes **at most one row** (lowest id), matching both the built-in tool — which requires a unique match and errors on ambiguity — and this class's own `replace()`. (The built-in store was never affected; only the pgvector mirror. No loss occurred on the reference deployment: every theme's history is continuous.)
164
+
165
+ Found in production: one `memory_entries` row sat with a NULL embedding and **zero-length content**, arrived through the built-in tool's `replace` path. Two defects met there.
166
+
167
+ - **Nothing rejected empty content on write.** `on_memory_write` filtered on `target` and `action` but never on content, so an `add`/`replace` carrying nothing created a row that can never be embedded — `embed()` raises `EmbeddingError("empty input")` unconditionally for empty or whitespace text. Such writes are now ignored (`remove` is exempt: it legitimately arrives with empty content and targets the row via `old_text`).
168
+ - **`backfill_null_embeddings` retried it forever.** The sweep selected `WHERE embedding IS NULL` with no content filter, so every nightly run re-fetched the row, called `embed()`, failed, and moved on — permanently pinning `failed` above zero and making `remaining == 0` unreachable. That is the damaging half: it destroys the one signal an operator watches, because you can no longer distinguish a permanently-stuck row from a new genuine failure. Un-embeddable rows are now **skipped and reported separately** as `unembeddable` (skipping them silently would be equally misleading), so `remaining` can actually reach zero again.
169
+
170
+ ## New in v0.5.2 — documentation only
171
+
172
+ **No code changes.** `git diff v0.5.1..v0.5.2` touches only `README.md` and one test file; nothing under `hermes_pgvector/` differs, so the installed behaviour is byte-for-byte identical to v0.5.1. There is no reason to redeploy for this release.
173
+
174
+ It exists because PyPI renders a project's README **frozen at upload time**: two fixes that landed after v0.5.1 shipped were visible on GitHub but not on the package page.
175
+
176
+ - **The v0.5.1 release notes were out of order.** The section sat before v0.5.0 instead of after it, so the newest release was buried mid-list. These sections run oldest-to-newest.
177
+ - A test asserted a `failed` count against a dry-run baseline that is hardcoded to `0`, making the comparison a no-op. Asserted directly now, with the reasoning recorded rather than the misleading framing.
178
+
179
+ If you are on v0.5.1 you already have every fix in this release. If you are on **v0.5.0 or earlier, upgrade** — v0.5.1 fixed a data-loss bug where a single `memory remove` deleted a whole theme's mirrored memory.
180
+
170
181
  ## Multi-agent / per-minion themes
171
182
 
172
183
  Each systemd-run minion sets one header on its OpenAI client; everything else flows automatically:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hermes-memory-pgvector
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: Postgres + pgvector memory provider plugin for hermes-agent. Multi-agent storage layer with per-minion themes, identity governance, agent attribution + delegation provenance, async writer, no LLM in the memory hot path.
5
5
  Author: Andrea Borghi
6
6
  License: BSD-3-Clause
@@ -125,17 +125,6 @@ Still a storage-layer feature: **no LLM, no entity graph, no new tables or colum
125
125
 
126
126
  - **Dependency floor raised**: `psycopg[binary]>=3.3.5` (upstream bugfix release, 2026-08-31: prepared-statement invalidation on `ALTER`/`DISCARD`, DataError fixes for malformed COPY/jsonb data, client-encoding aliases). No code changes.
127
127
 
128
- ## New in v0.5.1 — `memory remove` no longer wipes a whole theme
129
-
130
- Patch release, but **upgrade promptly**: it fixes a data-loss bug. No schema changes, no migrations, no API changes.
131
-
132
- - **`remove` deleted every mirrored entry for a theme, not one.** `_worker` passed `old_text=item.content` — but the built-in tool's remove op carries its target in `old_text` and leaves `content` empty, and the host forwards `old_text` via *metadata*. So `item.content` was always `""`, `store.remove` built `content LIKE '%%'`, and that matches every row: a single `memory remove` deleted the entire mirror for that `(agent_identity, target)`. Verified against Postgres — `DELETE … WHERE c LIKE '%%'` removes all rows. `_worker` now reads `extra["old_text"]`, and `store.remove()` **refuses an empty pattern outright**, so no caller can reach that delete by omission. `remove()` also now deletes **at most one row** (lowest id), matching both the built-in tool — which requires a unique match and errors on ambiguity — and this class's own `replace()`. (The built-in store was never affected; only the pgvector mirror. No loss occurred on the reference deployment: every theme's history is continuous.)
133
-
134
- Found in production: one `memory_entries` row sat with a NULL embedding and **zero-length content**, arrived through the built-in tool's `replace` path. Two defects met there.
135
-
136
- - **Nothing rejected empty content on write.** `on_memory_write` filtered on `target` and `action` but never on content, so an `add`/`replace` carrying nothing created a row that can never be embedded — `embed()` raises `EmbeddingError("empty input")` unconditionally for empty or whitespace text. Such writes are now ignored (`remove` is exempt: it legitimately arrives with empty content and targets the row via `old_text`).
137
- - **`backfill_null_embeddings` retried it forever.** The sweep selected `WHERE embedding IS NULL` with no content filter, so every nightly run re-fetched the row, called `embed()`, failed, and moved on — permanently pinning `failed` above zero and making `remaining == 0` unreachable. That is the damaging half: it destroys the one signal an operator watches, because you can no longer distinguish a permanently-stuck row from a new genuine failure. Un-embeddable rows are now **skipped and reported separately** as `unembeddable` (skipping them silently would be equally misleading), so `remaining` can actually reach zero again.
138
-
139
128
  ## New in v0.5.0 — import rename (BREAKING), read-side identity gate, config-contract fixes
140
129
 
141
130
  > ### Breaking upgrade — read before installing
@@ -200,6 +189,28 @@ Correctness release from a full-codebase review. **No schema changes and no new
200
189
  - **`save_config` stops deleting your settings.** It replaced the whole `plugins.pgvector` block with schema-declared keys, silently dropping hand-edited ones that are read at runtime (`identity_aliases`, `embed_write_backoff`). It merges now.
201
190
  - **Fail-soft hardening** (invariant #4): `sync_turn` is wrapped, config casts are guarded, and the recall tools coerce non-string `query`/`scope`/`target` instead of raising `AttributeError` out of the hook. `hermes-pgvector install --remove` now fails closed and requires `--force` on a directory that isn't a generated shim, instead of deleting it outright.
202
191
 
192
+ ## New in v0.5.1 — `memory remove` no longer wipes a whole theme
193
+
194
+ Patch release, but **upgrade promptly**: it fixes a data-loss bug. No schema changes, no migrations, no API changes.
195
+
196
+ - **`remove` deleted every mirrored entry for a theme, not one.** `_worker` passed `old_text=item.content` — but the built-in tool's remove op carries its target in `old_text` and leaves `content` empty, and the host forwards `old_text` via *metadata*. So `item.content` was always `""`, `store.remove` built `content LIKE '%%'`, and that matches every row: a single `memory remove` deleted the entire mirror for that `(agent_identity, target)`. Verified against Postgres — `DELETE … WHERE c LIKE '%%'` removes all rows. `_worker` now reads `extra["old_text"]`, and `store.remove()` **refuses an empty pattern outright**, so no caller can reach that delete by omission. `remove()` also now deletes **at most one row** (lowest id), matching both the built-in tool — which requires a unique match and errors on ambiguity — and this class's own `replace()`. (The built-in store was never affected; only the pgvector mirror. No loss occurred on the reference deployment: every theme's history is continuous.)
197
+
198
+ Found in production: one `memory_entries` row sat with a NULL embedding and **zero-length content**, arrived through the built-in tool's `replace` path. Two defects met there.
199
+
200
+ - **Nothing rejected empty content on write.** `on_memory_write` filtered on `target` and `action` but never on content, so an `add`/`replace` carrying nothing created a row that can never be embedded — `embed()` raises `EmbeddingError("empty input")` unconditionally for empty or whitespace text. Such writes are now ignored (`remove` is exempt: it legitimately arrives with empty content and targets the row via `old_text`).
201
+ - **`backfill_null_embeddings` retried it forever.** The sweep selected `WHERE embedding IS NULL` with no content filter, so every nightly run re-fetched the row, called `embed()`, failed, and moved on — permanently pinning `failed` above zero and making `remaining == 0` unreachable. That is the damaging half: it destroys the one signal an operator watches, because you can no longer distinguish a permanently-stuck row from a new genuine failure. Un-embeddable rows are now **skipped and reported separately** as `unembeddable` (skipping them silently would be equally misleading), so `remaining` can actually reach zero again.
202
+
203
+ ## New in v0.5.2 — documentation only
204
+
205
+ **No code changes.** `git diff v0.5.1..v0.5.2` touches only `README.md` and one test file; nothing under `hermes_pgvector/` differs, so the installed behaviour is byte-for-byte identical to v0.5.1. There is no reason to redeploy for this release.
206
+
207
+ It exists because PyPI renders a project's README **frozen at upload time**: two fixes that landed after v0.5.1 shipped were visible on GitHub but not on the package page.
208
+
209
+ - **The v0.5.1 release notes were out of order.** The section sat before v0.5.0 instead of after it, so the newest release was buried mid-list. These sections run oldest-to-newest.
210
+ - A test asserted a `failed` count against a dry-run baseline that is hardcoded to `0`, making the comparison a no-op. Asserted directly now, with the reasoning recorded rather than the misleading framing.
211
+
212
+ If you are on v0.5.1 you already have every fix in this release. If you are on **v0.5.0 or earlier, upgrade** — v0.5.1 fixed a data-loss bug where a single `memory remove` deleted a whole theme's mirrored memory.
213
+
203
214
  ## Multi-agent / per-minion themes
204
215
 
205
216
  Each systemd-run minion sets one header on its OpenAI client; everything else flows automatically:
@@ -1,5 +1,5 @@
1
1
  name: pgvector
2
- version: 0.5.1
2
+ version: 0.5.2
3
3
  description: "Postgres + pgvector storage layer for hermes-agent. Mirrors built-in MEMORY.md / USER.md + captures every substantive (user, assistant) chat turn into a conversations table. Per-agent themes via X-Hermes-Session-Key with v0.4 identity governance (PII/DM-key bucketing, bench isolation, allow-list). Agent attribution + parent->child delegation provenance (memory_agents / memory_agent_edges). 768-dim semantic recall with hybrid vector+full-text (RRF) ranking, embedding backfill, conversation TTL. Async writer + connection pool. No LLM mediation."
4
4
  pip_dependencies:
5
5
  - "psycopg[binary]>=3.3.5,<4"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hermes-memory-pgvector"
7
- version = "0.5.1"
7
+ version = "0.5.2"
8
8
  description = "Postgres + pgvector memory provider plugin for hermes-agent. Multi-agent storage layer with per-minion themes, identity governance, agent attribution + delegation provenance, async writer, no LLM in the memory hot path."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -2,6 +2,13 @@
2
2
 
3
3
  No DB required for the write-path half; the backfill half is live-mode.
4
4
 
5
+ IMPORTANT: point PG_TEST_DSN at a THROWAWAY database, never production. The
6
+ backfill cases call backfill_null_embeddings(), which sweeps the WHOLE table --
7
+ so on a shared database it would stamp every pre-existing NULL-embedding row
8
+ with this test's constant [0.1]*768 vector. Those rows are then no longer NULL
9
+ and a real backfill can never repair them. The `failed` assertion is table-wide
10
+ for the same reason.
11
+
5
12
  Found in production. One row in `memory_entries` sat with `embedding IS NULL`
6
13
  and `length(content) = 0` -- its metadata (`old_text`, `tool_name`) showing it
7
14
  arrived through the built-in tool's `replace` path with empty new content.
@@ -176,10 +183,6 @@ def test_backfill_remaining_can_reach_zero_despite_an_empty_row(store):
176
183
  _insert_raw(s, agent, "another embeddable note for the sweep")
177
184
  assert _scoped_backlog(s, agent) == 1, "one embeddable row to start"
178
185
 
179
- before = s.backfill_null_embeddings(
180
- embed_fn=lambda t: [0.1] * 768, tables=["memory_entries"], dry_run=True
181
- )["memory_entries"]["failed"]
182
-
183
186
  report = s.backfill_null_embeddings(
184
187
  embed_fn=lambda t: [0.1] * 768, tables=["memory_entries"]
185
188
  )
@@ -187,7 +190,13 @@ def test_backfill_remaining_can_reach_zero_despite_an_empty_row(store):
187
190
  assert _scoped_backlog(s, agent) == 0, (
188
191
  "the backlog must exclude un-embeddable rows so it can actually reach 0"
189
192
  )
190
- assert report["memory_entries"]["failed"] == before, (
193
+ # Asserted directly, not as a delta against a dry-run baseline: dry_run
194
+ # hardcodes "failed": 0 (store.py), so such a baseline is always 0 and the
195
+ # comparison would be theatre. `failed` is table-wide, which is sound here
196
+ # only because this fixture requires a THROWAWAY database (stated in the
197
+ # module docstring above) -- on a shared one, a pre-existing failing row
198
+ # would surface here, and that is worth knowing rather than hiding.
199
+ assert report["memory_entries"]["failed"] == 0, (
191
200
  "an un-embeddable row must be skipped, not counted as a failure"
192
201
  )
193
202
  assert report["memory_entries"]["unembeddable"] >= 1