Files
el/engram
will.anderson 005e84e5d3 self-review 2026-08-02: bound the WM breakthrough storm; stop punishing semantic relevance for recency
Working memory was thrashing behind a healthy-looking gauge. wm_active sat
at 22-24 while breakthroughs ran 661-903 and evictions 485-717 PER 60s tick
- roughly 825-1125 nodes cycling in 5-call lockstep.

Root cause: the breakthrough path was an anti-starvation mechanism that reset
its own counter on firing, with no budget and no refractory. A node failing
its type threshold 5 times was force-promoted at exactly 0.10 and had its
suppression_count reset to 0, so it immediately restarted the identical
climb. Since BREAKTHROUGH_WEIGHT (0.10) > WM_FLOOR (0.05), every one of them
cleared the admission floor and entered the rank contest tied at 0.10, where
the tie-break degenerated to node-array index order. Cap-evicted nodes are
skipped by retrieval reinforcement, so they never got an access_ts record and
the STI inhibition-of-return damper never applied to them. That closed the
loop: re-suppressed, completely unmarked, forever.

An anti-starvation rule that resets its own counter without a bound is not a
fairness valve, it is an oscillator.

Fixes in engram_activate Pass 2:
- ENGRAM_BREAKTHROUGH_BUDGET (WM_CAP/4 = 6) caps intrusive thoughts per call.
- ENGRAM_BREAKTHROUGH_COOLDOWN (55) via NEGATIVE suppression_count. The field
  already serializes as %d and parses through eg_get_int_field, so negatives
  round-trip through snapshots with no struct or format change.
- Blocked breakthroughs no longer reset the counter; it saturates so a starved
  node surfaces on a later call instead of restarting from zero.
- Graded breakthrough weight by nearness to own threshold, so the rank
  tie-break is cognitive rather than insertion order. Invariant preserved:
  WM_FLOOR < weight < min(type_threshold).

Also: moved the additive cosine term AFTER the STI multiplier. It was applied
before, so an incumbent re-reached 30s later took t_n/(t_n+120) = 0.2x, which
cut the semantic term's ceiling from 0.20 to 0.04 - below every per-type
threshold. Meaning-match was being punished for having been recently useful.
Inhibition-of-return should rotate the structural score, not the semantic one.

Also: _eg_act_wm_evicted counted 3 of 5 eviction paths. The two carry-over
paths were silent, so the reported rate was an undercount of unknown
magnitude - while being used to diagnose an eviction pathology. All five now
increment.

Also: route_sync returned {"nodes":[],"edges":[]} when the snapshot export
failed. The soul's sync_ok check only tests for "" and "{}", so that
placeholder passed as a healthy sync: last_sync_ok_ts stamped, sync_age_ms
green, sync_empty never fired, added:0 forever. A broken sync was
indistinguishable from a quiet healthy one - the exact class this route was
added to fix. Returns a real error now.

Verified live (boot 20 vs boot 19): breakthroughs 661-903 -> 36/tick,
evictions 485-717 -> 12-46/tick against a counter that now covers more paths,
wm_active unchanged at 22-24, wm_avg_weight 0.138-0.273 -> 0.186-0.446.
Working memory is holding strong nodes instead of breakthrough-floor filler.
2026-08-02 08:48:59 -05:00
..
2026-04-30 13:49:28 -05:00

Engram

A local-first memory substrate for accumulating intelligence.

An engram is the physical trace of a memory in the brain — the actual encoded substrate, not an abstraction above it. That's what this is.


Why existing databases are wrong for this use case

Relational databases store rows and retrieve them by predicate. Key-value stores retrieve by exact key. Vector databases retrieve by geometric proximity. All of them share the same fundamental model: you store data in, you query it out. Storage and retrieval are separate systems.

The brain doesn't work this way.

When you remember something, you don't query your hippocampus. You activate a memory trace and the pattern propagates. Long-term potentiation — the strengthening of synaptic connections through co-activation — is simultaneously the storage mechanism and the retrieval mechanism. The structure that holds the memory is the same structure that surfaces it.

No existing database models this. Engram does.


The Spreading Activation Model

