Blog / release

What ships in pgmnemo v0.6.0

2026-05-22 · pgmnemo contributors · ~5 min read
releasehybrid-searchtemporalrrf

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)
Agerw=0.1 boost=1rw=0.1 boost=3rw=0.05 boost=10
7 days0.9930.9770.962
90 days0.9050.7410.607
365 days0.0170.000 ⚠0.130
← Back to all posts