self-review 2026-07-30: WM absolute admission floor + anchor coherence + centroid new-entrant gate

Working memory was pinned saturated (24/24, wm_saturated:1 on every
heartbeat) because every cap path only trimmed the population down TO
the cap — rank-based eviction guarantees a full WM whenever >=24 nodes
hold any weight, so sub-cap fill was unreachable and the saturation
flag carried no information.

- ENGRAM_WM_FLOOR 0.05: absolute admission bar (Soar WM forgetting,
  Derbinsky & Laird ICCM 2012 — removal by absolute threshold, not
  rank) applied in Pass 4, carry-over, Pass 5, and load-cap. Fill can
  now drain below 24 during quiet periods.
- Zero wm_anchor at every eviction site: stale anchors on evicted
  nodes were a latent resurrection bug.
- Context centroid folds only NEW WM entrants: incumbents re-promoted
  every scan no longer re-entrench the centroid each call, breaking
  the WM->centroid->e_eff->re-selection positive feedback (fixation
  driver behind the wm_top0_streak=1407 incident).

Verified live: wm_active 3->22->23, wm_saturated:0 post-restart.
This commit is contained in:
2026-07-30 08:45:15 -05:00
parent 6ebe3d0d66
commit 7f66529510
+212 -9
View File
@@ -5642,6 +5642,22 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
* context while preventing flooding. Enforced in Pass 4 (per-call) and Pass 5
* (global across prior-promoted nodes). */
#define ENGRAM_WM_CAP 24
/* ENGRAM_WM_FLOOR: absolute admission floor for a working-memory slot
* (2026-07-30 self-review). Before this, Pass 4/Pass 5/load-cap only ever
* trimmed the WM population down TO the cap and never below it rank-based
* eviction guarantees the cap is filled whenever 24 nodes hold any nonzero
* weight, so wm_active was pinned at 24/24 and wm_saturated:1 was
* definitionally true on every heartbeat (carried no information). Soar's WM
* forgetting (Derbinsky & Laird, ICCM 2012) removes elements by comparing
* activation to an ABSOLUTE threshold θ, independent of how many other
* elements exist fill below capacity is a reachable, meaningful state
* ("low cognitive load"). This floor is the weight-domain analogue of that θ:
* any slot whose weight sinks below it is dropped even when WM is under cap.
* Value 0.05 = the lowest per-type promotion threshold (Safety/DharmaSelf in
* engram_type_threshold) and the existing boot-time launder floor, and sits
* below ENGRAM_BREAKTHROUGH_WEIGHT (0.10) so intrusive-thought breakthroughs
* still surface. Applied: Pass 4, carry-over, Pass 5, load-cap. */
#define ENGRAM_WM_FLOOR 0.05
#define ENGRAM_INHIBITION_FACTOR 0.1
/* ── ACT-R / Petrov hybrid base-level learning (2026-07-22 self-review) ──────
@@ -5965,6 +5981,40 @@ static void engram_bll_parse_access(EngramNode* nn, const char* s) {
#define ENGRAM_EMBED_BREAKER_LIMIT 3
#define ENGRAM_EMBED_BREAKER_COOLDOWN_MS 300000
/* ── Context centroid (2026-07-29 self-review) ───────────────────────────────
* Closes the last gap from the 2026-07-21 decay/embedding brief: cosine was
* computed against the per-call query embedding ONLY, so every curiosity scan
* was semantically memoryless the 4 rotating seed phrases fully determined
* what ignited, with zero continuity from what the system actually touched.
*
* Mechanism (brief spec): a running EMA centroid over touch embeddings,
* c normalize(μ·c + (1μ)·e_touch), μ = ENGRAM_CTX_MU
* where a "touch" is (a) the query embedding each activate call and (b) the
* embeddings of up to ENGRAM_CTX_TOUCH_MAX top WM-promoted survivors of that
* call promotion is the retrieval event (same rule as BLL reinforcement).
*
* Feedback-loop guard: scoring does NOT use the raw centroid. The lit
* failure mode of centroid memories (EMA of your own outputs runaway
* attractor; cf. the wm_top0_streak=1407 freeze this store already hit) is
* bounded by scoring against a query-dominant blend:
* e_eff = normalize(α·e_q + (1α)·c), α = ENGRAM_CTX_QALPHA
* so the exogenous rotating seeds always contribute the majority of the
* scoring direction; the centroid is a context tint, not the signal.
*
* Observability: _eg_act_ctx_cos = cos(e_q, c) BEFORE the query is blended
* in. ~1.0 centroid aligned with current query; low context and query
* have diverged (expected at domain-rotation boundaries); -2.0 no centroid
* yet / embedder down. Exposed via engram_act_stats_json heartbeat ISE so
* drift is diagnosable from telemetry. In-memory only: context is
* short-term by definition, a restart legitimately starts cold. */
#define ENGRAM_CTX_MU 0.90
#define ENGRAM_CTX_QALPHA 0.65
#define ENGRAM_CTX_TOUCH_MAX 8
static float* _eg_ctx_c = NULL;
static int32_t _eg_ctx_dim = 0;
static double _eg_act_ctx_cos = -2.0;
static int _eg_embed_consec_fail = 0;
static int64_t _eg_embed_breaker_until = 0;
@@ -6003,6 +6053,38 @@ static double eg_cosine(const float* a, const float* b, int32_t dim) {
return dot / (sqrt(na) * sqrt(nb));
}
/* eg_ctx_blend — fold one touch embedding into the context centroid:
* c normalize(μ·c + (1μ)·e). Initializes the centroid (normalized copy)
* on first touch or dim change; silently skips degenerate vectors. */
static void eg_ctx_blend(const float* e, int32_t dim) {
if (!e || dim <= 0) return;
double ne = 0.0;
for (int32_t i = 0; i < dim; i++) ne += (double)e[i] * (double)e[i];
if (ne <= 0.0) return;
ne = sqrt(ne);
if (!_eg_ctx_c || _eg_ctx_dim != dim) {
float* c = malloc((size_t)dim * sizeof(float));
if (!c) return;
for (int32_t i = 0; i < dim; i++) c[i] = (float)((double)e[i] / ne);
free(_eg_ctx_c);
_eg_ctx_c = c;
_eg_ctx_dim = dim;
return;
}
double nc = 0.0;
for (int32_t i = 0; i < dim; i++) {
double v = ENGRAM_CTX_MU * (double)_eg_ctx_c[i]
+ (1.0 - ENGRAM_CTX_MU) * ((double)e[i] / ne);
_eg_ctx_c[i] = (float)v;
nc += v * v;
}
if (nc > 0.0) {
nc = sqrt(nc);
for (int32_t i = 0; i < dim; i++)
_eg_ctx_c[i] = (float)((double)_eg_ctx_c[i] / nc);
}
}
/* Fetch an embedding from Ollama. Returns malloc'd float[dim] or NULL.
* Truncates input to ENGRAM_EMBED_MAX_CHARS and JSON-escapes it. Honors the
* circuit breaker; a NULL return is always safe to ignore (fail-soft). */
@@ -7580,6 +7662,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
/* Reset per-call observability counters (2026-07-27). */
_eg_act_breakthroughs = 0;
_eg_act_wm_evicted = 0;
_eg_act_ctx_cos = -2.0;
/* ── Embedding backfill + query embedding (2026-07-24, bl-b2d1c944) ──
* Backfill: embed up to N un-embedded eligible nodes per call, newest
@@ -7621,21 +7704,52 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
q_emb = v; q_dim = d;
}
}
/* Per-node cosine vs the query, computed once, consumed twice: semantic
/* ── Context centroid fold-in (2026-07-29) ──────────────────────────
* Record drift BEFORE blending (cos of the query against yesterday's
* context), then fold the query in as a touch, then build the
* query-dominant effective scoring vector. See the ENGRAM_CTX_* block
* for the design and the feedback-loop guard rationale. */
float* e_eff = NULL;
if (q_emb) {
if (_eg_ctx_c && _eg_ctx_dim == q_dim)
_eg_act_ctx_cos = eg_cosine(q_emb, _eg_ctx_c, q_dim);
eg_ctx_blend(q_emb, q_dim);
if (_eg_ctx_c && _eg_ctx_dim == q_dim) {
e_eff = malloc((size_t)q_dim * sizeof(float));
if (e_eff) {
double nq = 0.0;
for (int32_t i = 0; i < q_dim; i++)
nq += (double)q_emb[i] * (double)q_emb[i];
nq = (nq > 0.0) ? sqrt(nq) : 1.0;
double nn = 0.0;
for (int32_t i = 0; i < q_dim; i++) {
double v = ENGRAM_CTX_QALPHA * ((double)q_emb[i] / nq)
+ (1.0 - ENGRAM_CTX_QALPHA) * (double)_eg_ctx_c[i];
e_eff[i] = (float)v;
nn += v * v;
}
if (nn <= 0.0) { free(e_eff); e_eff = NULL; }
}
}
}
/* Per-node cosine vs the effective query (query ⊕ context centroid;
* plain query on cold start), computed once, consumed twice: semantic
* seeding below and the additive WM term in Pass 2 (use similarity
* twice, coherently HippoRAG). cosq stays NULL when the embedder is
* unavailable; every consumer degrades to pure lexical behavior. */
double* cosq = NULL;
if (q_emb) {
const float* qv = e_eff ? e_eff : q_emb;
cosq = calloc((size_t)g->node_count, sizeof(double));
if (cosq) {
for (int64_t i = 0; i < g->node_count; i++) {
EngramNode* n = &g->nodes[i];
cosq[i] = (n->emb && n->emb_dim == q_dim)
? eg_cosine(n->emb, q_emb, q_dim) : -2.0;
? eg_cosine(n->emb, qv, q_dim) : -2.0;
}
}
}
free(e_eff); e_eff = NULL; /* only needed to fill cosq */
/* Per-node layer-1 tracking. */
double* best_bg = calloc((size_t)g->node_count, sizeof(double));
@@ -7996,6 +8110,16 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
* becomes useless. (Ported from 2026-06-26 self-review branch; observed
* 525 promoted for "knowledge", 524 at breakthrough floor 0.25, 1 natural.) */
{
/* Absolute admission floor (2026-07-30): drop sub-floor candidates
* BEFORE rank-trimming, so the cap is filled only by nodes that clear
* an absolute bar fill below ENGRAM_WM_CAP becomes reachable and
* wm_saturated becomes an informative signal. See ENGRAM_WM_FLOOR. */
for (int64_t i = 0; i < g->node_count; i++) {
if (wm_weights[i] > 0.0 && wm_weights[i] < ENGRAM_WM_FLOOR) {
wm_weights[i] = 0.0;
_eg_act_wm_evicted++;
}
}
int64_t cap_count = 0;
for (int64_t i = 0; i < g->node_count; i++) {
if (wm_weights[i] > 0.0) cap_count++;
@@ -8035,6 +8159,19 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
}
}
/* Pre-persist residency snapshot (2026-07-30): record which nodes held a
* WM slot BEFORE this call's results are written back. Used below to fold
* only NEW WM entrants into the context centroid an incumbent that gets
* re-promoted every scan no longer re-entrenches the centroid each time,
* which was the remaining positive-feedback path in the WMcentroid
* e_effre-selection loop (fixation driver; cf. wm_top0_streak=1407
* incident). NULL on OOM fold falls back to previous behavior. */
unsigned char* was_wm = malloc((size_t)g->node_count);
if (was_wm) {
for (int64_t i = 0; i < g->node_count; i++)
was_wm[i] = (g->nodes[i].working_memory_weight > 0.0) ? 1 : 0;
}
/* Persist working_memory_weight (post Pass 4) to node store.
*
* Conversational thread continuity (ENGRAM_WM_DECAY):
@@ -8070,6 +8207,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
double B = engram_bll_base_level(cn, now_ms);
if (B < ENGRAM_BLL_TAU) {
cn->working_memory_weight = 0.0;
cn->wm_anchor = 0.0; /* keep anchor coherent with eviction */
} else {
double keep = 1.0 / (1.0 + exp(-(B - ENGRAM_BLL_TAU)
/ ENGRAM_BLL_S));
@@ -8081,13 +8219,24 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
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;
/* Evict floor raised 0.01 → ENGRAM_WM_FLOOR (2026-07-30):
* one consistent absolute bar across all WM entry/exit paths. */
if (w < ENGRAM_WM_FLOOR) {
cn->working_memory_weight = 0.0;
cn->wm_anchor = 0.0;
} else {
cn->working_memory_weight = w;
}
}
} else {
g->nodes[i].working_memory_weight = wm_weights[i];
/* Anchor the promotion weight: carry-over decay above computes
* from this fixed point rather than compounding per call. */
* from this fixed point rather than compounding per call.
* Zero the anchor when the slot empties (2026-07-30): a stale
* anchor on an evicted node was a latent resurrection bug if the
* carry-over entry guard ever changes. */
if (wm_weights[i] > 0.0) g->nodes[i].wm_anchor = wm_weights[i];
else g->nodes[i].wm_anchor = 0.0;
}
}
@@ -8101,6 +8250,19 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
* activations outcompete older decayed ones. (Ported from 2026-06-26
* self-review branch.) */
{
/* Absolute admission floor, global pass (2026-07-30): see
* ENGRAM_WM_FLOOR. Sub-floor residents are dropped even when the
* global population is under cap this is what lets wm_active
* drain below 24 during quiet periods. */
for (int64_t i = 0; i < g->node_count; i++) {
EngramNode* fn = &g->nodes[i];
if (fn->working_memory_weight > 0.0 &&
fn->working_memory_weight < ENGRAM_WM_FLOOR) {
fn->working_memory_weight = 0.0;
fn->wm_anchor = 0.0;
_eg_act_wm_evicted++;
}
}
int64_t global_wm_count = 0;
for (int64_t i = 0; i < g->node_count; i++) {
if (g->nodes[i].working_memory_weight > 0.0) global_wm_count++;
@@ -8131,6 +8293,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
continue; /* fills a slot */
}
n->working_memory_weight = 0.0; /* evict: over global cap */
n->wm_anchor = 0.0; /* keep anchor coherent */
}
}
/* If malloc failed, skip — WM over cap this call, no data corruption. */
@@ -8174,7 +8337,8 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
int64_t rcount = 0;
if (!results) {
free(best_bg); free(best_hops); free(reached); free(seeds);
free(fr); free(inhibition); free(wm_weights); free(cosq); return out;
free(fr); free(inhibition); free(wm_weights); free(cosq);
free(was_wm); return out;
}
for (int64_t i = 0; i < g->node_count; i++) {
if (!reached[i]) continue;
@@ -8201,6 +8365,28 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
}
results[j + 1] = key;
}
/* ── Context centroid: fold in the touched nodes (2026-07-29) ───────
* Results are sorted promoted-first by wm_weight desc, so the first
* ENGRAM_CTX_TOUCH_MAX embedded entries with wm > 0 are exactly the
* strongest WM survivors of THIS call the same "promotion is the
* retrieval event" rule the BLL reinforcement pass uses. μ=0.9 EMA
* keeps any single scan's touches a minority contribution. */
{
int touched = 0;
for (int64_t i = 0; i < rcount && touched < ENGRAM_CTX_TOUCH_MAX; i++) {
if (results[i].wm <= 0.0) break; /* promoted block exhausted */
EngramNode* n = &g->nodes[results[i].idx];
if (!n->emb || n->emb_dim <= 0) continue;
/* New-entrant gate (2026-07-30): skip nodes that already held a
* WM slot before this call incumbents must not keep pulling
* the centroid toward themselves. Fresh topical shifts (new
* entrants + the query fold at call start) steer it instead. */
if (was_wm && was_wm[results[i].idx]) continue;
eg_ctx_blend(n->emb, n->emb_dim);
touched++;
}
}
free(was_wm);
for (int64_t i = 0; i < rcount; i++) {
el_val_t entry = el_map_new(0);
entry = el_map_set(entry, EL_STR(el_strdup("node")),
@@ -8402,6 +8588,15 @@ static const char* eg_skip_ws(const char* p) {
* the activation path. Same top-K-by-weight logic as engram_activate
* Pass 5 (Cowan 2001: WM capacity is global). */
static void eg_enforce_wm_cap_on_load(EngramStore* g) {
/* Absolute admission floor (2026-07-30): see ENGRAM_WM_FLOOR. */
for (int64_t i = 0; i < g->node_count; i++) {
EngramNode* fn = &g->nodes[i];
if (fn->working_memory_weight > 0.0 &&
fn->working_memory_weight < ENGRAM_WM_FLOOR) {
fn->working_memory_weight = 0.0;
fn->wm_anchor = 0.0;
}
}
int64_t wm_count = 0;
for (int64_t i = 0; i < g->node_count; i++) {
if (g->nodes[i].working_memory_weight > 0.0) wm_count++;
@@ -8428,6 +8623,7 @@ static void eg_enforce_wm_cap_on_load(EngramStore* g) {
if (n->working_memory_weight > cutoff) continue;
if (slots_at_cutoff > 0) { slots_at_cutoff--; continue; }
n->working_memory_weight = 0.0; /* evict: over cap at load */
n->wm_anchor = 0.0; /* keep anchor coherent */
}
}
@@ -8508,7 +8704,7 @@ el_val_t engram_load(el_val_t path) {
* continuity across a restart while stale pinned weights decay
* out over successive boots; sub-0.05 residue drops to zero. */
nn->working_memory_weight *= 0.5;
if (nn->working_memory_weight < 0.05) nn->working_memory_weight = 0.0;
if (nn->working_memory_weight < ENGRAM_WM_FLOOR) nn->working_memory_weight = 0.0;
nn->suppression_count = (int32_t)eg_get_int_field(obj, "suppression_count");
/* layer_id defaults to ENGRAM_LAYER_DEFAULT (core-identity)
* for snapshots that predate the layered schema. We can't
@@ -9218,13 +9414,20 @@ el_val_t engram_stats_json(void) {
el_val_t engram_act_stats_json(void) {
int64_t now = engram_now_ms();
int breaker_open = (now < _eg_embed_breaker_until) ? 1 : 0;
char buf[192];
char buf[256];
/* ctx_cos (2026-07-29): cos(query, context centroid) at the LAST
* activate call, measured before the query was folded in. ~1.0 =
* context aligned with current query; low = divergence (expected at
* curiosity domain-rotation boundaries); -2.0 = no centroid yet or
* embedder down. The drift gauge for the context-centroid mechanism. */
snprintf(buf, sizeof(buf),
"{\"wm_evicted\":%lld,\"breakthroughs\":%lld,"
"\"embed_breaker_open\":%d,\"embed_consec_fail\":%d}",
"\"embed_breaker_open\":%d,\"embed_consec_fail\":%d,"
"\"ctx_cos\":%.3f}",
(long long)_eg_act_wm_evicted,
(long long)_eg_act_breakthroughs,
breaker_open, _eg_embed_consec_fail);
breaker_open, _eg_embed_consec_fail,
_eg_act_ctx_cos);
return el_wrap_str(el_strdup(buf));
}