Skip to content

Consolidation & Decay

The Consolidation Engine maintains and optimizes the memory graph through scheduled background processing inspired by biological memory consolidation. It applies exponential decay, discovers non-obvious associations, clusters similar memories, and implements controlled forgetting to prevent unbounded memory growth.

This page covers the MemoryConsolidator and ConsolidationScheduler classes and their integration with the Flask API. For the enrichment pipeline that processes new memories, see Enrichment Pipeline. For the overall background processing architecture, see Background Processing.


The consolidation engine runs independently from client requests on configurable schedules. Unlike enrichment (which processes individual memories) or embedding generation (which handles batches of new memories), consolidation operates on the entire memory graph to maintain its health over time.

Consolidation in the Background Processing System

Section titled “Consolidation in the Background Processing System”
graph TB
    API["Flask API<br/>(app.py)"]

    subgraph "Background Workers"
        EnrichQ["Enrichment Queue<br/>Per-memory processing"]
        EmbedQ["Embedding Queue<br/>Batch generation"]
        Sched["ConsolidationScheduler<br/>(consolidation.py)"]
    end

    subgraph "Consolidation Tasks"
        Decay["Decay Task<br/>calculate_relevance_score()"]
        Creative["Creative Task<br/>discover_creative_associations()"]
        Cluster["Cluster Task<br/>cluster_similar_memories()"]
        Forget["Forget Task<br/>apply_controlled_forgetting()"]
    end

    Falkor[("FalkorDB<br/>memories graph")]
    Qdrant[("Qdrant<br/>vectors")]

    API -->|"POST /memory"| EnrichQ
    API -->|"POST /memory"| EmbedQ
    API -->|"Starts on init"| Sched

    Sched -->|"Hourly"| Decay
    Sched -->|"Hourly"| Creative
    Sched -->|"6 hours"| Cluster
    Sched -->|"Daily"| Forget

    Decay -->|"Update scores"| Falkor
    Creative -->|"Create DISCOVERED edges"| Falkor
    Cluster -->|"Create MetaMemory nodes"| Falkor
    Forget -->|"Archive/delete"| Falkor
    Forget -->|"Delete vectors"| Qdrant

The MemoryConsolidator class implements the four consolidation tasks. It depends on a graph store (FalkorDB) and optionally a vector store (Qdrant) for deletions during forgetting.

Key Parameters:

ParameterDefaultPurpose
base_decay_rate0.1Daily exponential decay rate
reinforcement_bonus0.2Strength added when memory accessed
relationship_preservation0.3Extra weight for connected memories
min_cluster_size3Minimum memories per cluster
similarity_threshold0.75Cosine similarity for clustering
archive_threshold0.2Archive below this relevance
delete_threshold0.05Delete below this relevance

The ConsolidationScheduler manages when each consolidation task runs based on configured intervals.

Default Schedule Configuration:

TaskInterval (Environment Variable)Default Value
decayCONSOLIDATION_DECAY_INTERVAL_SECONDS3600 (1 hour)
creativeCONSOLIDATION_CREATIVE_INTERVAL_SECONDS3600 (1 hour)
clusterCONSOLIDATION_CLUSTER_INTERVAL_SECONDS21600 (6 hours)
forgetCONSOLIDATION_FORGET_INTERVAL_SECONDS86400 (24 hours)

The decay task updates relevance_score for all memories using exponential decay based on age, access patterns, relationships, importance, and confidence.

graph LR
    Memory["Memory Node<br/>(FalkorDB)"]

    subgraph "Factors"
        Age["Age Factor<br/>exp(-0.1 * days)"]
        Access["Access Factor<br/>exp(-0.05 * days_since_access)"]
        Rel["Relationship Factor<br/>1 + 0.3 * log(1 + rel_count)"]
        Imp["Importance<br/>0.5 + user_importance"]
        Conf["Confidence<br/>0.7 + 0.3 * confidence"]
    end

    Memory --> Age
    Memory --> Access
    Memory --> Rel
    Memory --> Imp
    Memory --> Conf

    Age --> Score["Relevance Score<br/>= decay * access * rel * imp * conf"]
    Access --> Score
    Rel --> Score
    Imp --> Score
    Conf --> Score

    Score --> Update["SET m.relevance_score"]

