Skip to content

feat(retrieval): Wilson prior, exploration slot, embeddings + three-leg fusion#83

Merged
emp3thy merged 20 commits into
mainfrom
feat/retrieval-quality
Jul 23, 2026
Merged

feat(retrieval): Wilson prior, exploration slot, embeddings + three-leg fusion#83
emp3thy merged 20 commits into
mainfrom
feat/retrieval-quality

Conversation

@emp3thy

@emp3thy emp3thy commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Summary

Implements PR-A of the retrieval-quality design (docs/superpowers/specs/2026-07-23-retrieval-quality-design.md):

  1. Wilson-score ranking prior (services/scoring.py) — lower bound (z=1.96) on (useful+overlooked)/(useful+overlooked+ignored), computed in Python; replaces the useful_count + 3*overlooked ORDER BY and the fix(memory): close the rating loop and stop diluting what gets served #81 ignored-demotion CASE (constants deleted). Hit rate now beats raw serve-count: 3/4 outranks 67/192.
  2. Exploration slot — last shortlist slot per polarity bucket reserved for the best memory with <3 rated exposures. Wilson scores untested rows 0; without the slot they'd never earn data. Bounded dilution by construction.
  3. Write-time embeddings — reflections (synthesis new/augment; UI edit) and semantic memories (create/update/promote) embed via SyncEmbedder: fresh OllamaEmbedder(timeout=5, max_retries=1) per bridge call (httpx client is loop-bound), 60s circuit breaker, never raises. Lazy self-heal on query retrieves (cap 20) + one-shot python -m better_memory.cli.backfill_embeddings. Migration 0014 adds semantic_embeddings (additive).
  4. Three-leg RRF fusion — Wilson rank + BM25 + vec-kNN, rrf_k=60 matching search/hybrid.py; every leg degrades independently (no embedder / breaker open / no match ⇒ exactly the prior behaviour, test-pinned incl. multi-row order equality).

Validation

  • Full suite 1545 passed / 22 skipped / 0 failed; pyright 0 errors.
  • Per-task spec+quality reviews (12 tasks), final whole-branch review: 0 Critical/Important.
  • Live A/B gate (48 sandboxed sessions on this branch): distinct useful 128/693 = 18.47% vs deployed baseline 62/270 = 22.96%; one-sided two-proportion z = −1.574 > −1.645 ⇒ not statistically below baseline (PASS). Two disclosed confounds pull the point estimate down: (a) the exploration slot deliberately serves under-rated memories that mostly rate ignored at first — the designed cost of acquiring ranking signal; (b) live-installed contextual hooks fired inside A8 sessions (absent in the baseline arm); excluding that channel, z = −1.381. The metric to watch post-merge is live useful% trend as exploration converts untested memories into rated ones.

Deploy

After merge: restart Claude Code (migration 0014 applies on server start), then python -m better_memory.cli.backfill_embeddings to embed the historical corpus.

Carried follow-ups (triaged Minor): agentcore backend still uses the legacy linear ranking client-side; sync_embed finally-clause exception masking polish; storage/sqlite.py ReflectionService wiring-for-consistency (dead write path today).

🤖 Generated with Claude Code

emp3thy and others added 19 commits July 23, 2026 07:58
…tings)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 48-session gate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eted

Replaces the popularity + overlooked-weight + ignored-demotion CASE
ORDER BY in retrieve_reflections with wilson_lower_bound over
(useful+overlooked)/rated, computed in Python and sorted
(wilson DESC, confidence DESC, updated_at DESC). Deletes
OVERLOOKED_RANKING_WEIGHT, IGNORED_DEMOTION_FLOOR,
IGNORED_DEMOTION_WEIGHT from memory_rating.py and their two
superseded pinning tests.

semantic.py's list_for_project also imported the deleted constants;
bridged its ORDER BY to plain popularity (useful_count DESC,
created_at DESC) pending Task 3's full Wilson rewrite there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ries

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bedder and 60s breaker

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ncEmbedder

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cEmbedder

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ction edit

The management UI built ReflectionService and SemanticMemoryService
without a sync_embedder, so UI-driven writes (edit reflection,
create/update semantic memory) never populated the vec0 embeddings
tables — only MCP/synthesis writes did, requiring a manual CLI backfill.

- create_app() gains a sync_embedder kwarg (defaults to auto-detect from
  get_config().embeddings_backend, mirroring mcp/server.py and
  storage/sqlite.py); wired into ReflectionService and all
  SemanticMemoryService construction sites.
- ReflectionService.update_text now re-embeds via a new module-level
  _write_reflection_embedding() (extracted from
  ReflectionSynthesisService._store_embedding so both classes share it) —
  previously an edit left a stale vector pointing at the old text.
- tests/ui/conftest.py's client fixture pins
  BETTER_MEMORY_EMBEDDINGS_BACKEND=sqlite so ordinary route tests don't
  depend on a live Ollama instance (default backend is ollama); tests
  proving the wiring build their own app with an injected fake embedder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…self-heal

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ade order

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Claude BugBot Analysis

Found 1 potential bug in this PR.

medium: 1

Reviewed the retrieval-quality PR (Wilson-score ranking, exploration slot, RRF vector fusion, SyncEmbedder circuit breaker, and embedding write/backfill paths) in depth; the ranking/fusion/bucketing logic all checked out correctly against their tests, but the SyncEmbedder wiring creates two independent circuit-breaker instances instead of the one shared, process-wide breaker the code documents.

Comment thread better_memory/storage/sqlite.py Outdated
…er and backend

Addresses PR #83 review: SqliteBackend built a second SyncEmbedder, splitting
the process-wide circuit breaker into two independent ones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Claude BugBot Analysis

Reviewed the retrieval-quality PR (Wilson-score ranking, reflection/semantic embeddings, SyncEmbedder circuit breaker, exploration slot) across all changed source files; no genuine new defects found. The previously reported SyncEmbedder-instantiated-twice bug is fixed: build_backend/SqliteBackend now accept and forward the caller's shared sync_embedder instead of constructing a second one.

No bugs were detected in this PR.

@emp3thy
emp3thy merged commit 27ee492 into main Jul 23, 2026
3 checks passed
@emp3thy
emp3thy deleted the feat/retrieval-quality branch July 23, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant