Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hebbian Plasticity

m1nd’s graph learns. Every query changes the edge weights. Paths that lead to useful results get stronger. Paths that lead to noise get weaker. Over time, the graph evolves to match how your team thinks about your codebase. No other code intelligence tool does this.

The neuroscience principle

In 1949, Donald Hebb proposed a theory of synaptic learning: “When an axon of cell A is near enough to excite cell B and repeatedly or persistently takes part in firing it, some growth process or metabolic change takes place in one or both cells such that A’s efficiency, as one of the cells firing B, is increased.”

The popular summary: neurons that fire together wire together.

The converse is equally important: neurons that fire independently weaken their connections. This bidirectional learning – strengthening co-active pathways, weakening inactive ones – is the foundation of how biological neural networks adapt to experience.

m1nd applies this principle to code graphs. Nodes are modules. Edges are relationships. “Firing” means being activated by a spreading activation query. When an agent confirms that a result was useful, the edges connecting those activated nodes strengthen. When the agent marks results as wrong, those paths weaken. The graph remembers what worked.

The five-step learning cycle

Every query that passes through m1nd’s PlasticityEngine triggers a five-step update cycle. This runs automatically – no explicit training phase required.

Step 1: Hebbian strengthening

For every edge where both the source and target were activated in the query results, the weight increases:

delta_w = learning_rate * activation_source * activation_target
new_weight = min(current_weight + delta_w, weight_cap)

The default learning rate is 0.08 (from LearningRate::DEFAULT). The weight cap is 3.0 – no edge can grow stronger than 3x its original weight. This prevents runaway positive feedback.

From the source code:

#![allow(unused)]
fn main() {
// Hebbian: delta_w = lr * act_src * act_tgt
let delta = lr * src_act.get() * tgt_act;
let new_weight = (current + delta).min(cap);
}

The product activation_source * activation_target means that strongly co-activated pairs get the largest boost. A pair where both nodes scored 0.8 gets 0.08 * 0.8 * 0.8 = 0.051 added to their edge weight. A pair where one scored 0.2 gets only 0.08 * 0.8 * 0.2 = 0.013. This is faithful to Hebb’s rule: the strength of the update is proportional to the correlation of the firing.

Step 2: Synaptic decay

Edges whose source nodes were not activated in this query decay slightly:

new_weight = max(current_weight * (1 - decay_rate), weight_floor)

The default decay rate is 0.005 per query (0.5%). The weight floor is 0.05 – edges never decay below 5% of their original weight. This ensures that even unused paths retain some connectivity, preventing the graph from fragmenting.

#![allow(unused)]
fn main() {
let decay_factor = 1.0 - self.config.decay_rate.get(); // 0.995
let new_weight = (current * decay_factor).max(floor);
}

The asymmetry is intentional. Strengthening applies a fixed delta (additive). Decay applies a multiplicative factor. This means strong edges resist decay (a weight-3.0 edge loses 0.015 per query) while weak edges decay faster in relative terms (a weight-0.1 edge loses 0.0005 per query). Frequently activated paths grow monotonically. Rarely activated paths slowly fade but never disappear.

Step 3: Long-Term Potentiation / Long-Term Depression

After an edge has been strengthened 5 consecutive times (the LTP threshold), it receives a one-time +0.15 bonus:

#![allow(unused)]
fn main() {
if !graph.edge_plasticity.ltp_applied[j]
    && graph.edge_plasticity.strengthen_count[j] >= self.config.ltp_threshold
{
    let new_weight = (current + self.config.ltp_bonus.get()).min(cap);
    graph.edge_plasticity.ltp_applied[j] = true;
}
}

Conversely, after an edge has been weakened 5 consecutive times (the LTD threshold), it receives a one-time -0.15 penalty:

#![allow(unused)]
fn main() {
if !graph.edge_plasticity.ltd_applied[j]
    && graph.edge_plasticity.weaken_count[j] >= self.config.ltd_threshold
{
    let new_weight = (current - self.config.ltd_penalty.get()).max(floor);
    graph.edge_plasticity.ltd_applied[j] = true;
}
}

These thresholds model biological LTP/LTD – the transition from short-term to long-term memory. Five consecutive activations is a signal of sustained relevance, not a fluke. The one-time bonus/penalty is permanent: it does not reset, and it does not apply again for the same edge. This prevents unbounded weight inflation from repeated queries.

Step 4: Homeostatic normalization

After strengthening and LTP/LTD, the total incoming weight for each node is checked against a ceiling of 5.0:

#![allow(unused)]
fn main() {
if total_incoming > ceiling {
    let scale = ceiling / total_incoming;
    for each incoming edge:
        new_weight = current * scale;
}
}

This is homeostatic plasticity – a biological mechanism that prevents individual neurons from becoming over-stimulated. In m1nd, it prevents hub nodes (like config.py or main.py) from accumulating so much incoming weight that they dominate every activation query regardless of the actual query content.

The normalization is proportional: all incoming edges are scaled by the same factor. This preserves relative strengths while enforcing an absolute ceiling. A node with 10 incoming edges at weight 1.0 each (total 10.0) would have all edges scaled to 0.5, bringing the total to 5.0.

Step 5: Query memory recording

The query, its seeds, and its activated nodes are recorded in a bounded ring buffer (capacity: 1000 queries). This memory serves two purposes:

  1. Priming signal: future queries that share seeds with past queries get a boost from nodes that frequently appeared in those past results. This implements a form of associative memory – “things I looked at near authentication tend to be relevant when I look at authentication again.”

  2. Seed bigrams: pairs of seeds that co-occur across multiple queries are tracked. This supports the m1nd.warmup tool, which uses query memory to pre-activate frequently queried paths.

