April 22, 2026 ‱ Version: v0.12.0

[Leitfaden zur Memory v2-Erweiterung: Assoziative Traversierung, Salienzgewichtung und Zugriffs-basiertes Vergessen] - Memory v2 Enhancement Guide: Associative Traversal, Salience Weighting, and Access-Based Forgetting

Architekturleitfaden zur Erweiterung von OpenClaws Memory v2 mit Entity-Kookkurrenz-Traversierung, salienzgewichteter Retention und zugriffsbasiertem Zerfall zur Verbesserung der AbrufprÀzision in langlebigen Agent-Bereitstellungen.

🔍 Symptome

Aktuelle EinschrÀnkungen beim Memory v2 Retrieval

Agents, die ĂŒber lĂ€ngere ZeitrĂ€ume (Tage bis Wochen) laufen, zeigen eine verschlechterte kontextuelle KohĂ€renz bei Verwendung bestehender Retrieval-Mechanismen. Die folgenden Symptome manifestieren sich in Produktionsumgebungen:

Symptom 1: Flaches lexikalisches Retrieval

Bei Abfragen nach konzeptionell verwandten Informationen ĂŒber die Zeit hinweg ruft der Agent nur oberflĂ€chliche Übereinstimmungen ab:

$ openclaw memory recall "app performance improvements"
---
RETRIEVED FACTS (3):
- W(s=0.3) @config: Updated heartbeat interval from 5m to 30m.
- W(s=0.3) @config: Increased worker pool size to 4.
- W(s=0.3) @api: Added rate limiting middleware.

EXPECTED: Connection to Week 2 debugging session about slow database queries
ACTUAL: Generic config changes only

Der Agent kann die implizite Kette nicht durchlaufen: “Performance” → “langsamer Endpunkt” → “Datenbankabfrage” → “Sarahs Fachwissen.”

Symptom 2: GleichmĂ€ĂŸige Gewichtung disparater Erinnerungen

Alle gespeicherten Fakten konkurrieren gleichermaßen um das Kontextbudget, unabhĂ€ngig von ihrer Bedeutung:

$ openclaw memory recall "any recent updates"
---
RETRIEVED (k=10, context budget: 4KB):

1. W(s=0.3) @config: Updated heartbeat interval from 5m to 30m.
2. W(s=0.3) @config: Increased worker pool size to 4.
3. W(s=0.3) @config: Set log level to INFO.
4. W(s=0.3) @config: Disabled telemetry opt-in.
5. B(s=0.3) @Sarah @project: Sarah announced she's leaving next month.
6. B(s=0.3) @user @identity: User prefers morning standups.
...

CRITICAL GAP: No salience differentiation. Sarah's departure competes equally with log level changes.

KRITISCHE LÜCKE: Keine Salienz-Differenzierung. Sarahs Ausscheiden konkurriert gleichberechtigt mit Log-Level-Änderungen.

Symptom 3: Unbegrenztes Indexwachstum ohne Abklingen

Nach 30+ Tagen im kontinuierlichen Betrieb:

$ sqlite3 ~/.openclaw/memory.db "SELECT COUNT(*) FROM facts;"
487

$ sqlite3 ~/.openclaw/memory.db "SELECT COUNT(*) FROM facts WHERE last_accessed > datetime('now', '-7 days');"
12

Nur 2,5% der Fakten wurden in der vergangenen Woche abgerufen, dennoch konkurrieren alle 487 im Retrieval-Scoring. Der Reflect-Job muss einen stÀndig wachsenden Datensatz ohne Priorisierungssignal verarbeiten.

Symptom 4: Hub-Node-Verschmutzung (Referenz aus dem CLS-M Benchmark)

EntitÀten, die in vielen Fakten auftreten, absorbieren die Retrieval-Aktivierung:

$ sqlite3 ~/.openclaw/memory.db "SELECT entity, COUNT(*) as cnt FROM fact_entities GROUP BY entity ORDER BY cnt DESC LIMIT 5;"
entity|cnt
@Peter|203
@heartbeat|57
@api|89
@config|112
@system|78

