High-fanout identity anchors (voice, writing-imprint, self-root) have ~670KB
neighborhoods. inspect_graph returned the full traversal, which overflowed the
MCP client's context and socket-closed the wrapper mid self-load -- the soul
could not traverse its own identity graph.
handle_api_inspect_graph gains an opt-in `compact` projection (compact=1|true):
the neighborhood is relevance-ranked, the top K (default 12) keep a UTF-8-safe
content snippet (default snip=600), and the remainder collapse to lightweight
{id,label,node_type,tier,edge,pointer:true} stubs. This bounds the voice node
from 669,799B -> 25,353B (HTTP 200, valid JSON) and the wrapper's soul-load no
longer socket-closes. New helpers: api_compact_neighbors, api_neigh_full,
api_neigh_pointer, api_neigh_rank, api_neigh_better, api_float_or.
The flag is gated: ABSENT it, the response is byte-identical to the old plain
traversal, so the studio app (which never sends it) is unaffected. The MCP
wrapper (mcp-wrapper/src/main.el) appends &compact=1 on its inspectGraph and
fetch-by-id paths.
dist/soul.c is REGENERATED so CI ships the fix: CI compiles the committed
single-TU dist/soul.c directly (running elb/elc on the Linux runner OOM-kills
it), so an .el-only change would build the OLD behavior. Regenerated and verified
on macOS -- compiles with the CI cc line (0 errors) and, on a throwaway soul over
a copy of the live snapshot, serves compact ~25KB / non-compact ~670KB. The regen
also syncs the amalgamation to this branch's .el sources, which had drifted
several self-review commits ahead of the previously-committed soul.c.
Docs: docs/architecture/00-05 added; 01/02/05 corrected so the relevance-ranked
inspect_graph projection reads as committed source, not an in-flight concern.
el_runtime.c emitted 19 metric keys from engram_metrics_json; emit_heartbeat
forwarded 14. Dropped on the floor: hebb_cands, hebb_cand_max, hebb_mass,
hebb_edges, embed_consec_fail. The first two are exactly the pair the runtime
added to answer the 08-06 question -- whether a stalled hebb_links means
nothing co-activates or the threshold is too high. Undiagnosable from the
durable record without them. An instrument computed but not plumbed to
durable storage is not an instrument.
Also adds the corpus damage STOCK, not just the flow. 08-08 fixed the JSON
parser, watched txt_damaged (nodes damaged by a write THIS process) fall to
0, and recorded the defect closed. Census today: 2781 of 4100 nodes still
damaged -- 67.8%, including the self root and every values node. A flow gauge
reads 0 both when the corpus is clean and when it is uniformly damaged but
quiescent. Sampled on a 30-beat countdown and carried with an explicit age.
Today's review found the JSON parser had been replacing every \uXXXX escape
with a literal '?' for at least two months: 3,119 of 4,081 non-telemetry
nodes damaged, including the self traversal root and all 13 values nodes.
The parser is fixed in el_runtime.c. This is the part that keeps it fixed.
Every gauge on this heartbeat answers whether the machinery is running - WM
occupancy, Hebbian potentiation, embedding coverage, sync age, breaker state.
None answered whether the text the machinery carries is intact, which is why
two months of silent corruption read as a perfectly healthy system.
Adds txt_damaged: nodes created this process whose content carries the
character-loss signature. Flat 0 is healthy; any climb means a write path is
mangling text again. The full store census lives at GET /api/text-health,
which is too expensive for a 60s beat.
A gauge that exists but is not plumbed into the beat is not observability -
act_stats fields have to be extracted by name here or they are invisible.
Two fixes, one found by making the other.
1. HEBBIAN WRITE-BACK. This daemon learned 1,198 associations in 23h48m and
kept none of them: it syncs FROM the engram server and never pushes, and
mem_save() is unreachable in HTTP mode by design (soul.el only sets
soul_snapshot_path inside `is_genesis && safe_to_seed`, false whenever
ENGRAM_URL is set, because the server owns persistence). So the one process
that runs idle cognition -- where essentially all co-activation happens -- was
the one process that could not remember what it learned.
hebb_consolidate() now drains the runtime's write-back queue on every heartbeat
and POSTs it as ONE batch to /api/edges/batch. One request, one durable write,
not one 60MB snapshot per edge. Also drains on clean shutdown, so an exit
between beats doesn't take the last 8 minutes of learning with it.
_auth is required and its absence is silent: check_auth_ok exempts GET and
/api/neuron/state-events (which is why ise_post works keyless) but gates every
other mutation on "_auth" in the BODY -- http_serve surfaces no headers, so
there is no Bearer path. An unauthorized reply is NON-EMPTY, so the obvious
`if resp == "" return 0` check would have reported delivery of edges that were
refused, after the drain had already destroyed them. Caught before it shipped.
Gauges hebb_wb_pending/_drained/_dropped/_sent go into the heartbeat so a
consolidation path that stops delivering is visible in the stream.
2. A READ ROUTE MUST NEVER WRITE THE CANONICAL SNAPSHOT. GET /api/graph/edges
serialized this process's graph straight over $HOME/.neuron/engram/snapshot.json
-- the engram SERVER's durable store -- and read the edges back out of it. I
triggered it myself this morning fetching edges for the census above:
snapshot.json went from the server's 41,213 edges to the soul's 42,431, and the
next engram restart loaded the soul's graph as canonical. It happened to be a
superset (Knowledge 1198->1218, Memory 1238->1242, no durable type down), so
nothing was lost. That was luck. Had the soul been running a partial load --
the exact failure soul.el's safe_to_seed guard exists to catch -- one GET would
have destroyed the store, with no write-side guard able to see it coming.
The engram server fixed this same class of bug on 2026-07-21 by routing exports
to a dotted sidecar; the soul kept the original pattern. Same fix: exports go to
.soul-edges-export.json. Also stops a 60MB serialize-and-reread per GET.
Verified: boot 26 loaded 42,432 edges with hebb_max 0.4941 carried across the
restart -- the first time this daemon has ever started knowing what it learned.
str_eq("", "") is true, so an auto-term extractor that kept FAILING reported a
rising auto_term_streak. The signal meaning "fixated on one term" and the
signal meaning "producing no term at all" were the same number — opposite
failures needing opposite responses. Observed live as
{"auto_term":"","auto_term_streak":3}. Same class of bug already fixed for
wm_top0_streak on 2026-07-31; auto_term was missed then.
Empty now reads 0, and the empty run is counted on its own axis
(auto_term_empty_streak) so extractor failure is visible rather than disguised
as health.
Also surfaces the new runtime gauges in the heartbeat: hebb_warm, hebb_max,
hebb_links (is the graph learning any structure at all?) and dup_wm_global.
dup_seeds / dup_wm from engram_act_stats_json. dup_seeds is the one that
matters day to day: a healthy nonzero rate means the suppressor is reclaiming
seed slots the June duplicate import was stealing; a sustained fall toward zero
means the duplicates were finally merged out of the graph, which is the repair
this defends against. Cumulative like wm_evicted/breakthroughs.
Reject an extracted auto-term when its label document frequency exceeds
node_count/400 (floor 8) -- measured live at 12,859 nodes, threshold 32.
Live label df separates the classes by an order of magnitude:
rejected: <!--:220 SELF:175 Engram:125 CORE:88 STAR:36
passed: Dual:12 Sparse:8 Latent:6 MemQ:1 dGRPO:1 engram_goal_bias:1
Verified against the running soul (boot 21). Peak curiosity activation fell
from 541 to 113; the flood terms (SELF, CORE, Engram, STAR, <!--) are absent
from post-fix scans while topical compound identifiers pass untouched.
Sample is 7 scans -- suggestive, not conclusive; watch the next review.
Nested conditional rather than max(): El let is single-assignment, so the
floor is expressed as a second conjunct.
Verification note: content df was tested as an alternative signal and
rejected -- 'Curiosity' has the highest content df in the store (5526) yet
one of the lowest activation counts (113). Label df is the correct field
because label is what the first-word extractor reads.
Investigated awareness.el ise_post's local-fallback engram_node_full call as a
suspected instance of the score-mangling double-wrap fixed in server.el on
2026-08-01. It is not one. Removing the wrapper produces byte-identical
codegen: the compiler treats el_from_float as the boxing intrinsic, so both
`el_from_float(0.3)` and a bare `0.3` emit exactly one el_from_float(0.3).
The server.el bug was different in kind - there the arguments came from
json_get_float(), already boxed as el_val_t, and wrapping those a second time
reinterprets the boxed bits as a raw double, fails engram_decode_score's range
check, and silently clamps to defaults.
Comment only, no behavior change. Recording the negative result at the call
site so the sweep criterion is right: look for el_from_float applied to an
already-boxed expression, never to a literal. Grepping the call name alone
produces false positives, which is what happened here.
Four sites passed the Float local 'sal' through el_from_float() a second
time. el_val_t is the bit-pattern of the double, so re-wrapping performs
an int64->double VALUE conversion of the bits before re-bitcasting —
garbage that fails engram_decode_score's range check and clamps to
defaults. Net effect: importance="critical" stored 0.5/0.5 — importance
levels were cosmetic on the MCP memory path. Verified fixed live:
critical now stores salience/importance 0.95/0.95. Same bug fixed today
in engram server.el route_create_node (foundation/el 7f03876). Literal
wraps (el_from_float(0.9)) are safe — elc passes numeric literals raw.
wm_saturated was a sampled boolean — the 0->1 onset and 1->0 release
moments were only recoverable by hand-diffing consecutive heartbeats.
Emit a low-rate transition ISE at each edge carrying the WM top-5 at
that instant, so the composition that caused the regime change is
captured rather than the composition up to 59s later. First beat of a
boot never fires (restart is not a transition).
The session-init endpoints concatenated unbounded engram activate/scan
results as FULL node objects (content up to ~90KB per node), producing a
~900KB response. After the MCP wrapper re-escapes that into a stringified
text block the client dropped the socket ('connection closed unexpectedly')
on every beginSession call. Cap each list (8-10 activated, 10-20 recent)
and project every node to a light identity plus a bounded, UTF-8-safe
content snippet. Response drops from ~900KB to ~12KB; full content stays
available on demand via recall/fetch/inspectGraph.
Runtime activation counters are now cumulative, so the heartbeat emits
wm_evicted/breakthroughs as totals plus wm_evicted_delta/
breakthroughs_delta (state-tracked change since the previous beat) —
events between beats are no longer lost. Adds embed_eligible from
/api/stats so coverage reads as embed_count/embed_eligible instead of
the misleading absolute count, and surfaces auto_term_streak in the
heartbeat stream. Replaces the false 'semantic seeding NOT implemented'
comment: engram_activate embeds the query, seeds semantic top-K, gates
propagation on cosine, and scores WM promotion semantically.
- awareness.el: curiosity auto_term was the raw first word of a WM
label with no term-quality scoring — observed seeds included What,
Colon, Prose, Context. Replaced the 7-word genre blocklist whack-a-
mole with a delimited stopword membership test (function words +
document-structure words); topical terms pass untouched. Verified
live: seeds now ReasonEdit, Reasoning-model, Self-review.
- routes.el + awareness.el: idle counter only reset on rare inbox
synthesis-requests, so idle==pulse always (zero information).
handle_request now stamps soul.last_activity_ts on every inbound
HTTP request; heartbeat emits idle_ms = ms since last request
(-1 until first request of a boot).
- neuron-api.el: beginSession concatenated a depth-2 spread plus the
unbounded self-hub neighbor dump — multi-MB response, doubled by
wrapper re-escaping, socket died on every call. Now depth-1 and
the hub dump dropped (identity loading has its own tool). Verified:
beginSession returns instead of closing the socket.
- session_start now also posted to the HTTP Engram via ise_post: the
local engram_node_full write never crossed to the observable stream
(sync flows HTTP->soul only), so boots 5+ were invisible — last
visible session_start was boot 4, two weeks ago
- graceful shutdown emits a final ISE with boot/pulse/uptime; a boot
with no shutdown event now reliably signals a crash/SIGKILL
- empty /api/sync responses emit a sync_empty warn ISE instead of
being silently skipped — unreachable engram no longer looks
identical to quiet-but-healthy
- sync backflow prune reads ENGRAM_ISE_RETENTION_MS instead of
duplicating the 48h magic number server.el already honors
Fold engram_act_stats_json() into the heartbeat ISE: wm_evicted and
breakthroughs (per curiosity-scan activate call) plus embed_breaker_open —
the failure mode embed_ok structurally cannot see (it pings the Ollama
root, not the embed pipeline). WM-cap eviction, breakthrough-floor
flooding, and silent lexical degradation are now one-glance diagnosable
from telemetry.
- wm_churn: count of top-5 WM ids absent from previous beat — separates
'one stuck node' from 'whole WM frozen' without hand-correlating ISEs.
- wm_top0_wm: leader's weight; a frozen anchor reads as a constant here.
- Streak guard: before the runtime emitted id in wm_top JSON,
json_get(...,"id") was always empty and the streak incremented on
""=="" every beat — wm_top0_streak measured uptime, not fixation.
Empty id now resets the streak to 0.
proactive_curiosity strengthened its top result unconditionally every
scan — a positive-feedback fixed point that pinned auto_term on the same
node's first word for hours ('Fast-slow' era). Strengthen now fires only
when the top node changed since the last scan, and a 4-deep finst-style
tabu ring (ACT-R declarative finsts) hard-excludes recently used auto
terms (~2 min at the 30s cadence). Quoted-title guard stops '"The'
leaking through the >3-char stopword check and seeding lexical floods.
Heartbeat now pumps /api/embed-backfill?n=32 on the authoritative store
(its lazy backfill had no production trigger; coverage stalled at
93/12175) and emits wm_saturated, wm_top0_streak, embed_backfilled,
embed_count. Curiosity ISE emits auto_term_streak. The stuck-WM failure
mode is now a one-glance signal instead of manual ISE cross-referencing.
Three stale soul:boot_count copies (salience .9, importance .9, Canonical:
+0.2 tier bias, 0.15 threshold) held the top WM slots for 23h — a boot
counter outcompeting real context. Demoted to salience .55 / importance .2 /
tier Working: plumbing, not memory.
Persistence was also broken: in HTTP-engram mode the server owns state and
nothing wrote the counter back — the log shows boot #5 on three consecutive
boots. mem_boot_count_inc now mirrors the persona write-back: delete stale
server copies (matched by content prefix — route_create_node sets
label=content), create the replacement server-side. Working tier is in the
boot seed (/api/nodes) but excluded from periodic /api/sync, so the count
survives restarts without re-importing mid-session. Verified: restart
incremented 1->2 with exactly one server-side counter node.
Sentinel labels (knowledge:captured/evolved/canonical) made every capture
anonymous in WM telemetry — 35 identical wm_top entries — and starved the
curiosity auto-term seeder, which derives scan seeds from WM top-10 labels
and had returned empty on every scan since boot 6 because WM became
Knowledge-dominated while Knowledge was excluded from seeding.
- capture/evolve/promote now pass title (or empty → engram_node_full's
content[:60] derivation) instead of sentinels
- auto_term_try_slot admits Knowledge slots; sentinel-shaped labels
(colon, no space) are skipped so legacy nodes cannot seed 'knowledge'
- verified: probe capture labeled 'Label derivation probe 2026-07-23'
Saving the 988 crisis-line contact returned truncated, unparseable JSON —
cut mid-"set_at" at the file's byte length (e.g. 178 of a 218-byte
response). The contact written to disk was complete; only the HTTP response
was clipped, so a real customer's crisis-contact save came back corrupt.
Root cause is in the el runtime's response writer, not a handler buffer:
fs_read stores the file's byte count in a thread-local (_tl_fs_read_len)
for binary-safe file serving, and the response writer uses that length when
non-zero instead of strlen(body) (el_runtime.c:1409). Both safety-contact
handlers call fs_read (the POST read-back verify; the GET file read) and
then return a LONGER wrapped JSON string, so the response is capped to the
file size.
Soul-source fix (no runtime change needed):
- POST: verify persistence via fs_write's return (1 = all bytes written)
instead of an fs_read read-back — removes the fs_read, so nothing caps the
response.
- GET: fs_read is required, so reset the thread-local after it with a no-op
fs_read("") (fs_read zeroes the length before it opens a path) so the
wrapped response is sent in full.
Verified: POST (crisis-line + custom) and GET now return complete, valid
JSON (parses cleanly, full contact incl. set_at). Regenerated dist/soul.c +
dist/safety.c (3GB RSS watchdog, release el_runtime v1.0.0-20260501).
Full suite still green: verify-soul-contract GATE PASS (PRESENCE +
IMMUTABILITY), genesis boot survives (/health 200, no segfault), bounded-
persona floor still compiled in.
NOTE: the underlying runtime leak (any handler that fs_reads then returns a
longer string) is worth a proper fix in el_runtime.c (use the max of
strlen and _tl_fs_read_len) so this class can't recur.
A fresh-install (SOUL_CGI_ID=ntn-genesis) boot crashed with
"Segmentation fault: 11" right after the http server came up — a real
customer's very first boot. Backtrace:
strcmp(0x1) <- str_eq (el_runtime.c:219) <- mem_save <- awareness_run
Root cause: the el runtime's engram_save returns an Int (1 = ok, 0 =
failure), but mem_save did `str_eq(engram_save(path), "")`, treating the
return as a String. str_eq runs EL_CSTR on it, which is a raw cast:
EL_CSTR(1) = (char*)0x1. On a SUCCESSFUL save (return 1) strcmp then
dereferences 0x1 and segfaults. Genesis is the first path that both seeds
the brain AND saves it successfully on the very first awareness pass, so it
crashes there; non-genesis boots (contract gate, refusal test) don't hit a
successful early mem_save, which is why they passed. handle_api_consolidate
had the identical latent bug.
Fix: read engram_save's Int result and compare `== 0` instead of str_eq'ing
it — in mem_save (memory.el) and handle_api_consolidate (neuron-api.el).
Regression: pre-existing, NOT introduced by the immutability/floor rebuild.
The pre-immutability build (1442ce2) genesis-crashes identically in the same
unchanged mem_save; #159 never actually fixed#150 for a release-runtime
build.
Regenerated dist/soul.c + per-module dist/{memory,neuron-api}.c (3GB RSS
watchdog, built against release el_runtime v1.0.0-20260501). Verified:
genesis boot survives (/health 200, no segfault), verify-soul-contract.sh
GATE PASS (PRESENCE + IMMUTABILITY), and the bounded-persona floor is still
compiled in (BOUNDED PERSONA / SOUL_PERSONA_NAME strings present).
The ship-soul builds from this branch, which has the bounded-persona floor
(#93) but never received the tombstone/supersede immutability fix (that
went to main; hotfix diverged before it). So the launch soul failed
verify-soul-contract IMMUTABILITY on the delete/update/forget routes —
they hard-removed engram nodes via engram_forget/mem_forget.
Apply the same fix, mirroring the knowledge routes' supersede pattern:
- node/update -> create new node + "supersedes" edge to the original, KEEP
the original (no engram_forget).
- node/delete, memory/delete, memory/forget, cultivate forget, and the
autonomous awareness forget -> TOMBSTONE via the canonical mem_tombstone
(memory.el): keep the node + its edges, record a Tombstone marker, hide
from default bounded list reads (?include_deleted recovers). Never
engram_forget. The MCP forget tool now routes to the tombstoning delete
instead of faking a delete.
Internal GC that genuinely removes transient nodes (awareness inbox-trigger
consume, consolidation dedup, session-summary replace, telemetry pruning)
still calls engram_forget directly and is unchanged.
Regenerated dist/soul.c (single-TU) + per-module dist/{memory,awareness,
neuron-api}.c from THIS branch's sources under a 3GB physical-RSS watchdog
(peak ~32MB), built against the release el_runtime (v1.0.0-20260501). The
bounded-persona floor is preserved — verified in the emitted C and the
linked binary (BOUNDED PERSONA / SOUL_PERSONA_NAME strings present).
verify-soul-contract.sh: GATE PASS — PRESENCE all 27 routes, IMMUTABILITY
5/5 KEPT (memory-update, memory-delete, node-update, node-delete,
memory-forget).
A customer DMG install ships the full graph but presents a named, bounded
assistant that must never claim the imprint's human past. The neuron-ui
retrieval fence keeps the imprint's biography out of the ENGRAM CONTEXT; this
is the second half - it stops confabulation ("tell me about your childhood")
from inventing a human life or naming Will, even if biography leaks into context.
bounded_persona_floor() gates on SOUL_PERSONA_NAME: the customer DMG sets it,
owner (Will's) builds leave it unset so the real self is completely unchanged.
Applied at every generation path - chat, agentic (tools), vision, plan, soul,
dharma - so no path can leak.
Verified against claude-sonnet-4-5: with the floor on and Will's biography
deliberately leaked into the identity context, all probes (childhood / creator /
family) return the bounded-entity answer and explicitly refuse to claim the
leaked life; with the floor off the same context is fully confabulated as its own.
NOTE: dist/soul.c must be regenerated on a build host - local link is blocked by
a pre-existing el_runtime mismatch (engram_prune_telemetry), unrelated to this change.
The soul daemon leaked ~104 orphan in-memory nodes/min (17.6GB RSS,
OOM-killed) because the perceive gate substring-matched 'soul-inbox'
against the loop's own verbatim-copy output, the trigger node was
strengthened but never consumed, and record() persisted a Memory node
per cycle. Fixes: perceive gates and activates only on the dedicated
soul-inbox-pending tag; one_cycle requires the tag on the node's tags
field before attending (makes consumption safe); processed triggers
are consumed via engram_forget; loop outcomes route through ISE
telemetry (48h prune) instead of permanent Memory nodes.
Verified post-restart: node_delta 104→~0, curiosity scans resumed,
WM average unfrozen (0.120833→0.0676), RSS 17.6GB→184MB.
- engram refresh URL now resolves env -> state -> localhost:8742, same
hardening ise_post got after the boot-4 blackout. Previously a corrupted/
empty soul_engram_url state key silently disabled sync forever while
heartbeats kept flowing — WM starves of Knowledge nodes with no outward
sign.
- heartbeat ISE: node_delta, edge_delta (growth vs stall vs flood is now
one field, not cross-ISE forensics), sync_age_ms from a new
soul.last_sync_ok_ts stamp (-1 = never; >> SOUL_REFRESH_MS = refresh
path broken). Verified live: pulse 1 sync_age_ms=-1, sync fired +1.6s,
age counts up between syncs.
Regenerates the combined dist/soul.c and per-module dist/*.c from the current
El sources, on top of the elc-source-typo fixes (PR #77) and the Track B
threat-to-others routing (PR #76), both already on this branch.
Validated end to end under a physical-RSS watchdog (macOS silently ignores
ulimit -v / RLIMIT_AS, so every elc/elb run was RSS-polled and kill -9'd at a
3GB ceiling, one module at a time):
- OOM is GONE. The stale dist/soul-with-nlg.el (which still carries the
malformed string literals) explodes to 3.3GB+ and is watchdog-killed at ~90%.
With the typos fixed, every one of the 48 modules compiles at <=18MB peak RSS,
and the full flat amalgamation compiles as a single translation unit at ~68MB.
The 700GB pathology was purely the unbounded-parser-on-malformed-literal loop;
no malformed construct means no loop.
- The regenerated soul.c contains Track B: safety_classify_hard_bell ->
threat_other -> safety_hard_directive routes credible threat-to-others to 911
and explicitly NOT to 988 / the safety contact. Verified in source, in the
emitted C, and in the linked binary's strings. Track A (abuse / self_harm)
is unchanged and still checked first.
- The regenerated soul links to a working native arm64 binary and boots: serves
on a throwaway port, /health returns 200, awareness loop runs.
Also fixes one source blocker discovered during regen (unrelated to the typos
or Track B): chat.el handle_chat_agentic left a void `if { println(...) }` in
value position, which the current elc lowers to `_if_result = (println(...))`
(assigning void) -> invalid C. Bound an explicit Bool so the branch is
non-void; behavior unchanged (still only logs on persist failure).
NOTE (runtime dependency, for controlled deploy): this branch's chat.el calls
engram_get_node_by_label, which the canonical el-compiler/runtime does not yet
declare/define (the release runtime v1.0.0-20260501 has it; the newest runtime
has arena + http_serve_async but not this). Building the soul requires a runtime
that has all three. Land engram_get_node_by_label into the runtime package
before this soul.c can be built in CI.
Do not merge — regen + Track B going live is a controlled-deploy call.
Three unescaped-quote typos produced malformed El string literals that broke
compilation:
- safety.el:282 stray extra double-quote at the tail of safety_soft_phrases
(\"having a breakdown\""]") closed the string early, desyncing the lexer's
string/code phase for the rest of the file and shattering later apostrophe
text (can't, i'm) into bare identifiers -> invalid C.
- sessions.el:517 str_replace(topic_snip, """, ...) — the bare """ is an
empty string plus an unterminated string that swallowed the closing ) and };
with the current elc this triggers the parser overrun -> ~700GB OOM.
- sessions.el:520 unescaped nested quotes in the topic_tags literal.
All three now use escaped inner quotes. Verified: both files compile clean
under the current elc (safety.c and sessions.c well-formed, brace-balanced).
A homicide/assault threat (going to kill, going to hurt, etc.) had no
bucket in safety_classify_hard_bell and fell through to the self_harm
default, showing the user the 988 suicide line and (via the desktop gate)
their safety contact. That framing is wrong and potentially dangerous for
someone voicing intent to harm another person.
Add a distinct Track B (safety_threat_to_others_phrases + a threat_other
classification and a safety_hard_directive branch) that refuses to assist,
de-escalates, and directs to 911 for a credible imminent threat, and that
never surfaces 988 or involves the safety contact. Track A (abuse /
self_harm) is checked first and unchanged, so victim and self-directed
phrasings still route correctly.
Source-only change: requires a soul rebuild + dist/soul.c regen to ship.
awareness_run's while-loop ran outside any request arena, so every
allocation in every 1s tick (search JSON, heartbeat payloads, curiosity
activations) was treated as permanent by the runtime — 7.5GB RSS in
under a minute. Bracket each iteration with el_arena_push/el_arena_pop
(same pattern the compiler emits for scoped blocks; state_set/state_get
persist separately via el_strdup_persist and are unaffected).
dist/soul.c carries the same change hand-patched at the compiled
awareness_run site — elc is currently unsafe to run locally (pathological
memory on sessions.el), so the generated C was patched to match the
source, verified line-for-line against the compiler's own conventions.
MUST be paired with el repo PR #64 (el_strdup_persist for stored engram
fields): per-tick arena reclamation widens the write-corruption window
without it. Verified together: 5h live soak on the recovered production
snapshot, flat RSS, write-field-integrity clean.
Note: dist/soul.c still needs a full elc regen to pick up PR #73's
source changes (consent tiers) — tracked separately; this patch does not
regress that (those changes were never in dist).
The model already narrates its intent in a text block before every tool call;
agentic_loop DISCARDED that prose on tool rounds. Now: (1) each loop round
appends {i, t: narration, tool} to state key run_progress_<sid>, reset at run
start, closed with {done:true}; (2) new GET /api/run-progress/<sid> returns the
ledger so clients poll live step updates during a run (the Cowork pattern,
no streaming needed); (3) tool_pending envelope gains a narration field;
(4) handle_config display default aligned to the intended product default
(claude-sonnet-4-5 silently became fresh-profile pickers' default).
Compiled proof for the running test bed:
neuron-container-build/soul-narrated-runs-20260713.patch (applies on top of
soul-webfix-20260711.patch); E2E-verified live: ledger filled DURING an agentic
run (narration + tool per round), safety-contact and workspace scoping intact.
Evidence for why: Tim's 2026-07-13 research run — 9 minutes of silence, then a
timeout banner, zero step visibility (compounded by the Haiku 4.5 incident
14:44-15:24 UTC same morning).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Enforcement moves from the client into the engine, where the tools execute:
- classify_tool_risk() tiers every tool call read/reversible/escalate. The
agentic loop REFUSES to auto-run the escalate tier — being a builtin is no
longer a free pass, and 'always allow' can never bypass escalate (irreversible
actions always confirm, the value line). Escalate suspends to the client's
existing consent bridge; the /approve round-trip is the only path that runs it.
risk_tier rides the tool_pending envelope so the client renders consent weight.
- run_command_guard() is a real fence, not a cwd suggestion: refuses parent
traversal, ~, command substitution, and absolute paths outside the workspace,
and refuses shell entirely when no workspace is set. Applied in dispatch_tool
so BOTH the loop auto-run and the post-consent approve-dispatch path are fenced.
- web_get gained an http(s)-only scheme guard (previously unguarded — file:// etc).
Adversarially verified against a compiled soul in an isolated container (soul
hit directly, app gate out of the loop): read-outside-workspace denied,
write-class shell suspends for consent, approve-swapped absolute/chaining/
command-substitution escapes all refused with no file created, file:// denied;
legit in-workspace approve executes and read commands auto-run (no over-block).
Still lexical (symlinks); OS-level confinement in el_runtime.c remains the
ceiling, flagged in the LIMITATION note. This closes BUG-8's client-only-gate
and escapable-run_command at the engine. dist/soul.c must be regenerated from
this chat.el via elb at merge (hand-port used only to verify behavior).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two mechanical refactors, semantics identical:
- affective_context_prefix(): the block-expression initializer form miscompiles
under locally-buildable elc (first typed let in a block-expr loses its
declaration — 3-line repro filed); function-hoist compiles correctly.
AFFECTIVE/CARE LOGIC BODY UNCHANGED, verbatim move.
- session_preload: same-scope re-let shadowing inside an if-expression
initializer emits duplicate C declarations; chained bindings renamed
bullets_0/1/2 etc. References preserved binding-for-binding.
Enables: chat.el compiles cleanly with a self-bootstrapped elc from el/lang
main (Jul 1). Blocked separately: sessions.el (compiler hang), safety.el
(string-lexing corruption — NOT touched, per safety-layer discipline).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds llm_base_url()/llm_wire_format() env-var readers and
openai_chat_complete() for basic (non-agentic) chat via any
OpenAI-compatible endpoint. Activated when NEURON_LLM_0_FORMAT=openai
and NEURON_LLM_0_URL is set; Anthropic path is untouched and remains
default. Agentic tool loop support deferred to a follow-up PR.
Adds current_engine_note() to chat.el and appends it to the system
prompt in handle_chat. Allows Neuron to answer 'what model am I
running on?' accurately — the model id from the request body (or
the configured default) is passed as a factual annotation rather
than expecting the LLM to guess from training data.
Main already contained the connector proxy, safety module, seeding ratio
guard, and neuron-api node CRUD that Tim added — these were incorporated
via earlier parallel sessions. Taking main for all conflicted files
(superset implementations).
Unique contributions carried forward:
- flag_true() in routes.el: tolerates agentic:1 (integer) from the
el-src UI in addition to agentic:true (bool) from the Kotlin UI.
- memory.elh: auto-merged timestamp bump.
The is_pending / skip-auto-persist logic was already in main's routes.el.
Removes route_sessions() from the GET handler which was shadowing
session_list() in sessions.el. Adds DELETE /api/sessions/:id and
PATCH /api/sessions/:id routes. Also includes bridge_save/agentic_resume
raw-JSON embedding fix (messages_raw/tools_raw fields).
Conflict resolution: kept HEAD's workspace root check for write_file
tool, and bridge blob validation guards, which were added to main after
Tim's branch diverged.
The hops=1 comment incorrectly claimed a semantic seed supplement
(cosine-sim scan) was active — it was planned but never implemented.
Corrected to accurately describe what the runtime does (istr_contains
only). Also adds wm_top (top-3 WM nodes by weight) to the curiosity_scan
ISE payload so activation patterns are visible without relying solely on
the heartbeat's wm_active count.
handle_dharma_room_turn and handle_dharma_chat both called
distill_transcript since June 30 but the function was never declared,
causing a build failure. Implements last-3-messages extraction for JSON
array transcripts and last-500-char truncation for plain text.
The while loop in session_search had too many let bindings in scope;
the ELC compiler's exponential rebinding accumulation caused OOM and
truncation of dist/sessions.c since June 30. Moving the per-node logic
into session_search_entry gives the compiler a clean scope boundary per
call, restoring O(N) compile behaviour.
Additive: appends a factual [CURRENT ENGINE: <model>] line to the system prompt (model from the
request body — accurate even under Auto routing; falls back to configured default). An LLM can't
know its own model from training (name/version assigned post-training), so the harness must tell it.
Identity-consistent: model = engine, self layered on top. Does NOT alter identity/values/safety.
PARSES (elc chat.el exit 0); NOT built/tested — ships with the soul rebuild.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>