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;