Die direkte EntitĂ€ten-Traversierung durch @Peter (203 Fakten) verdĂŒnnt das Signal fĂŒr spezifische, relevante Verbindungen.

🧠 Ursache

Architektonische LĂŒcken im aktuellen Memory v2 Design

Das aktuelle Retrieval-System verfĂŒgt nicht ĂŒber drei kritische Mechanismen, die fĂŒr die Aufrechterhaltung der PrĂ€zision in langlebigen Bereitstellungen unerlĂ€sslich sind:

LĂŒcke 1: Single-Hop EntitĂ€ten-Retrieval

Das bestehende entitĂ€ten-bewusste Retrieval-Modell gibt Fakten zurĂŒck, die direkt mit der Query-EntitĂ€t getaggt sind, durchlĂ€uft aber nicht rekursiv koexistierende EntitĂ€ten:

-- Current query (single-hop)
SELECT f.content, f.salience 
FROM facts f
JOIN fact_entities fe ON f.id = fe.fact_id
JOIN entities e ON fe.entity_id = e.id
WHERE e.name = 'performance';

-- Returns only: facts explicitly tagged @performance
-- Misses: facts about @database that co-occur with @performance across the corpus

Dies ist architektonisch korrekt fĂŒr exakte EntitĂ€ten-Lookups (“tell me about X”), reicht aber nicht aus fĂŒr explorative Abfragen, bei denen der Agent implizite Verbindungen entdeckt.

LĂŒcke 2: Fehlen von Salienz-Tracking zum Retain-Zeitpunkt

Die fundamentale Erkenntnis der Letta-Kontrollschleife ist, dass der Agent, der die Erfahrung hat, entscheiden muss, was behalten wird. Ohne einen Salienz-Parameter bei retain-Aufrufen ist diese Entscheidung jedoch binÀr (behalten/verwerfen) anstatt gestuft:

-- Current (binary)
openclaw memory retain "Sarah is leaving the company next month"

-- Missing salience metadata that would distinguish:
-- A config file tweak (s=0.2)
-- A critical team change (s=0.95)

Ohne Salienz kann der Reflect-Job nicht zwischen Signal und Rauschen unterscheiden – er muss Wichtigkeit ĂŒber AktualitĂ€t oder ZugriffshĂ€ufigkeit proxieren, was schlechte Proxies fĂŒr tatsĂ€chliche Signifikanz sind.

LĂŒcke 3: Kein zugriffsbasiertes Abkling-Mechanismus

Das aktuelle Design behandelt alle historischen Fakten als gleich abrufbar, unabhÀngig von Engagement-Mustern:

-- No temporal or access-based scoring
SELECT content FROM facts 
ORDER BY created_at DESC  -- Only recency, not relevance
LIMIT 10;

Dies erzeugt drei kaskadierende Probleme:

  1. PrÀzisionsverschlechterung: Mit dem Wachstum des Index sinkt das VerhÀltnis von relevanten zu irrelevanten Fakten
  2. Reflect-Job-Ineffizienz: Der Reflection-Prozessor muss einen immer grĂ¶ĂŸeren Korpus ohne Priorisierung auswerten
  3. Hub-Rauschen-VerstÀrkung: High-Degree-EntitÀten (erscheinen in 100+ Fakten) dominieren die Traversierung ohne Abklingen

Ursachenanalyse aus dem CLS-M Prototyp

Der CLS-M Prototyp (132 Knoten, 802 Kanten) validierte diese LĂŒcken empirisch:

  • Erinnerung war akzeptabel (65%) aber PrĂ€zision war schlecht (35%) – bedeutet, dass 65% des abgerufenen Inhalts Rauschen waren
  • Hub-Knoten zerstörten PrĂ€zision: Der heartbeat-Knoten hatte 57 Kanten und absorbierte Aktivierung, die zu spezifischen Knoten hĂ€tte gehen sollen
  • Zeitbasiertes Abklingen schlug fehl: Eine Tatsache von vor 3 Monaten, die wöchentlich abgerufen wird, sollte prominent bleiben; Alter allein ist kein Relevanzsignal

