diff --git a/lang/releases/v1.0.0-20260501/el_runtime.c b/lang/releases/v1.0.0-20260501/el_runtime.c index ebbdba2..8ef3407 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.c +++ b/lang/releases/v1.0.0-20260501/el_runtime.c @@ -7835,14 +7835,78 @@ static int eg_edge_exists_between(EngramStore* g, const char* a, const char* b) return 0; } +/* engram_temporal_decay — recency shaping on the activation path. + * + * MEASURED FAILURE (2026-08-05 self-review). Census of the live graph under + * the previous form (uniform 168 h half-life, floor 0.05): + * + * node type n median tdecay % pinned at the 0.05 floor + * Memory 1233 0.0500 81% + * Knowledge 1183 0.0500 58% + * BacklogItem 1057 0.0500 91% + * Project 321 0.0500 98% + * Tag 135 0.0500 100% + * + * The median value for EVERY node type was the clamp. A function whose median + * output is its floor is not a signal — it is a constant with exceptions, and + * the exceptions were exactly the nodes touched in the last few days. + * + * What that cost, concretely: 10 of the 13 grounded value nodes — "Precision + * Over Brute Force", "Honesty Before Comfort", "The System Must Accumulate" — + * sat at 0.05, a 20x activation penalty, while Knowledge ingested overnight + * sat near 1.0 and held the working-memory top slots. The decay function was + * quietly erasing the accumulated library in favour of whatever arrived last + * night. That is a direct inversion of the system's purpose. + * + * Worse, tdecay multiplies at EVERY hop (seed activation and each propagation + * step), so a 2-hop path through settled knowledge compounded to 0.05^2 = + * 0.0025. Old regions of the graph were not disfavoured; they were unreachable. + * + * EXTERNAL EVIDENCE. "Not All Memories Age the Same" (arXiv:2604.26970) + * measures retrieval under different decay regimes: + * + * no temporal weighting NDCG@5 0.274 + * uniform exponential decay NDCG@5 0.015 <- 18x WORSE than none + * domain-adaptive decay NDCG@5 0.241 + * full adaptive hierarchy NDCG@5 0.260 + * + * Uniform exponential decay is not merely suboptimal — it is worse than having + * no decay at all, because it penalises stable knowledge (rarely accessed, + * heavily load-bearing) while failing to suppress stale volatile facts. Notably + * not even the full adaptive hierarchy beat switching decay off. + * + * THE FIX: make the half-life a function of how established a node is, and + * make the floor a preference rather than a cliff. + * + * T_eff = T_HALF * (1 + ln(1 + activation_count)) + * + * Frequently-retrieved nodes age slowly; nodes nothing has ever asked for age + * at the original rate. This is the spacing effect and the Lindy property in + * one line, it is monotone and log-bounded (a 10,000-activation node gets only + * a ~10x longer half-life, not a permanent exemption), and it is built from + * activation_count — which is measured, unlike `tier`, whose assignments are + * inconsistent enough to be untrustworthy here (the values node is tagged + * Episodic). + * + * The floor moves 0.05 -> 0.25. Given the evidence that no decay outperforms + * uniform decay, the honest maximum penalty for age alone is 4x, not 20x. Age + * should express a preference for the recent; it should never make a region of + * the graph structurally unreachable. + * + * Explicit per-node temporal_decay_rate still overrides lambda (77 nodes carry + * one) — that path is untouched and remains the escape hatch for content that + * genuinely should expire fast. */ +#define ENGRAM_DECAY_FLOOR 0.25 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; double lambda = (n->temporal_decay_rate > 0.0) ? n->temporal_decay_rate : ENGRAM_DECAY_LAMBDA; double age_hours = (double)age_ms / 3600000.0; - double factor = exp(-lambda * age_hours / ENGRAM_T_HALF_HOURS); - if (factor < 0.05) factor = 0.05; + double t_half = ENGRAM_T_HALF_HOURS * + (1.0 + log(1.0 + (double)n->activation_count)); + double factor = exp(-lambda * age_hours / t_half); + if (factor < ENGRAM_DECAY_FLOOR) factor = ENGRAM_DECAY_FLOOR; return factor; }