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:
+59
-5
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user