self-review 2026-08-02: bound the WM breakthrough storm; stop punishing semantic relevance for recency

Working memory was thrashing behind a healthy-looking gauge. wm_active sat
at 22-24 while breakthroughs ran 661-903 and evictions 485-717 PER 60s tick
- roughly 825-1125 nodes cycling in 5-call lockstep.

Root cause: the breakthrough path was an anti-starvation mechanism that reset
its own counter on firing, with no budget and no refractory. A node failing
its type threshold 5 times was force-promoted at exactly 0.10 and had its
suppression_count reset to 0, so it immediately restarted the identical
climb. Since BREAKTHROUGH_WEIGHT (0.10) > WM_FLOOR (0.05), every one of them
cleared the admission floor and entered the rank contest tied at 0.10, where
the tie-break degenerated to node-array index order. Cap-evicted nodes are
skipped by retrieval reinforcement, so they never got an access_ts record and
the STI inhibition-of-return damper never applied to them. That closed the
loop: re-suppressed, completely unmarked, forever.

An anti-starvation rule that resets its own counter without a bound is not a
fairness valve, it is an oscillator.

Fixes in engram_activate Pass 2:
- ENGRAM_BREAKTHROUGH_BUDGET (WM_CAP/4 = 6) caps intrusive thoughts per call.
- ENGRAM_BREAKTHROUGH_COOLDOWN (55) via NEGATIVE suppression_count. The field
  already serializes as %d and parses through eg_get_int_field, so negatives
  round-trip through snapshots with no struct or format change.
- Blocked breakthroughs no longer reset the counter; it saturates so a starved
  node surfaces on a later call instead of restarting from zero.
- Graded breakthrough weight by nearness to own threshold, so the rank
  tie-break is cognitive rather than insertion order. Invariant preserved:
  WM_FLOOR < weight < min(type_threshold).

Also: moved the additive cosine term AFTER the STI multiplier. It was applied
before, so an incumbent re-reached 30s later took t_n/(t_n+120) = 0.2x, which
cut the semantic term's ceiling from 0.20 to 0.04 - below every per-type
threshold. Meaning-match was being punished for having been recently useful.
Inhibition-of-return should rotate the structural score, not the semantic one.

Also: _eg_act_wm_evicted counted 3 of 5 eviction paths. The two carry-over
paths were silent, so the reported rate was an undercount of unknown
magnitude - while being used to diagnose an eviction pathology. All five now
increment.

Also: route_sync returned {"nodes":[],"edges":[]} when the snapshot export
failed. The soul's sync_ok check only tests for "" and "{}", so that
placeholder passed as a healthy sync: last_sync_ok_ts stamped, sync_age_ms
green, sync_empty never fired, added:0 forever. A broken sync was
indistinguishable from a quiet healthy one - the exact class this route was
added to fix. Returns a real error now.

