-
-
Notifications
You must be signed in to change notification settings - Fork 9
Memory Hebbian
Biological Analog: The brain doesn't retrieve memories by content similarity alone. It uses associative networks (neurons that fire together wire together), temporal sequences (what happened next?), n-body event groupings (multi-entity episodes), and synaptic tag cross-capture (conceptually linked contexts). Spector Memory implements all four as graph structures that augment vector recall, supported by a central EntityDirectory.
graph TB
subgraph "RecallPipeline"
RP["Vector Search → 6-Phase Scoring → Top-K Seed Set"]
end
RP --> S5c["Step 5c: Hebbian<br/>Spreading Activation"]
RP --> S5d["Step 5d: Temporal<br/>Chain Extension"]
RP --> S5e["Step 5e: Entity<br/>Graph Traversal"]
RP --> S5f["Step 5f: Cross-Capture<br/>Tag Traversal"]
S5c --> M["Merge & Dedup → Re-sort → Final Top-K"]
S5d --> M
S5e --> M
S5f --> M
subgraph "Layer 1 — Hebbian Association"
HG["HebbianGraphMemory<br/>CSR co-activation edges"]
CAT["CoActivationRecordMemory<br/>Tag-level STDP learning"]
end
subgraph "Layer 2 — Temporal Causal"
TC["TemporalChainMemory<br/>Session-linked sequences"]
end
subgraph "Layer 3 — Hyperedge Event-Episode"
HEG["HyperEntityGraphMemory<br/>n-body entity groupings"]
ED["EntityDirectory<br/>Identity companion registry"]
end
subgraph "Layer 4 — Cross-Capture Graph"
CCG["CoActivationRecordMemory<br/>Tag co-occurrence traversal<br/>+ inverted index"]
end
S5c --> HG
S5c --> CAT
S5d --> TC
S5e --> ED
S5e --> HEG
S5f --> CCG
style RP fill:#4a90d9,color:white
style M fill:#00b894,color:white
style HG fill:#e74c3c,color:white
style TC fill:#f39c12,color:white
style HEG fill:#e91e63,color:white
style CCG fill:#00b4d8,color:white
tip: Graceful Degradation Each graph step is additive — it can only ADD candidates to the result set, never remove. If a graph is null, empty, or throws an exception, the step is a no-op. Zero risk of regression.
"Neurons that fire together, wire together." — Donald Hebb, 1949
The Hebbian graph stores memory-to-memory edges with association weights. When two memories are co-ingested within the same session, their edge is strengthened. During recall, the graph discovers associated memories that pure vector similarity might miss.
graph LR
A["Memory #42<br/>'database error'"] ---|"weight: 0.83<br/>co-ingested 5×"| B["Memory #87<br/>'connection pool'"]
A ---|"weight: 0.47<br/>co-ingested 2×"| C["Memory #103<br/>'retry strategy'"]
B ---|"weight: 0.63<br/>co-ingested 3×"| C
style A fill:#e74c3c,color:white
style B fill:#3498db,color:white
style C fill:#2ecc71,color:white
| Property | Value |
|---|---|
| Max degree | 24 neighbors per memory (configurable) |
| Edge format | 12B — 4B neighbor + 4B weight + 2B lastCycle + 1B bridgeScore + 1B flags |
| Storage layout | CSR (Compressed Sparse Row) — stores only actual edges, ~90% memory reduction vs. fixed-width |
| Eviction | Multi-signal importance scoring (weight, recency, bridge centrality, redundancy, arousal, Zeigarnik) |
| Decay | 0.9× multiplicative factor per consolidation cycle; bridge-protected edges floored instead of evicted |
| Spreading activation | BFS with depth=2, attenuated by edge weight |
| Persistence | Binary CSR file (V3 format, "HCSR" magic) with offset + edge segments |
- Ingestion: When memories are co-ingested within the same session, the bidirectional edge between them is strengthened
- Recall: After the 6-phase scorer produces a seed set, the graph discovers associated memories via 2-hop BFS. These are added to the result set with 0.3× score attenuation
Beyond memory-to-memory edges, the CoActivationTracker tracks tag co-occurrence patterns:
- Undirected co-activation counts: How often two tags appear together in ingested memories
- Directed STDP edges: Spike-Timing Dependent Plasticity — if tag A is consistently recalled before tag B, the directed edge A→B is strengthened, creating predictive associations
info: STDP — Spike-Timing Dependent Plasticity This creates predictive associations: "when I think of A, I should also think of B." The listener runs after each recall on a Virtual Thread, updating STDP weights with zero impact on recall latency.
Synaptic tags that co-occur share Plasticity-Related Proteins, creating associative bridges between conceptually related memory traces. — Sajikumar & Frey, 2004
In neuroscience, Synaptic Tagging and Capture (STC) theory shows that synaptic tags form associative networks via three mechanisms:
- Cross-Tagging: Tagged synapses on the same neuron share PRPs (Plasticity-Related Proteins) across pathways
- Memory Co-allocation: Temporally proximate events with shared tags get co-allocated to overlapping neuronal populations
- Dendritic Clustering: Co-tagged synapses spatially cluster on dendrites for efficient PRP sharing
The Cross-Capture Graph reuses the existing CoActivationRecordMemory tag co-occurrence data for graph traversal during recall. It adds a tag → memory inverted index that maps each synaptic tag to the set of memories carrying that tag.
Query: "How do we handle database connection pooling?"
Query tags: {database, connection-pool}
Cross-Capture Graph traversal:
database → [timeout (0.87), retry (0.72), postgres (0.68)]
connection-pool → [hikari (0.91), database (0.87), config (0.74)]
Discovered memories via related tags:
→ "HikariCP configuration best practices" (via hikari tag)
→ "retry backoff strategy for DB connections" (via retry tag)
→ "PostgreSQL timeout settings" (via postgres + timeout tags)
| Property | Value |
|---|---|
| Data source |
OffHeapPairTable (existing co-occurrence counts) |
| Inverted index |
ConcurrentHashMap<Long, CopyOnWriteArrayList<Integer>> (tag hash → memory slots) |
| Fan-factor attenuation |
1/√(degree) — ACT-R spreading activation dilution |
| Attenuation factor | 0.25× (configurable via spector.memory.cross-capture.attenuation) |
| Max tag neighbors | 5 (configurable via spector.memory.cross-capture.max-tag-neighbors) |
| Max memories per tag | 10 (configurable via spector.memory.cross-capture.max-memories-per-tag) |
| Index lifecycle | Rebuilt on startup from memory headers; updated during ingestion |
| Kernel shape |
MemoryShape.HASHTABLE (ordinal 8) — honest shape per ADR-0009 |
- Ingestion: Each extracted synaptic tag is indexed in the inverted index
- Recall: Step 5f traverses the co-occurrence graph to find related tags, then looks up memories carrying those tags. Added to the result set with configurable attenuation
- Graceful degradation: If the inverted index is empty or the cross-capture step throws, recall proceeds unchanged
| Signal | Hebbian | Temporal | Entity | Cross-Capture |
|---|---|---|---|---|
| Different vocabulary, same concepts | ❌ | ❌ | ❌ | ✅ |
| Never co-ingested | ❌ | ❌ | Maybe | ✅ |
| Different sessions | ✅ | ❌ | ✅ | ✅ |
| No explicit entities | ❌ | ❌ | ❌ | ✅ |
"What was the budget of the project managed by the person who met with me yesterday?"
The Entity Graph stores typed entities and typed relations extracted from ingested text. This enables multi-hop knowledge traversal that pure vector similarity cannot achieve.
Entities are extracted at ingestion time via the EntityExtractor SPI:
| Mode | Description |
|---|---|
NONE (default) |
No extraction — entity graph features disabled |
LLM |
Uses an LLM with a structured prompt to identify entities and relations |
CUSTOM |
Any user-provided EntityExtractor implementation |
Enable LLM entity extraction:
SpectorMemory.builder()
.entityExtractionMode(EntityExtractionMode.LLM)
.textGenerationProvider(provider)
.build();Spector uses an open-schema type registry — unlike traditional NER systems with fixed type sets, the entity graph accepts any type string the LLM identifies. Well-known types are pre-seeded for backward compatibility, but novel types (e.g., VEHICLE, REGULATION, RECIPE) are automatically registered on first use.
21 well-known entity types (pre-seeded):
| Category | Types |
|---|---|
| People & Org |
PERSON, ORGANIZATION, TEAM, ROLE
|
| Projects |
PROJECT, PRODUCT, TASK
|
| Knowledge |
CONCEPT, TOPIC, SKILL, DECISION
|
| Technology |
TECHNOLOGY, TOOL, API, ARTIFACT
|
| World |
EVENT, LOCATION, DATE_TIME
|
| Process & Data |
PROCESS, METRIC, DOCUMENT
|
| Catch-all | OTHER |
21 well-known relation types (pre-seeded):
| Category | Types |
|---|---|
| People |
MANAGES, REPORTS_TO, KNOWS, ASSIGNED_TO, AUTHORED
|
| Work |
WORKS_ON, CREATED_BY, OWNS, IMPLEMENTS
|
| Structure |
PART_OF, CONTAINS, DEPENDS_ON, USES
|
| Causality |
CAUSES, BLOCKS, SUPERSEDES, PRECEDES, FOLLOWS
|
| Location | LOCATED_AT |
| General |
RELATED_TO, OTHER
|
tip: Dynamic Types If the LLM identifies an entity as
SOFTWAREor a relation asDEPLOYED_ON, these are automatically registered in the type registry and stored as first-class types. No code changes or schema migrations required.
- Ingestion: The LLM extracts entities from text → entities are added to the graph → entities are linked to their source memory (with weighted adjacency) → relations are added between entities
- Recall: Entities are extracted from the query → matched in the graph by name → 2-hop BFS traversal → memory references collected → added to result set with 0.25× attenuation per hop × fan factor (1/√refCount, modeling ACT-R spreading activation dilution)
- Consolidation: Entity–entity edges decay over reflection cycles. Entity→memory adjacency weights decay via LTD (Long-Term Depression, 0.95× per cycle, pruned below 0.2). Similar entity names are merged via Levenshtein distance. Fragmented adjacency blocks are compacted.
- Reinforcement (LTP): When a memory re-mentions an already-linked entity, the adjacency weight is reinforced by +0.2 (Long-Term Potentiation) instead of creating a duplicate link.
The entity graph supports typed BFS traversal with optional relation filtering:
| Method | Description |
|---|---|
traverse(startEntity, filter, maxHops) |
BFS with optional relation type filter |
collectMemories(startEntity, filter, maxHops) |
Collect all memory indices reachable within N hops |
findEntity(name) |
Case-insensitive entity lookup |
memoriesForEntity(entityId) |
All memory indices linked to an entity (unlimited) |
fanFactor(entityId) |
Returns 1/√(refCount) for spreading activation dilution |
memoryRefWeight(entityId, adjIdx) |
Read individual adjacency link weight |
decayAdjacencyWeights(factor, threshold) |
LTD decay: multiply all weights, prune below threshold |
compactAdjacency() |
Defragment adjacency segment, reclaim dead blocks |
Entity nodes use a fixed 64-byte cache-line-aligned layout with a separate adjacency segment for entity→memory links:
Entity Node (64B, 8-byte aligned — V2):
[type:4B][pad:4B][nameHash:8B]
[adjOffset:4B][adjCount:4B][adjCapacity:4B][pad:4B] ← pointer into adjacency segment
[pad:4B][degree:4B][edgeStart:4B][pad:20B]
Entity Edge (16B — V2):
[targetId:4B][relationType:4B][weight:4B]
[lastCycle:2B][bridgeScore:1B][flags:1B]
Adjacency Entry (8B):
[memIdx:4B][weight:4B] ← weighted link to a memory slot
This design allows unlimited entity→memory associations (no fixed cap), with amortized O(1) growth via block doubling. Each entity starts with 8 adjacency slots and grows as needed. Max 48 entity–entity edges per entity (configurable), with multi-signal importance eviction.
"What happened after the deployment failed?"
The Temporal Chain links memories ingested within the same session into a doubly-linked list, enabling temporal navigation — both forward ("what happened next?") and backward ("what led to this?").
graph LR
M1["Memory #12<br/>'deploy started'"] --> M2["Memory #13<br/>'tests passed'"]
M2 --> M3["Memory #14<br/>'deploy failed'"]
M3 --> M4["Memory #15<br/>'rollback initiated'"]
style M1 fill:#3498db,color:white
style M2 fill:#2ecc71,color:white
style M3 fill:#e74c3c,color:white
style M4 fill:#f39c12,color:white
- Ingestion: When a new memory is ingested within the same session, a bidirectional link is created to the previously ingested memory
- Recall: For each seed result, the chain follows forward (3 hops) and backward (3 hops) to discover temporally adjacent memories. Forward links get 0.8× score, backward links get 0.7×
| Method | Description |
|---|---|
followForward(startIdx, maxHops) |
"What happened next?" |
followBackward(startIdx, maxHops) |
"What happened before?" |
link(currentIdx, prevIdx, sessionId) |
Link two memories within a session |
All graph components persist alongside memory data in DISK mode:
| Component | File | Format |
|---|---|---|
| HebbianGraphMemory | hebbian.graph |
CSR V3 ("HCSR" magic) — offset segment + edge segment. Auto-migrates legacy V2 files. |
| CoActivationRecordMemory | coactivation.dat |
Pair table + edge table + hash→tag map |
| EntityDirectory | entity-directory.graph |
Entity node segment + memory link adjacency segment + name index ("EDIR" magic) |
| HyperEntityGraphMemory | hyper-entity.graph |
Hyperedge segment + vertex segment + incidence index + incidence list ("HYEG" magic) |
| TemporalChainMemory | temporal.chain |
Raw linked-list segment ("TPCH" magic, V2) |
| TypeRegistryMemory |
entity-types.reg / relation-types.reg
|
Type name ↔ ID mappings |
| Layer | Per-Node/Edge | At 100K memories | At 1M memories |
|---|---|---|---|
| Hebbian CSR (L1) | 4B offset + 12B × avg degree (~2) | ~2.8 MB | ~28 MB |
| CoActivation | ~1MB total | ~1 MB | ~1 MB |
| Cross-Capture Inverted Index (L4) | 12B per tag×memory entry | ~2-5 MB | ~20-50 MB |
| Entity Directory | 64B node + 8B × adj | ~7.2 MB | ~72 MB |
| HyperEntity (L3) | 32B hyperedge + 8B × vertices + 4B incidence | ~5 MB | ~50 MB |
| Temporal (L2) | 16B | 1.6 MB | 16 MB |
| Total | ~20-23 MB | ~187-217 MB |
tip: CSR Memory Savings The CSR (Compressed Sparse Row) Hebbian layout stores only actual edges rather than pre-allocating MAX_DEGREE slots per node. At observed average degree ~2.0, this reduces Hebbian memory by ~90% compared to the legacy fixed-width layout (292B/node → ~28B/node).
This is small compared to the vector store (100K × 768-dim × 1B quantized = 75 MB).
Traditional vector search treats each query independently. The 4-layer graph creates emergent intelligence:
example: Scenario: Multi-Signal Recall 1. Agent queries "why is the app slow?" 2. Vector search → finds memory about "application latency" 3. Hebbian (Layer 1) → that memory was co-ingested with "connection pool settings" → adds it to results 4. Temporal (Layer 2) → follows the chain: connection pool → timeout config → retry backoff → adds all three 5. Hyperedge Event-Episode (Layer 3) → looks up entity "DatabaseService" in the
EntityDirectoryand set-intersects its incidence list in theHyperEntityGraph→ recalls a hyperedge connecting {Alice, Project Alpha, DatabaseService} representing Alice's recent DB migration event → adds it 6. Cross-Capture (Layer 4) → the tag "latency" co-occurs frequently with "prometheus" and "p99" in the co-occurrence graph → discovers a memory about "Prometheus alerting thresholds for p99 latency" that shares no entities, vocabulary, or session with the seed set → adds it
The final result set contains memories that no single retrieval signal could have found alone.
Collapsing pairwise relationships into n-body groupings.
The HyperEntityGraph replaces the traditional binary Entity Graph model by grouping related entities into hyperedges — single graph atoms that connect 3-8 entities with typed roles.
graph TD
subgraph "Binary EntityGraph (3 edges)"
A1["Alice"] -->|MANAGES| B1["Project Alpha"]
A1 -->|WORKS_AT| C1["Spectrayan"]
B1 -->|BELONGS_TO| C1
end
subgraph "HyperEntityGraph (1 hyperedge)"
HE["Hyperedge\n{Alice, Project Alpha, Spectrayan}\ntype: MANAGES_AT"]
A2["Alice\nrole: AGENT"] --- HE
B2["Project Alpha\nrole: OBJECT"] --- HE
C2["Spectrayan\nrole: LOCATION"] --- HE
end
style HE fill:#9b59b6,color:white
style A1 fill:#3498db,color:white
style B1 fill:#2ecc71,color:white
style C1 fill:#e74c3c,color:white
style A2 fill:#3498db,color:white
style B2 fill:#2ecc71,color:white
style C2 fill:#e74c3c,color:white
| Property | Value |
|---|---|
| Max vertices per hyperedge | 3-8 entities with typed roles |
| Max hyperedges per entity | 64 (participation cap with LRU eviction) |
| Complexity reduction | 40-60% fewer graph atoms vs. binary decomposition |
| Traversal | Set intersection: O(hyperedges_per_entity × avg_vertices) |
Hyperedge Node (32B):
[edgeId:4B][type:4B][weight:4B][vertexCount:4B]
[vertexOffset:4B][memoryIdx:4B][timestamp:8B]
Vertex Entry (8B):
[entityId:4B][roleId:4B]
Incidence Index (4B × entityCapacity):
[hyperedgeListOffset] → per-entity list of participating hyperedges
Incidence List Entry (4B):
[hyperedgeId]
The HyperEntityGraph has graduated to be the sole primary graph structure for entity and event representation, completely replacing the legacy binary EntityGraph (excised in P4 graduation #456). Rather than decomposing events into multiple binary relationships, all entities and their roles in a memory are grouped directly into hyperedges. This provides a 40-60% reduction in graph atoms and eliminates semantic loss from binary decomposition. Identity mapping (name-to-ID conversion) and fan-factor attenuation are handled by a dedicated EntityDirectory companion.
Graph expansion occurs during the later stages of the RecallPipeline. After vector, BM25, and sparse search produce the initial top-K seed candidate set, Spector traverses Hebbian, temporal, and entity edges to expand the context.
-
graphExpansionThreshold(float): The minimum score threshold a neighbor memory must meet to be added to the result set. Neighbors below this threshold are pruned to prevent context dilution. Defaults to0.7. -
enableTrace(boolean): When enabled, the returned trace results will log the exact edge traversed (e.g., Hebbian co-activation, temporal causal step, or entity relation) for each expanded candidate.
- Seed Set Generation: Layer 1/2/3 queries produce a seed candidate list.
- Edge Traversal: For each seed candidate, Hebbian CSR edges, Temporal chains, and Entity relations are traversed.
-
Threshold Gating: Neighbor memories are evaluated. If a neighbor's activation score (weighted by edge weight, importance, and temporal decay) exceeds
graphExpansionThreshold, it is added to the result set. - Deduplication & Sorting: Fused scoring merges the original and expanded candidates, sorting them before returning the final top-K.
- :material-lightning-bolt: [[6-Phase Scoring Pipeline|Memory--Scoring-Pipeline]] — the SIMD hot-loop that produces the seed set
- :material-sleep: [[Habituation — Anti-Filter Bubble|Memory--Habituation]] — preventing repetitive recall
- :material-head-cog: [[Dopamine — Surprise Detection|Memory--Dopamine]] — auto-importance scoring
- :material-brain: [[Architecture|Memory--Architecture]] — how graphs fit in the full pipeline
- Home
- Getting Started
-
Cognitive Memory
- Overview
- Getting Started
- Use Cases & Configuration
- API Reference
- Architecture
- The 6-Phase Scoring Pipeline
- Retrieval Stack
- Cognitive Profiles
- Salience & Importance
-
Biological Systems
- Overview
- Cortex — Tier Stores
- Hippocampus — Sleep Consolidation
- Synapse — Tags & Scoring
- Dopamine — Surprise Detection
- Amygdala — Emotional Valence
- 4-Layer Cognitive Graph
- Habituation — Anti-Filter Bubble
- Inhibition — Suppression
- Interference — Deduplication
- Prospective — Future Intents
- Metamemory — Self-Reflection
- Sync — Persistence & Replication
- Performance & Internals
- Cognitive Evaluation
- Synapse & Cortex
- Architecture
- Community