MemorySyncMemorySync
Memory Model

Common Mistakes

Real bugs that have shown up in this codebase, the symptoms they produced, and what was added to keep them from recurring. Read this before designing your own integrations.

Treating an empty vector as a failure

If the embedding service is unreachable, the row is still committed with vector = NULL. The ingest API does not return an error in that case; it returns the row. A background pass re-embeds it later. Code that asserts a non-null vector right after POST /memory/add will flake during embedding outages.

Relying on text equality for deduplication

Early code paths created duplicate rows because text was used as the dedup key. The fix was the extracted_key column — a canonical, lowercased, normalised key produced by the extraction pipeline. If you write your own dedup logic, key off extracted_key, not text.

Mixing naive and aware datetimes

The recency factor of the ranker reads created_at and computes age in hours. Older code paths wrote timezone-naive timestamps; the recall path now defends with an explicit timezone-aware check and treats naive values as UTC. If you cache timestamps client-side, store them as ISO 8601 with the offset.

Hard-deleting before the grace window elapses

A worker crash mid-deletion used to risk skipping the seven-day soft-delete window. The remediation was a DeletionProgress checkpoint table — every deletion is broken into ordered steps, each step is recorded as it completes, and recovery resumes from the last completed step. Never write tooling that touches retention columns directly; go through the deletion endpoints.

Forgetting to re-upsert the vector on a tier change

When a memory's tier changes, the index payload becomes stale because the index stores tier as a metadata field used for tier-restricted recall. The platform now always re-upserts the vector on tier transitions. If you script a bulk tier change, mirror the same behaviour or your filtered queries will lie.

Letting metadata grow unbounded

The metadata_json column accepts arbitrary objects, but the API rejects payloads above the configured size limit with HTTP 413. A previous OOM incident traced back to a customer pushing megabytes of telemetry into the metadata blob. Use tags and structured fields where you can; reserve metadata for things you actually search on.

Not handling decryption failures gracefully

If a tenant's encryption key has been rotated and the row references an old key version that has expired, decryption fails. The recall path returns the row with the text replaced by [REDACTED — DECRYPTION FAILED] and a flag for the support team. Your client should render that string as-is rather than crashing on a missing field.

Asking importance to do recall's job

Setting importance: 1.0 on every memory does not make it always come back — the ranker normalises factors per result batch. A memory with importance = 1 stuck in cold tier still loses to a hot-tier memory with importance = 0.4. Tier first, importance second.

Skipping the project header

If a user has an active project but the request omits X-Project-ID, the recall returns memories from all their projects with project_id IS NULL intermixed. This was originally a silent cross-project leak — now it surfaces in audit logs as a missing-scope warning. Set the header on every call your client makes.