Call engram_wm_top_json(5) in emit_heartbeat() and embed the result as
wm_top field in the heartbeat JSON payload. Each entry carries label,
node_type, tier, and wm_weight. This closes the WM composition blindspot:
previously the heartbeat showed wm_active=670 with no breakdown of what
was in working memory. With wm_top visible, ISE-dominated WM is immediately
detectable (all entries show node_type=InternalStateEvent), as was the case
on this session's first post-restart heartbeat before the runtime fix.
Calls engram_wm_avg_weight() (new builtin) in emit_heartbeat() and appends
wm_avg_weight field to the heartbeat JSON payload. This makes activation
quality visible in the ISE stream — a heartbeat showing wm_active=2000 and
wm_avg_weight=0.075 reveals the sparse-graph problem directly (many nodes
barely clearing the threshold), vs wm_avg_weight=0.4+ which would indicate
dense, high-confidence activations.
Rebuilt dist/neuron from soul.el (which imports awareness.el). Build uses
single self-contained dist/neuron.c to avoid duplicate-symbol linker errors
from the dist/ directory containing stale soul_new.c / soul-rebuilt.c files.
Two awareness loop bugs fixed:
1. Seed rotation never worked: dist/awareness.c was compiled from stale
source (pre-fix awareness.el still had broken ts_minutes % 4). Compiled
C showed `minute_block = (ts / 60000); EL_NULL; 4;` — minute_block was
always ts_minutes (millions), never 0-3. if(minute_block==1/2/3) never
matched. Fix: recompile from current awareness.el which has the correct
modulo workaround: ts_minutes - minute_q4 (via + - / only).
2. Heartbeat/curiosity silent for 24h at 99% CPU: old design used idle-tick
counting (idle_n >= beat_interval). Failed when perceive() inbox guard
false-positives on "soul-inbox" substring matches in knowledge nodes —
did_work=true every tick, idle_n never accumulated, neither signal fired.
Fix: wall-clock elapsed time (time_now() - last_ts >= interval_ms).
Heartbeat fires regardless of load. New SOUL_HEARTBEAT_MS env var (default
60000ms) avoids the broken EL * operator. Verified: heartbeat ISEs flowing
at pulse 3 within 2 minutes of restart.
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.
Rebuilt awareness.c and neuron.c from source using the updated elc (which now
correctly recognizes http_serve_async as a 2-arg builtin). Rebuilt the neuron
binary against the updated el_runtime.c which now sorts InternalStateEvent scans
by created_at DESC. The soul daemon now posts heartbeats that surface immediately
at offset 0 of the ISE scan, rather than being buried behind 20K older entries.
Two fixes:
1. proactive_curiosity() was calling engram_activate_json with multi-word phrases
("memory knowledge context"). engram_activate finds seeds via istr_contains
(substring match), so the phrase had to appear verbatim in a node's content.
Almost no node contains the exact string "memory knowledge context", so only
0-2 nodes activated per curiosity scan. Fixed by activating each word separately:
"memory", "knowledge", "context" → 3 independent activate calls → hundreds of
nodes promoted to WM per cycle.
2. dist/neuron.c called http_serve() (blocking accept loop) which made awareness_run()
unreachable. soul.el correctly specifies http_serve_async but elc silently drops
unknown builtins, leaving blocking http_serve in the compiled C. Patched neuron.c
to call http_serve_async directly — HTTP server runs in a background pthread,
awareness_run() runs on the main thread as intended.
engram_wm_count() exists and counts nodes with working_memory_weight > 0.
emit_heartbeat() and proactive_curiosity() were both calling
engram_node_count() (total graph size: ~17K) instead — every heartbeat
and curiosity_scan ISE had been reporting wm_active=17769 since the graph
grew past ~3K nodes, making the metric meaningless for observability.
Fix: use engram_wm_count() for the wm_active field in both ISE payloads.