# How Memory Recall Works

This page explains how Anteroom retrieves durable memories at runtime and injects them into the agent's context — separate from RAG, with scope-based visibility and its own token budget.

## The Pipeline at a Glance

Every turn (both CLI and web UI) runs this sequence alongside RAG retrieval:

1. **Strip stale memory** from the previous turn's system prompt
2. **Embed your message** (reusing the RAG embedding service)
3. **Search `memory_artifact_embeddings`** via the `memories` usearch index, filtered to namespaces you can see
4. **Fallback lexically when needed** — if vector recall is unavailable, unindexed, or yields no qualifying hits, scan visible `active` memories with a narrow lexical heuristic
5. **Filter by status and distance** — semantic hits still require `active` status and the configured similarity threshold; fallback hits stay limited to visible `active` memories
6. **Trim to token budget** — memories are added until `max_tokens` would be exceeded
7. **Wrap each memory** in an `<untrusted-content>` envelope
8. **Append a `## Recalled Memories`** section to the system prompt

Step 1 guarantees stale memory from a prior turn never leaks into the next one. Steps 3-6 are independent from RAG — memory gets its own budget (default 800 tokens) and doesn't compete with the source/RAG budget (default 2000 tokens). The lexical fallback is intentionally narrow: it only considers visible namespaces, only `active` memories, and it reuses the same `max_memories` and `max_tokens` limits as the semantic path.

---

## What Counts as a Memory

Memories are `memory` artifacts (#919) stored in the `artifacts` table with typed metadata:

- `memory_scope`: `user`, `project`, or `local`
- `memory_category`: `preference`, `project_fact`, `decision`, or `workflow_hint`
- `memory_status`: `active`, `candidate`, `pending_review`, `rejected`, or `archived`
- `provenance`: conversation / message / workflow / source artifact references

Only `active` memories are eligible for recall. Candidates, rejected, and archived memories are stored but excluded — the promotion/review pipeline lives in #920.

---

## Scope-Based Visibility

Memory scope maps to artifact namespace, and recall filters by namespace before ranking:

| Scope | Namespace | Visible when… |
|---|---|---|
| `user` | `user` | Always |
| `project` | *project slug* | Current space's name matches the project slug |
| `local` | `local` | Current space is explicitly local |

A space that belongs to `project A` can never see memories scoped to `project B` — the recall query's `namespace IN (…)` clause enforces this at the database level, and `retrieve_memories()` defensively re-checks the namespace of each returned row before including it. Scope isolation is covered by an explicit regression test.

Missing or ambiguous space context collapses to **`user`-only** visibility — the safe default.

---

## Untrusted Content Framing

Every recalled memory is wrapped in an `<untrusted-content>` envelope with origin `memory:<fqn>`, using the same `context_trust.wrap_untrusted()` pipeline RAG uses. This is mandatory — not optional — because:

- Memory content can carry injection payloads from earlier untrusted turns (tool output, RAG, pasted text)
- Even user-authored memories are untrusted once persisted — the author's intent is not preserved across turns
- The two-level trust model (`trusted` vs `untrusted`) is sufficient; no new trust level was introduced

See [Prompt Injection Defense](../security/prompt-injection-defense.md) for the broader threat model.

---

## Budget and Degradation

| Scenario | Reason string | Behaviour |
|---|---|---|
| `memory_recall.enabled` is `False` | `Memory recall disabled` | Skip silently |
| Query under 10 characters | `Query too short for recall` | Skip |
| No usearch memory index | `no_vec_support` | Try bounded lexical fallback; if it misses, return empty and CLI shows "Memory recall not configured" |
| Embedding service missing | `Embedding service unavailable` | Try bounded lexical fallback; if it misses, return empty |
| Embedding call raises | `Embedding failed` | Try bounded lexical fallback; if it misses, return empty |
| No rows in `memory_artifact_embeddings` | `no_embeddings_yet` | Try bounded lexical fallback; if it misses, return empty |
| All candidates above threshold | `no_matches_within_threshold` | Try bounded lexical fallback; if it misses, return empty |
| Similarity search raises | `search_failed` | Try bounded lexical fallback; if it misses, return empty |

The runtime never raises from recall. If the lexical fallback finds memories, recall succeeds normally and downstream attribution shows those entries just like semantic hits. If fallback also misses, the runtime returns `([], "<reason>")` and the agent turn continues without memory.

---

## Ordering vs RAG

Memory is appended **after** the RAG section. The model sees retrieved source chunks first, then durable memories. This matches the ordering shown by [Context Attribution](attribution.md) and keeps behaviour predictable when both pipelines fire on the same turn.

---

## Configuration

See [Config Reference](config-reference.md#memory_recall) for every field, default, and environment-variable override.

Defaults:

- `enabled`: auto-detect (use if embeddings are available)
- `max_memories`: 5 per turn
- `max_tokens`: 800 (independent of RAG's 2000)
- `similarity_threshold`: 0.5 (cosine distance; lower = stricter)
- `show_status`: true (CLI renders a status line per turn)

---

## Related Issues

- #919 — typed memory artifacts (storage model)
- #920 — memory promotion and review flows
- #923 — runtime context attribution (surfaces recalled memories in UI)
- #625 — memory retention policy and eviction
- #1454 — selective auto-propose memory candidates from assistant turns (proposal-side counterpart)
