[Guía de mejora de Memory v2: recorrido asociativo, ponderación de importancia y olvido basado en acceso] - Memory v2 Enhancement Guide: Associative Traversal, Salience Weighting, and Access-Based Forgetting
Guía arquitectónica para extender Memory v2 de OpenClaw con recorrido de co-ocurrencia de entidades, retención ponderada por importancia y decaimiento basado en acceso para mejorar la precisión de recuperación en despliegues de agentes de larga duración.
🔍 Síntomas
Limitaciones Actuales de Recuperación en Memory v2
Los agentes que se ejecutan durante períodos prolongados (días a semanas) muestran una coherencia contextual degradada al utilizar los mecanismos de recuperación existentes. Los siguientes síntomas se manifiestan en despliegues en producción:
Síntoma 1: Recuperación Léxica Superficial
Al consultar información conceptualmente relacionada a lo largo del tiempo, el agente recupera solo coincidencias a nivel superficial:
$ 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 onlyEl agente no puede atravesar la cadena implícita: “performance” → “slow endpoint” → “database query” → “Sarah’s expertise.”
Síntoma 2: Ponderación Igualitaria de Recuerdos Dispares
Todos los hechos almacenados compiten igualmente por el presupuesto de contexto independientemente de su importancia:
$ 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.Síntoma 3: Crecimiento Ilimitado del Índice Sin Decaimiento
Después de 30+ días de operación continua:
$ sqlite3 ~/.openclaw/memory.db "SELECT COUNT(*) FROM facts;"
487
$ sqlite3 ~/.openclaw/memory.db "SELECT COUNT(*) FROM facts WHERE last_accessed > datetime('now', '-7 days');"
12Solo el 2.5% de los hechos fueron accedidos en la última semana, sin embargo, los 487 compiten en la puntuación de recuperación. El trabajo de reflexión debe procesar un conjunto cada vez mayor sin señal de priorización.
Síntoma 4: Contaminación por Nodos Hub (Referencia del Benchmark CLS-M)
Las entidades que aparecen en muchos hechos absorben la activación de recuperación:
$ 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|78La traversía directa de entidades a través de @Peter (203 hechos) diluye la señal para conexiones específicas y relevantes.
🧠 Causa raíz
Brechas Arquitectónicas en el Diseño Actual de Memory v2
El sistema de recuperación actual carece de tres mecanismos críticos que son esenciales para mantener la precisión en despliegues de larga duración:
Brecha 1: Recuperación de Entidades de Un Solo Salto
El modelo de recuperación consciente de entidades existente devuelve hechos directamente etiquetados con la entidad de consulta pero no atraviesa recursivamente entidades co-ocurrentes:
-- 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 corpusEsto es arquitectónicamente correcto para búsqueda exacta de entidades (“dime sobre X”) pero insuficiente para consultas exploratorias donde el agente descubre conexiones implícitas.
Brecha 2: Ausencia de Seguimiento de Importancia en el Momento de Retención
La visión fundamental del bucle de control de Letta es que el agente que tiene la experiencia debe decidir qué retener. Sin embargo, sin un parámetro de importancia en las llamadas de retención, esta decisión es binaria (conservar/descartar) en lugar de gradual:
-- 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)Sin importancia, el trabajo de reflexión no puede distinguir la señal del ruido—debe usar como proxy la recurrencia o la frecuencia de acceso, que son proxies deficientes para la importancia real.
Brecha 3: Sin Mecanismo de Decaimiento Basado en Acceso
El diseño actual trata todos los hechos históricos como igualmente recuperables independientemente de los patrones de compromiso:
-- No temporal or access-based scoring
SELECT content FROM facts
ORDER BY created_at DESC -- Only recency, not relevance
LIMIT 10;Esto crea tres problemas en cascada:
- Degradación de precisión: A medida que crece el índice, la proporción de hechos relevantes vs. irrelevantes disminuye
- Ineficiencia del trabajo de reflexión: El procesador de reflexión debe evaluar un corpus cada vez mayor sin priorización
- Amplificación del ruido de hubs: Entidades de alto grado (apareciendo en 100+ hechos) dominan la traversía sin decaimiento
Análisis de Causa Raíz del Prototipo CLS-M
El prototipo CLS-M (132 nodos, 802 aristas) validó estas brechas empíricamente:
- El recall fue aceptable (65%) pero la precisión fue pobre (35%)—lo que significa que el 65% del contenido recuperado era ruido
- Los nodos hub destruyeron la precisión: El nodo
heartbeattenía 57 aristas, absorbiendo activación que debería haber ido a nodos específicos - El decaimiento basado en tiempo falló: Un hecho de hace 3 meses que es accedido semanalmente debería permanecer prominente; la edad por sí sola no es una señal de relevancia
La solución no es construir un grafo de conocimiento separado sino extender el índice SQLite existente con:
- Seguimiento de co-ocurrencia de entidades mediante ponderación de frecuencia inversa de documentos (IDF)
- Importancia como parámetro de primera clase en operaciones de retención
- Decaimiento basado en acceso que se reinicia en la recuperación (no decaimiento puramente basado en edad)
🛠️ Solución paso a paso
Fase 1: Extensiones de Esquema para Índice SQLite
Agregar columnas de importancia y seguimiento de acceso al esquema existente:
-- 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);Fase 2: Tabla de Co-ocurrencia de Entidades
Construir matriz de co-ocurrencia desde el índice de hechos existente:
-- 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);Fase 3: Actualizaciones de Comandos CLI
Extender el comando retain con parámetro de importancia:
# 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.95Extender el comando recall con filtro de importancia y traversía asociativa:
# 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.5Fase 4: Algoritmo de Traversía Asociativa
Implementar traversía con límite de profundidad y decaimiento de activación:
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