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.
This commit is contained in:
bigmerge
2026-08-15 08:44:37 -05:00
parent 6660becfdb
commit 05e5d3c402
+59 -5
View File
@@ -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-1308-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;