Engram retrieval works through spreading activation:

  1. Seeds — you name one or more nodes you know are relevant (e.g. the current task, recent context, a concept you're reasoning about)

  2. Query embedding — you provide a semantic vector representing the direction of your current thought

  3. Propagation — activation flows outward from seeds through weighted edges. At each hop, strength attenuates multiplicatively:

    strength = parent_strength × edge_weight × target_salience × cosine_sim(query, target)
    
  4. Pruning — paths weaker than a threshold are cut (the attention filter)

  5. Return — the top-N nodes by activation strength

This is not a query. It is a pattern completion. The system surfaces what is most associatively relevant to the current context, weighted by how strongly those things have been reinforced over time.


The Four Memory Tiers

Tier Analogy Contents
Working Prefrontal working memory K most recently activated nodes — hot, fast
Episodic Hippocampus Time-ordered events and experiences
Semantic Neocortex Concept graph — long-term structural knowledge
Procedural Cerebellum / basal ganglia Patterns, workflows, habits

Nodes migrate between tiers based on salience decay and reinforcement. A frequently activated semantic node stays semantic. A rarely-touched episodic memory decays toward procedural background.


Salience — Forgetting as Adaptation

Salience is not stored permanently. It decays:

fn compute_salience(importance: f32, last_activated_ms: i64, activation_count: u64) -> f32 {
    let days_since = (now_ms() - last_activated_ms) as f32 / 86_400_000.0;
    importance * (1.0 / (1.0 + days_since)) * (activation_count as f32 + 1.0).ln()
}

Three signals:

  • Importance (0.01.0): set at creation, stable
  • Recency: decays toward zero as days pass without activation
  • Frequency: log-compressed count of activations

Forgetting in Engram is not a bug. It is adaptive pruning. Memories that are never activated again become less likely to surface during retrieval. They are not deleted — they remain in storage — but they stop competing for attention. This is exactly how biological memory works, and why it is adaptive rather than pathological.


Quick Start

use engram_core::{EngramDb, Node, Edge, NodeType, MemoryTier, RelationType};
use std::path::Path;

// Open or create a database
let db = EngramDb::open(Path::new("/var/lib/my-agent/memory"))?;

// Create a node with a semantic embedding
let node = Node::new(
    NodeType::Concept,
    vec![0.9, 0.1, 0.3, 0.7, 0.8, 0.2],   // embedding from your LLM
    b"Spreading activation surfaces relevant memories by pattern completion".to_vec(),
    MemoryTier::Semantic,
    0.9,   // importance
);
let id = db.put_node(node)?;

// Link it to related concepts
let related = db.put_node(Node::new(
    NodeType::Concept,
    vec![0.8, 0.2, 0.4, 0.6, 0.7, 0.3],
    b"Long-term potentiation: co-activation strengthens synaptic weight".to_vec(),
    MemoryTier::Semantic,
    0.85,
))?;
db.put_edge(Edge::new(id, related, RelationType::Causes, 0.9))?;

// Retrieve by spreading activation
let results = db.activate(
    &[id],                                     // seeds
    &[0.85, 0.15, 0.35, 0.65, 0.75, 0.25],   // query embedding
    3,                                         // max hops
    10,                                        // top-N results
)?;

for r in results {
    println!(
        "strength={:.4} hops={}{}",
        r.activation_strength,
        r.hops,
        String::from_utf8_lossy(&r.node.content)
    );
}

Project Structure

engram/
  crates/
    engram-core/        # The memory engine — storage, graph, activation, salience
    engram-ffi/         # C FFI stubs for cross-language bindings
  bindings/
    kotlin/             # Android / JVM binding notes
    typescript/         # WASM / Node binding notes
    go/                 # CGo binding notes
  examples/
    basic.rs            # Full walkthrough: insert, activate, search, decay

Public API

impl EngramDb {
    fn open(path: &Path) -> EngramResult<Self>;
    fn put_node(&self, node: Node) -> EngramResult<Uuid>;
    fn get_node(&self, id: Uuid) -> EngramResult<Option<Node>>;
    fn put_edge(&self, edge: Edge) -> EngramResult<()>;
    fn get_edges_from(&self, from_id: Uuid) -> EngramResult<Vec<Edge>>;
    fn get_edges_to(&self, to_id: Uuid) -> EngramResult<Vec<Edge>>;
    fn search_embedding(&self, embedding: &[f32], limit: usize) -> EngramResult<Vec<ScoredNode>>;
    fn activate(&self, seeds: &[Uuid], query_embedding: &[f32], max_depth: u8, limit: usize) -> EngramResult<Vec<ActivatedNode>>;
    fn traverse(&self, from: Uuid, relation: Option<RelationType>, max_depth: u8) -> EngramResult<Vec<Node>>;
    fn touch(&self, id: Uuid) -> EngramResult<()>;
    fn decay(&self, factor: f32) -> EngramResult<usize>;
    fn node_count(&self) -> EngramResult<usize>;
    fn edge_count(&self) -> EngramResult<usize>;
}

Dependencies

  • sled — embedded persistent B-tree (no daemon, no network, local-first)
  • bincode — compact binary serialization
  • uuid — stable node identity
  • serde — derive support
  • thiserror / anyhow — error handling

Design Decisions

Why sled? Local-first. No daemon. Transactional. Fast enough for the node counts Engram targets (< 1M nodes). When the right HNSW index is needed, it will layer on top of sled, not replace it.

Why flat cosine scan? Correct and simple. The graph structure itself is the primary retrieval mechanism. Vector search is a secondary signal. HNSW adds complexity and a compile dependency that isn't justified until retrieval quality at scale demands it.

Why multiplicative activation? Because memory is conjunctive. A path requires all of its links to be strong to carry signal. Addition would allow many weak associations to accumulate into false relevance. Multiplication enforces that every factor matters.

Why salience decay? Because not everything that was once important remains important. Adaptive forgetting is not failure — it is the mechanism that keeps attention on what's current. A memory system that never forgets is one that can never focus.