diff --git a/lang/releases/v1.0.0-20260501/el_runtime.c b/lang/releases/v1.0.0-20260501/el_runtime.c index 04f0948..1e92ea8 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.c +++ b/lang/releases/v1.0.0-20260501/el_runtime.c @@ -5698,6 +5698,28 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, * Source: act-r.psy.cmu.edu/.../894Cogsci09-Lebiere-Best.pdf */ #define ENGRAM_STI_TS 120.0 +/* Carry-over occupancy inhibition (2026-07-26 self-review). + * The STI multiplier above only runs in the REACHED branch of Pass 2. + * A node carried over WITHOUT being reached (Pass 4½ below) kept + * w = wm_anchor * keep, and for a node whose base-level was inflated + * during the pre-07-25 unconditional-reinforcement era, keep ≈ 1.0 for + * days — the anchor weight is re-emitted verbatim forever. Observed + * live: wm_top0_streak = 1407 heartbeats (~23 h), one node frozen at + * its anchor 0.589 while every reached candidate rotated at the 0.10 + * breakthrough floor. Deterministic argmax over a quasi-static score + * fixates regardless of any inhibition applied only to the reached set + * (Morita et al. 2021, citing Lebiere & Best 2009). + * Fix: key inhibition on OCCUPANCY, not retrieval recency — multiply + * the carried weight by the same d_s=1 closed form over hold time + * m(t_h) = t_c / (t_c + t_h), t_h = seconds since last_activated + * (carry-over nodes are deliberately never reinforced, so + * last_activated marks when the node last EARNED its slot). Power-law + * self-healing: a node re-reached by any future activation is + * re-scored fresh in Pass 2 and re-anchors. t_c = 3600 s → a carried + * node keeps ~92% after 5 min, 50% after 1 h, ~4% after 23 h. Unlike + * m(t_n), this cannot saturate to no-op while the node camps. */ +#define ENGRAM_CARRY_TC 3600.0 + /* qsort comparator — descending double, used by WM cap enforcement. */ static int engram_cmp_double_desc(const void* a, const void* b) { double da = *(const double*)a; @@ -6895,8 +6917,16 @@ void engram_strengthen(el_val_t node_id) { n->activation_count++; n->last_activated = engram_now_ms(); n->updated_at = n->last_activated; - /* Explicit strengthen is a presentation too (2026-07-22 self-review). */ - engram_bll_record_access(n, n->last_activated); + /* 2026-07-26 self-review: REMOVED the BLL access record added on + * 2026-07-22 ("explicit strengthen is a presentation too"). The + * 2026-07-25 STI multiplier reads the same access ring — so an + * explicit strengthen crushed the strengthened node's promotion + * score by ×t_n/(t_n+120) for the next ~2 minutes. The awareness + * loop strengthens exactly when a node NEWLY reaches WM top + * (novelty gating); recording an access here made that + * reinforcement self-defeating. Salience/activation_count bumps + * above carry the reinforcement; the access ring stays reserved + * for genuine retrieval events (promotions in engram_activate). */ } void engram_forget(el_val_t node_id) { @@ -7989,7 +8019,14 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { } else { double keep = 1.0 / (1.0 + exp(-(B - ENGRAM_BLL_TAU) / ENGRAM_BLL_S)); - double w = anchor * keep; + /* Occupancy inhibition (2026-07-26): decay the carried + * weight with hold time so an unreached incumbent cannot + * hold its anchor verbatim indefinitely. See + * ENGRAM_CARRY_TC comment for derivation. */ + double hold_s = (double)(now_ms - cn->last_activated) / 1000.0; + if (hold_s < 0.0) hold_s = 0.0; + double occ = ENGRAM_CARRY_TC / (ENGRAM_CARRY_TC + hold_s); + double w = anchor * keep * occ; cn->working_memory_weight = (w < 0.01) ? 0.0 : w; } } else { @@ -8361,6 +8398,10 @@ el_val_t engram_load(el_val_t path) { free(g->nodes[i].id); free(g->nodes[i].content); free(g->nodes[i].node_type); free(g->nodes[i].label); free(g->nodes[i].tier); free(g->nodes[i].tags); free(g->nodes[i].metadata); + /* 2026-07-26 self-review: emb was the one heap field not freed + * here — ~3 KB leaked per embedded node per reload (~11 MB per + * reload at 3.7k embedded). forget/prune already free it. */ + free(g->nodes[i].emb); g->nodes[i].emb = NULL; g->nodes[i].emb_dim = 0; } g->node_count = 0; for (int64_t i = 0; i < g->edge_count; i++) { @@ -9065,7 +9106,13 @@ el_val_t engram_wm_top_json(el_val_t n_v) { EngramNode* n = &g->nodes[idx[k]]; if (k > 0) jb_putc(&b, ','); jb_putc(&b, '{'); - jb_puts(&b, "\"label\":"); + /* 2026-07-26 self-review: id was never emitted here, so the + * awareness heartbeat's wm_top0_streak compared ""=="" and + * incremented unconditionally — the streak metric measured + * uptime, not fixation. */ + jb_puts(&b, "\"id\":"); + jb_emit_escaped(&b, n->id ? n->id : ""); + jb_puts(&b, ",\"label\":"); jb_emit_escaped(&b, n->label ? n->label : ""); jb_puts(&b, ",\"node_type\":"); jb_emit_escaped(&b, n->node_type ? n->node_type : "");