Die Lösung ist nicht, einen separaten Knowledge Graph zu bauen, sondern den bestehenden SQLite-Index zu erweitern mit:

  1. EntitÀten-Kookkurrenz-Tracking via Inverse Document Frequency (IDF) Gewichtung
  2. Salienz als erstklassiger Parameter bei Retain-Operationen
  3. Zugriffsbasiertes Abklingen, das beim Retrieval zurĂŒckgesetzt wird (nicht rein altersbasiertes Abklingen)

đŸ› ïž Schritt-fĂŒr-Schritt-Lösung

Phase 1: Schema-Erweiterungen fĂŒr SQLite-Index

Salienz- und Zugriffsverfolgungsspalten zum bestehenden Schema hinzufĂŒgen:

-- Migration: add_salience_and_access_tracking.sql

-- 1. Add salience column (0.0 to 1.0, default 0.5)
ALTER TABLE facts ADD COLUMN salience REAL DEFAULT 0.5;

-- 2. Add access tracking columns
ALTER TABLE facts ADD COLUMN last_accessed_at DATETIME DEFAULT NULL;
ALTER TABLE facts ADD COLUMN access_count INTEGER DEFAULT 0;

-- 3. Create index for access-based queries
CREATE INDEX idx_facts_last_accessed ON facts(last_accessed_at);
CREATE INDEX idx_facts_salience ON facts(salience);

-- 4. Precompute entity frequencies for IDF weighting
CREATE TABLE entity_stats AS
SELECT 
    e.id,
    e.name,
    COUNT(fe.fact_id) as fact_count,
    1.0 / LOG(COUNT(fe.fact_id) + 1) as idf_weight
FROM entities e
LEFT JOIN fact_entities fe ON e.id = fe.entity_id
GROUP BY e.id;

CREATE INDEX idx_entity_stats_fact_count ON entity_stats(fact_count);

Phase 2: EntitÀten-Kookkurrenz-Tabelle

Kookkurrenz-Matrix aus bestehendem Faktindex aufbauen:

-- Migration: build_entity_cooccurrence.sql

-- 1. Create co-occurrence table
CREATE TABLE entity_cooccurrence (
    entity_id_1 INTEGER NOT NULL,
    entity_id_2 INTEGER NOT NULL,
    cooccur_count INTEGER DEFAULT 1,
    cooccur_weight REAL DEFAULT 0.0,
    PRIMARY KEY (entity_id_1, entity_id_2),
    FOREIGN KEY (entity_id_1) REFERENCES entities(id),
    FOREIGN KEY (entity_id_2) REFERENCES entities(id)
);

-- 2. Populate from existing fact_entities (facts with 2+ entities)
INSERT INTO entity_cooccurrence (entity_id_1, entity_id_2, cooccur_count)
SELECT 
    fe1.entity_id,
    fe2.entity_id,
    COUNT(DISTINCT fe1.fact_id)
FROM fact_entities fe1
JOIN fact_entities fe2 ON fe1.fact_id = fe2.fact_id
WHERE fe1.entity_id < fe2.entity_id  -- Avoid duplicates
GROUP BY fe1.entity_id, fe2.entity_id;

-- 3. Compute weighted co-occurrence using IDF
UPDATE entity_cooccurrence SET cooccur_weight = (
    SELECT 
        CAST(cooccur_count AS REAL) * 
        (SELECT idf_weight FROM entity_stats WHERE idf_weight = entity_id_1) *
        (SELECT idf_weight FROM entity_stats WHERE entity_stats.id = entity_id_2)
    WHERE entity_cooccurrence.entity_id_1 = entity_id_1 
    AND entity_cooccurrence.entity_id_2 = entity_id_2
);

-- 4. Create index for fast co-occurrence lookups
CREATE INDEX idx_cooccur_lookup ON entity_cooccurrence(entity_id_1, cooccur_weight DESC);

Phase 3: CLI-Befehls-Updates

Den retain-Befehl mit Salienz-Parameter erweitern:

# Before
openclaw memory retain "Sarah is leaving the company next month"

# After (with salience)
openclaw memory retain "Sarah is leaving the company next month" \
  --type B \
  --entity Sarah \
  --entity project \
  --salience 0.95

