What ships in pgmnemo v0.6.0
v0.6.0 fixes one core problem (RRF fusion for hybrid search) and closes 6 questions from Agency, our first production user. No breaking changes — ALTER EXTENSION pgmnemo UPDATE TO '0.6.0' and you're done.
🎯 Smarter result ranking (RRF)
The problem: when you call recall_lessons('rate limit handling'), two engines run — vector (by meaning, scores 0.4–0.9) and BM25 (by exact words, scores 0.005–0.05). We combined them as 0.4×vec + 0.4×bm25, but BM25 scores are 10–100× smaller, so BM25 was effectively ignored in the sort. Like mixing height in meters with salary in dollars — height loses every time.
Now: the final sort uses Reciprocal Rank Fusion — each engine's rank positions, not raw scores. A document both engines place in the top 3 beats one only a single engine scored highly.
You get: expected +1.5–2pp recall@10 on LongMemEval. Bench gate before release: p<0.05 and ≥+1pp, otherwise we roll back. Your code changes: nothing — same function, same return shape, better ranking.
🕰 New as_of_ts — time travel through memory
The problem: an agent works across phases (RESEARCH → PLAN → IMPLEMENT) and saves lessons during IMPLEMENT. Re-run RESEARCH later and it sees lessons from its own future — breaking bitemporal consistency.
SELECT * FROM pgmnemo.recall_lessons(
query_text => 'rate limit',
as_of_ts => '2026-05-22 14:00:00+00' -- ← new
);
Filters WHERE created_at <= as_of_ts. Leave it NULL and behavior is identical to v0.5.1. You get: reproducible experiments, backtesting, and "why didn't the agent find lesson X back then?" debugging.
📊 New ghost_count in stats()
The problem: in Agency's production, 793 of 2054 active lessons (38.6%) are "ghosts" — no commit_sha, no artifact_hash, so you can't verify they link to real work. The provenance gate excludes them, but there was no metric to see how many remained.
SELECT * FROM pgmnemo.stats(); -- now includes ghost_count BIGINT
You get: one command to see unverified lessons. Alert on "disable include_unverified when ghost_count < 100."
🔔 NOTICE on dedup in ingest()
When the bitemporal close+create fires on a repeated content_hash, ingest() now emits NOTICE: content_hash_dedup_fired lesson_id=12345 — parseable via grep / connection.notices. No breaking change if you don't listen for it.
📖 Docs: rollback & temporal-weight tuning
Postgres has no extension downgrade — docs/MIGRATION.md §Rollback now documents the pre-upgrade backup and recovery path. And docs/USAGE.md §Tuning adds the recency formula with a table, so historical corpora don't silently drop out of recall:
recency_factor = exp(-recency_weight × temporal_boost × age_days / 90)
| Age | rw=0.1 boost=1 | rw=0.1 boost=3 | rw=0.05 boost=10 |
|---|---|---|---|
| 7 days | 0.993 | 0.977 | 0.962 |
| 90 days | 0.905 | 0.741 | 0.607 |
| 365 days | 0.017 | 0.000 ⚠ | 0.130 |