Adaptive Memory
An associative memory system using spreading activation. Memories are stored in SQLite with FTS5 full-text search, and retrieved using BM25 text matching combined with graph-based activation spreading through explicit relationships.
Core Concepts
Memories
Entries with id, datetime, text, and optional source. Stored in SQLite with FTS5 full-text indexing. IDs are sequential integers assigned on insertion.
Relationships
Symmetric connections between memories. Created only via explicit strengthen calls - no auto-generated relationships. Multiple strengthen events accumulate; effective strength is the sum of all events (with optional decay).
Relationships are stored canonically (from_mem < to_mem) as an event log. This allows strength to build up over time through repeated strengthening.
Spreading Activation
Search works by:
- FTS5 Search: Find memories matching query using BM25 ranking
- Seed Selection: Top BM25 results become seeds with energy 0.1-1.0 (proportional to relevance)
- Energy Propagation: Energy spreads through relationship graph
- Energy is distributed across neighbors (PageRank-style normalization)
- Each hop multiplies by
energy_decay(default 0.5) - Propagation stops when energy < 0.01 threshold
- Results: Memories sorted by ID (timeline order) with accumulated energy scores
Context Expansion
Instead of pre-computed temporal relationships, use --context N to fetch N memories before/after each result by ID. This is like grep -B/-A for temporal context.
Installation
From crates.io
From source
# Binary at: target/release/adaptive-memory
CLI Usage
adaptive-memory [OPTIONS] <COMMAND>
Commands:
init Initialize the database
add Add a new memory
amend Amend (update) an existing memory's text
search Search for memories
strengthen Strengthen relationships between memories
connect Connect memories (only if no existing relationship)
tail Show the latest N memories
list List memories by ID range
stats Show database statistics
stray Sample unconnected (stray) memories
Global Options:
--db <PATH> Database path (default: ~/.adaptive_memory.db)
Initialize Database
Add Memory
)
)
Examples:
# Simple memory
# With source
# Historical entry
Output:
Search Memories
)
)
)
)
Examples:
# Basic search
# With temporal context (like grep -B2 -A2)
# Limit results
# Deeper activation spread (reach more distant associations)
Output:
Results are sorted by memory ID (timeline order). The energy field indicates relevance:
- ~1.0 = direct BM25 match
- ~0.5 = one hop from a seed
- < 0.1 = reached via multi-hop spreading
Context items (from --context) have energy: 0.0 and is_context: true.
Strengthen Relationships
Create explicit associations between memories.
<IDS> Comma-separated )
Examples:
# Link two related memories (adds 1.0 strength)
# Link multiple (creates all pairs, 1.0 each)
# 4 IDs = 6 pairs, each gets 1.0 strength
Output:
Connect Memories
Like strengthen, but only creates relationships if none exist between the pair.
<IDS> Comma-separated )
Example:
# Connect memories only if not already related
Amend Memory
Update the text of an existing memory. Only allowed if the memory has no relationships to later memories (preserves integrity of memories that later entries depend on).
<ID> Memory
<TEXT> New
Example:
# Fix a typo in memory 42
List Memories
List memories by ID range.
)
)
Examples:
# List memories 10-20
# List last 50 memories
Tail
Show the latest N memories (shorthand for list --limit N).
)
Example:
# Show last 5 memories
Stats
Show database statistics including memory count, relationship count, and graph metrics.
Output:
Stray
Sample unconnected (stray) memories - useful for finding memories that could benefit from being linked to others.
)
Example:
# Find 5 unconnected memories to review
FTS5 Query Syntax
The search query uses SQLite FTS5 syntax, which supports powerful search operators:
| Syntax | Meaning | Example |
|---|---|---|
word |
Match word | meeting |
word1 word2 |
Match both (implicit AND) | project meeting |
word1 OR word2 |
Match either | cat OR dog |
"phrase" |
Exact phrase | "weekly standup" |
word* |
Prefix match | meet* matches meeting, meetings |
NOT word |
Exclude | meeting NOT standup |
NEAR(w1 w2, N) |
Words within N tokens | NEAR(rust memory, 5) |
^word |
Match at start of field | ^TODO |
Special characters: Characters like +, -, @ have special meaning in FTS5. To search for literal special characters, quote them: "2024-01-15" or "email@example.com".
Examples:
# All memories with "rust" AND "async"
# Either term
# Exact phrase
# Prefix matching
# Exclude term
# Words near each other
Library Usage
use ;
Configuration
Compile-time Constants (src/lib.rs)
| Constant | Default | Description |
|---|---|---|
ENERGY_THRESHOLD |
0.01 | Stop propagation below this energy |
MAX_SPREADING_ITERATIONS |
5000 | Safety limit on activation iterations |
MAX_STRENGTHEN_SET |
10 | Max memories per strengthen call |
DEFAULT_LIMIT |
100 | Default result limit |
Runtime Parameters (SearchParams)
| Parameter | Default | Description |
|---|---|---|
limit |
100 | Max results (also seed count for FTS) |
decay_factor |
0.0 | Relationship strength decay over memory distance |
energy_decay |
0.5 | Energy multiplier per hop (0.5 = halves each hop) |
context |
0 | Fetch N memories before/after each result |
Tuning energy_decay
Controls how far activation spreads through the graph:
| Value | Behavior | Max Depth |
|---|---|---|
| 0.3 | Shallow spread, stick close to seeds | ~4 hops |
| 0.5 | Balanced (default) | ~7 hops |
| 0.7 | Deep spread, reach distant associations | ~12 hops |
Energy at each hop (starting from seed with energy 1.0):
Hop: 0 1 2 3 4
0.5: 1.0 0.50 0.25 0.125 0.0625
0.7: 1.0 0.70 0.49 0.343 0.240
Tuning decay_factor
Controls how relationship strength fades over memory distance:
effective_strength = stored_strength × exp(-distance × decay_factor)
| Value | At 10 memories | At 50 memories | At 100 memories |
|---|---|---|---|
| 0.0 | 100% (no decay) | 100% | 100% |
| 0.01 | 90% | 61% | 37% |
| 0.03 | 74% | 22% | 5% |
| 0.05 | 61% | 8% | 0.7% |
Default is 0.0 (no decay). The ln_1p compression and PageRank-style normalization already prevent old relationships from dominating.
Database Schema
(
id INTEGER PRIMARY KEY,
datetime TEXT NOT NULL,
text TEXT NOT NULL,
source TEXT
);
CREATE VIRTUAL TABLE memories_fts USING fts5(text, content=memories, content_rowid=id);
(
id INTEGER PRIMARY KEY,
from_mem INTEGER NOT NULL,
to_mem INTEGER NOT NULL,
created_at_mem INTEGER NOT NULL,
strength REAL NOT NULL,
CHECK (from_mem < to_mem)
);
How It Works
Adding a Memory
- Insert into
memoriestable - FTS5 trigger auto-indexes the text
- No relationships created (use
strengthenor--contextfor associations)
Searching
- FTS5: BM25-ranked text matches become seeds
- Spreading Activation:
- Seeds get energy proportional to BM25 score
- Energy spreads through relationships (delta propagation)
- Neighbors' strengths are normalized (sum to 1.0) - energy is distributed, not amplified
- Raw strength is compressed via
ln(1+x)for diminishing returns
- Context Expansion: Optionally fetch surrounding memories by ID
- Results: Sorted by memory ID (timeline order)
Strengthening
- For each pair of IDs, add relationship event with strength 1.0
- Events accumulate - the pair's effective strength grows with repeated strengthening
- ln_1p compression means: 1st event → 0.69 effective, 10 events → 2.40, 100 events → 4.62
Tips
- Source field: Tag memories for filtering/identification (e.g., "slack", "journal", "calendar")
- Strengthen after retrieval: If a search surfaces related memories, strengthen them to reinforce the association
- Context for temporal: Use
--context Ninstead of pre-computed temporal links - Batch import: Use
-dto preserve original timestamps when importing historical data - Quote special chars: FTS5 special characters (
+,-,*, etc.) should be quoted for literal matching
License
MIT