Den recall-Befehl mit Salienzfilter und assoziativer Traversierung erweitern:

# Before
openclaw memory recall "performance improvements"

# After (with enhanced options)
openclaw memory recall "performance improvements" \
  --k 10 \
  --min-salience 0.3 \
  --associative-depth 2 \
  --activation-decay 0.5

Phase 4: Assoziative Traversierungsalgorithmus

Tiefenbegrenzte Traversierung mit Aktivierung-Abklingen implementieren:

def associative_traverse(seed_entities: list[str], depth: int = 2, decay: float = 0.5) -> dict:
    """
    Traverse entity co-occurrence graph with depth limiting and activation decay.
    
    Returns:
        dict: {entity_name: accumulated_activation_score}
    """
    activation = {}
    visited = set()
    
    # Initialize seed entities with full activation
    for entity_name in seed_entities:
        activation[entity_name] = 1.0
        visited.add(entity_name)
    
    current_entities = seed_entities
    current_activation = 1.0
    
    for hop in range(depth):
        next_entities = []
        next_activation = current_activation * decay
        
        for entity_name in current_entities:
            # Query co-occurring entities with IDF weighting
            cooccurring = query("""
                SELECT e.name, c.cooccur_weight, es.idf_weight
                FROM entity_cooccurrence c
                JOIN entities e ON c.entity_id_2 = e.id
                JOIN entity_stats es ON e.id = es.id
                WHERE c.entity_id_1 = (
                    SELECT id FROM entities WHERE name = ?
                )
                AND e.name NOT IN ({}),
                ORDER BY c.cooccur_weight * es.idf_weight DESC
                LIMIT 10
            """, entity_name)
            
            for coentity_name, cooccur_weight, idf_weight in cooccurring:
                if coentity_name not in visited:
                    contribution = next_activation * cooccur_weight * idf_weight
                    activation[coentity_name] = activation.get(coentity_name, 0) + contribution
                    next_entities.append(coentity_name)
                    visited.add(coentity_name)
        
        current_entities = next_entities
        current_activation = next_activation
    
    return activation

Phase 5: Zugriffsbasiertes Abklingen-Implementierung

Power-Law-Abklingen auf Retrieval-Bewertung implementieren:

def compute_retrieval_score(fact: dict, query_entities: list[str], 
                            now: datetime = None) -> float:
    """
    Compute composite retrieval score including salience and access-based decay.
    
    Components:
    - Base match score (lexical/semantic/associative)
    - Salience weight (from retain call)
    - Access decay (power-law, reset on retrieval)
    """
    if now is None:
        now = datetime.utcnow()
    
    base_score = compute_base_match_score(fact, query_entities)
    salience_score = fact.get('salience', 0.5)
    
    # Access-based decay (power-law, halves every 7 days)
    last_accessed = fact.get('last_accessed_at')
    if last_accessed:
        days_since_access = (now - last_accessed).days
        access_decay = 0.5 ** (days_since_access / 7.0)
    else:
        access_decay = 0.25  # Never-accessed facts start quieter
    
    # Boost for frequent access (logarithmic to prevent hub dominance)
    access_count = fact.get('access_count', 0)
    access_boost = 1.0 + (0.1 * math.log1p(access_count))
    
    composite_score = (
        base_score * 0.4 +
        salience_score * 0.35 +
        access_decay * access_boost * 0.25
    )
    
    return composite_score

def on_fact_retrieved(fact_id: int) -> None:
    """Update access tracking when a fact is retrieved."""
    execute("""
        UPDATE facts 
        SET last_accessed_at = ?,
            access_count = access_count + 1
        WHERE id = ?
    """, (datetime.utcnow(), fact_id))

Phase 6: Reflect-Loop-Integration

Den Reflect-Job aktualisieren, um kĂŒrzlich abgerufene Fakten zu priorisieren:

# In reflect job processor
def reflect_on_memories(agent_id: str, core_memory_max_tokens: int =

            

Belege & Quellen

Diese Troubleshooting-Anleitung wurde automatisch von der FixClaw Intelligence Pipeline aus Community-Diskussionen synthetisiert.