Memory Writing Layer — System Design
Generated frommemory-apisource. Covers the full path from anadd()call to durable storage in PostgreSQL and Qdrant.
1. OverviewMermaid diagrams (the best option)
The memory write layer is a queue-first async pipeline. The HTTP request returns quickly with ajob_id. All heavy work — extraction, conflict resolution, vector indexing, and domain overlays — runs in a Celery worker.
2. Entry Point — POST /v1/memories/add
File: api/routers/memories.py
Step 1 — Idempotency check
If the request includes anIdempotency-Key header, the router checks Redis for a previously cached response for that (tenant_id, idempotency_key) pair. A cache hit replays the original response without re-running any logic.
Step 2 — Quality Gate
QualityGateService.check() runs synchronously before any database write. Four sequential layers:
A blocked request returns HTTP
200 with status: "L1" / "L2" / "L3" / "L4". The LLM call is never blocked by a gate failure.
Quality score formula (L2):
- Embeds the incoming conversation text
- Compares cosine similarity against the last 5 query embeddings stored in Redis per user
- Conflicting salient entities (acronyms, numbers, subject-like tokens) bypass the similarity check — a new exam name or score is never treated as a duplicate
payload.source present) skip L3 entirely — legitimate service events look similar by design.
Step 3 — Proxy user resolve
ProxyUserService.resolve() maps (tenant_id, external_user_id) → a ProxyUser row in PostgreSQL. Creates the row on first encounter.
Step 4 — Queue the job
MemoryService.queue_memory_add() creates an ExtractionJob row and dispatches process_extraction_job to Celery. The router reads the queue ETA from Redis to set processing_eta_seconds and the X-MemoryOS-Processing response header.
3. Quality Gate Detail
File:api/services/quality_gate.py
Redis failures are caught and circuit-breaker-guarded. A Redis outage skips L1 and L3 rather than blocking the request.
4. Celery Pipeline — process_extraction_job
File: api/tasks/extraction_tasks.py
The task entry point is process_extraction_job(job_payload). It calls run_extraction_pipeline() which does the real work.
Retry logic
Pipeline steps inside run_extraction_pipeline()
4a. Load context
4b. Source event context
If the job has asource_event_id and source.explicit = True, the pipeline builds a provenance snapshot to pass to the extractor, enabling “authenticated service event mode.”
4c. Extraction — ExtractionService.extract_sync()
File: api/services/extraction_service.py
Two-pass extraction for complex conversations:
Pass 1 — Compositional pre-pass (conditional):
Triggered when the conversation has ≥ 4 messages, ≥ 240 chars, ≥ 2 user turns, and ≥ 2 signal groups (identity, team, project, goal, preference, timeline). Makes a fast LLM call to find entities and cross-message relationships. Output is appended to the main extraction prompt as hints.
Pass 2 — Main extraction:
Additional filters on each candidate:
importance_scoremust be ≥ 2.0- Category must be in
{preference, fact, goal, procedure, relationship, expertise} - Content length: 10–500 chars
- Temporary session memory patterns are rejected (regex: “current debugging session”, “next terminal command”, etc.)
LLMService tries providers in configured order (LLM_PROVIDER_ORDER=gemini,openai,anthropic). A circuit breaker per provider handles 5xx / timeout / rate-limit failures with automatic fallthrough.
4d. Pending candidate management
4e. Conflict resolution — ConflictResolver.check_and_store()
File: api/services/conflict_resolver.py
For each extracted memory:
- Embed with
EmbeddingService - Vector search in Qdrant for semantically similar existing memories
- Classify the conflict
- Apply resolution:
Every change is recorded in
MemoryVersion (append-only). Cross-user conflicts (shared facts) use recency and confidence weighting. Personal conflicts route to ClarificationQueue for the user to resolve at next session.
Domain-aware conflict routing:
- EdTech: personal student facts →
ClarificationQueue; institutional facts → tenant dashboard review - Support: similar routing for customer vs. workspace facts
4f. Vector storage
Each stored memory gets an embedding written to Qdrant (memories collection). The embedding model and dimensions are configurable (EMBEDDING_MODEL, EMBEDDING_DIMENSIONS).
A vector_sync_outbox table catches any Qdrant write failures for async repair.
4g. Domain schema overlay
Runs after general extraction and conflict resolution.Domain overlay failures are isolated — the general memories already committed are never rolled back.
4h. Hot-tier cache invalidation
After commit,_invalidate_proxy_user_cache(proxy_user_id) deletes the Redis hot-memories key for the user (user:{proxy_user_id}:hot_memories). The next retrieval will rebuild from PostgreSQL + Qdrant.
4i. Job status tracking
- PostgreSQL
ExtractionJobrow tracksstatus,attempts,memories_created,completed_at,dead_lettered_at - Redis key
job:{job_id}:statustracks live status for fastGET /jobs/{job_id}polling (TTL 1 hour)
5. Data Written per Successful add()
6. Queue Architecture
File:api/tasks/queue_router.py
Tenants are routed to different Celery queues based on plan tier: