Back to Blog
AI

How Shodh Memory Works: Hebbian Learning & Cognitive Architecture

December 12, 202510 min readBy Shodh Team · AI Research
Hebbian-learningcognitive-sciencearchitecturememory-systemsAI

Shodh Memory isn't just a key-value store with vector search. It's built on cognitive science principles—the same mechanisms that make human memory work. This post explains the architecture.

The Problem with Naive Memory

Most AI memory systems are glorified databases: store embeddings, run similarity search, return top-k results. This misses fundamental properties of biological memory:

  • Memories strengthen with use and weaken without it
  • Related memories activate each other (spreading activation)
  • Raw experiences consolidate into abstract knowledge over time
  • Not all information is equally important

Hebbian Learning: "Fire Together, Wire Together"

Donald Hebb's 1949 theory states that when neurons repeatedly fire together, the connection between them strengthens. In Shodh Memory, when two memories are retrieved in the same context, we strengthen their association:

hebbian.rsrust
/// When memories A and B are retrieved together, strengthen their edge
pub fn strengthen_association(&mut self, memory_a: &str, memory_b: &str) {
    let edge = self.get_or_create_edge(memory_a, memory_b);

    // Hebbian strengthening with diminishing returns
    let delta = LEARNING_RATE * (1.0 - edge.strength);
    edge.strength = (edge.strength + delta).min(1.0);

    edge.activation_count += 1;
    edge.last_activated = Utc::now();

    // Check for Long-Term Potentiation
    if edge.activation_count >= LTP_THRESHOLD && !edge.potentiated {
        edge.potentiated = true;  // Becomes permanent
    }
}

The key insight: frequently co-accessed memories form strong associations. When you retrieve "authentication" and "JWT tokens" together multiple times, future queries for "authentication" will naturally surface JWT-related memories.

Long-Term Potentiation (LTP)

In neuroscience, LTP is the process where synapses become permanently strengthened after sufficient repetition. We implement this:

ltp.rsrust
const LTP_THRESHOLD: u32 = 5;  // Activations needed for potentiation
const LTP_DECAY_FACTOR: f32 = 0.1;  // Potentiated edges decay 10x slower

impl Edge {
    pub fn decay(&mut self) -> bool {
        let half_life = if self.potentiated {
            BASE_HALF_LIFE * 10.0  // Much slower decay
        } else {
            BASE_HALF_LIFE * (1.0 + self.strength as f64)
        };

        let elapsed_days = self.days_since_access();
        self.strength *= (-0.693 / half_life * elapsed_days).exp() as f32;

        self.strength < MIN_STRENGTH  // Return true if should prune
    }
}

After 5 co-activations, an association becomes "potentiated"—it decays 10x slower. This means core knowledge persists while ephemeral associations fade.

Spreading Activation

When you recall a memory, activation spreads to connected memories. This models how human memory retrieval works—thinking of "Paris" activates "France", "Eiffel Tower", "croissants":

spreading_activation.rsrust
pub fn spreading_activation(
    &self,
    seed_memories: &[MemoryId],
    depth: usize,
    decay_factor: f32,
) -> HashMap<MemoryId, f32> {
    let mut activations: HashMap<MemoryId, f32> = HashMap::new();
    let mut frontier: VecDeque<(MemoryId, f32, usize)> = VecDeque::new();

    // Initialize with seed memories at full activation
    for id in seed_memories {
        activations.insert(id.clone(), 1.0);
        frontier.push_back((id.clone(), 1.0, 0));
    }

    while let Some((current, activation, current_depth)) = frontier.pop_front() {
        if current_depth >= depth {
            continue;
        }

        // Spread to neighbors
        for (neighbor, edge) in self.get_neighbors(&current) {
            let spread_activation = activation * edge.strength * decay_factor;

            if spread_activation > MIN_ACTIVATION {
                let existing = activations.get(&neighbor).unwrap_or(&0.0);
                if spread_activation > *existing {
                    activations.insert(neighbor.clone(), spread_activation);
                    frontier.push_back((neighbor, spread_activation, current_depth + 1));
                }
            }
        }
    }

    activations
}

Memory Consolidation

Raw episodic memories ("API timed out at 3pm") aren't useful long-term. Consolidation extracts semantic facts ("API has 30-second timeout"):

consolidation.rsrust
pub struct ConsolidationEngine {
    /// Minimum times a pattern must appear to become a fact
    min_support: usize,
    /// Minimum age before consolidation (days)
    min_age_days: u32,
}

impl ConsolidationEngine {
    pub fn consolidate(&self, episodic_memories: &[Memory]) -> Vec<SemanticFact> {
        // Cluster similar memories by embedding similarity
        let clusters = self.cluster_by_embedding(episodic_memories);

        clusters.into_iter()
            .filter(|c| c.len() >= self.min_support)
            .map(|cluster| {
                SemanticFact {
                    content: self.extract_common_pattern(&cluster),
                    confidence: cluster.len() as f32 / episodic_memories.len() as f32,
                    source_count: cluster.len(),
                    fact_type: self.classify_fact_type(&cluster),
                }
            })
            .collect()
    }
}

Three-Tier Architecture

Shodh Memory organizes storage in three tiers:

TierPurposeLifetime
Working MemoryCurrent context, active associationsSession
Episodic MemorySpecific events and experiencesDays to weeks
Semantic MemoryConsolidated facts and knowledgePermanent (potentiated)

Salience Scoring

Not all memories are equally important. Salience combines multiple signals:

salience.rsrust
pub fn compute_salience(memory: &Memory, query_embedding: &[f32]) -> f32 {
    let recency = recency_score(memory.last_accessed);      // 30%
    let frequency = frequency_score(memory.access_count);    // 25%
    let importance = importance_score(memory.memory_type);   // 25%
    let similarity = cosine_similarity(&memory.embedding, query_embedding); // 20%

    0.30 * recency +
    0.25 * frequency +
    0.25 * importance +
    0.20 * similarity
}

fn importance_score(memory_type: MemoryType) -> f32 {
    match memory_type {
        MemoryType::Decision => 1.3,
        MemoryType::Error => 1.25,
        MemoryType::Learning => 1.25,
        MemoryType::Pattern => 1.2,
        MemoryType::Task => 1.15,
        MemoryType::Context => 1.1,
        _ => 1.0,
    }
}

The Result

These mechanisms combine to create memory that:

  • Gets smarter with use — Frequently accessed knowledge surfaces faster
  • Forgets appropriately — Unused information fades, reducing noise
  • Connects ideas — Related concepts strengthen each other
  • Prioritizes importance — Decisions and errors rank higher than casual observations

The implementation is ~6MB of Rust, runs entirely locally, and achieves sub-10ms retrieval on tens of thousands of memories. It's cognitive science, not magic.

Blog | Shodh | Shodh RAG