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:
Vendored
BIN
Binary file not shown.
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user