self-review 2026-07-26: fix WM frozen-anchor fixation, strengthen self-inhibition, load-path emb leak

- Carry-over branch: occupancy inhibition m = t_c/(t_c+t_hold), t_c=3600s
  (ENGRAM_CARRY_TC). An unreached incumbent held its wm_anchor verbatim
  (keep~1.0 for BLL inflated in the pre-07-25 era) — observed 23h at WM
  top while every reached node rotated at the 0.10 breakthrough floor.
  STI only runs in the reached branch; inhibition must key on occupancy,
  not retrieval recency (Morita 2021 / Lebiere & Best 2009).
- engram_strengthen: drop the 07-22 BLL access record — the 07-25 STI
  multiplier reads the same ring, so novelty reinforcement self-inhibited
  its target for ~2 minutes.
- engram_load reset: free n->emb (~3KB/embedded node leaked per reload).
- engram_wm_top_json: emit id — its absence made the heartbeat's
  wm_top0_streak compare ""=="" and measure uptime, not fixation.
This commit is contained in:
2026-07-26 08:40:49 -05:00
parent 227f158a05
commit 11dc138a93
+51 -4
View File
@@ -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 : "");