Calculation Steps:

  1. Age-based decay: exp(-0.1 * age_days) — Exponential decay from creation timestamp
  2. Access reinforcement: 1.0 if accessed within 24 hours, else exp(-0.05 * days_since_access)
  3. Relationship preservation: 1 + 0.3 * log(1 + relationship_count) — Connected memories decay slower
  4. Importance scaling: 0.5 + importance (scales from 0.5 to 1.5)
  5. Confidence bonus: 0.7 + 0.3 * confidence (adds up to 30%)
  6. Combined score: Product of all factors, capped at 1.0

Cypher Query Pattern:

MATCH (m:Memory)
WHERE (m.archived IS NULL OR m.archived = false)
AND m.importance >= $importance_threshold -- Optional filter
RETURN m.id, m.timestamp, m.importance, m.last_accessed, m.relevance_score

The consolidator then updates each memory with:

MATCH (m:Memory {id: $id})
SET m.relevance_score = $score

To avoid O(N) graph queries during decay, relationship counts are cached with hourly invalidation:

The hour_key parameter causes automatic cache invalidation every hour, balancing freshness with an 80% query reduction.


The creative task discovers non-obvious connections between memories by randomly sampling the graph and analyzing semantic similarity and temporal patterns.

graph TB
    Sample["Sample Memories<br/>WHERE relevance_score > 0.3<br/>ORDER BY rand()<br/>LIMIT 20"]

    subgraph "Pairwise Analysis"
        Check["Check Existing Edges<br/>MATCH (m1)-[r]-(m2)"]
        Similarity["Calculate Cosine Similarity<br/>_cosine_similarity()"]
        Type["Determine Connection Type"]
    end

    subgraph "Connection Rules"
        R1["Decision + Decision<br/>similarity < 0.3<br/>→ CONTRASTS_WITH"]
        R2["Insight + Pattern<br/>similarity > 0.5<br/>→ EXPLAINS"]
        R3["Different types<br/>similarity > 0.7<br/>→ SHARES_THEME"]
        R4["Same week<br/>similarity < 0.4<br/>→ PARALLEL_CONTEXT"]
    end

    Sample --> Check
    Check -->|"No edge exists"| Similarity
    Similarity --> Type
    Type --> R1
    Type --> R2
    Type --> R3
    Type --> R4

    R1 --> Create["CREATE (m1)-[r:DISCOVERED]->(m2)"]
    R2 --> Create
    R3 --> Create
    R4 --> Create

Connection Types Discovered:

TypeConditionsConfidenceExample
CONTRASTS_WITHBoth Decision, similarity < 0.30.6Opposing architectural choices
EXPLAINSInsight + Pattern, similarity > 0.50.7Insight explains pattern
SHARES_THEMEDifferent types, similarity > 0.7similarityCross-domain patterns
PARALLEL_CONTEXTSame week, similarity < 0.40.5Unrelated concurrent work

Edge Properties:

All discovered edges are labeled with DISCOVERED in FalkorDB and carry metadata about the connection type and confidence:

{
"type": "CONTRASTS_WITH",
"confidence": 0.6,
"discovered_at": "2025-01-15T10:00:00Z",
"algorithm": "creative_association"
}

The cluster task groups semantically similar memories using a graph-based clustering algorithm (similar to DBSCAN) and creates MetaMemory nodes to represent patterns.

graph TB
    Load["Load Memories with Embeddings<br/>WHERE m.embeddings IS NOT NULL<br/>AND m.relevance_score > 0.3"]

    Build["Build Adjacency Graph<br/>For each pair:<br/>if cosine_similarity >= 0.75:<br/>  connect(i, j)"]

    DFS["Find Connected Components<br/>DFS traversal to find clusters"]

    Filter["Filter by Size<br/>Keep clusters >= 3 memories"]

    subgraph "Meta-Memory Creation"
        Theme["Identify Dominant Type<br/>max(set(types), key=count)"]
        Span["Calculate Temporal Span<br/>max(timestamps) - min(timestamps)"]
        Create["CREATE MetaMemory Node<br/>type: 'MetaPattern'<br/>cluster_size: N"]
        Link["CREATE (meta)-[:SUMMARIZES]->(m)"]
    end

    Load --> Build
    Build --> DFS
    DFS --> Filter
    Filter --> Theme
    Theme --> Span
    Span --> Create
    Create --> Link

