Skip to content

feat: add embedding ingestion and hybrid retrieval - #7

Merged
fabbrik merged 3 commits into
mainfrom
feat/2-6-hybrid-retrieval
Sep 22, 2026
Merged

fabbrik merged 3 commits into
mainfrom
feat/2-6-hybrid-retrieval

Conversation

@fabbrik

@fabbrik fabbrik commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Summary

Story 2.6. Records become semantically findable, without the canonical path ever depending on a model provider.

  • Ingestion. After a record commits, ExperienceIndexingService embeds only its sanitized retrieval summary — task ID, task summary, reflection lesson — through a replaceable, domain-typed port. IEmbeddingGenerator never reaches Abstractions or Core.
  • Derived, never authoritative. The write is conditional on the record still being at that exact revision: a stale write is rejected, and a deleted record cannot be recreated. Every indexing failure leaves the record committed, text-searchable and retryable, and the post-commit hook is bounded by its own timeout.
  • Only eligible records are embedded. Quarantined, revoked, superseded and candidate records never have their text sent to a provider, matching the statuses the search can actually return.
  • Reindexing is explicit, scoped, keyset-paged and idempotent: an unchanged model and content hash skips the provider entirely.
  • Hybrid retrieval. Text and vector channels run inside the one existing timeout, share eligibility, and merge by the higher normalized relevance. A model or dimension mismatch, an unavailable provider, or a channel failure yields a flagged text-only result — including when a provider cancels for its own reasons, which is what an HttpClient timeout looks like.
  • Schema. The new package owns and applies its own migration, so a text-only deployment never runs CREATE EXTENSION vector (a superuser operation). The HNSW index is created out of band, and a test proves the planner actually uses it.

Route

Plain Npgsql with Pgvector, which epics.md sanctions alongside the VectorData connector. The hybrid query must join embeddings to records (status and confidence live there), match optional scope fields with IS NOT DISTINCT FROM, and work against an unconstrained vector column — none of which the connector expresses. Recorded with its reasoning in the spec.

Review

Three reviewers raised 33 findings; 32 fixed, 1 deferred (a batch embedding overload for large reindexes). The serious ones: the schema forced pgvector on text-only hosts; ineligible records' text was being sent to a provider; a provider-side timeout returned an empty failure instead of the text answer; and the post-commit hook was unbounded.

Test plan

  • dotnet build --configuration Release: 0 warnings, 0 errors
  • dotnet test --configuration Release: 643 of 643 pass (Core 382, Postgres 125, Vectors 43). Locally with TESTCONTAINERS_RYUK_DISABLED=true.
  • Container tests: ingestion, stale and deleted writes, cascade delete, reindex paging and idempotence, semantic retrieval on non-overlapping words, model and dimension fallbacks, provider outage leaving the record committed, HNSW plan use, and a stock postgres:16 migration proving text-only hosts need no extension.
  • All embeddings in tests are deterministic; no model credentials anywhere.
  • CI green

🤖 Generated with Claude Code

fabbrik and others added 3 commits September 18, 2026 21:17
Add ExperienceFinalizationService: one call that loads a completed captured
run, evaluates its own closed verification round, checks host authorization and
the host's storage decision, reflects, creates the record as Candidate, and
commits its initial lifecycle event to Validated or Quarantined. Record,
reflection and event IDs derive from the run, so a retry re-derives them and
converges instead of duplicating.

Required checks can now name the evaluator kind that may satisfy them, and Core
and Storage.Postgres each expose a service-registration extension. The MAF
adapter finalizes a fully captured run through the host's resolver.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add IExperienceCandidateSource with a PostgreSQL implementation: scope, status
and confidence filtering plus a full-text match over a generated tsvector
column added by migration 0003. Core's ExperienceRetrievalService applies
expiry and environment eligibility, then ranks candidates on relevance,
confidence, recency, status and environment compatibility with configurable
validated weights, exposing every normalized component and effective weight.

Retrieval is bounded by a timeout that returns an empty result with a timeout
signal rather than throwing, caller cancellation stays distinct, and a capped
candidate pool is reported through Truncated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add IExperienceEmbeddingIndex and a domain-typed embedding generator port,
with a new AgentExperience.Storage.Postgres.Vectors package implementing them
over plain Npgsql and Pgvector. Embeddings are derived data: indexing runs
after the canonical commit, embeds only the sanitized retrieval summary of an
eligible record, and writes conditionally on the record's exact revision, so a
stale write is rejected and a deleted record is never recreated.

Retrieval gains a vector channel merged with the text channel under the same
eligibility, timeout and ceiling. A model or dimension mismatch, an
unavailable provider, or a vector-channel failure produces an explicit
flagged text-only result rather than an incompatible comparison.

The vectors package owns and applies its own schema, so a text-only
deployment never runs CREATE EXTENSION vector.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fabbrik
fabbrik merged commit c147228 into main Sep 22, 2026
1 check passed
@fabbrik
fabbrik deleted the feat/2-6-hybrid-retrieval branch September 22, 2026 21:37
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