Verified live (boot 20 vs boot 19): breakthroughs 661-903 -> 36/tick,
evictions 485-717 -> 12-46/tick against a counter that now covers more paths,
wm_active unchanged at 22-24, wm_avg_weight 0.138-0.273 -> 0.186-0.446.
Working memory is holding strong nodes instead of breakthrough-floor filler.
This commit is contained in:
2026-08-02 08:48:59 -05:00
parent 7f03876e26
commit 005e84e5d3
4 changed files with 164 additions and 51 deletions
+102 -18
View File
@@ -5633,6 +5633,43 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
* exceed the floor, so naturally-promoted nodes survive multiple decay cycles.
* Invariant maintained: BREAKTHROUGH_WEIGHT < min(type_thresholds). */
#define ENGRAM_BREAKTHROUGH_WEIGHT 0.10
/* ENGRAM_BREAKTHROUGH_BUDGET / ENGRAM_BREAKTHROUGH_COOLDOWN (2026-08-02
* self-review): the breakthrough path was an unbounded, self-resetting loop.
* Every reached node failing its type threshold incremented suppression_count;
* on the 5th failure it was force-promoted at exactly 0.10 AND had its counter
* reset to 0 so it re-entered the identical cycle immediately. Because
* BREAKTHROUGH_WEIGHT (0.10) > WM_FLOOR (0.05), all of them cleared the
* absolute floor and entered the rank contest tied at 0.10, where all but a
* handful were evicted by ENGRAM_WM_CAP. Evicted nodes get no access_ts
* record (Pass 6 skips wm_weights<=0), so the short-term inhibition-of-return
* damper at ENGRAM_STI_TS never applied to them and they were re-suppressed
* completely unmarked. Steady state: N_suppressed/5 breakthroughs per call,
* essentially all of them evicted the same call.
*
* Live telemetry 2026-08-02 (boot 19, uptime 23h48m): breakthroughs_delta
* 661903 and wm_evicted_delta 485717 PER 60s heartbeat against wm_active
* pinned at 2224. At ~4 activate calls/minute that is ~165225 breakthroughs
* per call ~8251125 nodes cycling in 5-call lockstep. Working memory was
* not remembering; it was thrashing, and the churn drowned genuine promotion.
*
* Two bounds, both required:
* BUDGET at most WM_CAP/4 (6) intrusive thoughts may surface per call.
* Breakthrough is meant to be an occasional intrusive thought,
* not a stampede; it can never again exceed a quarter of WM.
* COOLDOWN on breakthrough, suppression_count is set NEGATIVE rather than
* 0, so the node needs COOLDOWN+SUPPRESSION_BREAKTHROUGH further
* suppressions before it may surface again (~60 calls 15 min at
* the current cadence, vs 5 calls 75s before). This is
* inhibition-of-return applied to the breakthrough path, matching
* the STI damper already applied to the natural path. Stored in
* the existing int32_t field serialized as %d and parsed via
* eg_get_int_field, so negative values round-trip through
* snapshots without a struct or format change.
* When the budget or cooldown blocks a breakthrough, suppression_count is NOT
* reset it saturates, so a starved node surfaces on a later call rather than
* restarting its climb. */
#define ENGRAM_BREAKTHROUGH_BUDGET (ENGRAM_WM_CAP / 4)
#define ENGRAM_BREAKTHROUGH_COOLDOWN 55
/* ENGRAM_WM_CAP: hard limit on concurrent working-memory nodes (2026-06-30
* self-review, porting fix from self-review 2026-06-26 branch). Without this,
* broad curiosity seeds like "knowledge" promote 500+ nodes simultaneously
@@ -6032,7 +6069,12 @@ static int64_t _eg_embed_breaker_until = 0;
* between) missed nearly every eviction/breakthrough event. Consumers wanting
* rates keep the previous reading and diff. Restart legitimately resets to 0. */
static int64_t _eg_act_breakthroughs = 0; /* forced promotions at the floor, cumulative */
static int64_t _eg_act_wm_evicted = 0; /* Pass 4 over-cap evictions, cumulative */
static int64_t _eg_act_wm_evicted = 0; /* ALL WM evictions, cumulative (see below) */
/* 2026-08-02 self-review: this counted only the three Pass 4 / Pass 5 floor
* and rank paths. The two carry-over eviction paths (base-level below τ, and
* decayed weight below WM_FLOOR) were silent, so the reported eviction rate
* was an undercount of unknown magnitude which mattered precisely while
* diagnosing the breakthrough storm. All five paths now increment. */
static int64_t engram_now_ms(void); /* defined in the store section below */
@@ -8006,6 +8048,8 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
free(best_bg); free(best_hops); free(reached); free(seeds);
free(fr); free(inhibition); free(cosq); return out;
}
/* Per-call breakthrough budget (2026-08-02) — see ENGRAM_BREAKTHROUGH_BUDGET. */
int64_t bt_budget = ENGRAM_BREAKTHROUGH_BUDGET;
for (int64_t i = 0; i < g->node_count; i++) {
if (!reached[i] || best_bg[i] <= 0.0) continue;
EngramNode* n = &g->nodes[i];
@@ -8043,21 +8087,10 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
if (inh > 1.0) inh = 1.0;
double suppress = 1.0 - (1.0 - ENGRAM_INHIBITION_FACTOR) * inh;
raw_wm *= suppress;
/* Additive semantic-relevance term (2026-07-24, bl-b2d1c944):
* shift-and-floor at S0 nomic-embed scores unrelated pairs
* 0.40.5, so raw cosine in a weighted sum would be a constant
* bias swamping the decayed base-level signal. Above S0 the term
* ramps 0 WM_WEIGHT, breaking ties among structurally equivalent
* candidates in favor of nodes that mean what the query means. */
if (cosq && cosq[i] > ENGRAM_EMBED_S0) {
raw_wm += ENGRAM_EMBED_WM_WEIGHT
* (cosq[i] - ENGRAM_EMBED_S0) / (1.0 - ENGRAM_EMBED_S0);
}
/* Short-term inhibition-of-return (2026-07-25, Lebiere-Best):
* damp by t_n/(t_n + t_s) where t_n = seconds since the most
* recent recorded access (WM promotion / strengthen). Applied
* after the cosine term so the WHOLE score rotates a node that
* just held a WM slot yields it even to semantically weaker
* recent recorded access (WM promotion / strengthen). A node that
* just held a WM slot yields it even to structurally stronger
* competitors, and recovers as t_n grows. access_ts is recorded
* at promotion, so persistent WM residents self-inhibit. Nodes
* with no access history (never promoted) are uninhibited.
@@ -8070,6 +8103,27 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
if (sti_tn < 0.1) sti_tn = 0.1;
raw_wm *= sti_tn / (sti_tn + ENGRAM_STI_TS);
}
/* Additive semantic-relevance term (2026-07-24, bl-b2d1c944):
* shift-and-floor at S0 nomic-embed scores unrelated pairs
* 0.40.5, so raw cosine in a weighted sum would be a constant
* bias swamping the decayed base-level signal. Above S0 the term
* ramps 0 WM_WEIGHT, breaking ties among structurally equivalent
* candidates in favor of nodes that mean what the query means.
*
* MOVED AFTER the STI damper (2026-08-02 self-review). It used to
* be added BEFORE, so the recency multiplier scaled the semantic
* term too: an incumbent re-reached 30s later took t_n/(t_n+120)
* = 0.2×, cutting the cosine term's ceiling from 0.20 to 0.04
* below every per-type threshold (0.150.40). Meaning-match was
* being punished for having been recently useful. Inhibition-of-
* return should rotate the STRUCTURAL score (what the graph
* dragged in), not the semantic one (what the query actually
* means); relevance to the current query is not stale merely
* because the node was in WM a moment ago. */
if (cosq && cosq[i] > ENGRAM_EMBED_S0) {
raw_wm += ENGRAM_EMBED_WM_WEIGHT
* (cosq[i] - ENGRAM_EMBED_S0) / (1.0 - ENGRAM_EMBED_S0);
}
/* Threshold gate: must exceed per-type threshold to enter working
* memory. Type threshold replaces the old flat 0.2 filter. */
if (raw_wm >= type_threshold) {
@@ -8077,14 +8131,41 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
if (n->suppression_count > 0) n->suppression_count = 0;
} else {
/* Node didn't make it through — increment suppression counter.
* After N consecutive suppressions: force breakthrough. */
* After N consecutive suppressions it MAY force a breakthrough,
* subject to the per-call budget and the negative-count cooldown
* (2026-08-02 see ENGRAM_BREAKTHROUGH_BUDGET/_COOLDOWN). */
n->suppression_count++;
if (n->suppression_count >= ENGRAM_SUPPRESSION_BREAKTHROUGH) {
wm_weights[i] = ENGRAM_BREAKTHROUGH_WEIGHT;
n->suppression_count = 0;
if (n->suppression_count >= ENGRAM_SUPPRESSION_BREAKTHROUGH
&& bt_budget > 0) {
/* Graded breakthrough weight (2026-08-02): previously every
* breakthrough landed on exactly ENGRAM_BREAKTHROUGH_WEIGHT,
* so hundreds tied at 0.10 and the rank-cap tie-break at the
* cutoff degenerated to node-array index order i.e. whoever
* was inserted earliest won, which is not a cognitive
* criterion. Scale within ±10% by how close the node came to
* its own threshold, so a near-miss outranks a node that was
* nowhere near. Stays strictly below min(type_threshold)
* (0.15) and strictly above ENGRAM_WM_FLOOR (0.05), which is
* the invariant ENGRAM_BREAKTHROUGH_WEIGHT documents. */
double near = (type_threshold > 0.0)
? (raw_wm / type_threshold) : 0.0;
if (near < 0.0) near = 0.0;
if (near > 1.0) near = 1.0;
wm_weights[i] = ENGRAM_BREAKTHROUGH_WEIGHT
* (0.9 + 0.2 * near);
/* Negative = cooldown. Must climb back through the cooldown
* before it can breach again. */
n->suppression_count = -ENGRAM_BREAKTHROUGH_COOLDOWN;
bt_budget--;
_eg_act_breakthroughs++;
} else {
wm_weights[i] = 0.0;
/* Budget-starved or cooling down: do NOT reset the counter —
* let it saturate so the node surfaces on a later call rather
* than restarting its climb from zero. Cap the ceiling so the
* int32 cannot drift unbounded over a long uptime. */
if (n->suppression_count > ENGRAM_SUPPRESSION_BREAKTHROUGH * 4)
n->suppression_count = ENGRAM_SUPPRESSION_BREAKTHROUGH * 4;
}
}
}
@@ -8217,6 +8298,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
if (B < ENGRAM_BLL_TAU) {
cn->working_memory_weight = 0.0;
cn->wm_anchor = 0.0; /* keep anchor coherent with eviction */
_eg_act_wm_evicted++; /* was uncounted before 2026-08-02 */
} else {
double keep = 1.0 / (1.0 + exp(-(B - ENGRAM_BLL_TAU)
/ ENGRAM_BLL_S));
@@ -8233,6 +8315,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
if (w < ENGRAM_WM_FLOOR) {
cn->working_memory_weight = 0.0;
cn->wm_anchor = 0.0;
_eg_act_wm_evicted++; /* was uncounted before 2026-08-02 */
} else {
cn->working_memory_weight = w;
}
@@ -8303,6 +8386,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
}
n->working_memory_weight = 0.0; /* evict: over global cap */
n->wm_anchor = 0.0; /* keep anchor coherent */
_eg_act_wm_evicted++; /* was uncounted before 2026-08-02 */
}
}
/* If malloc failed, skip — WM over cap this call, no data corruption. */