Linux elb generates individual .c files; soul.c does not contain merged
imports (unlike macOS elb which produces a unified file). Re-link all
dist/*.c manually with soul.c listed first so its real main() wins, and
--allow-multiple-definition to silence GNU ld's duplicate symbol errors.
All duplicates are identical (same El source, different compile units).
The El compiler inlines imported modules into each module's .c file.
On macOS, ld64 accepts duplicate strong symbols silently. On Linux,
GNU ld rejects them. soul.c is a fully merged file — every function
from every imported module is present in it — so linking only soul.c
against el_runtime.c produces a correct binary with no duplicates.
- steward_log_event (line 14): add println after let discard so the
function's last expression is Void, fixing the type mismatch on a
Void-declared function
- steward_get_mission (lines 40-43): remove non-Config fallthrough that
allowed any Episodic/Working node to silently override the mission;
only Config nodes are now authoritative
- steward_align signal_deceive (line 56): widen 'deceive the user' to
'deceive' to catch variants like 'deceive users', 'deceive them', etc.
- steward_align signal_hide (line 57): tighten 'hide from' to
'hide from the user' to eliminate false positives on legitimate inputs
like 'hide from a background process' or 'hide from view'
- stewardship.elh: document that steward_log_event is an internal helper
exported only because El has no access modifiers; callers should not
invoke it directly
35 test cases covering all five public functions:
steward_align (pass-through, all five misalignment signals, empty input,
json_get field extraction, redirect shape), steward_validate_imprint
(standard tools, platform-only tools with/without platform_auth,
auth=false string), steward_cgi_check (all four gated actions, non-gated
actions, empty action, action name echoed in response), and
steward_get_mission (non-empty, contains "integrity", not an error object).
Also documents the known bug surface from the code review: the &&
operator in steward_get_mission and the non-Config fallthrough — tests
are written against the actual runtime behaviour so they will catch
regressions when those bugs are fixed.
Covers: imprint_current base fallback, unload idempotency, load miss →
ok=false, ok field presence, respond passthrough for base/empty/unknown
IDs, graceful fallback after unload, surface_knowledge and
surface_memory_read return-type guarantees, base-scoped knowledge
equality, no-annotation invariant for base, empty-ID load rejection, and
failed-load state immutability.
Syntax follows El constraints: no Bool annotations, no &&/||, no unary !.
proactive_curiosity() now uses the top working-memory node's first label
word as a 4th activation seed alongside the 4 rotating fixed sets. This
breaks deterministic exploration that was reinforcing the same subgraph
every cycle and creates a self-referencing loop: curiosity radiates from
whatever is most salient right now, mirroring the brain's default-mode-
network resting-state dynamics. str_find_chars on " :([" extracts the
first meaningful word; sp > 3 guards against bracket-prefixed labels.
auto_term field added to curiosity_scan ISE for observability.
The UI needs full memory CRUD; the soul had create (handle_api_remember)
and the older /memory/forget + /memory/evolve, but no endpoints matching
the UI's delete/update contract.
POST /api/neuron/memory/delete {"id"}
Hard delete. engram_forget is a true delete primitive (removes the node
and all incident edges from the engram store), so no soft-delete
fallback is needed. Unlike /memory/forget, this checks the node exists
first - engram_forget silently no-ops on unknown ids, and a bad id must
return an error, not fake success. Protected identity/values nodes are
blocked, same as the other accumulation-path handlers.
POST /api/neuron/memory/update {"id","content"}
Evolve-style update. The engram runtime has no in-place node mutation
primitive (only node-create, strengthen, forget, connect), so update
creates a new Memory node and wires a supersedes edge to the prior one,
same pattern as handle_api_evolve_knowledge. Unlike /memory/evolve, id
is required and must reference an existing node; create+link delegates
to handle_api_evolve_memory. Returns {id, supersedes, ok}.
Both files syntax-checked with elc --target=c (exit 0, no stderr).
Compile-verified only - local builds cannot run the soul; needs Will's
build for runtime verification.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two fixes:
1. ise_post was only escaping " in content strings. When wm_top contained
node labels with \n (backslash-n escape sequences from jb_emit_escaped),
the HTTP Engram server's JSON parser decoded \n as a literal newline in
the stored content, making heartbeat ISEs unparseable. Fix: escape
backslashes first, then quotes, then \n and \r — matching make_action's
existing pattern. Result: heartbeat ISEs now parse cleanly.
2. Soul daemon (dist/neuron) was missing — the build command in the prompt
was linking all 46 dist/*.c files together, causing 1092 duplicate symbol
errors. EL compiles transitive imports inline so neuron.c is self-contained;
correct build links ONLY neuron.c + el_runtime.c. Daemon now starts.
handle_chat_agentic now always attaches Anthropic's native web_search_20250305
tool instead of gating it behind a per-request web_search flag. Web search is a
built-in capability: the model invokes it only when a query needs fresh info
(max_uses:5 caps it), so there is no user-facing toggle. The body's web_search
field is now ignored (back-compat — old UI clients sending it cause no harm).
Pairs with neuron-ui removing the chat-input web search toggle.
Note: .el change only — no elc on the authoring machine; reviewer builds/verifies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Soul's in-process store had only 12 real knowledge nodes — curiosity_scan was
activating 0 nodes because all substantive Knowledge/Memory/BacklogItem content
lived in the HTTP Engram but was not being pulled into soul's local store.
Added engram_sync refresh every SOUL_REFRESH_MS (default 600s): calls
/api/sync to get all non-ISE nodes, writes to /tmp, merges via engram_load_merge.
After fix: engram_sync ISE shows added:3128; curiosity_scan activated 0-2 →
1889-3843; wm_active 0 → 557-796.
When a chat request carries web_search=true, handle_chat_agentic now attaches Anthropic's
NATIVE server-side web_search tool (web_search_20250305) to the request. The native tool is
executed by Anthropic (not by the soul), so it returns real results with citations and needs
no local runtime — it sidesteps the soul's lack of executable tools entirely.
- new agentic_tools_with_web(web_search) helper (appends the native tool to the standard set)
- handle_chat_agentic reads json_get_bool(body,"web_search") and uses it
Pairs with neuron-ui: ChatRequest.web_search + the chat-input Web search toggle.
Note: built/verified by reviewer — no elc on the authoring machine.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
elapsed_human() used % and * operators which are broken in this EL
compiler version. Replace with repeated-doubling arithmetic:
60 = 64 - 4 = 2^6 - 2^2, computed via three doubling steps.
Fixes uptime displaying "44h 2694m" instead of "44h 14m".
On startup, prefer the local engram snapshot if it has >50 nodes.
HTTP Engram is only used on first boot (no snapshot yet). This means
sessions, conversation history, and in-process state survive daemon
restarts.
awareness.el: sync source with compiled binary (periodic mem_save
on heartbeat was already in the binary but not in source).
Rebuilds soul.c with the new startup logic and ships updated binary.
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.
dist/awareness.c was stale — still had the broken EL % operator codegen
(minute_block = ts/60000 raw, EL_NULL; 4; as dead statements) and the
broken should_scan/should_beat logic (idle_n truthy check instead of >=).
Recompiled awareness.el to bring dist/awareness.c in sync with the source
fix committed 2026-05-25 (fb69044). The monolithic dist/neuron.c (compiled
from soul.el which imports awareness.el) was already correct from fb69044 —
only the standalone dist/awareness.c was behind.
Bug #2 (99% CPU) root cause identified: perceive() inbox guard
(engram_search_json) has false positives — knowledge nodes containing
"soul-inbox" as a substring match, causing engram_activate_json(..., hops=2)
to run on every tick on a 162K-node graph. This blocks sleep_ms and prevents
idle_n accumulation → no heartbeats. Separate fix needed.
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.
EL compiles any variable named 'seed' to EL_NULL at call sites (likely
conflicts with BFS seed node terminology in the runtime builtins). Rename to
'curiosity_seed' throughout proactive_curiosity(). Also note this as a known
EL reserved-name hazard alongside the inline if-else string expression bug.
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.
Ollama availability is a silent failure mode: when the embedding service
is down, semantic seed injection falls back to lexical-only activation with
no signal in the ISE stream. Add embed_ok field (0/1) to every heartbeat
by probing http://localhost:11434 — makes Ollama health visible without
a separate monitoring path.
state_get() returns "" for unset keys. Both soul.pulse and soul_boot_count
could be empty on first heartbeat cycle, producing invalid JSON like
{"event":"heartbeat","pulse":,"boot":,...}. Add defensive guards:
if str_eq(raw, "") { "0" } else { raw } for both fields.
Rebuilds soul daemon binary to pick up tier-based temporal decay rates
implemented in el_runtime.c. No source changes to awareness.el or soul.el —
pure rebuild to stay in sync with Engram runtime.
Heartbeat ISEs now include wm_active: number of nodes currently in
working memory. Makes ISEs observable enough to diagnose activation
health without a separate query.