The 7 Dimensions of Memory
How Ember decomposes every moment into a signal constellation — and why each dimension matters
Every memory system in AI reduces to the same operation: embed text, search by similarity, return a ranked list.
One dimension. One axis. Semantic distance.
But human memory doesn't work this way. You don't remember Thanksgiving because of word similarity to “turkey.” You remember it because cinnamon + cold air + a specific voice + the feeling of being 12 years old all converge simultaneously.
Ember models this convergence explicitly. Every indexed memory and every incoming message is decomposed into a signal constellation — 7 independent scoring axes that must align before recall fires.
Here's how each one works.
1. Semantic
What it captures
Meaning-level similarity between text passages
This is the dimension every RAG system has. Ember uses MiniLM-L6-v2 to generate 384-dimensional embeddings entirely on-device. No API calls, no network latency — 12ms per embedding on CPU.
But here's the key difference: in Ember, semantic similarity is necessary but not sufficient. A high cosine score gets a memory past the first gate. It doesn't ignite it. The memory still needs convergence on at least 2 other dimensions to fire.
# Semantic is always extracted automatically
ember.index("Walking through autumn leaves in the park")
# → 384d embedding generated locally via MiniLM
# → No API key, no network, runs on CPUWeight: 0.30 (highest single weight, but still requires partners to fire).
2. Emotional
What it captures
Valence (grief → joy), arousal (calm → intense), and emotion labels
The emotional dimension uses a curated lexicon to extract three signals without any LLM call:
- Valence — A float from -1.0 (grief, despair) to +1.0 (joy, elation). Computed from emotion word frequencies with intensity weighting.
- Arousal — A float from 0.0 (calm, meditative) to 1.0 (excited, agitated). Independent from valence: peaceful joy and anxious anticipation are both valid states.
- Labels — Discrete emotion tags like “nostalgic,” “tender,” “bittersweet.” Matched against the memory's indexed labels with Jaccard overlap.
Scoring uses absolute difference for valence/arousal (closer = higher) and label overlap ratio. The three sub-scores are averaged.
ember.index(
"The last morning before we moved away",
emotions=["bittersweet", "nostalgic", "tender"],
# Valence and arousal are auto-extracted from the text,
# or you can provide explicit overrides:
# emotion_valence=0.2,
# emotion_arousal=0.3,
)Weight: 0.25. Emotional resonance is the second strongest signal. Two memories with identical semantic content but different emotional textures score very differently.
3. Sensory
What it captures
Visual, auditory, olfactory, tactile, and gustatory signals
This is the dimension that makes Ember feel different from everything else. Most memory systems ignore sensory detail entirely — they treat “the smell of rain on hot pavement” the same as “precipitation event.”
Ember maintains a curated vocabulary for each modality:
Scoring: for each modality present in both the message and the memory, compute the overlap ratio of detected markers. Then average across all modalities that have data on either side.
The power of sensory scoring is cross-modal convergence. A message that mentions both “cinnamon” (olfactory) and “crackling fire” (auditory) scores much higher against a winter cabin memory than either signal alone.
Weight: 0.15.
4. Temporal
What it captures
Season, time of day, and era references (childhood, college, present)
Temporal extraction uses regex + keyword matching — fast and deterministic. Three sub-signals:
- Season — summer, winter, autumn, spring. Exact match scoring. “August heat” maps to summer.
- Time of day — morning, afternoon, evening, night, dawn, dusk. “3 AM” maps to night.
- Era — childhood, teenage, college, early career, present. “When I was 8” maps to childhood.
A memory indexed with season="summer", era="childhood" will resonate with messages that reference hot days, fireflies, or being young — even if the semantic content is about something completely different.
Weight: 0.10.
5. Spatial
What it captures
GPS proximity, location names, and place types (beach, kitchen, city)
Added in v0.3.1, the spatial dimension supports three scoring paths:
- GPS proximity — Haversine distance between two coordinates. Within 500m = full score. Linear decay to 50km.
- Named locations — Exact and partial match on location strings. “El Porto” matches “El Porto Beach, CA.”
- Place types — Category matching: “beach” matches “beach,” “coast” matches “beach.” Extracted from location context.
# GPS-aware memory: fires when the user is physically nearby
ember.index(
"Dawn patrol at El Porto, cold wax and salt air",
latitude=33.895, longitude=-118.421,
location="El Porto, CA",
)
# At check time, pass the user's coordinates
result = ember.check(
"Beautiful morning",
context={"location": {"lat": 33.896, "lon": -118.422}},
)
# Spatial dimension fires — user is 100m from the memoryThis is how real memory works: you walk through your old neighborhood and things surface. The GPS scoring is especially powerful for mobile agents and wearable companions.
Weight: 0.05 (accent dimension — potent when combined).
6. Relational
What it captures
People mentioned or implied, and relationship context
Memories aren't just about what happened — they're about who was there. The relational dimension scores based on shared people between the current context and stored memories.
Scoring uses name overlap (exact + fuzzy) with a trust-level modifier. Memories involving high-trust relationships (family, close friends) get amplified. Casual mentions get dampened.
ember.index(
"Teaching Jake to ride a bike in the driveway",
people=["Jake"],
relationships={"Jake": "son"},
)
# Later, any mention of Jake amplifies relational scoring
result = ember.check("Jake's soccer game was amazing today")Weight: 0.10.
7. Musical
What it captures
Artists, tracks, genres, and musical associations
Music is one of the most powerful memory triggers in human experience. A song you haven't heard in years can instantly transport you to a specific place and time.
The musical dimension is optional — it only fires when music context exists on both sides. Scoring matches on artist name, track title, and genre. This makes it an accent dimension: silent most of the time, vivid when it activates.
ember.index(
"Road trip to Joshua Tree, windows down",
music={"artist": "Khruangbin", "genre": "psychedelic"},
season="summer",
)
# When the user mentions the same artist or genre,
# the musical dimension adds to the convergence scoreWeight: 0.05 (accent dimension).
Why Convergence Matters
The 7-dimensional design isn't about having more features. It's about modeling a fundamental property of human memory: single signals don't trigger recall.
The word “summer” alone doesn't make you remember anything. But “summer” + “BBQ smoke” + “evening” + “being a kid” hits different. That's temporal + sensory + temporal + era all converging.
Ember requires a minimum of 3 dimensions to fire. Below that threshold, the candidate is discarded — it's noise, not memory. Above it, the composite score determines intensity:
Custom Dimensions
The 7 built-in dimensions cover the most universal memory triggers. But Ember is extensible — you can register custom dimensions for domain-specific scoring:
from ember import register_dimension, register_extractor
@register_dimension("culinary")
def score_culinary(memory_data, message_data):
"""Score based on shared culinary references."""
mem_foods = set(memory_data.get("foods", []))
msg_foods = set(message_data.get("foods", []))
if not mem_foods or not msg_foods:
return 0.0
return len(mem_foods & msg_foods) / len(mem_foods | msg_foods)
@register_extractor("culinary")
def extract_culinary(text):
"""Extract food references from text."""
# Your extraction logic here
return {"foods": detected_foods}Custom dimensions participate in the same convergence pipeline. They get their own weight, their own extractor, and their own scoring logic. A culinary companion might add a “flavor” dimension. A fitness tracker might add “exertion.”
Robert Praul — github.com/ember-experiences