diff --git a/engram/dist/engram b/engram/dist/engram index b187b41..8a019f1 100755 Binary files a/engram/dist/engram and b/engram/dist/engram differ diff --git a/lang/releases/v1.0.0-20260501/el_runtime.c b/lang/releases/v1.0.0-20260501/el_runtime.c index 08ab3cd..55cc726 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.c +++ b/lang/releases/v1.0.0-20260501/el_runtime.c @@ -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];