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:
@@ -9775,11 +9775,65 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
|||||||
}
|
}
|
||||||
int64_t hebb_edge_cap =
|
int64_t hebb_edge_cap =
|
||||||
(int64_t)((double)g->edge_count * ENGRAM_HEBB_LINK_MAX_FRAC);
|
(int64_t)((double)g->edge_count * ENGRAM_HEBB_LINK_MAX_FRAC);
|
||||||
for (int s = 0; s < ENGRAM_HEBB_CAND_SLOTS
|
/* Select the STRONGEST qualifying candidates, not the first
|
||||||
&& formed < ENGRAM_HEBB_LINK_PER_CALL
|
* ones in hash-slot order (2026-08-15 self-review).
|
||||||
&& hebb_edge_total < hebb_edge_cap; s++) {
|
*
|
||||||
EgHebbCand* c = &_eg_hebb_cand[s];
|
* This loop used to scan slots ascending and stop at
|
||||||
if (!c->a || c->score < ENGRAM_HEBB_LINK_MIN) continue;
|
* 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 ||
|
if (engram_idmap_get(g, c->a) < 0 ||
|
||||||
engram_idmap_get(g, c->b) < 0) { /* node gone */
|
engram_idmap_get(g, c->b) < 0) { /* node gone */
|
||||||
eg_hebb_slot_clear(c); continue;
|
eg_hebb_slot_clear(c); continue;
|
||||||
|
|||||||
Reference in New Issue
Block a user