From 05e5d3c40235c8ea433f5d909cdfea19c0e13fa9 Mon Sep 17 00:00:00 2001 From: bigmerge Date: Sat, 15 Aug 2026 08:44:37 -0500 Subject: [PATCH] self-review 2026-08-15: consolidate the strongest Hebbian candidate, not the lowest-hash one The link-formation scan walked candidate slots ascending and stopped at ENGRAM_HEBB_LINK_PER_CALL (2). Slot index is a hash of the node id pair, so whenever more than two candidates cleared LINK_MIN in the same call, the two consolidated were the two with the lowest hash and a stronger association waited - indefinitely, since the scan restarts from slot 0 every call while the leader decays at ENGRAM_HEBB_DECAY. Measured 08-13..08-15: hebb_cand_max peaked at 0.4963, 3.3x LINK_MIN, during a ~14h stretch of continuous qualification at the 2/call cap. Same defect the 2026-08-02 review named and fixed for breakthrough weights (index order is not a cognitive criterion), never carried across to the one path that writes permanent structure - and there is no pruning path, so growth is one-way. Selection pressure matters most where the result is irreversible. No-op when <=2 candidates qualify; picks the best when more do. --- lang/releases/v1.0.0-20260501/el_runtime.c | 64 ++++++++++++++++++++-- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/lang/releases/v1.0.0-20260501/el_runtime.c b/lang/releases/v1.0.0-20260501/el_runtime.c index cb5a70c..f4d157b 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.c +++ b/lang/releases/v1.0.0-20260501/el_runtime.c @@ -9775,11 +9775,65 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { } int64_t hebb_edge_cap = (int64_t)((double)g->edge_count * ENGRAM_HEBB_LINK_MAX_FRAC); - for (int s = 0; s < ENGRAM_HEBB_CAND_SLOTS - && formed < ENGRAM_HEBB_LINK_PER_CALL - && hebb_edge_total < hebb_edge_cap; s++) { - EgHebbCand* c = &_eg_hebb_cand[s]; - if (!c->a || c->score < ENGRAM_HEBB_LINK_MIN) continue; + /* Select the STRONGEST qualifying candidates, not the first + * ones in hash-slot order (2026-08-15 self-review). + * + * This loop used to scan slots ascending and stop at + * ENGRAM_HEBB_LINK_PER_CALL (2). Slot index is + * engram_id_hash(lo)*1000003 ^ engram_id_hash(hi) mod 8192 — + * i.e. arbitrary with respect to association strength. So + * whenever more than two candidates cleared LINK_MIN in the + * same call, the two that got consolidated were the two with + * the LOWEST HASH, and a stronger association simply waited. + * It waited indefinitely, not just one round: the scan + * restarts from slot 0 every call, so a low-slot candidate + * that re-qualifies keeps winning the same race, while the + * leader decays at ENGRAM_HEBB_DECAY the whole time. + * + * Measured on this store over the 08-13→08-15 window: + * hebb_cand_max peaked at 0.4963 (08-14 06:57) — 3.3x + * LINK_MIN — during a ~14h stretch when candidates were + * qualifying continuously and links were being formed at the + * ≤2/call cap. The system was consolidating the associations + * it happened to reach first, while the association it had + * most strongly learned sat unconsolidated. + * + * This is the same defect the 2026-08-02 self-review named + * and fixed for breakthrough weights — "the tie-break at the + * cutoff degenerated to node-array index order, which is not + * a cognitive criterion" — but that fix was never carried + * across to link formation, which is the one path that writes + * PERMANENT structure. A wrong breakthrough costs one WM slot + * for one call; a wrong consolidation is an edge that never + * goes away (ENGRAM_HEBB_LINK_MAX_FRAC notes there is no + * pruning path — growth is one-way). Selection pressure + * matters most exactly where the result is irreversible. + * + * Cost: PER_CALL(2) x 8192 comparisons of a double, against an + * O(edge_count) relation scan (37k+) immediately above and an + * O(edge_count) eg_edge_exists_between per edge formed. Noise. + * + * Invalid winners (node deleted, edge already present) are + * cleared and do NOT consume one of the two slots — same as + * the old `continue`. Clearing strictly shrinks the candidate + * set, so the retry loop always terminates. */ + while (formed < ENGRAM_HEBB_LINK_PER_CALL + && hebb_edge_total < hebb_edge_cap) { + int best_s = -1; + double best_score = 0.0; + for (int s = 0; s < ENGRAM_HEBB_CAND_SLOTS; s++) { + EgHebbCand* cs = &_eg_hebb_cand[s]; + if (!cs->a) continue; + if (cs->score < ENGRAM_HEBB_LINK_MIN) continue; + /* strict > keeps the lowest slot on an exact tie, so + * selection stays deterministic across runs */ + if (best_s < 0 || cs->score > best_score) { + best_score = cs->score; + best_s = s; + } + } + if (best_s < 0) break; /* nothing qualifies this call */ + EgHebbCand* c = &_eg_hebb_cand[best_s]; if (engram_idmap_get(g, c->a) < 0 || engram_idmap_get(g, c->b) < 0) { /* node gone */ eg_hebb_slot_clear(c); continue;