diff --git a/engram/dist/engram b/engram/dist/engram index 8a019f1..0d9b977 100755 Binary files a/engram/dist/engram and b/engram/dist/engram differ diff --git a/lang/releases/v1.0.0-20260501/el_runtime.c b/lang/releases/v1.0.0-20260501/el_runtime.c index 55cc726..35b006f 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.c +++ b/lang/releases/v1.0.0-20260501/el_runtime.c @@ -6552,8 +6552,21 @@ el_val_t engram_edge_count(void) { } /* Compute temporal decay factor for a node given current time. - * effective contribution = salience * exp(-lambda * age_hours / T_half) - * Clamped to [0.05, 1.0] so very old nodes retain a meaningful floor. */ + * effective contribution = salience * exp(-lambda * age_hours / T_half) * freq_resist + * Clamped to [0.05, 1.0] so very old nodes retain a meaningful floor. + * + * ACT-R-inspired frequency resistance (added 2026-05-18 self-review): + * freq_resist = 1.0 + 0.1 * log(1 + activation_count) + * count=0 → 1.00×, count=9 → 1.23×, count=99 → 1.46×, count=999 → 1.69× + * + * Rationale: ACT-R Base-Level Learning (Anderson 1993, d=0.5 power law) shows + * that frequently-accessed memories resist temporal decay — the brain doesn't + * let well-rehearsed knowledge fade at the same rate as never-accessed nodes. + * Our activation_dampening already reduces per-query novelty for high-count + * nodes (correct — prevents runaway dominance in any single turn). But without + * freq_resist, that same node was decaying as fast as a node never activated, + * which is cognitively wrong. This multiplier corrects it: canonical nodes and + * heavily-used lessons resist decay, session-ephemeral nodes do not. */ static double engram_temporal_decay(const EngramNode* n, int64_t now_ms) { int64_t age_ms = now_ms - n->last_activated; if (age_ms <= 0) return 1.0; @@ -6561,6 +6574,10 @@ static double engram_temporal_decay(const EngramNode* n, int64_t now_ms) { : ENGRAM_DECAY_LAMBDA; double age_hours = (double)age_ms / 3600000.0; double factor = exp(-lambda * age_hours / ENGRAM_T_HALF_HOURS); + /* Frequency resistance: frequently-activated nodes decay more slowly. */ + double freq_resist = 1.0 + 0.1 * log(1.0 + (double)n->activation_count); + factor *= freq_resist; + if (factor > 1.0) factor = 1.0; if (factor < 0.05) factor = 0.05; return factor; } @@ -6758,11 +6775,16 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { } /* ── Semantic seed supplement ──────────────────────────────────────────── * When the query has an embedding vector, scan un-seeded nodes for cosine - * similarity ≥ 0.65 and inject the top-30 as additional Pass-1 seeds. + * similarity ≥ 0.70 and inject the top-30 as additional Pass-1 seeds. * This ensures semantically-related nodes that share no query tokens still * enter the BFS fan-out. Sem seeds get 85% of full act to stay slightly * weaker than exact lexical matches. Top-K=30 cap prevents queue flooding - * on large graphs (11K+ nodes post-ingestor). + * on large graphs (13K+ nodes post-ingestor). + * + * Floor raised from 0.65 → 0.70 (2026-05-18 self-review): field literature + * (SuperLocalMemory, floop) validates 0.70-0.75 as the noise floor for + * nomic-embed-text; 0.65 was admitting false positives that diluted BFS + * traversal quality. * * Without this pass, embeddings collected at write-time via engram_embed_node * were only used in goal_bias (Pass 2) for nodes that already had lexical @@ -6770,7 +6792,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { * completely invisible to activation regardless of embedding quality. */ if (qvec && qdim > 0) { typedef struct { int64_t idx; float sim; } SemEntry; - const float SEM_FLOOR = 0.65f; + const float SEM_FLOOR = 0.70f; SemEntry top_sem[30]; int64_t top_sem_cnt = 0; float top_sem_min = SEM_FLOOR;