self-review 2026-08-05: stop the decay function erasing the library
Census of the live graph under the uniform 168h half-life with floor 0.05: the MEDIAN tdecay for every single node type was 0.0500 — the clamp. Memory 81% at floor, Knowledge 58%, BacklogItem 91%, Project 98%, Tag 100%. A function whose median output is its floor is not a signal, it is a constant with exceptions, and the exceptions were whatever had been touched in the last few days. What that cost: 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. Since tdecay multiplies at every hop, a 2-hop path through settled knowledge compounded to 0.0025: those regions were not disfavoured, they were unreachable. The decay function was erasing the accumulated library in favour of whatever arrived last night. External corroboration — arXiv:2604.26970 measures retrieval under decay regimes: no temporal weighting NDCG@5 0.274, uniform exponential decay 0.015. Uniform decay is 18x WORSE than no decay, because it penalises stable knowledge while failing to suppress stale volatile facts. Not even their full adaptive hierarchy (0.260) beat switching decay off. Half-life is now scaled by how established a node is: T_eff = T_HALF * (1 + ln(1 + activation_count)) The spacing effect and the Lindy property in one line — monotone, log-bounded (a 10,000-activation node earns ~10x, never a permanent exemption), and built on activation_count, which is measured, unlike tier, whose assignments are too inconsistent to trust (the values node is tagged Episodic). Floor 0.05 -> 0.25. Given 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 must never make a region of the graph structurally unreachable. Effect: well-established Knowledge median tdecay 0.773 vs rarely-activated 0.417 — the frequency signal now does work where the old function returned its clamp for both. Values recover 0.05 -> 0.25 (the two frequently-touched ones to 0.79). Verified live: VBD whitepaper, component taxonomy and CGI now activate on a values query. Per-node temporal_decay_rate override untouched.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user