Clustering Parameters:

  • Similarity threshold: 0.75 (configurable via self.similarity_threshold)
  • Minimum cluster size: 3 memories (configurable via self.min_cluster_size)
  • Relevance filter: Only clusters memories with relevance_score > 0.3

MetaMemory Node Properties:

PropertyTypeDescription
labelstring"MetaPattern"
dominant_typestringMost common memory type in the cluster
cluster_sizeintegerNumber of memories in the cluster
temporal_span_daysfloatDays between oldest and newest memory
created_atISO datetimeWhen this meta-memory was created
contentstringAuto-generated cluster summary

MetaMemory nodes are connected to their member memories via SUMMARIZES relationships:

MATCH (meta:MetaPattern), (m:Memory {id: $member_id})
CREATE (meta)-[:SUMMARIZES]->(m)

The forget task archives low-relevance memories and permanently deletes very low-relevance memories, preventing unbounded graph growth.

graph LR
    Memory["Memory<br/>relevance_score"]

    High["relevance >= 0.2<br/>PRESERVE<br/>Update score only"]
    Archive["0.05 <= relevance < 0.2<br/>ARCHIVE<br/>SET archived = true"]
    Delete["relevance < 0.05<br/>DELETE<br/>DETACH DELETE + vector delete"]

    Memory -->|"Calculate current relevance"| High
    Memory --> Archive
    Memory --> Delete

    Archive --> Graph["Mark in FalkorDB<br/>SET m.archived = true<br/>SET m.archived_at = now()"]
    Delete --> GraphDel["Delete from FalkorDB<br/>DETACH DELETE m"]
    Delete --> VectorDel["Delete from Qdrant<br/>vector_store.delete()"]

Forgetting Lifecycle:

  1. Fresh memories (relevance > 0.2): Updated but preserved
  2. Low-relevance memories (0.05-0.2): Archived (kept in graph, marked archived = true)
  3. Very low-relevance memories (< 0.05): Permanently deleted from both stores

Archive Query:

MATCH (m:Memory)
WHERE (m.archived IS NULL OR m.archived = false)
AND m.relevance_score < 0.2
AND m.relevance_score >= 0.05
SET m.archived = true,
m.archived_at = $now

Delete Queries:

-- FalkorDB deletion
MATCH (m:Memory)
WHERE m.relevance_score < 0.05
AND (m.archived IS NULL OR m.archived = false)
DETACH DELETE m
-- Qdrant deletion (for each deleted memory ID)
vector_store.delete(memory_id)

Consolidation runs in a background thread started at Flask application initialization:

# app.py — startup code
scheduler = ConsolidationScheduler(
consolidator=consolidator,
decay_interval=int(os.getenv("CONSOLIDATION_DECAY_INTERVAL_SECONDS", 3600)),
creative_interval=int(os.getenv("CONSOLIDATION_CREATIVE_INTERVAL_SECONDS", 3600)),
cluster_interval=int(os.getenv("CONSOLIDATION_CLUSTER_INTERVAL_SECONDS", 21600)),
forget_interval=int(os.getenv("CONSOLIDATION_FORGET_INTERVAL_SECONDS", 86400)),
tick_seconds=int(os.getenv("CONSOLIDATION_TICK_SECONDS", 60)),
)
thread = threading.Thread(target=scheduler.run, daemon=True)
thread.start()

The scheduler checks every CONSOLIDATION_TICK_SECONDS (default: 60) whether any task is due for execution.

Administrators can manually trigger consolidation via the /consolidate endpoint:

POST /consolidate
Authorization: Bearer <admin_token>
{
"task": "decay" // Optional: run specific task only
}

If no task is specified, all four tasks run sequentially.

Consolidation state is persisted in a ConsolidationControl node in FalkorDB:

Node Creation:

MERGE (c:ConsolidationControl {id: 'singleton'})
ON CREATE SET
c.last_decay = null,
c.last_creative = null,
c.last_cluster = null,
c.last_forget = null,
c.history = '[]'

Update After Task:

MATCH (c:ConsolidationControl {id: 'singleton'})
SET c.last_decay = $now,
c.history = $updated_history

This allows the /consolidate/status endpoint to report when each task last ran and its result.


