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:
2026-08-05 08:45:52 -05:00
parent 3bf44dee2d
commit 3d05e0c2a9
+66 -2
View File
@@ -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;
}