hermes-memory-pgvector 0.4.0__tar.gz → 0.4.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.
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/PKG-INFO +52 -12
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/README.md +51 -11
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/hermes_memory_pgvector.egg-info/PKG-INFO +52 -12
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/hermes_memory_pgvector.egg-info/SOURCES.txt +4 -0
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/pgvector/__init__.py +166 -39
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/pgvector/__main__.py +149 -1
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/pgvector/embed.py +16 -1
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/pgvector/identity.py +11 -2
- hermes_memory_pgvector-0.4.2/pgvector/migrations/003_hybrid_search_fts.sql +51 -0
- hermes_memory_pgvector-0.4.2/pgvector/migrations/004_runtime_grants.sql +36 -0
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/pgvector/plugin.yaml +2 -2
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/pgvector/store.py +262 -21
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/pgvector/writer.py +27 -5
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/pyproject.toml +1 -1
- hermes_memory_pgvector-0.4.2/tests/test_hybrid_search.py +123 -0
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/tests/test_identity.py +20 -3
- hermes_memory_pgvector-0.4.2/tests/test_install_shim.py +74 -0
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/tests/test_smoke.py +17 -7
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/tests/test_store_v04.py +27 -2
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/LICENSE +0 -0
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/hermes_memory_pgvector.egg-info/dependency_links.txt +0 -0
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/hermes_memory_pgvector.egg-info/entry_points.txt +0 -0
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/hermes_memory_pgvector.egg-info/requires.txt +0 -0
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/hermes_memory_pgvector.egg-info/top_level.txt +0 -0
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/pgvector/migrations/001_schema.sql +0 -0
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/pgvector/migrations/002_agent_attribution.sql +0 -0
- {hermes_memory_pgvector-0.4.0 → hermes_memory_pgvector-0.4.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hermes-memory-pgvector
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -106,6 +106,21 @@ Four capabilities, all storage-layer (still no LLM in the hot path):
|
|
|
106
106
|
|
|
107
107
|
Maintenance CLI (`python -m pgvector`, installed as `hermes-pgvector`): `migrate · stats · backfill · prune · cleanup · remap` — destructive commands default to dry-run. v0.4.0 is a clean upgrade from v0.3.x: apply migration `002` to light up attribution/delegation; without it the new hooks no-op and everything else runs unchanged.
|
|
108
108
|
|
|
109
|
+
## New in v0.4.1 — hybrid recall (vector + full-text)
|
|
110
|
+
|
|
111
|
+
`recall_memory` and `recall_conversation` now fuse the HNSW **vector** ranking with a Postgres **full-text** ranking using **Reciprocal Rank Fusion** (RRF, `k=60`). A row surfaces if *either* ranker likes it, which fixes the two blind spots of pure cosine similarity:
|
|
112
|
+
|
|
113
|
+
- **Exact-lexical hits** the embedding smooths away — a specific error code, hostname, flag name, or rare identifier the agent quotes verbatim.
|
|
114
|
+
- **Text-only rows with a `NULL` embedding** (written while the embed endpoint was down) — invisible to the vector index, but the full-text leg finds them. So hybrid recall doubles as best-effort recovery until the next `backfill`.
|
|
115
|
+
|
|
116
|
+
Still a storage-layer feature: **no LLM, no entity graph, no new tables or columns** — just a GIN index over the existing `content` column (migration `003`) and a fused query. It stays inside invariant #1 (a second index over the same text is not a parallel ontology). Fail-soft as ever: a hybrid hiccup degrades to the proven pure-vector path, and a query that *itself* fails to embed degrades to full-text-only instead of erroring. Toggle with `plugins.pgvector.hybrid_search` (default `true`); the ambient `prefetch()` path stays pure-vector. Works without migration `003` — the GIN index only makes the full-text leg faster.
|
|
117
|
+
|
|
118
|
+
## New in v0.4.2 — pip-native install + hardening
|
|
119
|
+
|
|
120
|
+
- **`hermes-pgvector install`** — makes a plain `pip install hermes-memory-pgvector` deployable on ANY hermes-agent install: generates the `$HERMES_HOME/plugins/pgvector/` discovery shim (see *Install · Option 1*). No more vendored copies or editable checkouts.
|
|
121
|
+
- **Migration `004`** — `hermes-pgvector migrate` now grants the runtime role DML on `memory_entries`/`conversations` itself; the manual OWNER-transfer step is gone (fresh installs previously hit `permission denied` if it was skipped).
|
|
122
|
+
- **Correctness fixes** from a full-codebase review: `replace`/`remove` now match `old_text` as a *literal* substring (LIKE `%`/`_`/`\` metacharacters no longer over- or under-match — parity with the built-in tool's `in` semantics); the async writer drains its queue on shutdown instead of silently abandoning up to 255 accepted writes when full; a wrong-dimension embed model now surfaces as `expected 768 dims, got N` instead of a masking 404; DM-key bucketing no longer sweeps ordinary `:signal:`-containing theme names into `whatsapp-dm`; bulk MEMORY.md import circuit-breaks after 3 consecutive embed failures (a hanging endpoint can no longer block session start for minutes); `remap` re-checks its duplicate-drop guard under the advisory lock; tool errors redact credential-looking fragments and preserve `score: null` for full-text-only hybrid hits (with `rrf_score` now included); `recall_memory(scope='session')` returns a helpful error instead of silently matching nothing.
|
|
123
|
+
|
|
109
124
|
## Multi-agent / per-minion themes
|
|
110
125
|
|
|
111
126
|
Each systemd-run minion sets one header on its OpenAI client; everything else flows automatically:
|
|
@@ -130,7 +145,28 @@ Set `plugins.pgvector.allowed_themes` to that product/worker list to enforce it
|
|
|
130
145
|
|
|
131
146
|
## Install
|
|
132
147
|
|
|
133
|
-
### Option 1:
|
|
148
|
+
### Option 1: pip + discovery shim (recommended, v0.4.2+)
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# 1. Install the package into the SAME environment hermes-agent runs in
|
|
152
|
+
pip install hermes-memory-pgvector
|
|
153
|
+
|
|
154
|
+
# 2. Create the discovery shim
|
|
155
|
+
hermes-pgvector install # writes $HERMES_HOME/plugins/pgvector/
|
|
156
|
+
|
|
157
|
+
# 3. Apply ALL migrations (schema + attribution + FTS + runtime grants)
|
|
158
|
+
hermes-pgvector migrate --admin-dsn \
|
|
159
|
+
"dbname=<your-memory-db> user=postgres host=/var/run/postgresql"
|
|
160
|
+
|
|
161
|
+
# 4. Activate + verify
|
|
162
|
+
hermes config set memory.provider pgvector
|
|
163
|
+
sudo systemctl restart hermes.service
|
|
164
|
+
hermes memory status # expect: Provider: pgvector; Status: available
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Why the shim?** hermes-agent discovers memory providers by scanning plugin *directories* — `plugins/memory/<name>/` (bundled) and `$HERMES_HOME/plugins/<name>/` (user) — and never looks at installed packages. `pip install` alone is therefore invisible to it. `hermes-pgvector install` writes a two-line shim whose absolute import resolves to the pip-installed package, so upgrades are just `pip install -U hermes-memory-pgvector` + restart, and rollback is `pip install hermes-memory-pgvector==<prev>` + restart — the shim never changes. `--remove` deletes it; if the package is uninstalled the shim import fails cleanly and hermes falls back to built-in memory.
|
|
168
|
+
|
|
169
|
+
### Option 2: clone + run the installer script (from source)
|
|
134
170
|
|
|
135
171
|
```bash
|
|
136
172
|
git clone https://github.com/andreab67/hermes-memory-pgvector.git
|
|
@@ -144,7 +180,7 @@ That:
|
|
|
144
180
|
2. Copies `pgvector/` into `$HERMES_HOME/plugins/pgvector/` (defaults to `~/.hermes/plugins/pgvector/`).
|
|
145
181
|
3. Prints the admin migration + activation commands you run next.
|
|
146
182
|
|
|
147
|
-
### Option
|
|
183
|
+
### Option 3: manual
|
|
148
184
|
|
|
149
185
|
```bash
|
|
150
186
|
# Python deps
|
|
@@ -167,15 +203,19 @@ sudo -u postgres psql -d <your-memory-db> \
|
|
|
167
203
|
# runtime role needs — it self-grants, so no extra OWNER step for these).
|
|
168
204
|
sudo -u postgres psql -d <your-memory-db> \
|
|
169
205
|
-f ~/.hermes/plugins/pgvector/migrations/002_agent_attribution.sql
|
|
170
|
-
# (or apply every migration in order: hermes-pgvector migrate --admin-dsn "user=postgres host=/var/run/postgresql dbname=<your-memory-db>")
|
|
171
206
|
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
207
|
+
# v0.4.1: apply the hybrid-search full-text indexes (GIN over content on both
|
|
208
|
+
# tables). Optional — hybrid recall works without it, just seq-scans the FTS
|
|
209
|
+
# leg. No new tables/columns/GRANTs; needs no OWNER step.
|
|
210
|
+
sudo -u postgres psql -d <your-memory-db> \
|
|
211
|
+
-f ~/.hermes/plugins/pgvector/migrations/003_hybrid_search_fts.sql
|
|
212
|
+
|
|
213
|
+
# v0.4.2: grant the runtime role DML on the core tables (replaces the old
|
|
214
|
+
# manual "ALTER TABLE ... OWNER TO hermes" step; skips with a NOTICE if your
|
|
215
|
+
# runtime role isn't named 'hermes' — grant manually in that case).
|
|
216
|
+
sudo -u postgres psql -d <your-memory-db> \
|
|
217
|
+
-f ~/.hermes/plugins/pgvector/migrations/004_runtime_grants.sql
|
|
218
|
+
# (or apply every migration in order: hermes-pgvector migrate --admin-dsn "user=postgres host=/var/run/postgresql dbname=<your-memory-db>")
|
|
179
219
|
|
|
180
220
|
# Activate
|
|
181
221
|
hermes config set memory.provider pgvector
|
|
@@ -299,4 +339,4 @@ Bug reports + PRs welcome. Open an issue describing the failure mode + your envi
|
|
|
299
339
|
|
|
300
340
|
## License
|
|
301
341
|
|
|
302
|
-
[BSD 3-Clause](LICENSE) © 2026
|
|
342
|
+
[BSD 3-Clause](LICENSE) © 2026 Green Yoga Inc
|
|
@@ -73,6 +73,21 @@ Four capabilities, all storage-layer (still no LLM in the hot path):
|
|
|
73
73
|
|
|
74
74
|
Maintenance CLI (`python -m pgvector`, installed as `hermes-pgvector`): `migrate · stats · backfill · prune · cleanup · remap` — destructive commands default to dry-run. v0.4.0 is a clean upgrade from v0.3.x: apply migration `002` to light up attribution/delegation; without it the new hooks no-op and everything else runs unchanged.
|
|
75
75
|
|
|
76
|
+
## New in v0.4.1 — hybrid recall (vector + full-text)
|
|
77
|
+
|
|
78
|
+
`recall_memory` and `recall_conversation` now fuse the HNSW **vector** ranking with a Postgres **full-text** ranking using **Reciprocal Rank Fusion** (RRF, `k=60`). A row surfaces if *either* ranker likes it, which fixes the two blind spots of pure cosine similarity:
|
|
79
|
+
|
|
80
|
+
- **Exact-lexical hits** the embedding smooths away — a specific error code, hostname, flag name, or rare identifier the agent quotes verbatim.
|
|
81
|
+
- **Text-only rows with a `NULL` embedding** (written while the embed endpoint was down) — invisible to the vector index, but the full-text leg finds them. So hybrid recall doubles as best-effort recovery until the next `backfill`.
|
|
82
|
+
|
|
83
|
+
Still a storage-layer feature: **no LLM, no entity graph, no new tables or columns** — just a GIN index over the existing `content` column (migration `003`) and a fused query. It stays inside invariant #1 (a second index over the same text is not a parallel ontology). Fail-soft as ever: a hybrid hiccup degrades to the proven pure-vector path, and a query that *itself* fails to embed degrades to full-text-only instead of erroring. Toggle with `plugins.pgvector.hybrid_search` (default `true`); the ambient `prefetch()` path stays pure-vector. Works without migration `003` — the GIN index only makes the full-text leg faster.
|
|
84
|
+
|
|
85
|
+
## New in v0.4.2 — pip-native install + hardening
|
|
86
|
+
|
|
87
|
+
- **`hermes-pgvector install`** — makes a plain `pip install hermes-memory-pgvector` deployable on ANY hermes-agent install: generates the `$HERMES_HOME/plugins/pgvector/` discovery shim (see *Install · Option 1*). No more vendored copies or editable checkouts.
|
|
88
|
+
- **Migration `004`** — `hermes-pgvector migrate` now grants the runtime role DML on `memory_entries`/`conversations` itself; the manual OWNER-transfer step is gone (fresh installs previously hit `permission denied` if it was skipped).
|
|
89
|
+
- **Correctness fixes** from a full-codebase review: `replace`/`remove` now match `old_text` as a *literal* substring (LIKE `%`/`_`/`\` metacharacters no longer over- or under-match — parity with the built-in tool's `in` semantics); the async writer drains its queue on shutdown instead of silently abandoning up to 255 accepted writes when full; a wrong-dimension embed model now surfaces as `expected 768 dims, got N` instead of a masking 404; DM-key bucketing no longer sweeps ordinary `:signal:`-containing theme names into `whatsapp-dm`; bulk MEMORY.md import circuit-breaks after 3 consecutive embed failures (a hanging endpoint can no longer block session start for minutes); `remap` re-checks its duplicate-drop guard under the advisory lock; tool errors redact credential-looking fragments and preserve `score: null` for full-text-only hybrid hits (with `rrf_score` now included); `recall_memory(scope='session')` returns a helpful error instead of silently matching nothing.
|
|
90
|
+
|
|
76
91
|
## Multi-agent / per-minion themes
|
|
77
92
|
|
|
78
93
|
Each systemd-run minion sets one header on its OpenAI client; everything else flows automatically:
|
|
@@ -97,7 +112,28 @@ Set `plugins.pgvector.allowed_themes` to that product/worker list to enforce it
|
|
|
97
112
|
|
|
98
113
|
## Install
|
|
99
114
|
|
|
100
|
-
### Option 1:
|
|
115
|
+
### Option 1: pip + discovery shim (recommended, v0.4.2+)
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# 1. Install the package into the SAME environment hermes-agent runs in
|
|
119
|
+
pip install hermes-memory-pgvector
|
|
120
|
+
|
|
121
|
+
# 2. Create the discovery shim
|
|
122
|
+
hermes-pgvector install # writes $HERMES_HOME/plugins/pgvector/
|
|
123
|
+
|
|
124
|
+
# 3. Apply ALL migrations (schema + attribution + FTS + runtime grants)
|
|
125
|
+
hermes-pgvector migrate --admin-dsn \
|
|
126
|
+
"dbname=<your-memory-db> user=postgres host=/var/run/postgresql"
|
|
127
|
+
|
|
128
|
+
# 4. Activate + verify
|
|
129
|
+
hermes config set memory.provider pgvector
|
|
130
|
+
sudo systemctl restart hermes.service
|
|
131
|
+
hermes memory status # expect: Provider: pgvector; Status: available
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Why the shim?** hermes-agent discovers memory providers by scanning plugin *directories* — `plugins/memory/<name>/` (bundled) and `$HERMES_HOME/plugins/<name>/` (user) — and never looks at installed packages. `pip install` alone is therefore invisible to it. `hermes-pgvector install` writes a two-line shim whose absolute import resolves to the pip-installed package, so upgrades are just `pip install -U hermes-memory-pgvector` + restart, and rollback is `pip install hermes-memory-pgvector==<prev>` + restart — the shim never changes. `--remove` deletes it; if the package is uninstalled the shim import fails cleanly and hermes falls back to built-in memory.
|
|
135
|
+
|
|
136
|
+
### Option 2: clone + run the installer script (from source)
|
|
101
137
|
|
|
102
138
|
```bash
|
|
103
139
|
git clone https://github.com/andreab67/hermes-memory-pgvector.git
|
|
@@ -111,7 +147,7 @@ That:
|
|
|
111
147
|
2. Copies `pgvector/` into `$HERMES_HOME/plugins/pgvector/` (defaults to `~/.hermes/plugins/pgvector/`).
|
|
112
148
|
3. Prints the admin migration + activation commands you run next.
|
|
113
149
|
|
|
114
|
-
### Option
|
|
150
|
+
### Option 3: manual
|
|
115
151
|
|
|
116
152
|
```bash
|
|
117
153
|
# Python deps
|
|
@@ -134,15 +170,19 @@ sudo -u postgres psql -d <your-memory-db> \
|
|
|
134
170
|
# runtime role needs — it self-grants, so no extra OWNER step for these).
|
|
135
171
|
sudo -u postgres psql -d <your-memory-db> \
|
|
136
172
|
-f ~/.hermes/plugins/pgvector/migrations/002_agent_attribution.sql
|
|
137
|
-
# (or apply every migration in order: hermes-pgvector migrate --admin-dsn "user=postgres host=/var/run/postgresql dbname=<your-memory-db>")
|
|
138
173
|
|
|
139
|
-
#
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
174
|
+
# v0.4.1: apply the hybrid-search full-text indexes (GIN over content on both
|
|
175
|
+
# tables). Optional — hybrid recall works without it, just seq-scans the FTS
|
|
176
|
+
# leg. No new tables/columns/GRANTs; needs no OWNER step.
|
|
177
|
+
sudo -u postgres psql -d <your-memory-db> \
|
|
178
|
+
-f ~/.hermes/plugins/pgvector/migrations/003_hybrid_search_fts.sql
|
|
179
|
+
|
|
180
|
+
# v0.4.2: grant the runtime role DML on the core tables (replaces the old
|
|
181
|
+
# manual "ALTER TABLE ... OWNER TO hermes" step; skips with a NOTICE if your
|
|
182
|
+
# runtime role isn't named 'hermes' — grant manually in that case).
|
|
183
|
+
sudo -u postgres psql -d <your-memory-db> \
|
|
184
|
+
-f ~/.hermes/plugins/pgvector/migrations/004_runtime_grants.sql
|
|
185
|
+
# (or apply every migration in order: hermes-pgvector migrate --admin-dsn "user=postgres host=/var/run/postgresql dbname=<your-memory-db>")
|
|
146
186
|
|
|
147
187
|
# Activate
|
|
148
188
|
hermes config set memory.provider pgvector
|
|
@@ -266,4 +306,4 @@ Bug reports + PRs welcome. Open an issue describing the failure mode + your envi
|
|
|
266
306
|
|
|
267
307
|
## License
|
|
268
308
|
|
|
269
|
-
[BSD 3-Clause](LICENSE) © 2026
|
|
309
|
+
[BSD 3-Clause](LICENSE) © 2026 Green Yoga Inc
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hermes-memory-pgvector
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -106,6 +106,21 @@ Four capabilities, all storage-layer (still no LLM in the hot path):
|
|
|
106
106
|
|
|
107
107
|
Maintenance CLI (`python -m pgvector`, installed as `hermes-pgvector`): `migrate · stats · backfill · prune · cleanup · remap` — destructive commands default to dry-run. v0.4.0 is a clean upgrade from v0.3.x: apply migration `002` to light up attribution/delegation; without it the new hooks no-op and everything else runs unchanged.
|
|
108
108
|
|
|
109
|
+
## New in v0.4.1 — hybrid recall (vector + full-text)
|
|
110
|
+
|
|
111
|
+
`recall_memory` and `recall_conversation` now fuse the HNSW **vector** ranking with a Postgres **full-text** ranking using **Reciprocal Rank Fusion** (RRF, `k=60`). A row surfaces if *either* ranker likes it, which fixes the two blind spots of pure cosine similarity:
|
|
112
|
+
|
|
113
|
+
- **Exact-lexical hits** the embedding smooths away — a specific error code, hostname, flag name, or rare identifier the agent quotes verbatim.
|
|
114
|
+
- **Text-only rows with a `NULL` embedding** (written while the embed endpoint was down) — invisible to the vector index, but the full-text leg finds them. So hybrid recall doubles as best-effort recovery until the next `backfill`.
|
|
115
|
+
|
|
116
|
+
Still a storage-layer feature: **no LLM, no entity graph, no new tables or columns** — just a GIN index over the existing `content` column (migration `003`) and a fused query. It stays inside invariant #1 (a second index over the same text is not a parallel ontology). Fail-soft as ever: a hybrid hiccup degrades to the proven pure-vector path, and a query that *itself* fails to embed degrades to full-text-only instead of erroring. Toggle with `plugins.pgvector.hybrid_search` (default `true`); the ambient `prefetch()` path stays pure-vector. Works without migration `003` — the GIN index only makes the full-text leg faster.
|
|
117
|
+
|
|
118
|
+
## New in v0.4.2 — pip-native install + hardening
|
|
119
|
+
|
|
120
|
+
- **`hermes-pgvector install`** — makes a plain `pip install hermes-memory-pgvector` deployable on ANY hermes-agent install: generates the `$HERMES_HOME/plugins/pgvector/` discovery shim (see *Install · Option 1*). No more vendored copies or editable checkouts.
|
|
121
|
+
- **Migration `004`** — `hermes-pgvector migrate` now grants the runtime role DML on `memory_entries`/`conversations` itself; the manual OWNER-transfer step is gone (fresh installs previously hit `permission denied` if it was skipped).
|
|
122
|
+
- **Correctness fixes** from a full-codebase review: `replace`/`remove` now match `old_text` as a *literal* substring (LIKE `%`/`_`/`\` metacharacters no longer over- or under-match — parity with the built-in tool's `in` semantics); the async writer drains its queue on shutdown instead of silently abandoning up to 255 accepted writes when full; a wrong-dimension embed model now surfaces as `expected 768 dims, got N` instead of a masking 404; DM-key bucketing no longer sweeps ordinary `:signal:`-containing theme names into `whatsapp-dm`; bulk MEMORY.md import circuit-breaks after 3 consecutive embed failures (a hanging endpoint can no longer block session start for minutes); `remap` re-checks its duplicate-drop guard under the advisory lock; tool errors redact credential-looking fragments and preserve `score: null` for full-text-only hybrid hits (with `rrf_score` now included); `recall_memory(scope='session')` returns a helpful error instead of silently matching nothing.
|
|
123
|
+
|
|
109
124
|
## Multi-agent / per-minion themes
|
|
110
125
|
|
|
111
126
|
Each systemd-run minion sets one header on its OpenAI client; everything else flows automatically:
|
|
@@ -130,7 +145,28 @@ Set `plugins.pgvector.allowed_themes` to that product/worker list to enforce it
|
|
|
130
145
|
|
|
131
146
|
## Install
|
|
132
147
|
|
|
133
|
-
### Option 1:
|
|
148
|
+
### Option 1: pip + discovery shim (recommended, v0.4.2+)
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# 1. Install the package into the SAME environment hermes-agent runs in
|
|
152
|
+
pip install hermes-memory-pgvector
|
|
153
|
+
|
|
154
|
+
# 2. Create the discovery shim
|
|
155
|
+
hermes-pgvector install # writes $HERMES_HOME/plugins/pgvector/
|
|
156
|
+
|
|
157
|
+
# 3. Apply ALL migrations (schema + attribution + FTS + runtime grants)
|
|
158
|
+
hermes-pgvector migrate --admin-dsn \
|
|
159
|
+
"dbname=<your-memory-db> user=postgres host=/var/run/postgresql"
|
|
160
|
+
|
|
161
|
+
# 4. Activate + verify
|
|
162
|
+
hermes config set memory.provider pgvector
|
|
163
|
+
sudo systemctl restart hermes.service
|
|
164
|
+
hermes memory status # expect: Provider: pgvector; Status: available
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Why the shim?** hermes-agent discovers memory providers by scanning plugin *directories* — `plugins/memory/<name>/` (bundled) and `$HERMES_HOME/plugins/<name>/` (user) — and never looks at installed packages. `pip install` alone is therefore invisible to it. `hermes-pgvector install` writes a two-line shim whose absolute import resolves to the pip-installed package, so upgrades are just `pip install -U hermes-memory-pgvector` + restart, and rollback is `pip install hermes-memory-pgvector==<prev>` + restart — the shim never changes. `--remove` deletes it; if the package is uninstalled the shim import fails cleanly and hermes falls back to built-in memory.
|
|
168
|
+
|
|
169
|
+
### Option 2: clone + run the installer script (from source)
|
|
134
170
|
|
|
135
171
|
```bash
|
|
136
172
|
git clone https://github.com/andreab67/hermes-memory-pgvector.git
|
|
@@ -144,7 +180,7 @@ That:
|
|
|
144
180
|
2. Copies `pgvector/` into `$HERMES_HOME/plugins/pgvector/` (defaults to `~/.hermes/plugins/pgvector/`).
|
|
145
181
|
3. Prints the admin migration + activation commands you run next.
|
|
146
182
|
|
|
147
|
-
### Option
|
|
183
|
+
### Option 3: manual
|
|
148
184
|
|
|
149
185
|
```bash
|
|
150
186
|
# Python deps
|
|
@@ -167,15 +203,19 @@ sudo -u postgres psql -d <your-memory-db> \
|
|
|
167
203
|
# runtime role needs — it self-grants, so no extra OWNER step for these).
|
|
168
204
|
sudo -u postgres psql -d <your-memory-db> \
|
|
169
205
|
-f ~/.hermes/plugins/pgvector/migrations/002_agent_attribution.sql
|
|
170
|
-
# (or apply every migration in order: hermes-pgvector migrate --admin-dsn "user=postgres host=/var/run/postgresql dbname=<your-memory-db>")
|
|
171
206
|
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
207
|
+
# v0.4.1: apply the hybrid-search full-text indexes (GIN over content on both
|
|
208
|
+
# tables). Optional — hybrid recall works without it, just seq-scans the FTS
|
|
209
|
+
# leg. No new tables/columns/GRANTs; needs no OWNER step.
|
|
210
|
+
sudo -u postgres psql -d <your-memory-db> \
|
|
211
|
+
-f ~/.hermes/plugins/pgvector/migrations/003_hybrid_search_fts.sql
|
|
212
|
+
|
|
213
|
+
# v0.4.2: grant the runtime role DML on the core tables (replaces the old
|
|
214
|
+
# manual "ALTER TABLE ... OWNER TO hermes" step; skips with a NOTICE if your
|
|
215
|
+
# runtime role isn't named 'hermes' — grant manually in that case).
|
|
216
|
+
sudo -u postgres psql -d <your-memory-db> \
|
|
217
|
+
-f ~/.hermes/plugins/pgvector/migrations/004_runtime_grants.sql
|
|
218
|
+
# (or apply every migration in order: hermes-pgvector migrate --admin-dsn "user=postgres host=/var/run/postgresql dbname=<your-memory-db>")
|
|
179
219
|
|
|
180
220
|
# Activate
|
|
181
221
|
hermes config set memory.provider pgvector
|
|
@@ -299,4 +339,4 @@ Bug reports + PRs welcome. Open an issue describing the failure mode + your envi
|
|
|
299
339
|
|
|
300
340
|
## License
|
|
301
341
|
|
|
302
|
-
[BSD 3-Clause](LICENSE) © 2026
|
|
342
|
+
[BSD 3-Clause](LICENSE) © 2026 Green Yoga Inc
|
|
@@ -16,6 +16,10 @@ pgvector/store.py
|
|
|
16
16
|
pgvector/writer.py
|
|
17
17
|
pgvector/migrations/001_schema.sql
|
|
18
18
|
pgvector/migrations/002_agent_attribution.sql
|
|
19
|
+
pgvector/migrations/003_hybrid_search_fts.sql
|
|
20
|
+
pgvector/migrations/004_runtime_grants.sql
|
|
21
|
+
tests/test_hybrid_search.py
|
|
19
22
|
tests/test_identity.py
|
|
23
|
+
tests/test_install_shim.py
|
|
20
24
|
tests/test_smoke.py
|
|
21
25
|
tests/test_store_v04.py
|
|
@@ -23,6 +23,7 @@ Config in $HERMES_HOME/config.yaml under plugins.pgvector:
|
|
|
23
23
|
min_similarity: 0.30
|
|
24
24
|
embed_on_write: true
|
|
25
25
|
scope_default: "current" # 'current' | 'all'
|
|
26
|
+
hybrid_search: true # fuse vector + full-text (RRF) in recall tools
|
|
26
27
|
|
|
27
28
|
Tools exposed: `recall_memory` (one explicit search tool). All built-in
|
|
28
29
|
memory writes (add/replace/remove) are mirrored automatically via the
|
|
@@ -83,6 +84,17 @@ _NOISE_RE = re.compile(
|
|
|
83
84
|
logger = logging.getLogger(__name__)
|
|
84
85
|
|
|
85
86
|
|
|
87
|
+
# Credential-looking fragments that must never round-trip into a tool response
|
|
88
|
+
# (psycopg/libpq errors can echo conninfo verbatim; tool errors flow back to
|
|
89
|
+
# the model and can be persisted durably by conversation capture).
|
|
90
|
+
_CRED_RE = re.compile(r"(password|passfile|sslkey|sslpassword)=\S+", re.IGNORECASE)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _safe_err(exc: BaseException) -> str:
|
|
94
|
+
"""Exception text safe to return to the model: truncated + creds redacted."""
|
|
95
|
+
return _CRED_RE.sub(r"\1=[redacted]", str(exc)[:300])
|
|
96
|
+
|
|
97
|
+
|
|
86
98
|
# ---------------------------------------------------------------------------
|
|
87
99
|
# Tool schema — one explicit search over memory_entries
|
|
88
100
|
# ---------------------------------------------------------------------------
|
|
@@ -182,12 +194,19 @@ DEFAULTS = {
|
|
|
182
194
|
"min_similarity": 0.30,
|
|
183
195
|
"embed_on_write": True,
|
|
184
196
|
"scope_default": "current",
|
|
197
|
+
# v0.4.1 — hybrid recall: fuse the HNSW vector ranking with a Postgres
|
|
198
|
+
# full-text ranking via RRF in the recall_memory / recall_conversation
|
|
199
|
+
# tools. Recovers exact-lexical hits cosine smooths away + text-only rows
|
|
200
|
+
# with NULL embeddings. Degrades to pure vector on any error, and to
|
|
201
|
+
# full-text-only when the query fails to embed. Needs migration 003 for the
|
|
202
|
+
# GIN index (works without it, just slower — seq-scan FTS on a small table).
|
|
203
|
+
"hybrid_search": True,
|
|
185
204
|
"write_queue_maxsize": 256,
|
|
186
205
|
# v0.1.1 — bulk sync MEMORY.md / USER.md on init
|
|
187
206
|
"bulk_sync_on_init": True,
|
|
188
207
|
# v0.2 — conversation turn capture
|
|
189
208
|
"sync_turns": True,
|
|
190
|
-
"turn_min_chars": 40, # turns shorter than this
|
|
209
|
+
"turn_min_chars": 40, # turns shorter than this (after strip) are boilerplate noise
|
|
191
210
|
# v0.4 — identity governance
|
|
192
211
|
"allowed_themes": None, # None/empty = governance off; set a list to enforce an allow-list
|
|
193
212
|
"identity_aliases": {}, # {raw: canonical} remaps applied before normalization
|
|
@@ -249,6 +268,11 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
249
268
|
|
|
250
269
|
def initialize(self, session_id: str, **kwargs) -> None:
|
|
251
270
|
self._session_id = session_id
|
|
271
|
+
# Per-session log-signal reset (v0.4.2): the provider instance is
|
|
272
|
+
# reused across sessions, so without this a single embed failure in
|
|
273
|
+
# any past session silences the one-time warning for the process
|
|
274
|
+
# lifetime — a later session with a broken endpoint gets zero signal.
|
|
275
|
+
self._embed_warned = False
|
|
252
276
|
# Per-agent theme scoping — priority order:
|
|
253
277
|
# 1. gateway_session_key — from the `X-Hermes-Session-Key` header on
|
|
254
278
|
# API requests. This is the EXPLICIT minion-scope signal sent by
|
|
@@ -568,7 +592,7 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
568
592
|
logger.debug("pgvector on_delegation failed (ignored): %s", exc)
|
|
569
593
|
|
|
570
594
|
def on_session_end(self, messages: List[Dict[str, Any]]) -> None:
|
|
571
|
-
"""Best-effort:
|
|
595
|
+
"""Best-effort: capture session turns + bump last_seen. Fail-soft,
|
|
572
596
|
non-blocking. No LLM summary (invariant #2). No-op until 002 applied."""
|
|
573
597
|
if not self._healthy or not self._writer or not self._delegation_enabled:
|
|
574
598
|
return
|
|
@@ -581,6 +605,36 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
581
605
|
extra={"kind": classify_kind(self._agent_identity)},
|
|
582
606
|
metadata={},
|
|
583
607
|
)
|
|
608
|
+
# Capture substantive user/assistant turns for conversation recall.
|
|
609
|
+
if messages:
|
|
610
|
+
policy = self._config.get("conversation_embed_policy", "all")
|
|
611
|
+
for msg in messages:
|
|
612
|
+
role = (msg.get("role") or "").lower()
|
|
613
|
+
if role not in ("user", "assistant"):
|
|
614
|
+
continue
|
|
615
|
+
raw = msg.get("content") or ""
|
|
616
|
+
content = (
|
|
617
|
+
raw if isinstance(raw, str)
|
|
618
|
+
else " ".join(
|
|
619
|
+
p.get("text", "") for p in raw
|
|
620
|
+
if isinstance(p, dict) and p.get("type") == "text"
|
|
621
|
+
) if isinstance(raw, list) else str(raw)
|
|
622
|
+
)
|
|
623
|
+
if self._is_noise(content, min_chars=40):
|
|
624
|
+
continue
|
|
625
|
+
self._writer.enqueue(
|
|
626
|
+
action="turn",
|
|
627
|
+
agent_identity=self._agent_identity,
|
|
628
|
+
target="conversations",
|
|
629
|
+
content=content[:8000],
|
|
630
|
+
extra={
|
|
631
|
+
"role": role,
|
|
632
|
+
"session_id": self._session_id or "default",
|
|
633
|
+
"embed": self._should_embed_turn(role, content, policy),
|
|
634
|
+
"parent_session_id": self._parent_session_id,
|
|
635
|
+
},
|
|
636
|
+
metadata={},
|
|
637
|
+
)
|
|
584
638
|
except Exception as exc: # noqa: BLE001
|
|
585
639
|
logger.debug("pgvector on_session_end failed (ignored): %s", exc)
|
|
586
640
|
|
|
@@ -799,6 +853,14 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
799
853
|
agent_filter: Optional[str] = self._agent_identity
|
|
800
854
|
elif scope == "all":
|
|
801
855
|
agent_filter = None
|
|
856
|
+
elif scope == "session":
|
|
857
|
+
# Only recall_conversation supports 'session'. Falling through
|
|
858
|
+
# would silently filter on a literal 'session' theme and return
|
|
859
|
+
# zero rows — indistinguishable from "nothing found" (v0.4.2).
|
|
860
|
+
return tool_error(
|
|
861
|
+
"scope='session' is only valid for recall_conversation; "
|
|
862
|
+
"use 'current', 'all', or a theme name here."
|
|
863
|
+
)
|
|
802
864
|
else:
|
|
803
865
|
agent_filter = scope
|
|
804
866
|
|
|
@@ -808,6 +870,7 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
808
870
|
if target_filter not in (None, "memory", "user"):
|
|
809
871
|
return tool_error(f"Invalid target: {target_arg!r}")
|
|
810
872
|
|
|
873
|
+
hybrid = bool(self._config.get("hybrid_search", True))
|
|
811
874
|
try:
|
|
812
875
|
vec = embed(
|
|
813
876
|
query,
|
|
@@ -815,31 +878,63 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
815
878
|
model=self._config["embed_model"],
|
|
816
879
|
)
|
|
817
880
|
except EmbeddingError as exc:
|
|
818
|
-
|
|
881
|
+
if not hybrid:
|
|
882
|
+
return json.dumps({"results": [], "count": 0, "error": f"embed: {_safe_err(exc)}"})
|
|
883
|
+
# Hybrid on: the query text still drives a full-text search without a
|
|
884
|
+
# vector, so degrade to lexical recall instead of erroring.
|
|
885
|
+
logger.debug("recall_memory embed failed; full-text-only: %s", exc)
|
|
886
|
+
vec = None
|
|
819
887
|
|
|
820
888
|
try:
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
889
|
+
if hybrid:
|
|
890
|
+
rows = self._store.hybrid_search(
|
|
891
|
+
query_text=query,
|
|
892
|
+
query_embedding=vec,
|
|
893
|
+
agent_identity=agent_filter,
|
|
894
|
+
target=target_filter,
|
|
895
|
+
limit=limit,
|
|
896
|
+
)
|
|
897
|
+
else:
|
|
898
|
+
rows = self._store.search(
|
|
899
|
+
query_embedding=vec,
|
|
900
|
+
agent_identity=agent_filter,
|
|
901
|
+
target=target_filter,
|
|
902
|
+
limit=limit,
|
|
903
|
+
)
|
|
827
904
|
except Exception as exc: # noqa: BLE001
|
|
828
|
-
|
|
905
|
+
# Fail-soft: a hybrid hiccup with a usable vector falls back to the
|
|
906
|
+
# proven pure-vector path rather than returning nothing.
|
|
907
|
+
if hybrid and vec is not None:
|
|
908
|
+
try:
|
|
909
|
+
rows = self._store.search(
|
|
910
|
+
query_embedding=vec,
|
|
911
|
+
agent_identity=agent_filter,
|
|
912
|
+
target=target_filter,
|
|
913
|
+
limit=limit,
|
|
914
|
+
)
|
|
915
|
+
except Exception as exc2: # noqa: BLE001
|
|
916
|
+
return json.dumps({"results": [], "count": 0, "error": f"db: {_safe_err(exc2)}"})
|
|
917
|
+
else:
|
|
918
|
+
return json.dumps({"results": [], "count": 0, "error": f"db: {_safe_err(exc)}"})
|
|
829
919
|
|
|
830
920
|
results = []
|
|
831
921
|
for r in rows:
|
|
832
922
|
ts = r.get("updated_at") or r.get("created_at")
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
923
|
+
score = r.get("score")
|
|
924
|
+
entry = {
|
|
925
|
+
"id": r.get("id"),
|
|
926
|
+
"agent_identity": r.get("agent_identity"),
|
|
927
|
+
"target": r.get("target"),
|
|
928
|
+
"ts": ts.isoformat() if ts else None,
|
|
929
|
+
# None stays None (v0.4.2): a full-text-only hybrid hit has no
|
|
930
|
+
# cosine score; flattening it to 0.0 made a strong lexical
|
|
931
|
+
# match look identical to an orthogonal vector match.
|
|
932
|
+
"score": round(float(score), 4) if score is not None else None,
|
|
933
|
+
"content": (r.get("content") or "")[:2000],
|
|
934
|
+
}
|
|
935
|
+
if r.get("rrf_score") is not None:
|
|
936
|
+
entry["rrf_score"] = round(float(r["rrf_score"]), 6)
|
|
937
|
+
results.append(entry)
|
|
843
938
|
return json.dumps({"results": results, "count": len(results)})
|
|
844
939
|
|
|
845
940
|
def _handle_recall_conversation(self, args: Dict[str, Any]) -> str:
|
|
@@ -867,6 +962,7 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
867
962
|
else:
|
|
868
963
|
agent_filter = scope # treat as a specific theme name
|
|
869
964
|
|
|
965
|
+
hybrid = bool(self._config.get("hybrid_search", True))
|
|
870
966
|
try:
|
|
871
967
|
vec = embed(
|
|
872
968
|
query,
|
|
@@ -874,32 +970,57 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
874
970
|
model=self._config["embed_model"],
|
|
875
971
|
)
|
|
876
972
|
except EmbeddingError as exc:
|
|
877
|
-
|
|
973
|
+
if not hybrid:
|
|
974
|
+
return json.dumps({"results": [], "count": 0, "error": f"embed: {_safe_err(exc)}"})
|
|
975
|
+
logger.debug("recall_conversation embed failed; full-text-only: %s", exc)
|
|
976
|
+
vec = None
|
|
878
977
|
|
|
879
978
|
try:
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
979
|
+
if hybrid:
|
|
980
|
+
rows = self._store.hybrid_search_turns(
|
|
981
|
+
query_text=query,
|
|
982
|
+
query_embedding=vec,
|
|
983
|
+
agent_identity=agent_filter,
|
|
984
|
+
session_id=session_filter,
|
|
985
|
+
limit=limit,
|
|
986
|
+
)
|
|
987
|
+
else:
|
|
988
|
+
rows = self._store.search_turns(
|
|
989
|
+
query_embedding=vec,
|
|
990
|
+
agent_identity=agent_filter,
|
|
991
|
+
session_id=session_filter,
|
|
992
|
+
limit=limit,
|
|
993
|
+
)
|
|
886
994
|
except Exception as exc: # noqa: BLE001
|
|
887
|
-
|
|
995
|
+
if hybrid and vec is not None:
|
|
996
|
+
try:
|
|
997
|
+
rows = self._store.search_turns(
|
|
998
|
+
query_embedding=vec,
|
|
999
|
+
agent_identity=agent_filter,
|
|
1000
|
+
session_id=session_filter,
|
|
1001
|
+
limit=limit,
|
|
1002
|
+
)
|
|
1003
|
+
except Exception as exc2: # noqa: BLE001
|
|
1004
|
+
return json.dumps({"results": [], "count": 0, "error": f"db: {_safe_err(exc2)}"})
|
|
1005
|
+
else:
|
|
1006
|
+
return json.dumps({"results": [], "count": 0, "error": f"db: {_safe_err(exc)}"})
|
|
888
1007
|
|
|
889
1008
|
results = []
|
|
890
1009
|
for r in rows:
|
|
891
1010
|
ts = r.get("ts")
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
)
|
|
1011
|
+
score = r.get("score")
|
|
1012
|
+
entry = {
|
|
1013
|
+
"id": r.get("id"),
|
|
1014
|
+
"agent_identity": r.get("agent_identity"),
|
|
1015
|
+
"session_id": r.get("session_id"),
|
|
1016
|
+
"role": r.get("role"),
|
|
1017
|
+
"ts": ts.isoformat() if ts else None,
|
|
1018
|
+
"score": round(float(score), 4) if score is not None else None,
|
|
1019
|
+
"content": (r.get("content") or "")[:2000],
|
|
1020
|
+
}
|
|
1021
|
+
if r.get("rrf_score") is not None:
|
|
1022
|
+
entry["rrf_score"] = round(float(r["rrf_score"]), 6)
|
|
1023
|
+
results.append(entry)
|
|
903
1024
|
return json.dumps({"results": results, "count": len(results)})
|
|
904
1025
|
|
|
905
1026
|
# -- Setup hooks ---------------------------------------------------------
|
|
@@ -945,6 +1066,12 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
945
1066
|
"default": DEFAULTS["scope_default"],
|
|
946
1067
|
"choices": ["current", "all"],
|
|
947
1068
|
},
|
|
1069
|
+
{
|
|
1070
|
+
"key": "hybrid_search",
|
|
1071
|
+
"description": "v0.4.1: fuse the HNSW vector ranking with a Postgres full-text ranking (Reciprocal Rank Fusion) in recall_memory / recall_conversation. Recovers exact-lexical hits cosine smooths away and text-only rows with NULL embeddings; degrades to pure vector on error and to full-text-only when the query fails to embed. Apply migration 003 for the GIN index (works without it, just slower).",
|
|
1072
|
+
"default": "true",
|
|
1073
|
+
"choices": ["true", "false"],
|
|
1074
|
+
},
|
|
948
1075
|
{
|
|
949
1076
|
"key": "write_queue_maxsize",
|
|
950
1077
|
"description": "Bounded async-writer queue size; full = oldest writes drop with a warning",
|