self-review 2026-05-19: three awareness loop fixes

1. perceive() guard — gate on engram_search_json before running activation.
   engram_activate_json with no matching seeds cleared all WM weights every
   second during idle operation, destroying context built by MCP-layer calls.
   The search-based guard is a no-WM-side-effect pre-check.

2. emit_heartbeat() pulse field — replace broken if-else string default with
   int_to_str(pulse_count()). EL codegen initialises inline if-else result
   slots to 0, producing "pulse":, (invalid JSON) when the true branch fires.

3. proactive_curiosity() — new function that activates a rotating 4-domain seed
   every beat_interval/2 idle ticks to build working memory between heartbeats.
   Seeds rotate on wall-clock minute cycle to avoid single-topic WM dominance.
   Seed selection uses imperative let-rebinding (not inline if-else) to avoid
   the same EL codegen empty-string bug.
This commit is contained in:
2026-05-19 08:53:12 -05:00
parent 2099522c28
commit dd22130faf
2 changed files with 152 additions and 63 deletions
+59 -5
View File
@@ -73,10 +73,11 @@ fn embed_ok() -> Int {
}
fn emit_heartbeat() -> Void {
// Guard against empty state values producing invalid JSON (e.g. "pulse":,).
// state_get returns "" for unset keys; default to "0" for numeric fields.
let pulse_raw: String = state_get("soul.pulse")
let pulse: String = if str_eq(pulse_raw, "") { "0" } else { pulse_raw }
// Use pulse_count() / boot helper directly state_get returns "" for unset
// keys and the if-else defaulting can produce empty strings in some EL
// codegen paths, yielding malformed JSON like "pulse":,. Going through
// int_to_str(pulse_count()) guarantees a valid integer string.
let pulse: String = int_to_str(pulse_count())
let boot_raw: String = state_get("soul_boot_count")
let boot: String = if str_eq(boot_raw, "") { "0" } else { boot_raw }
let idle: String = int_to_str(idle_count())
@@ -91,6 +92,38 @@ fn emit_heartbeat() -> Void {
ise_post(payload)
}
// proactive_curiosity activate a rotating seed to exercise working memory
// during idle periods. Rotates through 4 domain seeds on a wall-clock minute
// cycle so no single topic dominates WM between heartbeats.
//
// Unlike perceive(), this intentionally calls engram_activate_json to build
// up WM weights. It only fires when the inbox is empty (no real work to do),
// so it never interferes with inbox processing.
//
// Returns true if any nodes were activated.
fn proactive_curiosity() -> Bool {
let ts: Int = time_now()
// Rotate seed 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.
let minute_block: Int = (ts / 60000) % 4
let seed: String = "memory knowledge context"
if minute_block == 1 { let seed = "self identity values architecture" }
if minute_block == 2 { let seed = "recent decision pattern lesson" }
if minute_block == 3 { let seed = "working active project" }
let results: String = engram_activate_json(seed, 2)
let found: Int = json_array_len(results)
let wmc: Int = engram_wm_count()
let ise: String = "{\"event\":\"curiosity_scan\",\"seed\":\"" + seed
+ "\",\"activated\":" + int_to_str(found)
+ ",\"wm_active\":" + int_to_str(wmc)
+ ",\"ts\":" + int_to_str(ts) + "}"
ise_post(ise)
return found > 0
}
fn pulse_count() -> Int {
let s: String = state_get("soul.pulse")
if str_eq(s, "") {
@@ -114,7 +147,16 @@ fn make_action(kind: String, payload: String) -> String {
}
fn perceive() -> String {
// Try the primary inbox first
// Guard: check for inbox nodes WITHOUT running activation first.
// engram_activate_json with no matching seeds zeroes all WM weights
// running it every second when the inbox is empty destroys working memory
// accumulated by MCP-layer activations. engram_search_json is a pure
// substring scan with no WM side-effects; use it as a cheap gate.
let inbox_check: String = engram_search_json("soul-inbox", 5)
let has_inbox: Bool = !str_eq(inbox_check, "") && !str_eq(inbox_check, "[]")
if !has_inbox { return "[]" }
// Only run the full activation pipeline when there is inbox content.
let from_pending: String = engram_activate_json("soul-inbox-pending", 2)
let pending_ok: Bool = !str_eq(from_pending, "") && !str_eq(from_pending, "[]")
if pending_ok {
@@ -296,6 +338,18 @@ fn awareness_run() -> Void {
let idle_n: Int = if !did_work { idle_inc() } else { 0 }
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) }
// Proactive curiosity: activate a rotating seed at half the heartbeat
// 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
if curiosity_interval < 1 { let curiosity_interval = 1 }
let should_scan: Bool = !did_work && idle_n > 0
&& idle_n % curiosity_interval == 0
&& !(idle_n % beat_interval == 0)
if should_scan {
let found_something: Bool = proactive_curiosity()
}
let should_beat: Bool = !did_work && idle_n > 0 && idle_n % beat_interval == 0
if should_beat {
emit_heartbeat()