diff --git a/lang/releases/v1.0.0-20260501/el_runtime.c b/lang/releases/v1.0.0-20260501/el_runtime.c index 0445841..c6fdd28 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.c +++ b/lang/releases/v1.0.0-20260501/el_runtime.c @@ -7233,11 +7233,20 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { /* ── TIER MIGRATION: upgrade tier based on activation_count ───────── * 0–4 → Working, 5–19 → Episodic, 20–49 → Semantic, 50+ → Procedural. - * Only upgrades; never downgrades. */ + * Only upgrades; never downgrades. + * + * InternalStateEvent nodes are excluded from tier migration (2026-06-05 + * self-review): ISEs are ephemeral and get activated by curiosity_scan + * substring matches. Without this guard they accumulate 50+ activations + * and reach Procedural tier (720h decay), permanently occupying WM slots + * and crowding out Knowledge/Memory nodes. ISEs should decay quickly on + * their Working-tier 48h schedule regardless of how often they're hit. */ { const char* ddir = engram_data_dir(); for (int64_t i = 0; i < g->node_count; i++) { EngramNode* n = &g->nodes[i]; + /* Skip ISEs — tier migration does not apply to ephemeral state events. */ + if (n->node_type && strcmp(n->node_type, "InternalStateEvent") == 0) continue; const char* target_tier = NULL; int64_t ac = n->activation_count; if (ac >= 50) target_tier = "Procedural"; @@ -8948,6 +8957,65 @@ el_val_t engram_wm_avg_weight(void) { return el_from_float(avg); } +/* engram_wm_top_json — return top N working-memory nodes (by wm weight) as a + * compact JSON array for ISE heartbeat reporting. + * + * Each element: {"label":"...","node_type":"...","tier":"...","wm":0.42} + * + * Purpose: the heartbeat ISE reports wm_active (count) and wm_avg_weight but + * gives zero visibility into WM *composition* — which types/tiers are active. + * After long uptime every WM slot is in steady-state decay+re-promotion so + * wm_promotion ISEs never fire (they only fire on 0→>0.1 transitions). + * This function fills the observability gap by snapshotting the current top-N + * WM nodes on every heartbeat. Inserted 2026-06-05 self-review. */ +el_val_t engram_wm_top_json(el_val_t n_v) { + int64_t top_n = (int64_t)n_v; + if (top_n <= 0) top_n = 10; + if (top_n > 50) top_n = 50; + EngramStore* g = engram_get(); + + /* Collect indices of promoted nodes. */ + int64_t* idx = malloc((size_t)(g->node_count + 1) * sizeof(int64_t)); + if (!idx) return el_wrap_str(el_strdup("[]")); + int64_t mc = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].working_memory_weight > 0.0) idx[mc++] = i; + } + + /* Insertion-sort descending by wm weight (mc is typically small). */ + for (int64_t i = 1; i < mc; i++) { + int64_t key = idx[i]; + double kw = g->nodes[key].working_memory_weight; + int64_t j = i; + while (j > 0 && g->nodes[idx[j-1]].working_memory_weight < kw) { + idx[j] = idx[j-1]; j--; + } + idx[j] = key; + } + + int64_t emit = mc < top_n ? mc : top_n; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (int64_t k = 0; k < emit; k++) { + EngramNode* n = &g->nodes[idx[k]]; + if (k > 0) jb_putc(&b, ','); + jb_putc(&b, '{'); + jb_puts(&b, "\"label\":"); + jb_emit_escaped(&b, n->label ? n->label : ""); + jb_puts(&b, ",\"node_type\":"); + jb_emit_escaped(&b, n->node_type ? n->node_type : ""); + jb_puts(&b, ",\"tier\":"); + jb_emit_escaped(&b, n->tier ? n->tier : ""); + char tmp[48]; + snprintf(tmp, sizeof(tmp), ",\"wm\":%.3f", n->working_memory_weight); + jb_puts(&b, tmp); + jb_putc(&b, '}'); + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + /* engram_list_layers_json — serialized counterpart of engram_list_layers. * Returns a JSON array, sorted by activation_priority ascending. */ el_val_t engram_list_layers_json(void) { diff --git a/lang/releases/v1.0.0-20260501/el_runtime.h b/lang/releases/v1.0.0-20260501/el_runtime.h index c733dd6..56adb6c 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.h +++ b/lang/releases/v1.0.0-20260501/el_runtime.h @@ -620,6 +620,7 @@ 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_wm_count(void); el_val_t engram_wm_avg_weight(void); /* avg wm weight of promoted nodes; float bits */ +el_val_t engram_wm_top_json(el_val_t n); /* top-N WM nodes by weight as compact JSON */ el_val_t engram_apply_decay_json(void); el_val_t engram_list_layers_json(void); /* engram_compile_layered_json — produce a prompt-ready text block split