diff --git a/lang/releases/v1.0.0-20260501/el_runtime.c b/lang/releases/v1.0.0-20260501/el_runtime.c index 1e92ea8..f1e62fd 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.c +++ b/lang/releases/v1.0.0-20260501/el_runtime.c @@ -5954,6 +5954,12 @@ static void engram_bll_parse_access(EngramNode* nn, const char* s) { #define ENGRAM_EMBED_SEED_K 8 #define ENGRAM_EMBED_SEED_MIN 0.60 #define ENGRAM_EMBED_BACKFILL_PER_CALL 8 +/* ENGRAM_QGATE_FLOOR: minimum propagation multiplier for an EMBEDDED target + * node with zero/negative query similarity. Query-aware spreading gate + * (arXiv:2606.30133) adapted for partial embedding coverage — see the + * propagation loop in engram_activate. 0.25 damps semantically unrelated + * branches ~4x without severing them. Unembedded targets are ungated. */ +#define ENGRAM_QGATE_FLOOR 0.25 #define ENGRAM_EMBED_MAX_CHARS 2000 #define ENGRAM_EMBED_TIMEOUT_MS 4000L #define ENGRAM_EMBED_BREAKER_LIMIT 3 @@ -5962,6 +5968,15 @@ static void engram_bll_parse_access(EngramNode* nn, const char* s) { static int _eg_embed_consec_fail = 0; static int64_t _eg_embed_breaker_until = 0; +/* ── Activation observability counters (2026-07-27 self-review) ────────────── + * The executive-filter pathologies this system has repeatedly debugged by + * inference (WM flooded with breakthrough-floor nodes, silent cap evictions, + * embedder wedged behind the breaker) were all invisible: no ISE, no stats + * field. These per-call counters record what the LAST engram_activate did; + * engram_act_stats_json() exposes them so the soul heartbeat can emit them. */ +static int64_t _eg_act_breakthroughs = 0; /* forced promotions at the floor */ +static int64_t _eg_act_wm_evicted = 0; /* Pass 4 over-cap evictions */ + static int64_t engram_now_ms(void); /* defined in the store section below */ static const char* eg_embed_url(void) { @@ -7562,6 +7577,10 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { int64_t now_ms = engram_now_ms(); + /* Reset per-call observability counters (2026-07-27). */ + _eg_act_breakthroughs = 0; + _eg_act_wm_evicted = 0; + /* ── Embedding backfill + query embedding (2026-07-24, bl-b2d1c944) ── * Backfill: embed up to N un-embedded eligible nodes per call, newest * first (append order ≈ creation order), so fresh content is semantic- @@ -7780,8 +7799,31 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { double tbonus = engram_temporal_proximity_bonus(on->created_at, seed_epoch); double tdecay = engram_temporal_decay(on, now_ms); double dampen = engram_activation_dampen(on); + /* ── Query-aware propagation gate (2026-07-27 self-review) ── + * Prior behavior was "query-blind" spreading: the query chose + * the seeds, but propagation depended only on graph structure, + * so high-degree hubs relayed activation into branches with no + * semantic relation to the query. Per arXiv:2606.30133, gating + * each increment by the TARGET node's query similarity + * (sigma(v) = max(cos(e_v, e_q), 0)) prunes low-information + * branches at every hop (+3.6..+7.4 F1 over uniform spreading, + * 1.5-4.9x faster via a shrinking working set). + * + * Adaptation for partial embedding coverage: the paper skips + * unembedded targets outright, but only eligible non-ISE/Tag + * nodes carry embeddings here — a hard gate would sever purely + * lexical/structural pathways. So: embedded targets get a soft + * gate FLOOR + (1-FLOOR)*clip(cos) (dissimilar nodes damped + * ~4x, never killed); unembedded targets pass ungated (no + * information, no penalty); cosq == NULL (embedder down) means + * no gating at all — same graceful degradation as seeding. */ + double qgate = 1.0; + if (cosq && cosq[oi] > -1.5) { + double c = cosq[oi] > 0.0 ? cosq[oi] : 0.0; + qgate = ENGRAM_QGATE_FLOOR + (1.0 - ENGRAM_QGATE_FLOOR) * c; + } double new_act = f.act * e->weight * SPREAD_DECAY * (1.0 + tbonus) - * tdecay * dampen; + * tdecay * dampen * qgate; /* Firing threshold per classic spreading-activation: sub-threshold * activation neither updates the target nor enqueues it, so weak * signals die out instead of flooding the whole graph with tiny @@ -7907,6 +7949,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { if (n->suppression_count >= ENGRAM_SUPPRESSION_BREAKTHROUGH) { wm_weights[i] = ENGRAM_BREAKTHROUGH_WEIGHT; n->suppression_count = 0; + _eg_act_breakthroughs++; } else { wm_weights[i] = 0.0; } @@ -7975,6 +8018,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { continue; /* fills a slot */ } wm_weights[i] = 0.0; /* over cap: evict */ + _eg_act_wm_evicted++; } } /* If malloc failed, skip cap — WM unbounded this call, no corruption. */ @@ -9132,17 +9176,45 @@ el_val_t engram_stats_json(void) { EngramStore* g = engram_get(); /* embedded_count: how far the lazy backfill has progressed. The single * observable that tells the daily self-review whether semantic - * activation is actually accumulating coverage. (2026-07-24) */ - int64_t embedded = 0; + * activation is actually accumulating coverage. (2026-07-24) + * + * embed_eligible_count (2026-07-27): embedded_count alone misleads — + * ~70%+ of the store is ISE/Tag/short-content nodes that are permanently + * ineligible for embedding, so raw embedded/node_count reads as "~30% + * coverage, something is broken" when eligible coverage may be complete. + * This exact misdiagnosis happened in today's self-review. Report the + * true denominator so coverage = embedded_count / embed_eligible_count. */ + int64_t embedded = 0, eligible = 0; for (int64_t i = 0; i < g->node_count; i++) { if (g->nodes[i].emb) embedded++; + if (eg_embed_eligible(&g->nodes[i])) eligible++; } - char buf[192]; + char buf[256]; snprintf(buf, sizeof(buf), "{\"node_count\":%lld,\"edge_count\":%lld,\"layer_count\":%zu," - "\"embedded_count\":%lld}", + "\"embedded_count\":%lld,\"embed_eligible_count\":%lld}", (long long)g->node_count, (long long)g->edge_count, g->layer_count, - (long long)embedded); + (long long)embedded, (long long)eligible); + return el_wrap_str(el_strdup(buf)); +} + +/* engram_act_stats_json — per-call activation observability + embedder + * breaker state. (2026-07-27 self-review.) wm_evicted/breakthroughs describe + * the LAST engram_activate call on this store; embed_breaker_open=1 means + * eg_embed_fetch is currently refusing calls (semantic activation silently + * degraded to lexical until the cooldown expires). The soul heartbeat folds + * this into its ISE so the pathologies are diagnosable from telemetry + * instead of inferred from wm_avg_weight hovering at the floor. */ +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]; + snprintf(buf, sizeof(buf), + "{\"wm_evicted\":%lld,\"breakthroughs\":%lld," + "\"embed_breaker_open\":%d,\"embed_consec_fail\":%d}", + (long long)_eg_act_wm_evicted, + (long long)_eg_act_breakthroughs, + breaker_open, _eg_embed_consec_fail); return el_wrap_str(el_strdup(buf)); } diff --git a/lang/releases/v1.0.0-20260501/el_runtime.h b/lang/releases/v1.0.0-20260501/el_runtime.h index a35d1ae..de43bd2 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.h +++ b/lang/releases/v1.0.0-20260501/el_runtime.h @@ -617,6 +617,7 @@ el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction); el_val_t engram_activate_json(el_val_t query, el_val_t depth); el_val_t engram_stats_json(void); +el_val_t engram_act_stats_json(void); el_val_t engram_cosine_sim(el_val_t id_a, el_val_t id_b); el_val_t engram_embed_backfill(el_val_t count); el_val_t engram_list_layers_json(void);