Files
correx/docs/decisions/adr-0010-l3-vector-retrieval.md

4.6 KiB
Raw Permalink Blame History

name, description, depth, links
name description depth links
Adr 0010 L3 Vector Retrieval SQLite-vec default, Qdrant optional — semantic retrieval for L3 context injection 2
../index.md
../architecture/context-layers.md
../epics/epic-7-resolution.md

ADR 0010: L3 context retrieval — SQLite-vec default with optional Qdrant backend

status: accepted
date: 11.05.2026 (May)
deciders: correx design team


context

L3 (durable project memory) holds persistent facts, architecture decisions, and key outputs across sessions. The context engine (Epic 7) assembles token-budgeted packs from L0L2; L3 is injected selectively when relevant.

Injecting all L3 entries unconditionally is not viable — the layer grows unboundedly and most entries will be irrelevant to any given stage. A relevance signal is needed to select which L3 entries enter a pack.

Semantic similarity via vector embeddings is the natural fit: embed each L3 entry at write time, query by the current stage's context at pack-build time, inject the top-K matches.

Determinism constraint (ADR-0000 #10): vector search is nondeterministic — the same query may return different results as the index evolves. Allowing live vector search to determine pack contents would break replay. This is resolved by committing the selected L3 entry IDs as an event (L3EntriesSelectedEvent) before pack assembly. Replay uses the committed IDs, not a live query.

decision

Correx will use SQLite-vec as the default vector store for L3 retrieval, with Qdrant as an optional configurable backend.

:core:context defines a VectorStore port (interface). :infrastructure:tools:sqlite-vec and :infrastructure:tools:qdrant provide implementations. The active backend is selected via configuration — no core module is coupled to either.

SQLite-vec (default)

  • embedded, zero-setup — consistent with the local-first posture established in ADR-0005
  • co-located with the primary SQLite event store; single-file deployment
  • accessed via SQLite JDBC — no additional client library
  • sufficient for local sessions where L3 grows to tens of thousands of entries

Qdrant (optional)

  • self-hosted Rust binary, HTTP/gRPC API
  • scales to large L3 indices and supports filtered ANN queries
  • activated by setting vectorStore.backend = qdrant in configuration
  • requires a sidecar process; not suitable for zero-setup local deployments

determinism contract

  1. at L3 entry write time: embed content, store vector in the active backend
  2. at pack-build time: query top-K by cosine similarity to the current stage context
  3. commit L3EntriesSelectedEvent(sessionId, stageId, selectedEntryIds, queryEmbedding)
  4. pack assembly reads selectedEntryIds from the event — not from a live query
  5. replay uses the committed event; the vector store is never queried during replay

The vector store is a read model for selection. The event log remains authoritative.

VectorStore port

interface VectorStore {
    fun upsert(id: ContextEntryId, vector: FloatArray, metadata: Map<String, String>)
    fun query(vector: FloatArray, topK: Int, filter: Map<String, String> = emptyMap()): List<VectorMatch>
    fun delete(id: ContextEntryId)
}

data class VectorMatch(val id: ContextEntryId, val score: Float)

consequences

positive:

  • zero-setup default — SQLite-vec adds no external process for local deployments
  • Qdrant available for production or large-scale use without changing core code
  • determinism preserved — vector search never touches replay path
  • selection is auditable and replayable via L3EntriesSelectedEvent
  • port/adapter pattern is consistent with existing infrastructure separation (ADR-0005)

negative:

  • SQLite-vec has practical limits at very large L3 indices (tens of millions of entries); Qdrant required at that scale
  • embedding generation (not vector retrieval) is a nondeterministic step — embedding model version changes may alter which entries are selected for future sessions (existing committed events are unaffected)
  • two infrastructure implementations to maintain

alternatives considered

  • Chroma: Python-native; adds a Python service dependency inconsistent with a JVM-first project.
  • pgvector: requires PostgreSQL; heavier than SQLite for local-first deployment.
  • Keyword/tag-based filtering: deterministic but misses semantic relevance; insufficient for open-ended L3 memory.
  • Inject all L3 entries: correct but not token-budget-safe as L3 grows.

status

accepted. Implementation deferred to the epic that introduces L3 write and retrieval infrastructure.