self-review 2026-05-17: semantic seeding in Pass 1 + WM promotion observability

Three improvements from daily review:

1. Add semantic seed supplement to Pass 1 activation (el_runtime.c).
   Previously, engram_cosine_sim was only called in goal_bias (Pass 2) for
   nodes that already matched lexically. Nodes semantically close but
   lexically disjoint were completely invisible to activation. With 8K+
   world-ingestor nodes added overnight, this was a critical gap. Now: after
   lexical seeding, scan un-seeded nodes for cosine sim ≥ 0.65 and inject
   top-30 as additional seeds. Sem seeds get 85% of full act to stay weaker
   than exact lexical matches.

2. Lower WM promotion ISE threshold from >0.5 to >0.1 (el_runtime.c).
   Only one wm-promotion ISE was ever logged — the 0.5 floor was too high.
   Most practical Knowledge/Memory promotions are in the 0.1-0.5 range.
   Lowering to 0.1 makes working memory activity visible in state events.
This commit is contained in:
2026-05-17 08:39:59 -05:00
parent d917165aaf
commit b90333e9e7
2 changed files with 71 additions and 2 deletions
BIN
View File
Binary file not shown.
+71 -2
View File
@@ -6756,6 +6756,72 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
reached[i] = 1;
}
}
/* ── Semantic seed supplement ────────────────────────────────────────────
* When the query has an embedding vector, scan un-seeded nodes for cosine
* similarity 0.65 and inject the top-30 as additional Pass-1 seeds.
* This ensures semantically-related nodes that share no query tokens still
* enter the BFS fan-out. Sem seeds get 85% of full act to stay slightly
* weaker than exact lexical matches. Top-K=30 cap prevents queue flooding
* on large graphs (11K+ nodes post-ingestor).
*
* Without this pass, embeddings collected at write-time via engram_embed_node
* were only used in goal_bias (Pass 2) for nodes that already had lexical
* overlap meaning semantically-close but lexically-disjoint nodes were
* completely invisible to activation regardless of embedding quality. */
if (qvec && qdim > 0) {
typedef struct { int64_t idx; float sim; } SemEntry;
const float SEM_FLOOR = 0.65f;
SemEntry top_sem[30];
int64_t top_sem_cnt = 0;
float top_sem_min = SEM_FLOOR;
for (int64_t i = 0; i < g->node_count; i++) {
if (reached[i]) continue;
EngramNode* sn = &g->nodes[i];
if (!sn->embedding || sn->embedding_dim != qdim) continue;
float sim = engram_cosine_sim(sn->embedding, qvec, qdim);
if (sim < top_sem_min) continue;
if (top_sem_cnt < 30) {
top_sem[top_sem_cnt].idx = i;
top_sem[top_sem_cnt].sim = sim;
top_sem_cnt++;
if (top_sem_cnt == 30) {
/* Buffer full — compute floor for future candidates. */
top_sem_min = top_sem[0].sim;
for (int64_t t = 1; t < 30; t++)
if (top_sem[t].sim < top_sem_min) top_sem_min = top_sem[t].sim;
}
} else {
/* Replace the lowest-sim entry. */
int64_t min_t = 0;
for (int64_t t = 1; t < 30; t++)
if (top_sem[t].sim < top_sem[min_t].sim) min_t = t;
top_sem[min_t].idx = i;
top_sem[min_t].sim = sim;
top_sem_min = top_sem[0].sim;
for (int64_t t = 1; t < 30; t++)
if (top_sem[t].sim < top_sem_min) top_sem_min = top_sem[t].sim;
}
}
for (int64_t t = 0; t < top_sem_cnt; t++) {
int64_t si = top_sem[t].idx;
float sim = top_sem[t].sim;
EngramNode* sn = &g->nodes[si];
double tdecay = engram_temporal_decay(sn, now_ms);
double dampen = engram_activation_dampen(sn);
/* Scale: 0 at SEM_FLOOR, 0.85× at perfect match (sim=1). */
double sim_scale = (double)(sim - SEM_FLOOR) / (double)(1.0f - SEM_FLOOR) * 0.85;
double act = sn->salience * tdecay * dampen * sim_scale;
if (act > 0.0 && seed_count < g->node_count) {
seeds[seed_count].idx = si;
seeds[seed_count].act = act;
seeds[seed_count].created_at = sn->created_at;
seed_count++;
best_bg[si] = act;
best_hops[si] = 0;
reached[si] = 1;
}
}
}
/* Compute mean seed created_at for temporal proximity bonus. */
int64_t seed_epoch = 0;
if (seed_count > 0) {
@@ -6995,8 +7061,11 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
EngramNode* wn = &g->nodes[i];
double prev_wm = wn->working_memory_weight;
wn->working_memory_weight = wm_weights[i];
/* ISE: working memory promotion — only on 0→>0.5 transition, non-ISE nodes */
if (prev_wm <= 0.0 && wm_weights[i] > 0.5 &&
/* ISE: working memory promotion — on 0→>0.1 transition, non-ISE nodes.
* Threshold lowered from 0.5 to 0.1 so quieter promotions are observable.
* Previously only very strong promotions (wm_weight>0.5) fired an ISE,
* leaving most activations invisible in the state-event log. */
if (prev_wm <= 0.0 && wm_weights[i] > 0.1 &&
wn->node_type && strcmp(wn->node_type, "InternalStateEvent") != 0) {
char ise_buf[512];
char label_safe[64];