self-review 2026-06-05: WM composition visibility + ISE tier-migration fix
Two improvements from daily self-review:
1. engram_wm_top_json(n) builtin — returns top-N WM nodes by weight as
compact JSON [{label,node_type,tier,wm},...]. After long uptime all WM
nodes cycle in steady-state decay+re-promotion so wm_promotion ISEs
never fire (only trigger on 0→>0.1 transitions). This gives continuous
visibility into WM composition on every heartbeat.
2. ISE tier-migration exclusion — InternalStateEvent nodes no longer
participate in tier migration. ISEs are activated by curiosity_scan
substring matches and accumulated 50+ activation_count, reaching
Procedural tier (720h decay). This permanently crowded WM with ephemeral
state events, hiding the Knowledge/Memory nodes that should dominate.
Fix: skip ISEs in the TIER MIGRATION pass; they stay on Working-tier
48h decay regardless of activation frequency.
wm_top_json is called from awareness.el emit_heartbeat (top-5 snapshot).
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user