VariableDefaultDescription
CONSOLIDATION_TICK_SECONDS60How often scheduler checks if tasks are due (seconds)
CONSOLIDATION_DECAY_INTERVAL_SECONDS3600How often decay task runs (1 hour)
CONSOLIDATION_CREATIVE_INTERVAL_SECONDS3600How often creative task runs (1 hour)
CONSOLIDATION_CLUSTER_INTERVAL_SECONDS21600How often cluster task runs (6 hours)
CONSOLIDATION_FORGET_INTERVAL_SECONDS86400How often forget task runs (24 hours)
CONSOLIDATION_DECAY_IMPORTANCE_THRESHOLDNoneOptional: Only decay memories with importance >= threshold
CONSOLIDATION_HISTORY_LIMIT20Max consolidation runs to keep in history

High-Traffic Production (>10k memories/day):

Terminal window
CONSOLIDATION_DECAY_INTERVAL_SECONDS=1800 # Every 30 minutes
CONSOLIDATION_CREATIVE_INTERVAL_SECONDS=7200 # Every 2 hours
CONSOLIDATION_CLUSTER_INTERVAL_SECONDS=43200 # Every 12 hours
CONSOLIDATION_FORGET_INTERVAL_SECONDS=86400 # Daily (keep at 24 hours)

Low-Traffic Development:

Terminal window
CONSOLIDATION_DECAY_INTERVAL_SECONDS=3600 # Every hour
CONSOLIDATION_CREATIVE_INTERVAL_SECONDS=3600 # Every hour
CONSOLIDATION_CLUSTER_INTERVAL_SECONDS=86400 # Daily
CONSOLIDATION_FORGET_INTERVAL_SECONDS=604800 # Weekly (longer retention)

Memory-Constrained Environments:

Terminal window
CONSOLIDATION_FORGET_INTERVAL_SECONDS=43200 # Every 12 hours (aggressive pruning)
CONSOLIDATION_DECAY_IMPORTANCE_THRESHOLD=0.3 # Only decay lower-importance memories

TaskComplexity1k Memories10k Memories100k Memories
DecayO(N)<1s~1s~10s
CreativeO(N²) sample<1s<2s<5s
ClusterO(N²) edges2-3s10-15s60-90s
ForgetO(N)<1s~2s~15s

Notes:

  • Decay benefits from 80% cache hit rate via relationship count caching
  • Creative samples only 20-30 memories, so scales with sample size not total memories
  • Cluster complexity depends on embedding similarity distribution
  • Forget includes vector store deletions which add latency

Typical Memory Footprint:

  • Baseline: ~5MB (LRU cache + worker overhead)
  • During decay: +1-2MB (result dictionaries)
  • During creative: +2-3MB (sample embeddings)
  • During cluster: +10-50MB (all embeddings + adjacency graph for 10k memories)

The consolidation engine has comprehensive test coverage in tests/test_consolidation_engine.py:

Key Test Fixtures:

  • FakeGraph: Simulates FalkorDB with configurable responses
  • FakeVectorStore: Simulates Qdrant for deletion tracking
  • freeze_time: Freezes datetime for deterministic decay calculations

Tests cover all four tasks, edge cases (empty graph, single memory, max thresholds), and the scheduler timing logic.

GET /consolidate/status (Admin token required):

{
"last_decay": "2025-01-15T10:00:00Z",
"last_creative": "2025-01-15T10:00:00Z",
"last_cluster": "2025-01-15T06:00:00Z",
"last_forget": "2025-01-15T00:00:00Z",
"history": [
{
"task": "decay",
"ran_at": "2025-01-15T10:00:00Z",
"memories_processed": 1542,
"duration_seconds": 0.8
}
]
}

Consolidation operations emit structured logs:

[consolidation] Starting decay task (1542 memories)
[consolidation] Decay complete: 1542 processed, 0.8s elapsed
[consolidation] Creative task: discovered 3 new associations
[consolidation] Cluster task: created 2 MetaMemory nodes from 8 clusters
[consolidation] Forget task: archived 12, deleted 3 memories

The consolidation engine implements memory principles from neuroscience research:

TaskBiological AnalogImplementation
DecaySynaptic weakening over timeExponential relevance decay based on age and access
CreativeREM sleep association formationRandom memory sampling + similarity analysis
ClusterMemory compression during sleepSemantic grouping + meta-pattern creation
ForgetControlled forgetting during consolidationArchival before deletion, importance-weighted

Key Research Connections:

For related API operations (triggering consolidation manually, viewing status), see Consolidation Operations.