How m1nd.learn works

The automatic plasticity cycle runs on every m1nd.activate call. But agents can also provide explicit feedback via m1nd.learn:

Positive feedback: m1nd.learn(feedback="correct", node_ids=[...])

When an agent confirms that specific nodes were useful:

  1. The edges connecting those nodes are strengthened with an amplified Hebbian update (the activation values are set to 1.0 for the confirmed nodes, producing maximum delta_w).
  2. The strengthen counters increment, moving edges closer to the LTP threshold.
  3. Query memory records the confirmed nodes with high weight, boosting them in future priming signals.

Negative feedback: m1nd.learn(feedback="wrong", node_ids=[...])

When an agent marks results as irrelevant:

  1. The edges connecting those nodes receive decay as if they were inactive, even though they were activated.
  2. The weaken counters increment, moving edges closer to the LTD threshold.
  3. Query memory records the rejection, reducing the priming signal for those nodes.

This feedback loop is what makes m1nd adaptive. A team that mostly works on the payment system will gradually strengthen all paths around payment-related modules. An agent investigating authentication will produce different results than an agent investigating billing – even on the same codebase – because their feedback histories have shaped different edge weight landscapes.

Plasticity state persistence

The learned weights are valuable. Losing them means losing the graph’s adaptation to your workflow. m1nd persists plasticity state in two ways:

Per-edge state (SynapticState)

Each edge’s plasticity state is captured as a serializable record:

#![allow(unused)]
fn main() {
pub struct SynapticState {
    pub source_label: String,
    pub target_label: String,
    pub relation: String,
    pub original_weight: f32,
    pub current_weight: f32,
    pub strengthen_count: u16,
    pub weaken_count: u16,
    pub ltp_applied: bool,
    pub ltd_applied: bool,
}
}

This is exported via PlasticityEngine::export_state() and persisted to M1ND_PLASTICITY_STATE (a JSON file). The export includes a NaN firewall (FM-PL-001): any non-finite weight falls back to the original weight. The write is atomic (temp file + rename, FM-PL-008) to prevent corruption on crash.

Importing state

When m1nd restarts, import_state restores learned weights. Edge identity matching uses (source_label, target_label, relation) triples – not numeric indices – because re-ingesting the codebase may produce different node numbering. This means plasticity survives codebase re-ingestion: if auth.py -> session.py was strengthened, that strengthening persists even if auth.py gets a different NodeId after re-ingest.

Weights are clamped to [weight_floor, weight_cap] on import. Invalid JSON triggers a schema validation error (FM-PL-007) rather than corrupting the graph.

Persistence frequency

The graph auto-persists every 50 queries and on server shutdown. This is a balance between durability (don’t lose too much learning) and disk I/O (don’t write on every query).

The drift tool

After persistence, the natural question is: how much has the graph changed? The m1nd.drift tool answers this.

m1nd.drift compares the current edge weights against their original (ingest-time) baselines and reports:

  • Total edges changed: how many edges have weights different from their original values.
  • Average weight change: the mean absolute delta across all modified edges.
  • Top strengthened edges: the edges that have grown the most relative to their baseline.
  • Top weakened edges: the edges that have decayed the most.
  • LTP/LTD counts: how many edges have crossed the long-term potentiation or depression thresholds.

This is designed for session recovery. When an agent starts a new session, m1nd.drift tells it what has changed since the graph was last loaded. The agent can see that “paths around the payment module strengthened significantly since yesterday” and adjust its investigation accordingly.

Session 1:
  ingest -> activate("auth") -> agent uses results -> learn(correct)
  → 740 edges strengthened, 12,340 edges decayed slightly

Session 2:
  drift(since=session_1) -> auth paths now 15% stronger on average
  activate("auth") -> better results, faster convergence to useful nodes

Session N:
  the graph has adapted to how your team thinks about auth

How this makes the graph adapt

The combination of automatic Hebbian updates, explicit feedback, LTP/LTD thresholds, and homeostatic normalization creates a self-tuning system:

  1. Short-term adaptation (within a session): edges on frequently queried paths strengthen immediately. The next query about the same topic converges faster.

  2. Long-term memory (across sessions): edges that cross the LTP threshold receive a permanent bonus. Persistent investigation patterns are encoded in the graph structure.

  3. Forgetting (controlled decay): paths that are never queried slowly fade toward the weight floor. This prevents the graph from becoming saturated with historical patterns that no longer reflect the codebase’s structure.

  4. Stability (homeostatic normalization): no node can accumulate unbounded incoming weight. Hub nodes stay important but do not become black holes that absorb all activation energy.

The result is a graph that starts generic (all edges at their ingest-time weights, reflecting code structure) and gradually becomes specific (edges weighted by how you use the codebase). Two teams working on the same repository will develop different plasticity landscapes. This is a feature: the graph models the team’s mental model, not just the code’s static structure.

Constants reference

ParameterDefaultPurpose
DEFAULT_LEARNING_RATE0.08Hebbian delta_w scaling
DEFAULT_DECAY_RATE0.005Per-query inactive edge decay
LTP_THRESHOLD5Consecutive strengthens for long-term bonus
LTD_THRESHOLD5Consecutive weakens for long-term penalty
LTP_BONUS0.15One-time weight bonus at LTP threshold
LTD_PENALTY0.15One-time weight penalty at LTD threshold
HOMEOSTATIC_CEILING5.0Max total incoming weight per node
WEIGHT_FLOOR0.05Minimum edge weight (never decays below)
WEIGHT_CAP3.0Maximum edge weight (never strengthens above)
DEFAULT_MEMORY_CAPACITY1000Ring buffer size for query memory
CAS_RETRY_LIMIT64Atomic weight update retries