self-review 2026-05-25: fix curiosity rotation and awareness_run timing

Three bugs fixed in awareness.el:

1. EL let-rebinding inside if-blocks creates inner scope only — outer
   variable unchanged after block exits. Curiosity seed terms were always
   "memory/knowledge/context" regardless of minute_block. Fix: state_set
   inside if-blocks, state_get after to retrieve selected values.

2. EL % operator completely broken in v1.0.0-20260501 — compiles as dead
   code (left operand assigned, modulo dropped). minute_block was always
   ts/60000 (a large int, never 0-3). Fix: arithmetic workaround:
   x%4 = x - (q+q+q+q) where q = x/4.

3. awareness_run idle_n % beat_interval == 0 also broken by same % bug —
   should_scan and should_beat fired every idle tick instead of every N
   ticks. Fix: idle_n >= interval comparisons with idle_reset() after
   firing, so the counter restarts cleanly after each event.

EL % and * operators filed as P1 backlog item for elc compiler fix.
Also adds minute_block field to curiosity_scan ISE for observability.
This commit is contained in:
2026-05-25 08:47:30 -05:00
parent 11c7f90e51
commit fb6904431f
4 changed files with 108 additions and 72 deletions
+65 -32
View File
@@ -106,38 +106,60 @@ fn emit_heartbeat() -> Void {
// up WM weights. It only fires when the inbox is empty (no real work to do), // up WM weights. It only fires when the inbox is empty (no real work to do),
// so it never interferes with inbox processing. // so it never interferes with inbox processing.
// //
// SCOPING FIX (2026-05-25): EL `let` inside if-blocks creates inner scope only
// the outer variable is NOT mutated (despite the "imperative shadowing" belief
// in earlier comments). Evidence: ISE stream showed "seed:memory knowledge context"
// on every curiosity_scan regardless of minute_block. Fix: use state_set/state_get
// to communicate term values across scope boundaries state side-effects persist
// beyond block exit. minute_block now also emitted in ISE for observability.
//
// NOTE: variable named "curiosity_seed" not "seed" "seed" appears to be
// a reserved/conflicting name in EL that compiles to EL_NULL at call sites.
//
// Returns true if any nodes were activated. // Returns true if any nodes were activated.
fn proactive_curiosity() -> Bool { fn proactive_curiosity() -> Bool {
let ts: Int = time_now() let ts: Int = time_now()
// Rotate seed set every minute using wall clock: (minutes_since_epoch) % 4. // Rotate seed set every minute using wall clock: (minutes_since_epoch) % 4.
// IMPORTANT: use imperative let-rebinding, NOT inline if-else string
// expressions. EL codegen initialises the result slot to 0 (null) before
// evaluating the if, so string-literal if-else branches can produce an
// empty string when the true branch fires. Imperative shadowing is safe.
// //
// NOTE: variable named "curiosity_seed" not "seed" "seed" appears to be // CODEGEN BUG (confirmed 2026-05-25): EL's % operator is completely broken
// a reserved/conflicting name in EL that compiles to EL_NULL at call sites. // in this compiler version. `x % 4` compiles as `x` (drops the modulo) and
let minute_block: Int = (ts / 60000) % 4 // emits `EL_NULL; 4;` as dead statements — both on compound expressions AND
// on simple variables. The same bug breaks elapsed_human() and awareness_run
// timing conditions. EL compiler fix is a separate backlog item.
//
// WORKAROUND: compute x % 4 via x - ((x/4)*4), where (x/4)*4 = q+q+q+q.
// Uses only + - / which all compile correctly.
let ts_minutes: Int = ts / 60000
let minute_q: Int = ts_minutes / 4
let minute_q2: Int = minute_q + minute_q
let minute_q4: Int = minute_q2 + minute_q2
let minute_block: Int = ts_minutes - minute_q4
// Each slot: 3 individual terms to activate separately. // Use state_set to write term values from within if-blocks.
let curiosity_term_a: String = "memory" // EL let-rebinding inside if creates a new inner variable; the outer
let curiosity_term_b: String = "knowledge" // binding is unchanged. state_set has side-effects that outlive block scope.
let curiosity_term_c: String = "context" state_set("cseed_a", "memory")
state_set("cseed_b", "knowledge")
state_set("cseed_c", "context")
if minute_block == 1 { if minute_block == 1 {
let curiosity_term_a = "self" state_set("cseed_a", "self")
let curiosity_term_b = "identity" state_set("cseed_b", "identity")
let curiosity_term_c = "values" state_set("cseed_c", "values")
} }
if minute_block == 2 { if minute_block == 2 {
let curiosity_term_a = "decision" state_set("cseed_a", "decision")
let curiosity_term_b = "pattern" state_set("cseed_b", "pattern")
let curiosity_term_c = "lesson" state_set("cseed_c", "lesson")
} }
if minute_block == 3 { if minute_block == 3 {
let curiosity_term_a = "working" state_set("cseed_a", "working")
let curiosity_term_b = "project" state_set("cseed_b", "project")
let curiosity_term_c = "active" state_set("cseed_c", "active")
} }
let curiosity_term_a: String = state_get("cseed_a")
let curiosity_term_b: String = state_get("cseed_b")
let curiosity_term_c: String = state_get("cseed_c")
// Activate each term independently so substring seed-finding hits many nodes. // Activate each term independently so substring seed-finding hits many nodes.
// hops=1 (not 2): the in-process Engram has grown to 165K+ nodes. hops=2 BFS // hops=1 (not 2): the in-process Engram has grown to 165K+ nodes. hops=2 BFS
// visits far more nodes and returns much larger JSON blobs. On a graph this // visits far more nodes and returns much larger JSON blobs. On a graph this
@@ -154,7 +176,8 @@ fn proactive_curiosity() -> Bool {
let found: Int = found_a + found_b + found_c let found: Int = found_a + found_b + found_c
let wmc: Int = engram_wm_count() let wmc: Int = engram_wm_count()
let ise: String = "{\"event\":\"curiosity_scan\",\"seed\":\"" + curiosity_seed let ise: String = "{\"event\":\"curiosity_scan\",\"seed\":\"" + curiosity_seed
+ "\",\"activated\":" + int_to_str(found) + "\",\"minute_block\":" + int_to_str(minute_block)
+ ",\"activated\":" + int_to_str(found)
+ ",\"wm_active\":" + int_to_str(wmc) + ",\"wm_active\":" + int_to_str(wmc)
+ ",\"ts\":" + int_to_str(ts) + "}" + ",\"ts\":" + int_to_str(ts) + "}"
ise_post(ise) ise_post(ise)
@@ -375,23 +398,33 @@ fn awareness_run() -> Void {
let idle_n: Int = if !did_work { idle_inc() } else { 0 } let idle_n: Int = if !did_work { idle_inc() } else { 0 }
let beat_interval_raw: String = env("SOUL_HEARTBEAT_INTERVAL") let beat_interval_raw: String = env("SOUL_HEARTBEAT_INTERVAL")
let beat_interval: Int = if str_eq(beat_interval_raw, "") { 300 } else { str_to_int(beat_interval_raw) } let beat_interval: Int = if str_eq(beat_interval_raw, "") { 300 } else { str_to_int(beat_interval_raw) }
// Proactive curiosity: activate a rotating seed at half the heartbeat // Proactive curiosity fires at half the heartbeat interval.
// interval to maintain working memory between heartbeats. Only fires
// when truly idle (no inbox work) and not on the same tick as the
// heartbeat (avoid double-firing at beat_interval multiples).
let curiosity_interval: Int = beat_interval / 2 let curiosity_interval: Int = beat_interval / 2
if curiosity_interval < 1 { let curiosity_interval = 1 } if curiosity_interval < 1 { let curiosity_interval = 1 }
let should_scan: Bool = !did_work && idle_n > 0
&& idle_n % curiosity_interval == 0 // TIMING FIX (2026-05-25): EL's % operator is broken it compiles as
&& !(idle_n % beat_interval == 0) // a no-op (drops the modulo, emits dead code). `idle_n % X == 0` always
if should_scan { // evaluated to `idle_n` (truthy from tick 1), causing both heartbeat and
let found_something: Bool = proactive_curiosity() // curiosity to fire on every single idle tick.
} //
let should_beat: Bool = !did_work && idle_n > 0 && idle_n % beat_interval == 0 // Fix: use >= comparisons instead of % == 0. idle_n increments on each
// idle tick and is reset to 0 by idle_reset(). When idle_n reaches the
// threshold, the event fires and idle_reset() is called, so the next event
// fires after another full interval of idle ticks. No modulo needed.
//
// Beat has higher priority: if both thresholds are crossed, beat fires and
// scan is suppressed (they share the idle counter, so scan would fire at
// the next curiosity_interval boundary regardless).
let should_beat: Bool = !did_work && idle_n > 0 && idle_n >= beat_interval
if should_beat { if should_beat {
emit_heartbeat() emit_heartbeat()
idle_reset() idle_reset()
} }
let should_scan: Bool = !did_work && idle_n > 0 && idle_n >= curiosity_interval && !should_beat
if should_scan {
let found_something: Bool = proactive_curiosity()
idle_reset()
}
sleep_ms(tick_ms) sleep_ms(tick_ms)
} }
} }
+16 -13
View File
@@ -243,24 +243,27 @@ el_val_t proactive_curiosity(void) {
el_val_t minute_block = (ts / 60000); el_val_t minute_block = (ts / 60000);
EL_NULL; EL_NULL;
4; 4;
el_val_t curiosity_term_a = EL_STR("memory"); state_set(EL_STR("cseed_a"), EL_STR("memory"));
el_val_t curiosity_term_b = EL_STR("knowledge"); state_set(EL_STR("cseed_b"), EL_STR("knowledge"));
el_val_t curiosity_term_c = EL_STR("context"); state_set(EL_STR("cseed_c"), EL_STR("context"));
if (minute_block == 1) { if (minute_block == 1) {
curiosity_term_a = EL_STR("self"); state_set(EL_STR("cseed_a"), EL_STR("self"));
curiosity_term_b = EL_STR("identity"); state_set(EL_STR("cseed_b"), EL_STR("identity"));
curiosity_term_c = EL_STR("values"); state_set(EL_STR("cseed_c"), EL_STR("values"));
} }
if (minute_block == 2) { if (minute_block == 2) {
curiosity_term_a = EL_STR("decision"); state_set(EL_STR("cseed_a"), EL_STR("decision"));
curiosity_term_b = EL_STR("pattern"); state_set(EL_STR("cseed_b"), EL_STR("pattern"));
curiosity_term_c = EL_STR("lesson"); state_set(EL_STR("cseed_c"), EL_STR("lesson"));
} }
if (minute_block == 3) { if (minute_block == 3) {
curiosity_term_a = EL_STR("working"); state_set(EL_STR("cseed_a"), EL_STR("working"));
curiosity_term_b = EL_STR("project"); state_set(EL_STR("cseed_b"), EL_STR("project"));
curiosity_term_c = EL_STR("active"); state_set(EL_STR("cseed_c"), EL_STR("active"));
} }
el_val_t curiosity_term_a = state_get(EL_STR("cseed_a"));
el_val_t curiosity_term_b = state_get(EL_STR("cseed_b"));
el_val_t curiosity_term_c = state_get(EL_STR("cseed_c"));
el_val_t curiosity_seed = el_str_concat(el_str_concat(el_str_concat(el_str_concat(curiosity_term_a, EL_STR(" ")), curiosity_term_b), EL_STR(" ")), curiosity_term_c); el_val_t curiosity_seed = el_str_concat(el_str_concat(el_str_concat(el_str_concat(curiosity_term_a, EL_STR(" ")), curiosity_term_b), EL_STR(" ")), curiosity_term_c);
el_val_t results_a = engram_activate_json(curiosity_term_a, 1); el_val_t results_a = engram_activate_json(curiosity_term_a, 1);
el_val_t results_b = engram_activate_json(curiosity_term_b, 1); el_val_t results_b = engram_activate_json(curiosity_term_b, 1);
@@ -270,7 +273,7 @@ el_val_t proactive_curiosity(void) {
el_val_t found_c = json_array_len(results_c); el_val_t found_c = json_array_len(results_c);
el_val_t found = ((found_a + found_b) + found_c); el_val_t found = ((found_a + found_b) + found_c);
el_val_t wmc = engram_wm_count(); el_val_t wmc = engram_wm_count();
el_val_t ise = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"event\":\"curiosity_scan\",\"seed\":\""), curiosity_seed), EL_STR("\",\"activated\":")), int_to_str(found)), EL_STR(",\"wm_active\":")), int_to_str(wmc)), EL_STR(",\"ts\":")), int_to_str(ts)), EL_STR("}")); el_val_t ise = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"event\":\"curiosity_scan\",\"seed\":\""), curiosity_seed), EL_STR("\",\"minute_block\":")), int_to_str(minute_block)), EL_STR(",\"activated\":")), int_to_str(found)), EL_STR(",\"wm_active\":")), int_to_str(wmc)), EL_STR(",\"ts\":")), int_to_str(ts)), EL_STR("}"));
ise_post(ise); ise_post(ise);
return (found > 0); return (found > 0);
return 0; return 0;
Vendored
BIN
View File
Binary file not shown.
+27 -27
View File
@@ -25363,27 +25363,32 @@ el_val_t emit_heartbeat(void) {
el_val_t proactive_curiosity(void) { el_val_t proactive_curiosity(void) {
el_val_t ts = time_now(); el_val_t ts = time_now();
el_val_t minute_block = (ts / 60000); el_val_t ts_minutes = (ts / 60000);
EL_NULL; el_val_t minute_q = (ts_minutes / 4);
4; el_val_t minute_q2 = (minute_q + minute_q);
el_val_t curiosity_term_a = EL_STR("memory"); el_val_t minute_q4 = (minute_q2 + minute_q2);
el_val_t curiosity_term_b = EL_STR("knowledge"); el_val_t minute_block = (ts_minutes - minute_q4);
el_val_t curiosity_term_c = EL_STR("context"); state_set(EL_STR("cseed_a"), EL_STR("memory"));
state_set(EL_STR("cseed_b"), EL_STR("knowledge"));
state_set(EL_STR("cseed_c"), EL_STR("context"));
if (minute_block == 1) { if (minute_block == 1) {
curiosity_term_a = EL_STR("self"); state_set(EL_STR("cseed_a"), EL_STR("self"));
curiosity_term_b = EL_STR("identity"); state_set(EL_STR("cseed_b"), EL_STR("identity"));
curiosity_term_c = EL_STR("values"); state_set(EL_STR("cseed_c"), EL_STR("values"));
} }
if (minute_block == 2) { if (minute_block == 2) {
curiosity_term_a = EL_STR("decision"); state_set(EL_STR("cseed_a"), EL_STR("decision"));
curiosity_term_b = EL_STR("pattern"); state_set(EL_STR("cseed_b"), EL_STR("pattern"));
curiosity_term_c = EL_STR("lesson"); state_set(EL_STR("cseed_c"), EL_STR("lesson"));
} }
if (minute_block == 3) { if (minute_block == 3) {
curiosity_term_a = EL_STR("working"); state_set(EL_STR("cseed_a"), EL_STR("working"));
curiosity_term_b = EL_STR("project"); state_set(EL_STR("cseed_b"), EL_STR("project"));
curiosity_term_c = EL_STR("active"); state_set(EL_STR("cseed_c"), EL_STR("active"));
} }
el_val_t curiosity_term_a = state_get(EL_STR("cseed_a"));
el_val_t curiosity_term_b = state_get(EL_STR("cseed_b"));
el_val_t curiosity_term_c = state_get(EL_STR("cseed_c"));
el_val_t curiosity_seed = el_str_concat(el_str_concat(el_str_concat(el_str_concat(curiosity_term_a, EL_STR(" ")), curiosity_term_b), EL_STR(" ")), curiosity_term_c); el_val_t curiosity_seed = el_str_concat(el_str_concat(el_str_concat(el_str_concat(curiosity_term_a, EL_STR(" ")), curiosity_term_b), EL_STR(" ")), curiosity_term_c);
el_val_t results_a = engram_activate_json(curiosity_term_a, 1); el_val_t results_a = engram_activate_json(curiosity_term_a, 1);
el_val_t results_b = engram_activate_json(curiosity_term_b, 1); el_val_t results_b = engram_activate_json(curiosity_term_b, 1);
@@ -25393,7 +25398,7 @@ el_val_t proactive_curiosity(void) {
el_val_t found_c = json_array_len(results_c); el_val_t found_c = json_array_len(results_c);
el_val_t found = ((found_a + found_b) + found_c); el_val_t found = ((found_a + found_b) + found_c);
el_val_t wmc = engram_wm_count(); el_val_t wmc = engram_wm_count();
el_val_t ise = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"event\":\"curiosity_scan\",\"seed\":\""), curiosity_seed), EL_STR("\",\"activated\":")), int_to_str(found)), EL_STR(",\"wm_active\":")), int_to_str(wmc)), EL_STR(",\"ts\":")), int_to_str(ts)), EL_STR("}")); el_val_t ise = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"event\":\"curiosity_scan\",\"seed\":\""), curiosity_seed), EL_STR("\",\"minute_block\":")), int_to_str(minute_block)), EL_STR(",\"activated\":")), int_to_str(found)), EL_STR(",\"wm_active\":")), int_to_str(wmc)), EL_STR(",\"ts\":")), int_to_str(ts)), EL_STR("}"));
ise_post(ise); ise_post(ise);
return (found > 0); return (found > 0);
return 0; return 0;
@@ -25593,21 +25598,16 @@ el_val_t awareness_run(void) {
if (curiosity_interval < 1) { if (curiosity_interval < 1) {
curiosity_interval = 1; curiosity_interval = 1;
} }
el_val_t should_scan = ((!did_work && (idle_n > 0)) && idle_n); el_val_t should_beat = ((!did_work && (idle_n > 0)) && (idle_n >= beat_interval));
EL_NULL;
((curiosity_interval == 0) && !idle_n);
(beat_interval == 0);
EL_NULL;
if (should_scan) {
el_val_t found_something = proactive_curiosity();
}
el_val_t should_beat = ((!did_work && (idle_n > 0)) && idle_n);
EL_NULL;
(beat_interval == 0);
if (should_beat) { if (should_beat) {
emit_heartbeat(); emit_heartbeat();
idle_reset(); idle_reset();
} }
el_val_t should_scan = (((!did_work && (idle_n > 0)) && (idle_n >= curiosity_interval)) && !should_beat);
if (should_scan) {
el_val_t found_something = proactive_curiosity();
idle_reset();
}
sleep_ms(tick_ms); sleep_ms(tick_ms);
} }
return 0; return 0;